From d8ce85010cc9b314b486a1b133984703c71196e5 Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Tue, 20 Jan 2026 08:44:40 -0600 Subject: [PATCH 01/16] =?UTF-8?q?=E2=80=9CCOMENTARIO=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PWGLF/Tasks/Nuspex/CMakeLists.txt | 5 + PWGLF/Tasks/Nuspex/multiplicitypt.cxx | 960 ++++++++++++++++++++++++++ 2 files changed, 965 insertions(+) create mode 100644 PWGLF/Tasks/Nuspex/multiplicitypt.cxx diff --git a/PWGLF/Tasks/Nuspex/CMakeLists.txt b/PWGLF/Tasks/Nuspex/CMakeLists.txt index 0247878bf85..a4ba69bfb88 100644 --- a/PWGLF/Tasks/Nuspex/CMakeLists.txt +++ b/PWGLF/Tasks/Nuspex/CMakeLists.txt @@ -180,4 +180,9 @@ o2physics_add_dpl_workflow(chargedparticle-raa PUBLIC_LINK_LIBRARIES O2Physics::AnalysisCore COMPONENT_NAME Analysis) +o2physics_add_dpl_workflow(multiplicity-pt + SOURCES multiplicitypt.cxx + PUBLIC_LINK_LIBRARIES O2::Framework O2Physics::AnalysisCore + COMPONENT_NAME Analysis) + endif() diff --git a/PWGLF/Tasks/Nuspex/multiplicitypt.cxx b/PWGLF/Tasks/Nuspex/multiplicitypt.cxx new file mode 100644 index 00000000000..d3d64510028 --- /dev/null +++ b/PWGLF/Tasks/Nuspex/multiplicitypt.cxx @@ -0,0 +1,960 @@ +#include "Common/Core/TrackSelection.h" +#include "Common/Core/TrackSelectionDefaults.h" +#include "Common/DataModel/EventSelection.h" +#include "Common/DataModel/Multiplicity.h" +#include "Common/DataModel/TrackSelectionTables.h" +#include "Common/DataModel/Centrality.h" +#include "Common/DataModel/PIDResponse.h" +#include "Common/DataModel/PIDResponseTPC.h" +#include "Common/DataModel/PIDResponseTOF.h" + +#include "Framework/ASoAHelpers.h" +#include "Framework/AnalysisDataModel.h" +#include "Framework/AnalysisTask.h" +#include "Framework/HistogramRegistry.h" +#include "Framework/O2DatabasePDGPlugin.h" +#include "Framework/StaticFor.h" +#include "Framework/runDataProcessing.h" +#include "ReconstructionDataFormats/Track.h" +#include "PWGLF/Utils/inelGt.h" + +#include "TDatabasePDG.h" +#include +#include +#include +#include +#include + +#include +#include + +using namespace o2; +using namespace o2::framework; +using namespace o2::framework::expressions; +using BCsRun3 = soa::Join; + +struct multiplicitypt { + + // Service + Service pdg; + + // Configurables - Matching spectraTOF approach + Configurable isRun3{"isRun3", true, "is Run3 dataset"}; + + // Event selection configurables (spectraTOF style) + Configurable cfgCutVertex{"cfgCutVertex", 10.0f, "Accepted z-vertex range"}; + Configurable cfgINELCut{"cfgINELCut", 0, "INEL event selection: 0 no sel, 1 INEL>0, 2 INEL>1"}; + Configurable askForCustomTVX{"askForCustomTVX", false, "Ask for custom TVX rather than sel8"}; + + // SET ALL THESE TO FALSE LIKE SPECTRATOF: + Configurable removeITSROFrameBorder{"removeITSROFrameBorder", false, "Remove ITS Read-Out Frame border"}; + Configurable removeNoSameBunchPileup{"removeNoSameBunchPileup", false, "Remove no same bunch pileup"}; + Configurable requireIsGoodZvtxFT0vsPV{"requireIsGoodZvtxFT0vsPV", false, "Require good Z vertex FT0 vs PV"}; + Configurable requireIsVertexITSTPC{"requireIsVertexITSTPC", false, "Require vertex ITSTPC"}; + Configurable removeNoTimeFrameBorder{"removeNoTimeFrameBorder", false, "Remove no time frame border"}; + + // Track selection configurables (spectraTOF style) + Configurable cfgCutEtaMax{"cfgCutEtaMax", 0.8f, "Max eta range for tracks"}; + Configurable cfgCutEtaMin{"cfgCutEtaMin", -0.8f, "Min eta range for tracks"}; + Configurable cfgCutY{"cfgCutY", 0.5f, "Y range for tracks"}; + Configurable cfgCutNsigma{"cfgCutNsigma", 3.0f, "nsigma cut range for tracks"}; + Configurable lastRequiredTrdCluster{"lastRequiredTrdCluster", -1, "Last cluster to require in TRD"}; + Configurable requireTrdOnly{"requireTrdOnly", false, "Require only tracks from TRD"}; + Configurable requireNoTrd{"requireNoTrd", false, "Require tracks without TRD"}; + + // Multiplicity estimator (like spectraTOF) + Configurable multiplicityEstimator{"multiplicityEstimator", 6, + "Multiplicity estimator: 0=NoMult, 1=MultFV0M, 2=MultFT0M, 3=MultFDDM, 4=MultTracklets, 5=MultTPC, 6=MultNTracksPV, 7=MultNTracksPVeta1, 8=CentFT0C, 9=CentFT0M, 10=CentFV0A"}; + + // Analysis switches + Configurable enableDCAHistograms{"enableDCAHistograms", false, "Enable DCA histograms"}; + Configurable enablePIDHistograms{"enablePIDHistograms", true, "Enable PID histograms"}; + + // Track cut configurables (spectraTOF style) + Configurable useCustomTrackCuts{"useCustomTrackCuts", true, "Flag to use custom track cuts"}; + Configurable itsPattern{"itsPattern", 0, "0 = Run3ITSibAny, 1 = Run3ITSallAny, 2 = Run3ITSall7Layers, 3 = Run3ITSibTwo"}; + Configurable requireITS{"requireITS", true, "Additional cut on the ITS requirement"}; + Configurable requireTPC{"requireTPC", true, "Additional cut on the TPC requirement"}; + Configurable requireGoldenChi2{"requireGoldenChi2", true, "Additional cut on the GoldenChi2"}; + Configurable minNCrossedRowsTPC{"minNCrossedRowsTPC", 70.f, "Additional cut on the minimum number of crossed rows in the TPC"}; + Configurable minNCrossedRowsOverFindableClustersTPC{"minNCrossedRowsOverFindableClustersTPC", 0.8f, "Additional cut on the minimum value of the ratio between crossed rows and findable clusters in the TPC"}; + Configurable maxChi2PerClusterTPC{"maxChi2PerClusterTPC", 4.f, "Additional cut on the maximum value of the chi2 per cluster in the TPC"}; + Configurable minChi2PerClusterTPC{"minChi2PerClusterTPC", 0.5f, "Additional cut on the minimum value of the chi2 per cluster in the TPC"}; + Configurable maxChi2PerClusterITS{"maxChi2PerClusterITS", 36.f, "Additional cut on the maximum value of the chi2 per cluster in the ITS"}; + Configurable maxDcaXYFactor{"maxDcaXYFactor", 1.f, "Additional cut on the maximum value of the DCA xy (multiplicative factor)"}; + Configurable maxDcaZ{"maxDcaZ", 0.1f, "Additional cut on the maximum value of the DCA z"}; + Configurable minTPCNClsFound{"minTPCNClsFound", 100.f, "Additional cut on the minimum value of the number of found clusters in the TPC"}; + Configurable min_ITS_nClusters{"min_ITS_nClusters", 5, "minimum number of found ITS clusters"}; + + // Basic track cuts + Configurable cfgTrkEtaCut{"cfgTrkEtaCut", 0.8f, "Eta range for tracks"}; + Configurable cfgTrkLowPtCut{"cfgTrkLowPtCut", 0.15f, "Minimum constituent pT"}; + + // Custom track cuts matching spectraTOF + TrackSelection customTrackCuts; + + // Histogram Registry + HistogramRegistry ue; + + // Table definitions - EXACT spectraTOF approach + using CollisionTableData = soa::Join; + using CollisionTableMC = soa::Join; + + // Track tables - TPC PID only + using TrackTableData = soa::Join; + using TrackTableMC = soa::Join; + + // MC tables - EXACT spectraTOF approach + using CollisionTableMCTrue = aod::McCollisions; + using ParticleTableMC = aod::McParticles; + + // Preslice for MC particles (like spectraTOF) + Preslice perMCCol = aod::mcparticle::mcCollisionId; + + // Multiplicity estimator enum (like spectraTOF) + enum MultCodes : int { + kNoMultiplicity = 0, + kMultFV0M = 1, + kMultFT0M = 2, + kMultFDDM = 3, + kMultTracklets = 4, + kMultTPC = 5, + kMultNTracksPV = 6, + kMultNTracksPVeta1 = 7, + kCentralityFT0C = 8, + kCentralityFT0M = 9, + kCentralityFV0A = 10 + }; + + // Particle species enum (from spectraTOF) + enum ParticleSpecies : int { + kPion = 0, + kKaon = 1, + kProton = 2, + kNSpecies = 3 + }; + + // PDG codes + static constexpr int PDGPion = 211; + static constexpr int PDGKaon = 321; + static constexpr int PDGProton = 2212; + + // ======================================================================== + // PROCESS FUNCTION DECLARATIONS - SPECTRATOF STYLE + // ======================================================================== + + // Data processing + void processData(CollisionTableData::iterator const& collision, + TrackTableData const& tracks); + PROCESS_SWITCH(multiplicitypt, processData, "process data", false); + + // MC processing - EXACT spectraTOF approach + void processMC(TrackTableMC const& tracks, + aod::McParticles const& particles, + CollisionTableMCTrue const& mcCollisions, + CollisionTableMC const& collisions); + PROCESS_SWITCH(multiplicitypt, processMC, "process MC", true); + + // True MC processing - EXACT spectraTOF approach + void processTrue(CollisionTableMCTrue const& mcCollisions, + ParticleTableMC const& particles); + PROCESS_SWITCH(multiplicitypt, processTrue, "process true MC", true); + + // ======================================================================== + // TRACK SELECTION FUNCTIONS - MATCHING spectraTOF + // ======================================================================== + + template + bool passesCutWoDCA(TrackType const& track) const + { + if (useCustomTrackCuts.value) { + for (int i = 0; i < static_cast(TrackSelection::TrackCuts::kNCuts); i++) { + if (i == static_cast(TrackSelection::TrackCuts::kDCAxy) || + i == static_cast(TrackSelection::TrackCuts::kDCAz)) { + continue; + } + if (!customTrackCuts.IsSelected(track, static_cast(i))) { + return false; + } + } + return true; + } + return track.isGlobalTrackWoDCA(); + } + + template + bool passesDCAxyCut(TrackType const& track) const + { + if (useCustomTrackCuts.value) { + if (!passesCutWoDCA(track)) { + return false; + } + const float maxDcaXY = maxDcaXYFactor.value * (0.0105f + 0.0350f / std::pow(track.pt(), 1.1f)); + if (std::abs(track.dcaXY()) > maxDcaXY) { + return false; + } + return true; + } + return track.isGlobalTrack(); + } + + template + bool passesTrackSelection(TrackType const& track) const + { + if (track.eta() < cfgCutEtaMin.value || track.eta() > cfgCutEtaMax.value) + return false; + + if (track.tpcChi2NCl() < minChi2PerClusterTPC.value || track.tpcChi2NCl() > maxChi2PerClusterTPC.value) + return false; + + if (!passesCutWoDCA(track)) + return false; + + return passesDCAxyCut(track); + } + + // ======================================================================== + // PID SELECTION FUNCTIONS - TPC ONLY (OLD NON-EXCLUSIVE METHOD) + // ======================================================================== + + template + bool passesPIDSelection(TrackType const& track) const + { + float nsigmaTPC = 0.f; + + if constexpr (species == kPion) { + nsigmaTPC = track.tpcNSigmaPi(); + } else if constexpr (species == kKaon) { + nsigmaTPC = track.tpcNSigmaKa(); + } else if constexpr (species == kProton) { + nsigmaTPC = track.tpcNSigmaPr(); + } + + // TPC-only PID (works for all pT, but better at low pT < 1 GeV/c) + return (std::abs(nsigmaTPC) < cfgCutNsigma.value); + } + + // ======================================================================== + // EXCLUSIVE PID SELECTION - Returns best hypothesis for a track + // ======================================================================== + + template + int getBestPIDHypothesis(TrackType const& track) const + { + // Return values: -1 = no ID, 0 = pion, 1 = kaon, 2 = proton + + float nsigmaPi = std::abs(track.tpcNSigmaPi()); + float nsigmaKa = std::abs(track.tpcNSigmaKa()); + float nsigmaPr = std::abs(track.tpcNSigmaPr()); + + // Find the hypothesis with smallest |nσ| that passes the cut + float minNSigma = 999.0f; + int bestSpecies = -1; + + if (nsigmaPi < cfgCutNsigma.value && nsigmaPi < minNSigma) { + minNSigma = nsigmaPi; + bestSpecies = kPion; + } + if (nsigmaKa < cfgCutNsigma.value && nsigmaKa < minNSigma) { + minNSigma = nsigmaKa; + bestSpecies = kKaon; + } + if (nsigmaPr < cfgCutNsigma.value && nsigmaPr < minNSigma) { + minNSigma = nsigmaPr; + bestSpecies = kProton; + } + + return bestSpecies; + } + + // ======================================================================== + // EVENT SELECTION FUNCTION - EXACT spectraTOF + // ======================================================================== + + template + bool isEventSelected(CollisionType const& collision) + { + if constexpr (fillHistograms) { + ue.fill(HIST("evsel"), 1.f); + if (collision.isInelGt0()) ue.fill(HIST("evsel"), 2.f); + if (collision.isInelGt1()) ue.fill(HIST("evsel"), 3.f); + } + + if (askForCustomTVX.value) { + if (!collision.selection_bit(aod::evsel::kIsTriggerTVX)) return false; + } else { + if (!collision.sel8()) return false; + } + + if constexpr (fillHistograms) ue.fill(HIST("evsel"), 4.f); + + if (removeITSROFrameBorder.value && !collision.selection_bit(aod::evsel::kNoITSROFrameBorder)) return false; + if constexpr (fillHistograms) ue.fill(HIST("evsel"), 5.f); + + if (removeNoSameBunchPileup.value && !collision.selection_bit(aod::evsel::kNoSameBunchPileup)) return false; + if constexpr (fillHistograms) ue.fill(HIST("evsel"), 6.f); + + if (requireIsGoodZvtxFT0vsPV.value && !collision.selection_bit(aod::evsel::kIsGoodZvtxFT0vsPV)) return false; + if constexpr (fillHistograms) ue.fill(HIST("evsel"), 7.f); + + if (requireIsVertexITSTPC.value && !collision.selection_bit(aod::evsel::kIsVertexITSTPC)) return false; + if constexpr (fillHistograms) ue.fill(HIST("evsel"), 8.f); + + if (removeNoTimeFrameBorder.value && !collision.selection_bit(aod::evsel::kNoTimeFrameBorder)) return false; + if constexpr (fillHistograms) ue.fill(HIST("evsel"), 9.f); + + if (std::abs(collision.posZ()) > cfgCutVertex.value) return false; + + if constexpr (fillHistograms) { + ue.fill(HIST("evsel"), 13.f); + if (collision.isInelGt0()) ue.fill(HIST("evsel"), 14.f); + if (collision.isInelGt1()) ue.fill(HIST("evsel"), 15.f); + } + + if (cfgINELCut.value == 1 && !collision.isInelGt0()) return false; + if (cfgINELCut.value == 2 && !collision.isInelGt1()) return false; + + return true; + } + + // ======================================================================== + // PRIMARY SELECTION - MATCHING spectraTOF + // ======================================================================== + + template + bool isGoodPrimary(ParticleType const& particle) const + { + auto pdgParticle = pdg->GetParticle(particle.pdgCode()); + if (!pdgParticle || pdgParticle->Charge() == 0.) + return false; + + if (!particle.isPhysicalPrimary()) + return false; + + if (std::abs(particle.eta()) >= cfgCutEtaMax.value) + return false; + if (particle.pt() < cfgTrkLowPtCut.value) + return false; + + if (std::abs(particle.y()) > cfgCutY.value) + return false; + + return true; + } + + // Particle-specific primary selection + template + bool isGoodPrimarySpecies(ParticleType const& particle) const + { + int pdgCode = std::abs(particle.pdgCode()); + int expectedPDG = 0; + + if constexpr (species == kPion) expectedPDG = PDGPion; + else if constexpr (species == kKaon) expectedPDG = PDGKaon; + else if constexpr (species == kProton) expectedPDG = PDGProton; + + if (pdgCode != expectedPDG) return false; + + return isGoodPrimary(particle); + } + + void init(InitContext const&); +}; + +WorkflowSpec defineDataProcessing(ConfigContext const& cfgc) +{ + return WorkflowSpec{adaptAnalysisTask(cfgc)}; +} + +void multiplicitypt::init(InitContext const&) +{ + // ======================================================================== + // CUSTOM TRACK CUTS INITIALIZATION - MATCHING spectraTOF + // ======================================================================== + + if (useCustomTrackCuts.value) { + LOG(info) << "Using custom track cuts matching spectraTOF approach"; + customTrackCuts = getGlobalTrackSelectionRun3ITSMatch(itsPattern.value); + + customTrackCuts.SetRequireITSRefit(requireITS.value); + customTrackCuts.SetRequireTPCRefit(requireTPC.value); + customTrackCuts.SetMinNClustersITS(min_ITS_nClusters.value); + customTrackCuts.SetRequireGoldenChi2(requireGoldenChi2.value); + customTrackCuts.SetMaxChi2PerClusterTPC(maxChi2PerClusterTPC.value); + customTrackCuts.SetMaxChi2PerClusterITS(maxChi2PerClusterITS.value); + customTrackCuts.SetMinNCrossedRowsTPC(minNCrossedRowsTPC.value); + customTrackCuts.SetMinNClustersTPC(minTPCNClsFound.value); + customTrackCuts.SetMinNCrossedRowsOverFindableClustersTPC(minNCrossedRowsOverFindableClustersTPC.value); + customTrackCuts.SetMaxDcaXYPtDep([](float /*pt*/) { return 10000.f; }); + customTrackCuts.SetMaxDcaZ(maxDcaZ.value); + + customTrackCuts.print(); + } + + // ======================================================================== + // AXIS DEFINITIONS + // ======================================================================== + + ConfigurableAxis ptBinning{ + "ptBinning", + {0.0, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, + 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, + 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.2, 2.4, 2.6, 2.8, + 3.0, 3.5, 4.0, 4.5, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, + 12.0, 14.0, 16.0, 18.0, 20.0, 25.0, 30.0, 40.0, 50.0}, + "pT bin limits" + }; + AxisSpec ptAxis = {ptBinning, "#it{p}_{T} (GeV/#it{c})"}; + + // ======================================================================== + // HISTOGRAM REGISTRY - INCLUSIVE + PARTICLE-SPECIFIC + // ======================================================================== + + // Event counting - EXACT spectraTOF approach + ue.add("MC/GenRecoCollisions", "Generated and Reconstructed MC Collisions", HistType::kTH1D, {{10, 0.5, 10.5}}); + auto hColl = ue.get(HIST("MC/GenRecoCollisions")); + hColl->GetXaxis()->SetBinLabel(1, "Collisions generated"); + hColl->GetXaxis()->SetBinLabel(2, "Collisions reconstructed"); + + // CRITICAL: Complete event counting system + ue.add("hEventsAllGen", "All generated events", HistType::kTH1F, {{1, 0.5, 1.5}}); + ue.add("hEventsPassPhysicsSelection", "Events passing physics selection", HistType::kTH1F, {{1, 0.5, 1.5}}); + ue.add("hEventsReconstructable", "Physics-selected events with reconstruction", HistType::kTH1F, {{1, 0.5, 1.5}}); + ue.add("hEventsSelectedReco", "Selected reconstructed events", HistType::kTH1F, {{1, 0.5, 1.5}}); + + // Event loss breakdown histogram + ue.add("hEventLossBreakdown", "Event loss breakdown", HistType::kTH1D, {{4, 0.5, 4.5}}); + auto hLoss = ue.get(HIST("hEventLossBreakdown")); + hLoss->GetXaxis()->SetBinLabel(1, "Physics selected"); + hLoss->GetXaxis()->SetBinLabel(2, "Reconstructed"); + hLoss->GetXaxis()->SetBinLabel(3, "Selected"); + hLoss->GetXaxis()->SetBinLabel(4, "Final efficiency"); + + // ======================================================================== + // INCLUSIVE CHARGED PARTICLE HISTOGRAMS + // ======================================================================== + + // ALL generated primaries (before any physics selection) + ue.add("Inclusive/hPtPrimGenAll", "All generated primaries (no cuts);#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + + // Generated primaries AFTER physics selection + ue.add("Inclusive/hPtPrimGen", "Generated primaries (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + + // Tracking Efficiency + ue.add("Inclusive/hPtNumEff", "Tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtDenEff", "Tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + + // Primary Fraction + ue.add("Inclusive/hPtAllReco", "All reconstructed tracks;#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtPrimReco", "Reconstructed primaries;#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtSecReco", "Reconstructed secondaries;#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + + // Measured spectra + ue.add("Inclusive/hPtMeasured", "All measured tracks;#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + + // ======================================================================== + // PARTICLE-SPECIFIC HISTOGRAMS (Pions, Kaons, Protons) + // ======================================================================== + + const std::array particleNames = {"Pion", "Kaon", "Proton"}; + const std::array particleSymbols = {"#pi^{#pm}", "K^{#pm}", "p+#bar{p}"}; + + for (int iSpecies = 0; iSpecies < kNSpecies; ++iSpecies) { + const auto& name = particleNames[iSpecies]; + const auto& symbol = particleSymbols[iSpecies]; + + // Generated histograms + ue.add(Form("%s/hPtPrimGenAll", name.c_str()), + Form("All generated %s (no cuts);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + ue.add(Form("%s/hPtPrimGen", name.c_str()), + Form("Generated %s (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + // Tracking efficiency + ue.add(Form("%s/hPtNumEff", name.c_str()), + Form("%s tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + ue.add(Form("%s/hPtDenEff", name.c_str()), + Form("%s tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + // Reconstructed histograms + ue.add(Form("%s/hPtAllReco", name.c_str()), + Form("All reconstructed %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + ue.add(Form("%s/hPtPrimReco", name.c_str()), + Form("Reconstructed primary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + ue.add(Form("%s/hPtSecReco", name.c_str()), + Form("Reconstructed secondary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + // Measured spectra + ue.add(Form("%s/hPtMeasured", name.c_str()), + Form("Measured %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + // PID quality histograms - TPC ONLY + if (enablePIDHistograms) { + ue.add(Form("%s/hNsigmaTPC", name.c_str()), + Form("TPC n#sigma %s;#it{p}_{T} (GeV/#it{c});n#sigma_{TPC}", symbol.c_str()), + HistType::kTH2D, {ptAxis, {200, -10, 10}}); + } + } + + // ======================================================================== + // MONITORING HISTOGRAMS + // ======================================================================== + ue.add("evsel", "Event selection", HistType::kTH1D, {{20, 0.5, 20.5}}); + auto h = ue.get(HIST("evsel")); + h->GetXaxis()->SetBinLabel(1, "Events read"); + h->GetXaxis()->SetBinLabel(2, "INEL>0"); + h->GetXaxis()->SetBinLabel(3, "INEL>1"); + h->GetXaxis()->SetBinLabel(4, "Trigger passed"); + h->GetXaxis()->SetBinLabel(5, "NoITSROFrameBorder"); + h->GetXaxis()->SetBinLabel(6, "NoSameBunchPileup"); + h->GetXaxis()->SetBinLabel(7, "IsGoodZvtxFT0vsPV"); + h->GetXaxis()->SetBinLabel(8, "IsVertexITSTPC"); + h->GetXaxis()->SetBinLabel(9, "NoTimeFrameBorder"); + h->GetXaxis()->SetBinLabel(13, "posZ passed"); + h->GetXaxis()->SetBinLabel(14, "INEL>0 (final)"); + h->GetXaxis()->SetBinLabel(15, "INEL>1 (final)"); + + ue.add("hEta", "Track eta;#eta;Counts", HistType::kTH1D, {{20, -0.8, 0.8}}); + ue.add("hPhi", "Track phi;#varphi (rad);Counts", HistType::kTH1D, {{64, 0, 2.0 * o2::constants::math::PI}}); + ue.add("hvtxZ", "Vertex Z (data);Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); + ue.add("hvtxZmc", "MC vertex Z;Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); + + LOG(info) << "Initialized multiplicitypt task with EXCLUSIVE PID for INCLUSIVE + PARTICLE-SPECIFIC (Pi, K, p) analysis"; +} + +// ======================================================================== +// DATA PROCESSING - WITH EXCLUSIVE PID +// ======================================================================== +void multiplicitypt::processData(CollisionTableData::iterator const& collision, TrackTableData const& tracks) +{ + if (!isEventSelected(collision)) { + return; + } + ue.fill(HIST("hvtxZ"), collision.posZ()); + + for (const auto& track : tracks) { + if (!passesTrackSelection(track)) { + continue; + } + + // Inclusive charged particle (always filled) + ue.fill(HIST("Inclusive/hPtMeasured"), track.pt()); + ue.fill(HIST("hEta"), track.eta()); + ue.fill(HIST("hPhi"), track.phi()); + + // Exclusive particle identification + int bestSpecies = getBestPIDHypothesis(track); + + if (bestSpecies == kPion) { + ue.fill(HIST("Pion/hPtMeasured"), track.pt()); + if (enablePIDHistograms) { + ue.fill(HIST("Pion/hNsigmaTPC"), track.pt(), track.tpcNSigmaPi()); + } + } + else if (bestSpecies == kKaon) { + ue.fill(HIST("Kaon/hPtMeasured"), track.pt()); + if (enablePIDHistograms) { + ue.fill(HIST("Kaon/hNsigmaTPC"), track.pt(), track.tpcNSigmaKa()); + } + } + else if (bestSpecies == kProton) { + ue.fill(HIST("Proton/hPtMeasured"), track.pt()); + if (enablePIDHistograms) { + ue.fill(HIST("Proton/hNsigmaTPC"), track.pt(), track.tpcNSigmaPr()); + } + } + } +} + +// ======================================================================== +// MC PROCESSING - WITH FIXED PRIMARY FRACTION CALCULATION +// ======================================================================== +void multiplicitypt::processMC(TrackTableMC const& tracks, + aod::McParticles const& particles, + CollisionTableMCTrue const& mcCollisions, + CollisionTableMC const& collisions) +{ + LOG(info) << "=== DEBUG processMC START ==="; + LOG(info) << "MC collisions: " << mcCollisions.size(); + LOG(info) << "Reconstructed collisions: " << collisions.size(); + + // ======================================================================== + // STEP 1: Identify which MC collisions are reconstructable + // ======================================================================== + + std::set reconstructableMCCollisions; + + for (const auto& mcCollision : mcCollisions) { + auto particlesInCollision = particles.sliceBy(perMCCol, mcCollision.globalIndex()); + + if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) { + continue; + } + if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) { + continue; + } + if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) { + continue; + } + + reconstructableMCCollisions.insert(mcCollision.globalIndex()); + } + + LOG(info) << "DEBUG: Physics-selected MC collisions: " << reconstructableMCCollisions.size(); + + // ======================================================================== + // STEP 2: Track reconstruction outcomes + // ======================================================================== + + std::set reconstructedMCCollisions; + std::set selectedMCCollisions; + std::set selectedCollisionIndices; + + for (const auto& collision : collisions) { + if (!collision.has_mcCollision()) { + continue; + } + + const auto& mcCollision = collision.mcCollision_as(); + int64_t mcCollId = mcCollision.globalIndex(); + + if (reconstructableMCCollisions.find(mcCollId) == reconstructableMCCollisions.end()) { + continue; + } + + reconstructedMCCollisions.insert(mcCollId); + + if (isEventSelected(collision)) { + selectedMCCollisions.insert(mcCollId); + selectedCollisionIndices.insert(collision.globalIndex()); + ue.fill(HIST("hvtxZ"), collision.posZ()); + } + } + + auto hEventsReconstructable = ue.get(HIST("hEventsReconstructable")); + auto hEventsSelectedReco = ue.get(HIST("hEventsSelectedReco")); + + hEventsReconstructable->SetBinContent(1, reconstructedMCCollisions.size()); + hEventsSelectedReco->SetBinContent(1, selectedMCCollisions.size()); + + int nReconstructableTotal = reconstructableMCCollisions.size(); + int nReconstructableWithReco = reconstructedMCCollisions.size(); + int nSelectedReco = selectedMCCollisions.size(); + + LOG(info) << "DEBUG: Reconstructed MC collisions: " << nReconstructableWithReco; + LOG(info) << "DEBUG: Selected MC collisions: " << nSelectedReco; + + if (nReconstructableTotal > 0) { + ue.fill(HIST("hEventLossBreakdown"), 1, nReconstructableTotal); + ue.fill(HIST("hEventLossBreakdown"), 2, nReconstructableWithReco); + ue.fill(HIST("hEventLossBreakdown"), 3, nSelectedReco); + ue.fill(HIST("hEventLossBreakdown"), 4, (nSelectedReco * 100.0 / nReconstructableTotal)); + } + + // ======================================================================== + // STEP 3: Process tracks with EXCLUSIVE PID - FIXED PRIMARY FRACTION + // ======================================================================== + + int totalTracksProcessed = 0; + int tracksFromSelectedEvents = 0; + int tracksPassingSelection = 0; + + std::array particleTracksIdentified = {0}; + std::array particleTracksPrimary = {0}; + std::array particleTracksSecondary = {0}; + + for (const auto& track : tracks) { + totalTracksProcessed++; + + if (!track.has_collision()) continue; + + const auto& collision = track.collision_as(); + + if (selectedCollisionIndices.find(collision.globalIndex()) == selectedCollisionIndices.end()) { + continue; + } + tracksFromSelectedEvents++; + + if (!passesTrackSelection(track)) continue; + tracksPassingSelection++; + + // ======================================================================== + // INCLUSIVE CHARGED PARTICLE ANALYSIS + // ======================================================================== + + ue.fill(HIST("Inclusive/hPtMeasured"), track.pt()); + ue.fill(HIST("Inclusive/hPtAllReco"), track.pt()); + ue.fill(HIST("hEta"), track.eta()); + ue.fill(HIST("hPhi"), track.phi()); + + // ======================================================================== + // EFFICIENCY NUMERATOR: Fill based on TRUE particle type + // ======================================================================== + + if (track.has_mcParticle()) { + const auto& particle = track.mcParticle(); + int pdgCode = std::abs(particle.pdgCode()); + + if (particle.isPhysicalPrimary()) { + ue.fill(HIST("Inclusive/hPtNumEff"), particle.pt()); + ue.fill(HIST("Inclusive/hPtPrimReco"), track.pt()); + + // Fill particle-specific efficiency numerator based on TRUE type + if (pdgCode == PDGPion) { + ue.fill(HIST("Pion/hPtNumEff"), particle.pt()); + } + if (pdgCode == PDGKaon) { + ue.fill(HIST("Kaon/hPtNumEff"), particle.pt()); + } + if (pdgCode == PDGProton) { + ue.fill(HIST("Proton/hPtNumEff"), particle.pt()); + } + } else { + ue.fill(HIST("Inclusive/hPtSecReco"), track.pt()); + } + } + + // ======================================================================== + // EXCLUSIVE PID - FIXED PRIMARY FRACTION LOGIC + // ======================================================================== + + int bestSpecies = getBestPIDHypothesis(track); + + // ======================================================================== + // PION CHANNEL + // ======================================================================== + if (bestSpecies == kPion) { + ue.fill(HIST("Pion/hPtMeasured"), track.pt()); + ue.fill(HIST("Pion/hPtAllReco"), track.pt()); + particleTracksIdentified[kPion]++; + + if (enablePIDHistograms) { + ue.fill(HIST("Pion/hNsigmaTPC"), track.pt(), track.tpcNSigmaPi()); + } + + if (track.has_mcParticle()) { + const auto& particle = track.mcParticle(); + + // KEY FIX: Primary fraction = fraction of identified pions that are primary + // This includes correctly identified pions AND misidentified kaons/protons + // that happen to be primary particles + if (particle.isPhysicalPrimary()) { + ue.fill(HIST("Pion/hPtPrimReco"), track.pt()); + particleTracksPrimary[kPion]++; + } else { + ue.fill(HIST("Pion/hPtSecReco"), track.pt()); + particleTracksSecondary[kPion]++; + } + } + } + + // ======================================================================== + // KAON CHANNEL + // ======================================================================== + else if (bestSpecies == kKaon) { + ue.fill(HIST("Kaon/hPtMeasured"), track.pt()); + ue.fill(HIST("Kaon/hPtAllReco"), track.pt()); + particleTracksIdentified[kKaon]++; + + if (enablePIDHistograms) { + ue.fill(HIST("Kaon/hNsigmaTPC"), track.pt(), track.tpcNSigmaKa()); + } + + if (track.has_mcParticle()) { + const auto& particle = track.mcParticle(); + + // KEY FIX: Primary fraction of identified kaons + // A misidentified pion that is primary still contributes to primary fraction + if (particle.isPhysicalPrimary()) { + ue.fill(HIST("Kaon/hPtPrimReco"), track.pt()); + particleTracksPrimary[kKaon]++; + } else { + ue.fill(HIST("Kaon/hPtSecReco"), track.pt()); + particleTracksSecondary[kKaon]++; + } + } + } + + // ======================================================================== + // PROTON CHANNEL + // ======================================================================== + else if (bestSpecies == kProton) { + ue.fill(HIST("Proton/hPtMeasured"), track.pt()); + ue.fill(HIST("Proton/hPtAllReco"), track.pt()); + particleTracksIdentified[kProton]++; + + if (enablePIDHistograms) { + ue.fill(HIST("Proton/hNsigmaTPC"), track.pt(), track.tpcNSigmaPr()); + } + + if (track.has_mcParticle()) { + const auto& particle = track.mcParticle(); + + // KEY FIX: Primary fraction of identified protons + if (particle.isPhysicalPrimary()) { + ue.fill(HIST("Proton/hPtPrimReco"), track.pt()); + particleTracksPrimary[kProton]++; + } else { + ue.fill(HIST("Proton/hPtSecReco"), track.pt()); + particleTracksSecondary[kProton]++; + } + } + } + } + + LOG(info) << "=== DEBUG TRACK COUNTING ==="; + LOG(info) << "Total tracks processed: " << totalTracksProcessed; + LOG(info) << "Tracks from selected events: " << tracksFromSelectedEvents; + LOG(info) << "Tracks passing selection: " << tracksPassingSelection; + LOG(info) << "Pions identified: " << particleTracksIdentified[kPion] + << ", primary: " << particleTracksPrimary[kPion] + << ", secondary: " << particleTracksSecondary[kPion]; + LOG(info) << "Kaons identified: " << particleTracksIdentified[kKaon] + << ", primary: " << particleTracksPrimary[kKaon] + << ", secondary: " << particleTracksSecondary[kKaon]; + LOG(info) << "Protons identified: " << particleTracksIdentified[kProton] + << ", primary: " << particleTracksPrimary[kProton] + << ", secondary: " << particleTracksSecondary[kProton]; + + // Calculate and log primary fractions + if (particleTracksIdentified[kPion] > 0) { + float pionPrimFrac = (float)particleTracksPrimary[kPion] / particleTracksIdentified[kPion]; + LOG(info) << "Pion primary fraction: " << pionPrimFrac * 100.0 << "%"; + } + if (particleTracksIdentified[kKaon] > 0) { + float kaonPrimFrac = (float)particleTracksPrimary[kKaon] / particleTracksIdentified[kKaon]; + LOG(info) << "Kaon primary fraction: " << kaonPrimFrac * 100.0 << "%"; + } + if (particleTracksIdentified[kProton] > 0) { + float protonPrimFrac = (float)particleTracksPrimary[kProton] / particleTracksIdentified[kProton]; + LOG(info) << "Proton primary fraction: " << protonPrimFrac * 100.0 << "%"; + } + + LOG(info) << "=== DEBUG processMC END ==="; +} + + +// ======================================================================== +// TRUE MC PROCESSING - WITH PARTICLE-SPECIFIC SIGNAL LOSS +// ======================================================================== +void multiplicitypt::processTrue(CollisionTableMCTrue const& mcCollisions, + ParticleTableMC const& particles) +{ + LOG(info) << "=== DEBUG processTrue START ==="; + LOG(info) << "Number of MC collisions: " << mcCollisions.size(); + + int nPassPhysicsSelection = 0; + int nParticlesFilledAll = 0; + int nParticlesFilledAfterPS = 0; + + std::array particleCountAll = {0}; + std::array particleCountAfterPS = {0}; + + for (const auto& mcCollision : mcCollisions) { + // Count EVERY generated event + ue.fill(HIST("hEventsAllGen"), 1.0); + + ue.fill(HIST("hvtxZmc"), mcCollision.posZ()); + auto particlesInCollision = particles.sliceBy(perMCCol, mcCollision.globalIndex()); + + // ======================================================================== + // Fill ALL generated primaries BEFORE physics selection + // ======================================================================== + for (const auto& particle : particlesInCollision) { + if (isGoodPrimary(particle)) { + ue.fill(HIST("Inclusive/hPtPrimGenAll"), particle.pt()); + nParticlesFilledAll++; + } + + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Pion/hPtPrimGenAll"), particle.pt()); + particleCountAll[kPion]++; + } + + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Kaon/hPtPrimGenAll"), particle.pt()); + particleCountAll[kKaon]++; + } + + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Proton/hPtPrimGenAll"), particle.pt()); + particleCountAll[kProton]++; + } + } + + // ======================================================================== + // Apply physics selection + // ======================================================================== + if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) continue; + + if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) continue; + if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) continue; + + // Count physics-selected events + ue.fill(HIST("hEventsPassPhysicsSelection"), 1.0); + nPassPhysicsSelection++; + + // Fill primaries AFTER physics selection + for (const auto& particle : particlesInCollision) { + if (isGoodPrimary(particle)) { + ue.fill(HIST("Inclusive/hPtDenEff"), particle.pt()); + ue.fill(HIST("Inclusive/hPtPrimGen"), particle.pt()); + nParticlesFilledAfterPS++; + } + + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Pion/hPtDenEff"), particle.pt()); + ue.fill(HIST("Pion/hPtPrimGen"), particle.pt()); + particleCountAfterPS[kPion]++; + } + + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Kaon/hPtDenEff"), particle.pt()); + ue.fill(HIST("Kaon/hPtPrimGen"), particle.pt()); + particleCountAfterPS[kKaon]++; + } + + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Proton/hPtDenEff"), particle.pt()); + ue.fill(HIST("Proton/hPtPrimGen"), particle.pt()); + particleCountAfterPS[kProton]++; + } + } + } + + LOG(info) << "=== DEBUG processTrue END ==="; + LOG(info) << "All generated events: " << mcCollisions.size(); + LOG(info) << "Passing physics selection: " << nPassPhysicsSelection; + LOG(info) << "Total primaries (before PS): " << nParticlesFilledAll; + LOG(info) << "Total primaries (after PS): " << nParticlesFilledAfterPS; + + LOG(info) << "=== PARTICLE-SPECIFIC STATISTICS ==="; + LOG(info) << "Pions - All: " << particleCountAll[kPion] + << ", After PS: " << particleCountAfterPS[kPion]; + LOG(info) << "Kaons - All: " << particleCountAll[kKaon] + << ", After PS: " << particleCountAfterPS[kKaon]; + LOG(info) << "Protons - All: " << particleCountAll[kProton] + << ", After PS: " << particleCountAfterPS[kProton]; +} From aa85c935246f68365973e31159d05f28833b0806 Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Tue, 20 Jan 2026 11:33:30 -0600 Subject: [PATCH 02/16] =?UTF-8?q?=E2=80=9CCOMENTARIO=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PWGLF/Tasks/Nuspex/multiplicitypt.cxx | 330 ++++++++++++++------------ 1 file changed, 178 insertions(+), 152 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/multiplicitypt.cxx b/PWGLF/Tasks/Nuspex/multiplicitypt.cxx index d3d64510028..dee716b05f5 100644 --- a/PWGLF/Tasks/Nuspex/multiplicitypt.cxx +++ b/PWGLF/Tasks/Nuspex/multiplicitypt.cxx @@ -1,12 +1,25 @@ +// 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. + +#include "PWGLF/Utils/inelGt.h" + #include "Common/Core/TrackSelection.h" #include "Common/Core/TrackSelectionDefaults.h" +#include "Common/DataModel/Centrality.h" #include "Common/DataModel/EventSelection.h" #include "Common/DataModel/Multiplicity.h" -#include "Common/DataModel/TrackSelectionTables.h" -#include "Common/DataModel/Centrality.h" #include "Common/DataModel/PIDResponse.h" -#include "Common/DataModel/PIDResponseTPC.h" #include "Common/DataModel/PIDResponseTOF.h" +#include "Common/DataModel/PIDResponseTPC.h" +#include "Common/DataModel/TrackSelectionTables.h" #include "Framework/ASoAHelpers.h" #include "Framework/AnalysisDataModel.h" @@ -16,16 +29,15 @@ #include "Framework/StaticFor.h" #include "Framework/runDataProcessing.h" #include "ReconstructionDataFormats/Track.h" -#include "PWGLF/Utils/inelGt.h" #include "TDatabasePDG.h" #include #include #include #include -#include #include +#include #include using namespace o2; @@ -39,22 +51,15 @@ struct multiplicitypt { // Service Service pdg; - // Configurables - Matching spectraTOF approach Configurable isRun3{"isRun3", true, "is Run3 dataset"}; - - // Event selection configurables (spectraTOF style) Configurable cfgCutVertex{"cfgCutVertex", 10.0f, "Accepted z-vertex range"}; Configurable cfgINELCut{"cfgINELCut", 0, "INEL event selection: 0 no sel, 1 INEL>0, 2 INEL>1"}; Configurable askForCustomTVX{"askForCustomTVX", false, "Ask for custom TVX rather than sel8"}; - - // SET ALL THESE TO FALSE LIKE SPECTRATOF: Configurable removeITSROFrameBorder{"removeITSROFrameBorder", false, "Remove ITS Read-Out Frame border"}; Configurable removeNoSameBunchPileup{"removeNoSameBunchPileup", false, "Remove no same bunch pileup"}; Configurable requireIsGoodZvtxFT0vsPV{"requireIsGoodZvtxFT0vsPV", false, "Require good Z vertex FT0 vs PV"}; Configurable requireIsVertexITSTPC{"requireIsVertexITSTPC", false, "Require vertex ITSTPC"}; Configurable removeNoTimeFrameBorder{"removeNoTimeFrameBorder", false, "Remove no time frame border"}; - - // Track selection configurables (spectraTOF style) Configurable cfgCutEtaMax{"cfgCutEtaMax", 0.8f, "Max eta range for tracks"}; Configurable cfgCutEtaMin{"cfgCutEtaMin", -0.8f, "Min eta range for tracks"}; Configurable cfgCutY{"cfgCutY", 0.5f, "Y range for tracks"}; @@ -62,16 +67,12 @@ struct multiplicitypt { Configurable lastRequiredTrdCluster{"lastRequiredTrdCluster", -1, "Last cluster to require in TRD"}; Configurable requireTrdOnly{"requireTrdOnly", false, "Require only tracks from TRD"}; Configurable requireNoTrd{"requireNoTrd", false, "Require tracks without TRD"}; - - // Multiplicity estimator (like spectraTOF) Configurable multiplicityEstimator{"multiplicityEstimator", 6, - "Multiplicity estimator: 0=NoMult, 1=MultFV0M, 2=MultFT0M, 3=MultFDDM, 4=MultTracklets, 5=MultTPC, 6=MultNTracksPV, 7=MultNTracksPVeta1, 8=CentFT0C, 9=CentFT0M, 10=CentFV0A"}; + "Multiplicity estimator: 0=NoMult, 1=MultFV0M, 2=MultFT0M, 3=MultFDDM, 4=MultTracklets, 5=MultTPC, 6=MultNTracksPV, 7=MultNTracksPVeta1, 8=CentFT0C, 9=CentFT0M, 10=CentFV0A"}; // Analysis switches Configurable enableDCAHistograms{"enableDCAHistograms", false, "Enable DCA histograms"}; Configurable enablePIDHistograms{"enablePIDHistograms", true, "Enable PID histograms"}; - - // Track cut configurables (spectraTOF style) Configurable useCustomTrackCuts{"useCustomTrackCuts", true, "Flag to use custom track cuts"}; Configurable itsPattern{"itsPattern", 0, "0 = Run3ITSibAny, 1 = Run3ITSallAny, 2 = Run3ITSall7Layers, 3 = Run3ITSibTwo"}; Configurable requireITS{"requireITS", true, "Additional cut on the ITS requirement"}; @@ -145,7 +146,7 @@ struct multiplicitypt { // ======================================================================== // PROCESS FUNCTION DECLARATIONS - SPECTRATOF STYLE // ======================================================================== - + // Data processing void processData(CollisionTableData::iterator const& collision, TrackTableData const& tracks); @@ -166,7 +167,7 @@ struct multiplicitypt { // ======================================================================== // TRACK SELECTION FUNCTIONS - MATCHING spectraTOF // ======================================================================== - + template bool passesCutWoDCA(TrackType const& track) const { @@ -219,12 +220,12 @@ struct multiplicitypt { // ======================================================================== // PID SELECTION FUNCTIONS - TPC ONLY (OLD NON-EXCLUSIVE METHOD) // ======================================================================== - + template bool passesPIDSelection(TrackType const& track) const { float nsigmaTPC = 0.f; - + if constexpr (species == kPion) { nsigmaTPC = track.tpcNSigmaPi(); } else if constexpr (species == kKaon) { @@ -232,7 +233,7 @@ struct multiplicitypt { } else if constexpr (species == kProton) { nsigmaTPC = track.tpcNSigmaPr(); } - + // TPC-only PID (works for all pT, but better at low pT < 1 GeV/c) return (std::abs(nsigmaTPC) < cfgCutNsigma.value); } @@ -240,20 +241,20 @@ struct multiplicitypt { // ======================================================================== // EXCLUSIVE PID SELECTION - Returns best hypothesis for a track // ======================================================================== - + template int getBestPIDHypothesis(TrackType const& track) const { // Return values: -1 = no ID, 0 = pion, 1 = kaon, 2 = proton - + float nsigmaPi = std::abs(track.tpcNSigmaPi()); float nsigmaKa = std::abs(track.tpcNSigmaKa()); float nsigmaPr = std::abs(track.tpcNSigmaPr()); - + // Find the hypothesis with smallest |nσ| that passes the cut float minNSigma = 999.0f; int bestSpecies = -1; - + if (nsigmaPi < cfgCutNsigma.value && nsigmaPi < minNSigma) { minNSigma = nsigmaPi; bestSpecies = kPion; @@ -266,56 +267,76 @@ struct multiplicitypt { minNSigma = nsigmaPr; bestSpecies = kProton; } - + return bestSpecies; } // ======================================================================== // EVENT SELECTION FUNCTION - EXACT spectraTOF // ======================================================================== - + template bool isEventSelected(CollisionType const& collision) { if constexpr (fillHistograms) { ue.fill(HIST("evsel"), 1.f); - if (collision.isInelGt0()) ue.fill(HIST("evsel"), 2.f); - if (collision.isInelGt1()) ue.fill(HIST("evsel"), 3.f); + if (collision.isInelGt0()) + ue.fill(HIST("evsel"), 2.f); + if (collision.isInelGt1()) + ue.fill(HIST("evsel"), 3.f); } if (askForCustomTVX.value) { - if (!collision.selection_bit(aod::evsel::kIsTriggerTVX)) return false; + if (!collision.selection_bit(aod::evsel::kIsTriggerTVX)) + return false; } else { - if (!collision.sel8()) return false; + if (!collision.sel8()) + return false; } - if constexpr (fillHistograms) ue.fill(HIST("evsel"), 4.f); + if constexpr (fillHistograms) + ue.fill(HIST("evsel"), 4.f); - if (removeITSROFrameBorder.value && !collision.selection_bit(aod::evsel::kNoITSROFrameBorder)) return false; - if constexpr (fillHistograms) ue.fill(HIST("evsel"), 5.f); + if (removeITSROFrameBorder.value && !collision.selection_bit(aod::evsel::kNoITSROFrameBorder)) + return false; + if constexpr (fillHistograms) + ue.fill(HIST("evsel"), 5.f); - if (removeNoSameBunchPileup.value && !collision.selection_bit(aod::evsel::kNoSameBunchPileup)) return false; - if constexpr (fillHistograms) ue.fill(HIST("evsel"), 6.f); + if (removeNoSameBunchPileup.value && !collision.selection_bit(aod::evsel::kNoSameBunchPileup)) + return false; + if constexpr (fillHistograms) + ue.fill(HIST("evsel"), 6.f); - if (requireIsGoodZvtxFT0vsPV.value && !collision.selection_bit(aod::evsel::kIsGoodZvtxFT0vsPV)) return false; - if constexpr (fillHistograms) ue.fill(HIST("evsel"), 7.f); + if (requireIsGoodZvtxFT0vsPV.value && !collision.selection_bit(aod::evsel::kIsGoodZvtxFT0vsPV)) + return false; + if constexpr (fillHistograms) + ue.fill(HIST("evsel"), 7.f); - if (requireIsVertexITSTPC.value && !collision.selection_bit(aod::evsel::kIsVertexITSTPC)) return false; - if constexpr (fillHistograms) ue.fill(HIST("evsel"), 8.f); + if (requireIsVertexITSTPC.value && !collision.selection_bit(aod::evsel::kIsVertexITSTPC)) + return false; + if constexpr (fillHistograms) + ue.fill(HIST("evsel"), 8.f); - if (removeNoTimeFrameBorder.value && !collision.selection_bit(aod::evsel::kNoTimeFrameBorder)) return false; - if constexpr (fillHistograms) ue.fill(HIST("evsel"), 9.f); + if (removeNoTimeFrameBorder.value && !collision.selection_bit(aod::evsel::kNoTimeFrameBorder)) + return false; + if constexpr (fillHistograms) + ue.fill(HIST("evsel"), 9.f); - if (std::abs(collision.posZ()) > cfgCutVertex.value) return false; + if (std::abs(collision.posZ()) > cfgCutVertex.value) + return false; if constexpr (fillHistograms) { ue.fill(HIST("evsel"), 13.f); - if (collision.isInelGt0()) ue.fill(HIST("evsel"), 14.f); - if (collision.isInelGt1()) ue.fill(HIST("evsel"), 15.f); + if (collision.isInelGt0()) + ue.fill(HIST("evsel"), 14.f); + if (collision.isInelGt1()) + ue.fill(HIST("evsel"), 15.f); } - if (cfgINELCut.value == 1 && !collision.isInelGt0()) return false; - if (cfgINELCut.value == 2 && !collision.isInelGt1()) return false; + if (cfgINELCut.value == 1 && !collision.isInelGt0()) + return false; + if (cfgINELCut.value == 2 && !collision.isInelGt1()) + return false; return true; } @@ -329,19 +350,19 @@ struct multiplicitypt { { auto pdgParticle = pdg->GetParticle(particle.pdgCode()); if (!pdgParticle || pdgParticle->Charge() == 0.) - return false; - + return false; + if (!particle.isPhysicalPrimary()) - return false; - + return false; + if (std::abs(particle.eta()) >= cfgCutEtaMax.value) - return false; + return false; if (particle.pt() < cfgTrkLowPtCut.value) - return false; - + return false; + if (std::abs(particle.y()) > cfgCutY.value) - return false; - + return false; + return true; } @@ -351,13 +372,17 @@ struct multiplicitypt { { int pdgCode = std::abs(particle.pdgCode()); int expectedPDG = 0; - - if constexpr (species == kPion) expectedPDG = PDGPion; - else if constexpr (species == kKaon) expectedPDG = PDGKaon; - else if constexpr (species == kProton) expectedPDG = PDGProton; - - if (pdgCode != expectedPDG) return false; - + + if constexpr (species == kPion) + expectedPDG = PDGPion; + else if constexpr (species == kKaon) + expectedPDG = PDGKaon; + else if constexpr (species == kProton) + expectedPDG = PDGProton; + + if (pdgCode != expectedPDG) + return false; + return isGoodPrimary(particle); } @@ -374,11 +399,11 @@ void multiplicitypt::init(InitContext const&) // ======================================================================== // CUSTOM TRACK CUTS INITIALIZATION - MATCHING spectraTOF // ======================================================================== - + if (useCustomTrackCuts.value) { LOG(info) << "Using custom track cuts matching spectraTOF approach"; customTrackCuts = getGlobalTrackSelectionRun3ITSMatch(itsPattern.value); - + customTrackCuts.SetRequireITSRefit(requireITS.value); customTrackCuts.SetRequireTPCRefit(requireTPC.value); customTrackCuts.SetMinNClustersITS(min_ITS_nClusters.value); @@ -390,14 +415,14 @@ void multiplicitypt::init(InitContext const&) customTrackCuts.SetMinNCrossedRowsOverFindableClustersTPC(minNCrossedRowsOverFindableClustersTPC.value); customTrackCuts.SetMaxDcaXYPtDep([](float /*pt*/) { return 10000.f; }); customTrackCuts.SetMaxDcaZ(maxDcaZ.value); - + customTrackCuts.print(); } // ======================================================================== // AXIS DEFINITIONS // ======================================================================== - + ConfigurableAxis ptBinning{ "ptBinning", {0.0, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, @@ -405,14 +430,13 @@ void multiplicitypt::init(InitContext const&) 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.5, 4.0, 4.5, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 25.0, 30.0, 40.0, 50.0}, - "pT bin limits" - }; + "pT bin limits"}; AxisSpec ptAxis = {ptBinning, "#it{p}_{T} (GeV/#it{c})"}; // ======================================================================== // HISTOGRAM REGISTRY - INCLUSIVE + PARTICLE-SPECIFIC // ======================================================================== - + // Event counting - EXACT spectraTOF approach ue.add("MC/GenRecoCollisions", "Generated and Reconstructed MC Collisions", HistType::kTH1D, {{10, 0.5, 10.5}}); auto hColl = ue.get(HIST("MC/GenRecoCollisions")); @@ -436,11 +460,11 @@ void multiplicitypt::init(InitContext const&) // ======================================================================== // INCLUSIVE CHARGED PARTICLE HISTOGRAMS // ======================================================================== - + // ALL generated primaries (before any physics selection) ue.add("Inclusive/hPtPrimGenAll", "All generated primaries (no cuts);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - + // Generated primaries AFTER physics selection ue.add("Inclusive/hPtPrimGen", "Generated primaries (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); @@ -466,50 +490,50 @@ void multiplicitypt::init(InitContext const&) // ======================================================================== // PARTICLE-SPECIFIC HISTOGRAMS (Pions, Kaons, Protons) // ======================================================================== - + const std::array particleNames = {"Pion", "Kaon", "Proton"}; const std::array particleSymbols = {"#pi^{#pm}", "K^{#pm}", "p+#bar{p}"}; - + for (int iSpecies = 0; iSpecies < kNSpecies; ++iSpecies) { const auto& name = particleNames[iSpecies]; const auto& symbol = particleSymbols[iSpecies]; - + // Generated histograms ue.add(Form("%s/hPtPrimGenAll", name.c_str()), Form("All generated %s (no cuts);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - + ue.add(Form("%s/hPtPrimGen", name.c_str()), Form("Generated %s (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - + // Tracking efficiency ue.add(Form("%s/hPtNumEff", name.c_str()), Form("%s tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - + ue.add(Form("%s/hPtDenEff", name.c_str()), Form("%s tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - + // Reconstructed histograms ue.add(Form("%s/hPtAllReco", name.c_str()), Form("All reconstructed %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - + ue.add(Form("%s/hPtPrimReco", name.c_str()), Form("Reconstructed primary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - + ue.add(Form("%s/hPtSecReco", name.c_str()), Form("Reconstructed secondary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - + // Measured spectra ue.add(Form("%s/hPtMeasured", name.c_str()), Form("Measured %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - + // PID quality histograms - TPC ONLY if (enablePIDHistograms) { ue.add(Form("%s/hNsigmaTPC", name.c_str()), @@ -517,7 +541,7 @@ void multiplicitypt::init(InitContext const&) HistType::kTH2D, {ptAxis, {200, -10, 10}}); } } - + // ======================================================================== // MONITORING HISTOGRAMS // ======================================================================== @@ -535,12 +559,12 @@ void multiplicitypt::init(InitContext const&) h->GetXaxis()->SetBinLabel(13, "posZ passed"); h->GetXaxis()->SetBinLabel(14, "INEL>0 (final)"); h->GetXaxis()->SetBinLabel(15, "INEL>1 (final)"); - + ue.add("hEta", "Track eta;#eta;Counts", HistType::kTH1D, {{20, -0.8, 0.8}}); ue.add("hPhi", "Track phi;#varphi (rad);Counts", HistType::kTH1D, {{64, 0, 2.0 * o2::constants::math::PI}}); ue.add("hvtxZ", "Vertex Z (data);Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); ue.add("hvtxZmc", "MC vertex Z;Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); - + LOG(info) << "Initialized multiplicitypt task with EXCLUSIVE PID for INCLUSIVE + PARTICLE-SPECIFIC (Pi, K, p) analysis"; } @@ -553,12 +577,12 @@ void multiplicitypt::processData(CollisionTableData::iterator const& collision, return; } ue.fill(HIST("hvtxZ"), collision.posZ()); - + for (const auto& track : tracks) { if (!passesTrackSelection(track)) { continue; } - + // Inclusive charged particle (always filled) ue.fill(HIST("Inclusive/hPtMeasured"), track.pt()); ue.fill(HIST("hEta"), track.eta()); @@ -566,20 +590,18 @@ void multiplicitypt::processData(CollisionTableData::iterator const& collision, // Exclusive particle identification int bestSpecies = getBestPIDHypothesis(track); - + if (bestSpecies == kPion) { ue.fill(HIST("Pion/hPtMeasured"), track.pt()); if (enablePIDHistograms) { ue.fill(HIST("Pion/hNsigmaTPC"), track.pt(), track.tpcNSigmaPi()); } - } - else if (bestSpecies == kKaon) { + } else if (bestSpecies == kKaon) { ue.fill(HIST("Kaon/hPtMeasured"), track.pt()); if (enablePIDHistograms) { ue.fill(HIST("Kaon/hNsigmaTPC"), track.pt(), track.tpcNSigmaKa()); } - } - else if (bestSpecies == kProton) { + } else if (bestSpecies == kProton) { ue.fill(HIST("Proton/hPtMeasured"), track.pt()); if (enablePIDHistograms) { ue.fill(HIST("Proton/hNsigmaTPC"), track.pt(), track.tpcNSigmaPr()); @@ -592,14 +614,14 @@ void multiplicitypt::processData(CollisionTableData::iterator const& collision, // MC PROCESSING - WITH FIXED PRIMARY FRACTION CALCULATION // ======================================================================== void multiplicitypt::processMC(TrackTableMC const& tracks, - aod::McParticles const& particles, - CollisionTableMCTrue const& mcCollisions, - CollisionTableMC const& collisions) + aod::McParticles const& particles, + CollisionTableMCTrue const& mcCollisions, + CollisionTableMC const& collisions) { LOG(info) << "=== DEBUG processMC START ==="; LOG(info) << "MC collisions: " << mcCollisions.size(); LOG(info) << "Reconstructed collisions: " << collisions.size(); - + // ======================================================================== // STEP 1: Identify which MC collisions are reconstructable // ======================================================================== @@ -608,7 +630,7 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, for (const auto& mcCollision : mcCollisions) { auto particlesInCollision = particles.sliceBy(perMCCol, mcCollision.globalIndex()); - + if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) { continue; } @@ -618,7 +640,7 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) { continue; } - + reconstructableMCCollisions.insert(mcCollision.globalIndex()); } @@ -636,16 +658,16 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, if (!collision.has_mcCollision()) { continue; } - + const auto& mcCollision = collision.mcCollision_as(); int64_t mcCollId = mcCollision.globalIndex(); - + if (reconstructableMCCollisions.find(mcCollId) == reconstructableMCCollisions.end()) { continue; } - + reconstructedMCCollisions.insert(mcCollId); - + if (isEventSelected(collision)) { selectedMCCollisions.insert(mcCollId); selectedCollisionIndices.insert(collision.globalIndex()); @@ -687,40 +709,42 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, for (const auto& track : tracks) { totalTracksProcessed++; - - if (!track.has_collision()) continue; - + + if (!track.has_collision()) + continue; + const auto& collision = track.collision_as(); - + if (selectedCollisionIndices.find(collision.globalIndex()) == selectedCollisionIndices.end()) { continue; } tracksFromSelectedEvents++; - - if (!passesTrackSelection(track)) continue; + + if (!passesTrackSelection(track)) + continue; tracksPassingSelection++; - + // ======================================================================== // INCLUSIVE CHARGED PARTICLE ANALYSIS // ======================================================================== - + ue.fill(HIST("Inclusive/hPtMeasured"), track.pt()); ue.fill(HIST("Inclusive/hPtAllReco"), track.pt()); ue.fill(HIST("hEta"), track.eta()); ue.fill(HIST("hPhi"), track.phi()); - + // ======================================================================== // EFFICIENCY NUMERATOR: Fill based on TRUE particle type // ======================================================================== - + if (track.has_mcParticle()) { const auto& particle = track.mcParticle(); int pdgCode = std::abs(particle.pdgCode()); - + if (particle.isPhysicalPrimary()) { ue.fill(HIST("Inclusive/hPtNumEff"), particle.pt()); ue.fill(HIST("Inclusive/hPtPrimReco"), track.pt()); - + // Fill particle-specific efficiency numerator based on TRUE type if (pdgCode == PDGPion) { ue.fill(HIST("Pion/hPtNumEff"), particle.pt()); @@ -735,13 +759,13 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, ue.fill(HIST("Inclusive/hPtSecReco"), track.pt()); } } - + // ======================================================================== // EXCLUSIVE PID - FIXED PRIMARY FRACTION LOGIC // ======================================================================== - + int bestSpecies = getBestPIDHypothesis(track); - + // ======================================================================== // PION CHANNEL // ======================================================================== @@ -749,14 +773,14 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, ue.fill(HIST("Pion/hPtMeasured"), track.pt()); ue.fill(HIST("Pion/hPtAllReco"), track.pt()); particleTracksIdentified[kPion]++; - + if (enablePIDHistograms) { ue.fill(HIST("Pion/hNsigmaTPC"), track.pt(), track.tpcNSigmaPi()); } - + if (track.has_mcParticle()) { const auto& particle = track.mcParticle(); - + // KEY FIX: Primary fraction = fraction of identified pions that are primary // This includes correctly identified pions AND misidentified kaons/protons // that happen to be primary particles @@ -769,7 +793,7 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, } } } - + // ======================================================================== // KAON CHANNEL // ======================================================================== @@ -777,14 +801,14 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, ue.fill(HIST("Kaon/hPtMeasured"), track.pt()); ue.fill(HIST("Kaon/hPtAllReco"), track.pt()); particleTracksIdentified[kKaon]++; - + if (enablePIDHistograms) { ue.fill(HIST("Kaon/hNsigmaTPC"), track.pt(), track.tpcNSigmaKa()); } - + if (track.has_mcParticle()) { const auto& particle = track.mcParticle(); - + // KEY FIX: Primary fraction of identified kaons // A misidentified pion that is primary still contributes to primary fraction if (particle.isPhysicalPrimary()) { @@ -796,7 +820,7 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, } } } - + // ======================================================================== // PROTON CHANNEL // ======================================================================== @@ -804,14 +828,14 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, ue.fill(HIST("Proton/hPtMeasured"), track.pt()); ue.fill(HIST("Proton/hPtAllReco"), track.pt()); particleTracksIdentified[kProton]++; - + if (enablePIDHistograms) { ue.fill(HIST("Proton/hNsigmaTPC"), track.pt(), track.tpcNSigmaPr()); } - + if (track.has_mcParticle()) { const auto& particle = track.mcParticle(); - + // KEY FIX: Primary fraction of identified protons if (particle.isPhysicalPrimary()) { ue.fill(HIST("Proton/hPtPrimReco"), track.pt()); @@ -823,7 +847,7 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, } } } - + LOG(info) << "=== DEBUG TRACK COUNTING ==="; LOG(info) << "Total tracks processed: " << totalTracksProcessed; LOG(info) << "Tracks from selected events: " << tracksFromSelectedEvents; @@ -837,7 +861,7 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, LOG(info) << "Protons identified: " << particleTracksIdentified[kProton] << ", primary: " << particleTracksPrimary[kProton] << ", secondary: " << particleTracksSecondary[kProton]; - + // Calculate and log primary fractions if (particleTracksIdentified[kPion] > 0) { float pionPrimFrac = (float)particleTracksPrimary[kPion] / particleTracksIdentified[kPion]; @@ -851,20 +875,19 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, float protonPrimFrac = (float)particleTracksPrimary[kProton] / particleTracksIdentified[kProton]; LOG(info) << "Proton primary fraction: " << protonPrimFrac * 100.0 << "%"; } - + LOG(info) << "=== DEBUG processMC END ==="; } - // ======================================================================== // TRUE MC PROCESSING - WITH PARTICLE-SPECIFIC SIGNAL LOSS // ======================================================================== void multiplicitypt::processTrue(CollisionTableMCTrue const& mcCollisions, - ParticleTableMC const& particles) + ParticleTableMC const& particles) { LOG(info) << "=== DEBUG processTrue START ==="; LOG(info) << "Number of MC collisions: " << mcCollisions.size(); - + int nPassPhysicsSelection = 0; int nParticlesFilledAll = 0; int nParticlesFilledAfterPS = 0; @@ -875,10 +898,10 @@ void multiplicitypt::processTrue(CollisionTableMCTrue const& mcCollisions, for (const auto& mcCollision : mcCollisions) { // Count EVERY generated event ue.fill(HIST("hEventsAllGen"), 1.0); - + ue.fill(HIST("hvtxZmc"), mcCollision.posZ()); auto particlesInCollision = particles.sliceBy(perMCCol, mcCollision.globalIndex()); - + // ======================================================================== // Fill ALL generated primaries BEFORE physics selection // ======================================================================== @@ -887,35 +910,38 @@ void multiplicitypt::processTrue(CollisionTableMCTrue const& mcCollisions, ue.fill(HIST("Inclusive/hPtPrimGenAll"), particle.pt()); nParticlesFilledAll++; } - + if (isGoodPrimarySpecies(particle)) { ue.fill(HIST("Pion/hPtPrimGenAll"), particle.pt()); particleCountAll[kPion]++; } - + if (isGoodPrimarySpecies(particle)) { ue.fill(HIST("Kaon/hPtPrimGenAll"), particle.pt()); particleCountAll[kKaon]++; } - + if (isGoodPrimarySpecies(particle)) { ue.fill(HIST("Proton/hPtPrimGenAll"), particle.pt()); particleCountAll[kProton]++; } } - + // ======================================================================== // Apply physics selection // ======================================================================== - if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) continue; - - if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) continue; - if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) continue; - + if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) + continue; + + if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) + continue; + if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) + continue; + // Count physics-selected events ue.fill(HIST("hEventsPassPhysicsSelection"), 1.0); nPassPhysicsSelection++; - + // Fill primaries AFTER physics selection for (const auto& particle : particlesInCollision) { if (isGoodPrimary(particle)) { @@ -923,19 +949,19 @@ void multiplicitypt::processTrue(CollisionTableMCTrue const& mcCollisions, ue.fill(HIST("Inclusive/hPtPrimGen"), particle.pt()); nParticlesFilledAfterPS++; } - + if (isGoodPrimarySpecies(particle)) { ue.fill(HIST("Pion/hPtDenEff"), particle.pt()); ue.fill(HIST("Pion/hPtPrimGen"), particle.pt()); particleCountAfterPS[kPion]++; } - + if (isGoodPrimarySpecies(particle)) { ue.fill(HIST("Kaon/hPtDenEff"), particle.pt()); ue.fill(HIST("Kaon/hPtPrimGen"), particle.pt()); particleCountAfterPS[kKaon]++; } - + if (isGoodPrimarySpecies(particle)) { ue.fill(HIST("Proton/hPtDenEff"), particle.pt()); ue.fill(HIST("Proton/hPtPrimGen"), particle.pt()); @@ -949,7 +975,7 @@ void multiplicitypt::processTrue(CollisionTableMCTrue const& mcCollisions, LOG(info) << "Passing physics selection: " << nPassPhysicsSelection; LOG(info) << "Total primaries (before PS): " << nParticlesFilledAll; LOG(info) << "Total primaries (after PS): " << nParticlesFilledAfterPS; - + LOG(info) << "=== PARTICLE-SPECIFIC STATISTICS ==="; LOG(info) << "Pions - All: " << particleCountAll[kPion] << ", After PS: " << particleCountAfterPS[kPion]; From 5d3db7203c3cfe6e7def6398f1415db2014180b0 Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Tue, 20 Jan 2026 12:19:09 -0600 Subject: [PATCH 03/16] Fix linter errors --- PWGLF/Tasks/Nuspex/multiplicitypt.cxx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/multiplicitypt.cxx b/PWGLF/Tasks/Nuspex/multiplicitypt.cxx index dee716b05f5..a7d1c6152db 100644 --- a/PWGLF/Tasks/Nuspex/multiplicitypt.cxx +++ b/PWGLF/Tasks/Nuspex/multiplicitypt.cxx @@ -25,10 +25,10 @@ #include "Framework/AnalysisDataModel.h" #include "Framework/AnalysisTask.h" #include "Framework/HistogramRegistry.h" +#include "Framework/Logger.h" #include "ReconstructionDataFormats/Track.h" #include "Framework/O2DatabasePDGPlugin.h" #include "Framework/StaticFor.h" #include "Framework/runDataProcessing.h" -#include "ReconstructionDataFormats/Track.h" #include "TDatabasePDG.h" #include @@ -37,16 +37,16 @@ #include #include -#include #include using namespace o2; using namespace o2::framework; using namespace o2::framework::expressions; +using namespace o2::constants::physics; using BCsRun3 = soa::Join; -struct multiplicitypt { +struct MultiplicityPt { // Service Service pdg; @@ -139,9 +139,9 @@ struct multiplicitypt { }; // PDG codes - static constexpr int PDGPion = 211; - static constexpr int PDGKaon = 321; - static constexpr int PDGProton = 2212; + static constexpr int PDGPion = Pdg::kPiPlus; + static constexpr int PDGKaon = Pdg::kKPlus; + static constexpr int PDGProton = Pdg::kProton; // ======================================================================== // PROCESS FUNCTION DECLARATIONS - SPECTRATOF STYLE @@ -391,10 +391,10 @@ struct multiplicitypt { WorkflowSpec defineDataProcessing(ConfigContext const& cfgc) { - return WorkflowSpec{adaptAnalysisTask(cfgc)}; + return WorkflowSpec{adaptAnalysisTask(cfgc)}; } -void multiplicitypt::init(InitContext const&) +void MultiplicityPt::init(InitContext const&) { // ======================================================================== // CUSTOM TRACK CUTS INITIALIZATION - MATCHING spectraTOF @@ -571,7 +571,7 @@ void multiplicitypt::init(InitContext const&) // ======================================================================== // DATA PROCESSING - WITH EXCLUSIVE PID // ======================================================================== -void multiplicitypt::processData(CollisionTableData::iterator const& collision, TrackTableData const& tracks) +void MultiplicityPt::processData(CollisionTableData::iterator const& collision, TrackTableData const& tracks) { if (!isEventSelected(collision)) { return; @@ -613,7 +613,7 @@ void multiplicitypt::processData(CollisionTableData::iterator const& collision, // ======================================================================== // MC PROCESSING - WITH FIXED PRIMARY FRACTION CALCULATION // ======================================================================== -void multiplicitypt::processMC(TrackTableMC const& tracks, +void MultiplicityPt::processMC(TrackTableMC const& tracks, aod::McParticles const& particles, CollisionTableMCTrue const& mcCollisions, CollisionTableMC const& collisions) @@ -882,7 +882,7 @@ void multiplicitypt::processMC(TrackTableMC const& tracks, // ======================================================================== // TRUE MC PROCESSING - WITH PARTICLE-SPECIFIC SIGNAL LOSS // ======================================================================== -void multiplicitypt::processTrue(CollisionTableMCTrue const& mcCollisions, +void MultiplicityPt::processTrue(CollisionTableMCTrue const& mcCollisions, ParticleTableMC const& particles) { LOG(info) << "=== DEBUG processTrue START ==="; From 9ab8dbbad58d3f8d34f42ae5fd0c4c87ae068f79 Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Tue, 20 Jan 2026 12:29:37 -0600 Subject: [PATCH 04/16] Fix linter errors: rename multiplicitypt.cxx to MultiplicityPt.cxx to match struct name --- PWGLF/Tasks/Nuspex/CMakeLists.txt | 2 +- PWGLF/Tasks/Nuspex/{multiplicitypt.cxx => MultiplicityPt.cxx} | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) rename PWGLF/Tasks/Nuspex/{multiplicitypt.cxx => MultiplicityPt.cxx} (99%) diff --git a/PWGLF/Tasks/Nuspex/CMakeLists.txt b/PWGLF/Tasks/Nuspex/CMakeLists.txt index a4ba69bfb88..8c0cbec54d0 100644 --- a/PWGLF/Tasks/Nuspex/CMakeLists.txt +++ b/PWGLF/Tasks/Nuspex/CMakeLists.txt @@ -181,7 +181,7 @@ o2physics_add_dpl_workflow(chargedparticle-raa COMPONENT_NAME Analysis) o2physics_add_dpl_workflow(multiplicity-pt - SOURCES multiplicitypt.cxx + SOURCES MultiplicityPt.cxx PUBLIC_LINK_LIBRARIES O2::Framework O2Physics::AnalysisCore COMPONENT_NAME Analysis) diff --git a/PWGLF/Tasks/Nuspex/multiplicitypt.cxx b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx similarity index 99% rename from PWGLF/Tasks/Nuspex/multiplicitypt.cxx rename to PWGLF/Tasks/Nuspex/MultiplicityPt.cxx index a7d1c6152db..6aba4b35477 100644 --- a/PWGLF/Tasks/Nuspex/multiplicitypt.cxx +++ b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx @@ -30,7 +30,6 @@ #include "Framework/StaticFor.h" #include "Framework/runDataProcessing.h" -#include "TDatabasePDG.h" #include #include #include From 5fee20c37f5bc905e6d79f0c7298b50b5c9aa442 Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Tue, 20 Jan 2026 12:39:59 -0600 Subject: [PATCH 05/16] Fix linter errors --- PWGLF/Tasks/Nuspex/MultiplicityPt.cxx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx index 6aba4b35477..b9a676b87d8 100644 --- a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx +++ b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx @@ -25,10 +25,11 @@ #include "Framework/AnalysisDataModel.h" #include "Framework/AnalysisTask.h" #include "Framework/HistogramRegistry.h" -#include "Framework/Logger.h" #include "ReconstructionDataFormats/Track.h" +#include "Framework/Logger.h" #include "Framework/O2DatabasePDGPlugin.h" #include "Framework/StaticFor.h" #include "Framework/runDataProcessing.h" +#include "ReconstructionDataFormats/Track.h" #include #include @@ -149,19 +150,19 @@ struct MultiplicityPt { // Data processing void processData(CollisionTableData::iterator const& collision, TrackTableData const& tracks); - PROCESS_SWITCH(multiplicitypt, processData, "process data", false); + PROCESS_SWITCH(MultiplicityPt, processData, "process data", false); // MC processing - EXACT spectraTOF approach void processMC(TrackTableMC const& tracks, aod::McParticles const& particles, CollisionTableMCTrue const& mcCollisions, CollisionTableMC const& collisions); - PROCESS_SWITCH(multiplicitypt, processMC, "process MC", true); + PROCESS_SWITCH(MultiplicityPt, processMC, "process MC", true); // True MC processing - EXACT spectraTOF approach void processTrue(CollisionTableMCTrue const& mcCollisions, ParticleTableMC const& particles); - PROCESS_SWITCH(multiplicitypt, processTrue, "process true MC", true); + PROCESS_SWITCH(MultiplicityPt, processTrue, "process true MC", true); // ======================================================================== // TRACK SELECTION FUNCTIONS - MATCHING spectraTOF @@ -564,7 +565,7 @@ void MultiplicityPt::init(InitContext const&) ue.add("hvtxZ", "Vertex Z (data);Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); ue.add("hvtxZmc", "MC vertex Z;Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); - LOG(info) << "Initialized multiplicitypt task with EXCLUSIVE PID for INCLUSIVE + PARTICLE-SPECIFIC (Pi, K, p) analysis"; + LOG(info) << "Initialized MultiplicityPt task with EXCLUSIVE PID for INCLUSIVE + PARTICLE-SPECIFIC (Pi, K, p) analysis"; } // ======================================================================== From 6af8231c66e547add02db5dbe6285175b26e7640 Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Tue, 20 Jan 2026 14:12:57 -0600 Subject: [PATCH 06/16] Fix linter errors --- PWGLF/Tasks/Nuspex/MultiplicityPt.cxx | 3 --- 1 file changed, 3 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx index b9a676b87d8..59cd71909c6 100644 --- a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx +++ b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx @@ -793,7 +793,6 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, } } } - // ======================================================================== // KAON CHANNEL // ======================================================================== @@ -820,7 +819,6 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, } } } - // ======================================================================== // PROTON CHANNEL // ======================================================================== @@ -847,7 +845,6 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, } } } - LOG(info) << "=== DEBUG TRACK COUNTING ==="; LOG(info) << "Total tracks processed: " << totalTracksProcessed; LOG(info) << "Tracks from selected events: " << tracksFromSelectedEvents; From ed834fa85b592aa548549616a87cf5fcb08bac32 Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Tue, 20 Jan 2026 14:20:23 -0600 Subject: [PATCH 07/16] Fix linter errors --- PWGLF/Tasks/Nuspex/MultiplicityPt.cxx | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx index 59cd71909c6..ee487ddc6fb 100644 --- a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx +++ b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx @@ -760,15 +760,8 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, } } - // ======================================================================== - // EXCLUSIVE PID - FIXED PRIMARY FRACTION LOGIC - // ======================================================================== - int bestSpecies = getBestPIDHypothesis(track); - // ======================================================================== - // PION CHANNEL - // ======================================================================== if (bestSpecies == kPion) { ue.fill(HIST("Pion/hPtMeasured"), track.pt()); ue.fill(HIST("Pion/hPtAllReco"), track.pt()); @@ -780,10 +773,6 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, if (track.has_mcParticle()) { const auto& particle = track.mcParticle(); - - // KEY FIX: Primary fraction = fraction of identified pions that are primary - // This includes correctly identified pions AND misidentified kaons/protons - // that happen to be primary particles if (particle.isPhysicalPrimary()) { ue.fill(HIST("Pion/hPtPrimReco"), track.pt()); particleTracksPrimary[kPion]++; @@ -792,11 +781,7 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, particleTracksSecondary[kPion]++; } } - } - // ======================================================================== - // KAON CHANNEL - // ======================================================================== - else if (bestSpecies == kKaon) { + } else if (bestSpecies == kKaon) { ue.fill(HIST("Kaon/hPtMeasured"), track.pt()); ue.fill(HIST("Kaon/hPtAllReco"), track.pt()); particleTracksIdentified[kKaon]++; @@ -818,11 +803,7 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, particleTracksSecondary[kKaon]++; } } - } - // ======================================================================== - // PROTON CHANNEL - // ======================================================================== - else if (bestSpecies == kProton) { + } else if (bestSpecies == kProton) { ue.fill(HIST("Proton/hPtMeasured"), track.pt()); ue.fill(HIST("Proton/hPtAllReco"), track.pt()); particleTracksIdentified[kProton]++; From 29126a0f46d72c6378e53911ee2cd915cc18032e Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Tue, 20 Jan 2026 14:29:55 -0600 Subject: [PATCH 08/16] Fix linter errors --- PWGLF/Tasks/Nuspex/MultiplicityPt.cxx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx index ee487ddc6fb..cbd729d398f 100644 --- a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx +++ b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx @@ -842,15 +842,15 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, // Calculate and log primary fractions if (particleTracksIdentified[kPion] > 0) { - float pionPrimFrac = (float)particleTracksPrimary[kPion] / particleTracksIdentified[kPion]; + float pionPrimFrac = static_cast particleTracksPrimary[kPion] / particleTracksIdentified[kPion]; LOG(info) << "Pion primary fraction: " << pionPrimFrac * 100.0 << "%"; } if (particleTracksIdentified[kKaon] > 0) { - float kaonPrimFrac = (float)particleTracksPrimary[kKaon] / particleTracksIdentified[kKaon]; + float kaonPrimFrac = static_cast particleTracksPrimary[kKaon] / particleTracksIdentified[kKaon]; LOG(info) << "Kaon primary fraction: " << kaonPrimFrac * 100.0 << "%"; } if (particleTracksIdentified[kProton] > 0) { - float protonPrimFrac = (float)particleTracksPrimary[kProton] / particleTracksIdentified[kProton]; + float protonPrimFrac = static_cast particleTracksPrimary[kProton] / particleTracksIdentified[kProton]; LOG(info) << "Proton primary fraction: " << protonPrimFrac * 100.0 << "%"; } From 67ab8d94498fbee7a87819f49d0f9fd99b80a84b Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Wed, 21 Jan 2026 10:22:59 -0600 Subject: [PATCH 09/16] Fix linter and alibuild errors --- PWGLF/Tasks/Nuspex/MultiplicityPt.cxx | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx index cbd729d398f..e388ba4678e 100644 --- a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx +++ b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx @@ -11,6 +11,7 @@ #include "PWGLF/Utils/inelGt.h" +#include "Common/Constants/PhysicsConstants.h" #include "Common/Core/TrackSelection.h" #include "Common/Core/TrackSelectionDefaults.h" #include "Common/DataModel/Centrality.h" @@ -37,6 +38,8 @@ #include #include +#include +#include #include using namespace o2; @@ -139,15 +142,10 @@ struct MultiplicityPt { }; // PDG codes - static constexpr int PDGPion = Pdg::kPiPlus; - static constexpr int PDGKaon = Pdg::kKPlus; - static constexpr int PDGProton = Pdg::kProton; + static constexpr int PDGPion = o2::constants::physics::PionPlus; + static constexpr int PDGKaon = o2::constants::physics::KaonPlus; + static constexpr int PDGProton = o2::constants::physics::Proton; - // ======================================================================== - // PROCESS FUNCTION DECLARATIONS - SPECTRATOF STYLE - // ======================================================================== - - // Data processing void processData(CollisionTableData::iterator const& collision, TrackTableData const& tracks); PROCESS_SWITCH(MultiplicityPt, processData, "process data", false); @@ -842,15 +840,15 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, // Calculate and log primary fractions if (particleTracksIdentified[kPion] > 0) { - float pionPrimFrac = static_cast particleTracksPrimary[kPion] / particleTracksIdentified[kPion]; + float pionPrimFrac = static_cast(particleTracksPrimary[kPion]) / particleTracksIdentified[kPion]; LOG(info) << "Pion primary fraction: " << pionPrimFrac * 100.0 << "%"; } if (particleTracksIdentified[kKaon] > 0) { - float kaonPrimFrac = static_cast particleTracksPrimary[kKaon] / particleTracksIdentified[kKaon]; + float kaonPrimFrac = static_cast(particleTracksPrimary[kKaon]) / particleTracksIdentified[kKaon]; LOG(info) << "Kaon primary fraction: " << kaonPrimFrac * 100.0 << "%"; } if (particleTracksIdentified[kProton] > 0) { - float protonPrimFrac = static_cast particleTracksPrimary[kProton] / particleTracksIdentified[kProton]; + float protonPrimFrac = static_cast(particleTracksPrimary[kProton]) / particleTracksIdentified[kProton]; LOG(info) << "Proton primary fraction: " << protonPrimFrac * 100.0 << "%"; } From 4f8af3530a3ddee866e29decd44404b576088d3b Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Wed, 21 Jan 2026 10:34:32 -0600 Subject: [PATCH 10/16] Fix linter and alibuild errors --- PWGLF/Tasks/Nuspex/MultiplicityPt.cxx | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx index e388ba4678e..79467bfccb5 100644 --- a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx +++ b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx @@ -11,6 +11,7 @@ #include "PWGLF/Utils/inelGt.h" +#include "Common/Constants/MathConstants.h" #include "Common/Constants/PhysicsConstants.h" #include "Common/Core/TrackSelection.h" #include "Common/Core/TrackSelectionDefaults.h" @@ -46,6 +47,7 @@ using namespace o2; using namespace o2::framework; using namespace o2::framework::expressions; using namespace o2::constants::physics; +using namespace o2::constants::math; using BCsRun3 = soa::Join; @@ -191,7 +193,10 @@ struct MultiplicityPt { if (!passesCutWoDCA(track)) { return false; } - const float maxDcaXY = maxDcaXYFactor.value * (0.0105f + 0.0350f / std::pow(track.pt(), 1.1f)); + constexpr float dcaXYConst = 0.0105f; + constexpr float dcaXYPtScale = 0.0350f; + constexpr float dcaXYPtPower = 1.1f; + const float maxDcaXY = maxDcaXYFactor.value * (dcaXYConst + dcaXYPtScale / std::pow(track.pt(), dcaXYPtPower)); if (std::abs(track.dcaXY()) > maxDcaXY) { return false; } @@ -249,8 +254,8 @@ struct MultiplicityPt { float nsigmaKa = std::abs(track.tpcNSigmaKa()); float nsigmaPr = std::abs(track.tpcNSigmaPr()); - // Find the hypothesis with smallest |nσ| that passes the cut - float minNSigma = 999.0f; + constexpr float largeNSigmaValue = 999.0f; + float minNSigma = largeNSigmaValue; int bestSpecies = -1; if (nsigmaPi < cfgCutNsigma.value && nsigmaPi < minNSigma) { @@ -540,10 +545,10 @@ void MultiplicityPt::init(InitContext const&) } } - // ======================================================================== - // MONITORING HISTOGRAMS - // ======================================================================== - ue.add("evsel", "Event selection", HistType::kTH1D, {{20, 0.5, 20.5}}); + constexpr int nEvSelBins = 20; + constexpr float evSelMin = 0.5f; + constexpr float evSelMax = 20.5f; + ue.add("evsel", "Event selection", HistType::kTH1D, {{nEvSelBins, evSelMin, evSelMax}}); auto h = ue.get(HIST("evsel")); h->GetXaxis()->SetBinLabel(1, "Events read"); h->GetXaxis()->SetBinLabel(2, "INEL>0"); @@ -559,7 +564,7 @@ void MultiplicityPt::init(InitContext const&) h->GetXaxis()->SetBinLabel(15, "INEL>1 (final)"); ue.add("hEta", "Track eta;#eta;Counts", HistType::kTH1D, {{20, -0.8, 0.8}}); - ue.add("hPhi", "Track phi;#varphi (rad);Counts", HistType::kTH1D, {{64, 0, 2.0 * o2::constants::math::PI}}); + ue.add("hPhi", "Track phi;#varphi (rad);Counts", HistType::kTH1D, {{64, 0, TwoPI}}); ue.add("hvtxZ", "Vertex Z (data);Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); ue.add("hvtxZmc", "MC vertex Z;Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); From 2d78b9c938f5f5e6e5e13c2d7309367c94877b17 Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Thu, 22 Jan 2026 10:56:23 -0600 Subject: [PATCH 11/16] Fix alibuild errors --- PWGLF/Tasks/Nuspex/MultiplicityPt.cxx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx index 79467bfccb5..cde65f730fc 100644 --- a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx +++ b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx @@ -11,8 +11,6 @@ #include "PWGLF/Utils/inelGt.h" -#include "Common/Constants/MathConstants.h" -#include "Common/Constants/PhysicsConstants.h" #include "Common/Core/TrackSelection.h" #include "Common/Core/TrackSelectionDefaults.h" #include "Common/DataModel/Centrality.h" @@ -46,8 +44,7 @@ using namespace o2; using namespace o2::framework; using namespace o2::framework::expressions; -using namespace o2::constants::physics; -using namespace o2::constants::math; + using BCsRun3 = soa::Join; From 5c763c1e838481350ae19cae91fe6b7879657f2f Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Fri, 23 Jan 2026 10:58:50 -0600 Subject: [PATCH 12/16] Fix alibuild errors --- PWGLF/Tasks/Nuspex/MultiplicityPt.cxx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx index cde65f730fc..710ebca2b73 100644 --- a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx +++ b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx @@ -141,9 +141,9 @@ struct MultiplicityPt { }; // PDG codes - static constexpr int PDGPion = o2::constants::physics::PionPlus; - static constexpr int PDGKaon = o2::constants::physics::KaonPlus; - static constexpr int PDGProton = o2::constants::physics::Proton; + static constexpr int PDGPion = 211; // π⁺ PDG code + static constexpr int PDGKaon = 321; // K⁺ PDG code + static constexpr int PDGProton = 2212; // p PDG code void processData(CollisionTableData::iterator const& collision, TrackTableData const& tracks); @@ -561,7 +561,7 @@ void MultiplicityPt::init(InitContext const&) h->GetXaxis()->SetBinLabel(15, "INEL>1 (final)"); ue.add("hEta", "Track eta;#eta;Counts", HistType::kTH1D, {{20, -0.8, 0.8}}); - ue.add("hPhi", "Track phi;#varphi (rad);Counts", HistType::kTH1D, {{64, 0, TwoPI}}); + ue.add("hPhi", "Track phi;#varphi (rad);Counts", HistType::kTH1D, {{64, 0, 2.0 * M_PI}}); ue.add("hvtxZ", "Vertex Z (data);Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); ue.add("hvtxZmc", "MC vertex Z;Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); From 6c2495e26426d409c2772f2eb71951cd5c41beb6 Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Tue, 3 Feb 2026 11:20:15 -0600 Subject: [PATCH 13/16] Add multiplicity analysis --- PWGLF/Tasks/Nuspex/MultiplicityPt.cxx | 799 +++++++++++++++++++++----- 1 file changed, 658 insertions(+), 141 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx index 710ebca2b73..6d30693af1b 100644 --- a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx +++ b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx @@ -9,18 +9,23 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. +#include "PWGLF/DataModel/LFParticleIdentification.h" +#include "PWGLF/DataModel/spectraTOF.h" #include "PWGLF/Utils/inelGt.h" +#include "Common/Core/RecoDecay.h" #include "Common/Core/TrackSelection.h" #include "Common/Core/TrackSelectionDefaults.h" #include "Common/DataModel/Centrality.h" #include "Common/DataModel/EventSelection.h" +#include "Common/DataModel/McCollisionExtra.h" #include "Common/DataModel/Multiplicity.h" -#include "Common/DataModel/PIDResponse.h" #include "Common/DataModel/PIDResponseTOF.h" #include "Common/DataModel/PIDResponseTPC.h" #include "Common/DataModel/TrackSelectionTables.h" +#include "CCDB/BasicCCDBManager.h" +#include "DataFormatsParameters/GRPMagField.h" #include "Framework/ASoAHelpers.h" #include "Framework/AnalysisDataModel.h" #include "Framework/AnalysisTask.h" @@ -31,12 +36,15 @@ #include "Framework/runDataProcessing.h" #include "ReconstructionDataFormats/Track.h" +#include "TPDGCode.h" #include #include #include #include +#include #include +#include #include #include #include @@ -53,6 +61,9 @@ struct MultiplicityPt { // Service Service pdg; + // Add CCDB service for magnetic field + Service ccdb; + Configurable isRun3{"isRun3", true, "is Run3 dataset"}; Configurable cfgCutVertex{"cfgCutVertex", 10.0f, "Accepted z-vertex range"}; Configurable cfgINELCut{"cfgINELCut", 0, "INEL event selection: 0 no sel, 1 INEL>0, 2 INEL>1"}; @@ -87,9 +98,17 @@ struct MultiplicityPt { Configurable maxChi2PerClusterITS{"maxChi2PerClusterITS", 36.f, "Additional cut on the maximum value of the chi2 per cluster in the ITS"}; Configurable maxDcaXYFactor{"maxDcaXYFactor", 1.f, "Additional cut on the maximum value of the DCA xy (multiplicative factor)"}; Configurable maxDcaZ{"maxDcaZ", 0.1f, "Additional cut on the maximum value of the DCA z"}; - Configurable minTPCNClsFound{"minTPCNClsFound", 100.f, "Additional cut on the minimum value of the number of found clusters in the TPC"}; + Configurable minTPCNClsFound{"minTPCNClsFound", 70.f, "Additional cut on the minimum value of the number of found clusters in the TPC"}; Configurable min_ITS_nClusters{"min_ITS_nClusters", 5, "minimum number of found ITS clusters"}; + // Phi cut parameters + Configurable applyPhiCut{"applyPhiCut", true, "Apply phi sector cut to remove problematic TPC regions"}; + Configurable pTthresholdPhiCut{"pTthresholdPhiCut", 2.0f, "pT threshold above which to apply phi cut"}; + Configurable phiCutLowParam1{"phiCutLowParam1", 0.119297, "First parameter for low phi cut"}; + Configurable phiCutLowParam2{"phiCutLowParam2", 0.000379693, "Second parameter for low phi cut"}; + Configurable phiCutHighParam1{"phiCutHighParam1", 0.16685, "First parameter for high phi cut"}; + Configurable phiCutHighParam2{"phiCutHighParam2", 0.00981942, "Second parameter for high phi cut"}; + // Basic track cuts Configurable cfgTrkEtaCut{"cfgTrkEtaCut", 0.8f, "Eta range for tracks"}; Configurable cfgTrkLowPtCut{"cfgTrkLowPtCut", 0.15f, "Minimum constituent pT"}; @@ -97,10 +116,27 @@ struct MultiplicityPt { // Custom track cuts matching spectraTOF TrackSelection customTrackCuts; + // TF1 pointers for phi cuts + TF1* fphiCutLow = nullptr; + TF1* fphiCutHigh = nullptr; + // Histogram Registry HistogramRegistry ue; - // Table definitions - EXACT spectraTOF approach + // ======================================================================== + // CENTRALITY/MULTIPLICITY CLASSES - Using same bins as before for consistency + // ======================================================================== + static constexpr int kCentralityClasses = 10; + static constexpr double CentClasses[kCentralityClasses + 1] = {0.0, 1.0, 5.0, 10.0, 15.0, 20.0, 30.0, 40.0, 50.0, 70.0, 100.0}; + + // Multiplicity percentile boundaries (computed on first pass) + std::vector multPercentileboundaries; + bool percentilesComputed = false; + + // Storage for multiplicity distribution (for percentile calculation) + std::vector multiplicityValues; + + // Table definitions - NO McCentFT0Ms dependency using CollisionTableData = soa::Join; using CollisionTableMC = soa::Join; @@ -110,14 +146,14 @@ struct MultiplicityPt { using TrackTableMC = soa::Join; - // MC tables - EXACT spectraTOF approach + // MC tables - NO McCentFT0Ms using CollisionTableMCTrue = aod::McCollisions; using ParticleTableMC = aod::McParticles; - // Preslice for MC particles (like spectraTOF) + // Preslice for MC particles Preslice perMCCol = aod::mcparticle::mcCollisionId; - // Multiplicity estimator enum (like spectraTOF) + // Multiplicity estimator enum enum MultCodes : int { kNoMultiplicity = 0, kMultFV0M = 1, @@ -132,7 +168,7 @@ struct MultiplicityPt { kCentralityFV0A = 10 }; - // Particle species enum (from spectraTOF) + // Particle species enum enum ParticleSpecies : int { kPion = 0, kKaon = 1, @@ -141,28 +177,245 @@ struct MultiplicityPt { }; // PDG codes - static constexpr int PDGPion = 211; // π⁺ PDG code - static constexpr int PDGKaon = 321; // K⁺ PDG code - static constexpr int PDGProton = 2212; // p PDG code + static constexpr int PDGPion = 211; + static constexpr int PDGKaon = 321; + static constexpr int PDGProton = 2212; void processData(CollisionTableData::iterator const& collision, - TrackTableData const& tracks); + TrackTableData const& tracks, + BCsRun3 const& bcs); PROCESS_SWITCH(MultiplicityPt, processData, "process data", false); - // MC processing - EXACT spectraTOF approach + // MC processing - First pass to build percentiles + void processPercentileCalibration(CollisionTableMCTrue const& mcCollisions, + ParticleTableMC const& particles); + PROCESS_SWITCH(MultiplicityPt, processPercentileCalibration, "Build multiplicity percentile calibration (run first)", false); + + // MC processing - Main analysis void processMC(TrackTableMC const& tracks, aod::McParticles const& particles, CollisionTableMCTrue const& mcCollisions, - CollisionTableMC const& collisions); + CollisionTableMC const& collisions, + BCsRun3 const& bcs); PROCESS_SWITCH(MultiplicityPt, processMC, "process MC", true); - // True MC processing - EXACT spectraTOF approach + // True MC processing void processTrue(CollisionTableMCTrue const& mcCollisions, ParticleTableMC const& particles); PROCESS_SWITCH(MultiplicityPt, processTrue, "process true MC", true); // ======================================================================== - // TRACK SELECTION FUNCTIONS - MATCHING spectraTOF + // MULTIPLICITY GETTER FUNCTIONS - Using raw charged particle count + // ======================================================================== + + // Count charged primaries in |eta| < 1.0 + template + int countChargedPrimaries(const MCCollisionType& mcCollision, const ParticleTableMC& particles) const + { + int nCharged = 0; + auto particlesInColl = particles.sliceBy(perMCCol, mcCollision.globalIndex()); + for (const auto& p : particlesInColl) { + if (!p.isPhysicalPrimary()) + continue; + auto pdgParticle = pdg->GetParticle(p.pdgCode()); + if (!pdgParticle || pdgParticle->Charge() == 0.) + continue; + if (std::abs(p.eta()) < 1.0) + nCharged++; + } + return nCharged; + } + + // For reconstructed collisions + template + float getMultiplicity(const CollisionType& collision) const + { + switch (multiplicityEstimator.value) { + case kNoMultiplicity: + return 50.f; + case kMultFV0M: + return collision.multZeqFV0A(); + case kMultFT0M: + return collision.multZeqFT0A() + collision.multZeqFT0C(); + case kMultFDDM: + return collision.multZeqFDDA() + collision.multZeqFDDC(); + case kMultTracklets: + return 0.f; + case kMultTPC: + return collision.multTPC(); + case kMultNTracksPV: + return collision.multZeqNTracksPV(); + case kMultNTracksPVeta1: + return collision.multNTracksPVeta1(); + case kCentralityFT0C: + case kCentralityFT0M: + case kCentralityFV0A: + return collision.multZeqNTracksPV(); + default: + return 0.f; + } + } + + // For MC collisions - returns RAW multiplicity + template + float getMultiplicityMC(const MCCollisionType& mcCollision, const ParticleTableMC& particles) const + { + return static_cast(countChargedPrimaries(mcCollision, particles)); + } + + // Convert raw multiplicity to percentile + float multiplicityToPercentile(float rawMult) const + { + if (!percentilesComputed || multPercentileboundaries.empty()) { + // If percentiles not computed, return raw multiplicity + return rawMult; + } + + // Find which percentile bin this multiplicity falls into + for (size_t i = 0; i < multPercentileboundaries.size() - 1; ++i) { + if (rawMult >= multPercentileboundaries[i] && rawMult < multPercentileboundaries[i + 1]) { + // Return the CENTER of the percentile bin + return CentClasses[i] + (CentClasses[i + 1] - CentClasses[i]) / 2.0; + } + } + + // Handle edge cases + if (rawMult < multPercentileboundaries[0]) { + return CentClasses[0]; + } + return CentClasses[kCentralityClasses]; + } + + // Get centrality class index from raw multiplicity + int getCentralityClass(float rawMult) const + { + if (!percentilesComputed || multPercentileboundaries.empty()) { + // Fallback: divide into equal bins + float maxMult = 150.0f; // Assumed maximum + int bin = static_cast((rawMult / maxMult) * kCentralityClasses); + return std::min(bin, kCentralityClasses - 1); + } + + // Use computed percentiles + for (int i = 0; i < kCentralityClasses; ++i) { + if (rawMult >= multPercentileboundaries[i] && rawMult < multPercentileboundaries[i + 1]) { + return i; + } + } + + // Outside range + if (rawMult < multPercentileboundaries[0]) + return 0; + return kCentralityClasses - 1; + } + + // ======================================================================== + // COMPUTE PERCENTILE BOUNDARIES + // ======================================================================== + void computePercentileBoundaries() + { + if (multiplicityValues.empty()) { + LOG(warning) << "No multiplicity values to compute percentiles from!"; + return; + } + + // Sort multiplicity values + std::sort(multiplicityValues.begin(), multiplicityValues.end()); + + LOG(info) << "Computing percentile boundaries from " << multiplicityValues.size() << " events"; + + // Compute percentile boundaries + multPercentileboundaries.clear(); + multPercentileboundaries.reserve(kCentralityClasses + 1); + + for (int i = 0; i <= kCentralityClasses; ++i) { + float percentile = CentClasses[i]; + size_t index = static_cast(percentile / 100.0 * multiplicityValues.size()); + if (index >= multiplicityValues.size()) { + index = multiplicityValues.size() - 1; + } + float boundary = multiplicityValues[index]; + multPercentileboundaries.push_back(boundary); + LOG(info) << "Percentile " << percentile << "% -> Multiplicity >= " << boundary; + } + + percentilesComputed = true; + + LOG(info) << "=== Percentile Boundaries Computed ==="; + for (int i = 0; i < kCentralityClasses; ++i) { + LOG(info) << "Class " << i << ": [" << CentClasses[i] << "%-" << CentClasses[i + 1] + << "%] = Mult [" << multPercentileboundaries[i] << "-" << multPercentileboundaries[i + 1] << ")"; + } + } + + // ======================================================================== + // MAGNETIC FIELD FUNCTION + // ======================================================================== + int getMagneticField(uint64_t timestamp) + { + static o2::parameters::GRPMagField* grpo = nullptr; + if (grpo == nullptr) { + grpo = ccdb->getForTimeStamp("GLO/Config/GRPMagField", timestamp); + if (grpo == nullptr) { + LOGF(fatal, "GRP object not found for timestamp %llu", timestamp); + return 0; + } + LOGF(info, "Retrieved GRP for timestamp %llu with magnetic field of %d kG", timestamp, grpo->getNominalL3Field()); + } + return grpo->getNominalL3Field(); + } + + // ======================================================================== + // PHI CUT FUNCTION + // ======================================================================== + template + bool passedPhiCut(const TrackType& track, float magField) const + { + if (!applyPhiCut.value) { + return true; + } + + if (track.pt() < pTthresholdPhiCut.value) { + return true; + } + + float pt = track.pt(); + float phi = track.phi(); + int charge = track.sign(); + + if (magField < 0) { + phi = o2::constants::math::TwoPI - phi; + } + if (charge < 0) { + phi = o2::constants::math::TwoPI - phi; + } + + phi += o2::constants::math::PI / 18.0f; + phi = std::fmod(phi, o2::constants::math::PI / 9.0f); + + if (phi < fphiCutHigh->Eval(pt) && phi > fphiCutLow->Eval(pt)) { + return false; + } + + return true; + } + + float getTransformedPhi(const float phi, const int charge, const float magField) const + { + float transformedPhi = phi; + if (magField < 0) { + transformedPhi = o2::constants::math::TwoPI - transformedPhi; + } + if (charge < 0) { + transformedPhi = o2::constants::math::TwoPI - transformedPhi; + } + transformedPhi += o2::constants::math::PI / 18.0f; + transformedPhi = std::fmod(transformedPhi, o2::constants::math::PI / 9.0f); + return transformedPhi; + } + + // ======================================================================== + // TRACK SELECTION FUNCTIONS // ======================================================================== template @@ -203,7 +456,7 @@ struct MultiplicityPt { } template - bool passesTrackSelection(TrackType const& track) const + bool passesTrackSelection(TrackType const& track, float magField = 0) const { if (track.eta() < cfgCutEtaMin.value || track.eta() > cfgCutEtaMax.value) return false; @@ -214,11 +467,14 @@ struct MultiplicityPt { if (!passesCutWoDCA(track)) return false; + if (applyPhiCut.value && !passedPhiCut(track, magField)) + return false; + return passesDCAxyCut(track); } // ======================================================================== - // PID SELECTION FUNCTIONS - TPC ONLY (OLD NON-EXCLUSIVE METHOD) + // PID SELECTION FUNCTIONS // ======================================================================== template @@ -234,19 +490,12 @@ struct MultiplicityPt { nsigmaTPC = track.tpcNSigmaPr(); } - // TPC-only PID (works for all pT, but better at low pT < 1 GeV/c) return (std::abs(nsigmaTPC) < cfgCutNsigma.value); } - // ======================================================================== - // EXCLUSIVE PID SELECTION - Returns best hypothesis for a track - // ======================================================================== - template int getBestPIDHypothesis(TrackType const& track) const { - // Return values: -1 = no ID, 0 = pion, 1 = kaon, 2 = proton - float nsigmaPi = std::abs(track.tpcNSigmaPi()); float nsigmaKa = std::abs(track.tpcNSigmaKa()); float nsigmaPr = std::abs(track.tpcNSigmaPr()); @@ -272,7 +521,7 @@ struct MultiplicityPt { } // ======================================================================== - // EVENT SELECTION FUNCTION - EXACT spectraTOF + // EVENT SELECTION FUNCTION // ======================================================================== template @@ -342,7 +591,7 @@ struct MultiplicityPt { } // ======================================================================== - // PRIMARY SELECTION - MATCHING spectraTOF + // PRIMARY SELECTION // ======================================================================== template @@ -366,7 +615,6 @@ struct MultiplicityPt { return true; } - // Particle-specific primary selection template bool isGoodPrimarySpecies(ParticleType const& particle) const { @@ -397,7 +645,7 @@ WorkflowSpec defineDataProcessing(ConfigContext const& cfgc) void MultiplicityPt::init(InitContext const&) { // ======================================================================== - // CUSTOM TRACK CUTS INITIALIZATION - MATCHING spectraTOF + // CUSTOM TRACK CUTS INITIALIZATION // ======================================================================== if (useCustomTrackCuts.value) { @@ -419,6 +667,28 @@ void MultiplicityPt::init(InitContext const&) customTrackCuts.print(); } + // ======================================================================== + // PHI CUT INITIALIZATION + // ======================================================================== + + if (applyPhiCut.value) { + fphiCutLow = new TF1("StandardPhiCutLow", + Form("%f/x/x+pi/18.0-%f", + phiCutLowParam1.value, phiCutLowParam2.value), + 0, 50); + fphiCutHigh = new TF1("StandardPhiCutHigh", + Form("%f/x+pi/18.0+%f", + phiCutHighParam1.value, phiCutHighParam2.value), + 0, 50); + + LOGF(info, "=== Phi Cut Parameters ==="); + LOGF(info, "Low cut: %.6f/x² + pi/18 - %.6f", + phiCutLowParam1.value, phiCutLowParam2.value); + LOGF(info, "High cut: %.6f/x + pi/18 + %.6f", + phiCutHighParam1.value, phiCutHighParam2.value); + LOGF(info, "Applied for pT > %.1f GeV/c", pTthresholdPhiCut.value); + } + // ======================================================================== // AXIS DEFINITIONS // ======================================================================== @@ -433,23 +703,39 @@ void MultiplicityPt::init(InitContext const&) "pT bin limits"}; AxisSpec ptAxis = {ptBinning, "#it{p}_{T} (GeV/#it{c})"}; + // Multiplicity axis - initially raw multiplicity, will represent percentiles after calibration + std::vector centBins(CentClasses, CentClasses + kCentralityClasses + 1); + AxisSpec multAxis = {centBins, "Centrality/Multiplicity Class (%)"}; + + // Raw multiplicity axis for calibration + AxisSpec rawMultAxis = {150, 0, 150, "N_{ch} (|#eta| < 1.0)"}; + // ======================================================================== - // HISTOGRAM REGISTRY - INCLUSIVE + PARTICLE-SPECIFIC + // HISTOGRAM REGISTRY // ======================================================================== - // Event counting - EXACT spectraTOF approach + // Multiplicity distribution for percentile calibration + ue.add("Calibration/hRawMultiplicity", "Raw multiplicity distribution;N_{ch};Events", + HistType::kTH1D, {rawMultAxis}); + + // Event counting ue.add("MC/GenRecoCollisions", "Generated and Reconstructed MC Collisions", HistType::kTH1D, {{10, 0.5, 10.5}}); auto hColl = ue.get(HIST("MC/GenRecoCollisions")); hColl->GetXaxis()->SetBinLabel(1, "Collisions generated"); hColl->GetXaxis()->SetBinLabel(2, "Collisions reconstructed"); - // CRITICAL: Complete event counting system - ue.add("hEventsAllGen", "All generated events", HistType::kTH1F, {{1, 0.5, 1.5}}); - ue.add("hEventsPassPhysicsSelection", "Events passing physics selection", HistType::kTH1F, {{1, 0.5, 1.5}}); - ue.add("hEventsReconstructable", "Physics-selected events with reconstruction", HistType::kTH1F, {{1, 0.5, 1.5}}); - ue.add("hEventsSelectedReco", "Selected reconstructed events", HistType::kTH1F, {{1, 0.5, 1.5}}); + // Event loss histograms + ue.add("MC/EventLoss/MultGenerated", "Generated events vs multiplicity", + HistType::kTH1D, {multAxis}); + ue.add("MC/EventLoss/MultBadVertex", "Events with bad vertex vs multiplicity", + HistType::kTH1D, {multAxis}); + ue.add("MC/EventLoss/MultPhysicsSelected", "Physics-selected events vs multiplicity", + HistType::kTH1D, {multAxis}); + ue.add("MC/EventLoss/MultReconstructed", "Reconstructed events vs multiplicity", + HistType::kTH1D, {multAxis}); + ue.add("MC/EventLoss/MultRecoSelected", "Reconstructed+selected events vs multiplicity", + HistType::kTH1D, {multAxis}); - // Event loss breakdown histogram ue.add("hEventLossBreakdown", "Event loss breakdown", HistType::kTH1D, {{4, 0.5, 4.5}}); auto hLoss = ue.get(HIST("hEventLossBreakdown")); hLoss->GetXaxis()->SetBinLabel(1, "Physics selected"); @@ -461,34 +747,63 @@ void MultiplicityPt::init(InitContext const&) // INCLUSIVE CHARGED PARTICLE HISTOGRAMS // ======================================================================== - // ALL generated primaries (before any physics selection) ue.add("Inclusive/hPtPrimGenAll", "All generated primaries (no cuts);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtPrimGenAllVsMult", "All generated primaries vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add("Inclusive/hPtPrimBadVertex", "Generated primaries (bad vertex);#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtPrimBadVertexVsMult", "Generated primaries (bad vertex) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", + HistType::kTH2D, {ptAxis, multAxis}); - // Generated primaries AFTER physics selection ue.add("Inclusive/hPtPrimGen", "Generated primaries (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtPrimGenVsMult", "Generated primaries (after phys sel) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add("Inclusive/hPtPrimRecoEv", "Generated primaries (reco events);#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtPrimRecoEvVsMult", "Generated primaries (reco events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add("Inclusive/hPtPrimGoodEv", "Generated primaries (good events);#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtPrimGoodEvVsMult", "Generated primaries (good events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", + HistType::kTH2D, {ptAxis, multAxis}); - // Tracking Efficiency ue.add("Inclusive/hPtNumEff", "Tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtNumEffVsMult", "Tracking efficiency numerator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", + HistType::kTH2D, {ptAxis, multAxis}); + ue.add("Inclusive/hPtDenEff", "Tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtDenEffVsMult", "Tracking efficiency denominator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", + HistType::kTH2D, {ptAxis, multAxis}); - // Primary Fraction ue.add("Inclusive/hPtAllReco", "All reconstructed tracks;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtAllRecoVsMult", "All reconstructed tracks vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", + HistType::kTH2D, {ptAxis, multAxis}); + ue.add("Inclusive/hPtPrimReco", "Reconstructed primaries;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtPrimRecoVsMult", "Reconstructed primaries vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", + HistType::kTH2D, {ptAxis, multAxis}); + ue.add("Inclusive/hPtSecReco", "Reconstructed secondaries;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtSecRecoVsMult", "Reconstructed secondaries vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", + HistType::kTH2D, {ptAxis, multAxis}); - // Measured spectra ue.add("Inclusive/hPtMeasured", "All measured tracks;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtMeasuredVsMult", "All measured tracks vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", + HistType::kTH2D, {ptAxis, multAxis}); // ======================================================================== - // PARTICLE-SPECIFIC HISTOGRAMS (Pions, Kaons, Protons) + // PARTICLE-SPECIFIC HISTOGRAMS // ======================================================================== const std::array particleNames = {"Pion", "Kaon", "Proton"}; @@ -498,43 +813,94 @@ void MultiplicityPt::init(InitContext const&) const auto& name = particleNames[iSpecies]; const auto& symbol = particleSymbols[iSpecies]; - // Generated histograms + // 1D versions ue.add(Form("%s/hPtPrimGenAll", name.c_str()), Form("All generated %s (no cuts);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtPrimBadVertex", name.c_str()), + Form("Generated %s (bad vertex);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtPrimGen", name.c_str()), Form("Generated %s (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtPrimRecoEv", name.c_str()), + Form("Generated %s (reco events);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + ue.add(Form("%s/hPtPrimGoodEv", name.c_str()), + Form("Generated %s (good events);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + // 2D versions (vs multiplicity class) + ue.add(Form("%s/hPtPrimGenAllVsMult", name.c_str()), + Form("All generated %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add(Form("%s/hPtPrimBadVertexVsMult", name.c_str()), + Form("Generated %s (bad vertex) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add(Form("%s/hPtPrimGenVsMult", name.c_str()), + Form("Generated %s (after phys sel) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add(Form("%s/hPtPrimRecoEvVsMult", name.c_str()), + Form("Generated %s (reco events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add(Form("%s/hPtPrimGoodEvVsMult", name.c_str()), + Form("Generated %s (good events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + // Tracking efficiency ue.add(Form("%s/hPtNumEff", name.c_str()), Form("%s tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtNumEffVsMult", name.c_str()), + Form("%s tracking eff numerator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); ue.add(Form("%s/hPtDenEff", name.c_str()), Form("%s tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtDenEffVsMult", name.c_str()), + Form("%s tracking eff denominator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); - // Reconstructed histograms + // Primary fraction ue.add(Form("%s/hPtAllReco", name.c_str()), Form("All reconstructed %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtAllRecoVsMult", name.c_str()), + Form("All reconstructed %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); ue.add(Form("%s/hPtPrimReco", name.c_str()), Form("Reconstructed primary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtPrimRecoVsMult", name.c_str()), + Form("Reconstructed primary %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); ue.add(Form("%s/hPtSecReco", name.c_str()), Form("Reconstructed secondary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtSecRecoVsMult", name.c_str()), + Form("Reconstructed secondary %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); // Measured spectra ue.add(Form("%s/hPtMeasured", name.c_str()), Form("Measured %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtMeasuredVsMult", name.c_str()), + Form("Measured %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); - // PID quality histograms - TPC ONLY + // PID quality if (enablePIDHistograms) { ue.add(Form("%s/hNsigmaTPC", name.c_str()), Form("TPC n#sigma %s;#it{p}_{T} (GeV/#it{c});n#sigma_{TPC}", symbol.c_str()), @@ -542,6 +908,23 @@ void MultiplicityPt::init(InitContext const&) } } + // ======================================================================== + // PHI CUT MONITORING + // ======================================================================== + + if (applyPhiCut.value) { + ue.add("PhiCut/hPtVsPhiPrimeBefore", "pT vs φ' before cut;p_{T} (GeV/c);φ'", + HistType::kTH2F, {{100, 0, 10}, {100, 0, 0.4}}); + ue.add("PhiCut/hPtVsPhiPrimeAfter", "pT vs φ' after cut;p_{T} (GeV/c);φ'", + HistType::kTH2F, {{100, 0, 10}, {100, 0, 0.4}}); + ue.add("PhiCut/hRejectionRate", "Track rejection rate by phi cut;p_{T} (GeV/c);Rejection Rate", + HistType::kTProfile, {{100, 0, 10}}); + } + + // ======================================================================== + // EVENT SELECTION HISTOGRAM + // ======================================================================== + constexpr int nEvSelBins = 20; constexpr float evSelMin = 0.5f; constexpr float evSelMax = 20.5f; @@ -565,30 +948,92 @@ void MultiplicityPt::init(InitContext const&) ue.add("hvtxZ", "Vertex Z (data);Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); ue.add("hvtxZmc", "MC vertex Z;Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); - LOG(info) << "Initialized MultiplicityPt task with EXCLUSIVE PID for INCLUSIVE + PARTICLE-SPECIFIC (Pi, K, p) analysis"; + LOG(info) << "=== Initialized MultiplicityPt task with ON-THE-FLY PERCENTILE COMPUTATION ==="; + LOG(info) << "Centrality classes: " << kCentralityClasses; + LOG(info) << "Multiplicity estimator: " << multiplicityEstimator.value; + LOG(info) << "IMPORTANT: Run processPercentileCalibration FIRST to build percentile boundaries!"; + if (applyPhiCut.value) { + LOG(info) << "Phi cut ENABLED for pT > " << pTthresholdPhiCut.value << " GeV/c"; + } } // ======================================================================== -// DATA PROCESSING - WITH EXCLUSIVE PID +// PERCENTILE CALIBRATION PASS // ======================================================================== -void MultiplicityPt::processData(CollisionTableData::iterator const& collision, TrackTableData const& tracks) +void MultiplicityPt::processPercentileCalibration(CollisionTableMCTrue const& mcCollisions, + ParticleTableMC const& particles) +{ + LOG(info) << "=== PERCENTILE CALIBRATION PASS ==="; + LOG(info) << "Processing " << mcCollisions.size() << " MC collisions"; + + multiplicityValues.clear(); + multiplicityValues.reserve(mcCollisions.size()); + + for (const auto& mcCollision : mcCollisions) { + // Apply basic cuts + if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) + continue; + + auto particlesInCollision = particles.sliceBy(perMCCol, mcCollision.globalIndex()); + + // Apply INEL cuts + if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) + continue; + if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) + continue; + + // Calculate multiplicity + float mcMult = getMultiplicityMC(mcCollision, particles); + multiplicityValues.push_back(mcMult); + + ue.fill(HIST("Calibration/hRawMultiplicity"), mcMult); + } + + // Compute percentile boundaries + computePercentileBoundaries(); + + LOG(info) << "=== PERCENTILE CALIBRATION COMPLETE ==="; + LOG(info) << "Processed " << multiplicityValues.size() << " events"; + LOG(info) << "Now run processMC and processTrue with these percentiles"; +} + +// ======================================================================== +// DATA PROCESSING +// ======================================================================== +void MultiplicityPt::processData(CollisionTableData::iterator const& collision, + TrackTableData const& tracks, + BCsRun3 const& /*bcs*/) { if (!isEventSelected(collision)) { return; } ue.fill(HIST("hvtxZ"), collision.posZ()); + float magField = 0; + if (applyPhiCut.value) { + const auto& bc = collision.bc_as(); + magField = getMagneticField(bc.timestamp()); + } + for (const auto& track : tracks) { - if (!passesTrackSelection(track)) { + if (applyPhiCut.value && track.pt() >= pTthresholdPhiCut.value) { + float phiPrime = getTransformedPhi(track.phi(), track.sign(), magField); + ue.fill(HIST("PhiCut/hPtVsPhiPrimeBefore"), track.pt(), phiPrime); + } + + if (!passesTrackSelection(track, magField)) { continue; } - // Inclusive charged particle (always filled) + if (applyPhiCut.value && track.pt() >= pTthresholdPhiCut.value) { + float phiPrime = getTransformedPhi(track.phi(), track.sign(), magField); + ue.fill(HIST("PhiCut/hPtVsPhiPrimeAfter"), track.pt(), phiPrime); + } + ue.fill(HIST("Inclusive/hPtMeasured"), track.pt()); ue.fill(HIST("hEta"), track.eta()); ue.fill(HIST("hPhi"), track.phi()); - // Exclusive particle identification int bestSpecies = getBestPIDHypothesis(track); if (bestSpecies == kPion) { @@ -611,29 +1056,53 @@ void MultiplicityPt::processData(CollisionTableData::iterator const& collision, } // ======================================================================== -// MC PROCESSING - WITH FIXED PRIMARY FRACTION CALCULATION +// MC PROCESSING - Using computed percentiles // ======================================================================== void MultiplicityPt::processMC(TrackTableMC const& tracks, aod::McParticles const& particles, CollisionTableMCTrue const& mcCollisions, - CollisionTableMC const& collisions) + CollisionTableMC const& collisions, + BCsRun3 const& /*bcs*/) { + if (!percentilesComputed) { + LOG(warning) << "Percentiles not computed yet! Run processPercentileCalibration first!"; + LOG(warning) << "Using fallback linear binning for now..."; + } + LOG(info) << "=== DEBUG processMC START ==="; LOG(info) << "MC collisions: " << mcCollisions.size(); LOG(info) << "Reconstructed collisions: " << collisions.size(); - // ======================================================================== - // STEP 1: Identify which MC collisions are reconstructable - // ======================================================================== + ue.fill(HIST("MC/GenRecoCollisions"), 1.f, mcCollisions.size()); + ue.fill(HIST("MC/GenRecoCollisions"), 2.f, collisions.size()); + + std::set physicsSelectedMCCollisions; + std::set reconstructedMCCollisions; + std::set selectedMCCollisions; - std::set reconstructableMCCollisions; + std::map mcCollisionMultiplicity; + std::map mcCollisionPercentile; + // First pass: classify MC collisions for (const auto& mcCollision : mcCollisions) { - auto particlesInCollision = particles.sliceBy(perMCCol, mcCollision.globalIndex()); + int64_t mcCollId = mcCollision.globalIndex(); + + float mcMult = getMultiplicityMC(mcCollision, particles); + mcCollisionMultiplicity[mcCollId] = mcMult; + + // Convert to percentile + float percentile = multiplicityToPercentile(mcMult); + mcCollisionPercentile[mcCollId] = percentile; + + ue.fill(HIST("MC/EventLoss/MultGenerated"), percentile); + + auto particlesInCollision = particles.sliceBy(perMCCol, mcCollId); if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) { + ue.fill(HIST("MC/EventLoss/MultBadVertex"), percentile); continue; } + if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) { continue; } @@ -641,17 +1110,13 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, continue; } - reconstructableMCCollisions.insert(mcCollision.globalIndex()); + physicsSelectedMCCollisions.insert(mcCollId); + ue.fill(HIST("MC/EventLoss/MultPhysicsSelected"), percentile); } - LOG(info) << "DEBUG: Physics-selected MC collisions: " << reconstructableMCCollisions.size(); - - // ======================================================================== - // STEP 2: Track reconstruction outcomes - // ======================================================================== + LOG(info) << "Physics-selected MC collisions: " << physicsSelectedMCCollisions.size(); - std::set reconstructedMCCollisions; - std::set selectedMCCollisions; + // Second pass: track reconstructed events std::set selectedCollisionIndices; for (const auto& collision : collisions) { @@ -662,43 +1127,42 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, const auto& mcCollision = collision.mcCollision_as(); int64_t mcCollId = mcCollision.globalIndex(); - if (reconstructableMCCollisions.find(mcCollId) == reconstructableMCCollisions.end()) { + if (physicsSelectedMCCollisions.find(mcCollId) == physicsSelectedMCCollisions.end()) { continue; } - reconstructedMCCollisions.insert(mcCollId); + float percentile = mcCollisionPercentile[mcCollId]; + + if (reconstructedMCCollisions.find(mcCollId) == reconstructedMCCollisions.end()) { + reconstructedMCCollisions.insert(mcCollId); + ue.fill(HIST("MC/EventLoss/MultReconstructed"), percentile); + } if (isEventSelected(collision)) { - selectedMCCollisions.insert(mcCollId); + if (selectedMCCollisions.find(mcCollId) == selectedMCCollisions.end()) { + selectedMCCollisions.insert(mcCollId); + ue.fill(HIST("MC/EventLoss/MultRecoSelected"), percentile); + } selectedCollisionIndices.insert(collision.globalIndex()); ue.fill(HIST("hvtxZ"), collision.posZ()); } } - auto hEventsReconstructable = ue.get(HIST("hEventsReconstructable")); - auto hEventsSelectedReco = ue.get(HIST("hEventsSelectedReco")); - - hEventsReconstructable->SetBinContent(1, reconstructedMCCollisions.size()); - hEventsSelectedReco->SetBinContent(1, selectedMCCollisions.size()); - - int nReconstructableTotal = reconstructableMCCollisions.size(); - int nReconstructableWithReco = reconstructedMCCollisions.size(); - int nSelectedReco = selectedMCCollisions.size(); + LOG(info) << "Reconstructed MC collisions: " << reconstructedMCCollisions.size(); + LOG(info) << "Selected MC collisions: " << selectedMCCollisions.size(); - LOG(info) << "DEBUG: Reconstructed MC collisions: " << nReconstructableWithReco; - LOG(info) << "DEBUG: Selected MC collisions: " << nSelectedReco; + int nPhysicsSelected = physicsSelectedMCCollisions.size(); + int nReconstructed = reconstructedMCCollisions.size(); + int nSelected = selectedMCCollisions.size(); - if (nReconstructableTotal > 0) { - ue.fill(HIST("hEventLossBreakdown"), 1, nReconstructableTotal); - ue.fill(HIST("hEventLossBreakdown"), 2, nReconstructableWithReco); - ue.fill(HIST("hEventLossBreakdown"), 3, nSelectedReco); - ue.fill(HIST("hEventLossBreakdown"), 4, (nSelectedReco * 100.0 / nReconstructableTotal)); + if (nPhysicsSelected > 0) { + ue.fill(HIST("hEventLossBreakdown"), 1, nPhysicsSelected); + ue.fill(HIST("hEventLossBreakdown"), 2, nReconstructed); + ue.fill(HIST("hEventLossBreakdown"), 3, nSelected); + ue.fill(HIST("hEventLossBreakdown"), 4, (nSelected * 100.0 / nPhysicsSelected)); } - // ======================================================================== - // STEP 3: Process tracks with EXCLUSIVE PID - FIXED PRIMARY FRACTION - // ======================================================================== - + // Process tracks int totalTracksProcessed = 0; int tracksFromSelectedEvents = 0; int tracksPassingSelection = 0; @@ -720,51 +1184,68 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, } tracksFromSelectedEvents++; - if (!passesTrackSelection(track)) + if (!collision.has_mcCollision()) continue; - tracksPassingSelection++; - // ======================================================================== - // INCLUSIVE CHARGED PARTICLE ANALYSIS - // ======================================================================== + const auto& mcCollision = collision.mcCollision_as(); + float percentile = mcCollisionPercentile[mcCollision.globalIndex()]; + + float magField = 0; + if (applyPhiCut.value) { + const auto& bc = collision.bc_as(); + magField = getMagneticField(bc.timestamp()); + } + if (!passesTrackSelection(track, magField)) { + continue; + } + tracksPassingSelection++; + + // Inclusive charged particle ue.fill(HIST("Inclusive/hPtMeasured"), track.pt()); + ue.fill(HIST("Inclusive/hPtMeasuredVsMult"), track.pt(), percentile); ue.fill(HIST("Inclusive/hPtAllReco"), track.pt()); + ue.fill(HIST("Inclusive/hPtAllRecoVsMult"), track.pt(), percentile); ue.fill(HIST("hEta"), track.eta()); ue.fill(HIST("hPhi"), track.phi()); - // ======================================================================== - // EFFICIENCY NUMERATOR: Fill based on TRUE particle type - // ======================================================================== - + // Efficiency numerator if (track.has_mcParticle()) { const auto& particle = track.mcParticle(); int pdgCode = std::abs(particle.pdgCode()); if (particle.isPhysicalPrimary()) { ue.fill(HIST("Inclusive/hPtNumEff"), particle.pt()); + ue.fill(HIST("Inclusive/hPtNumEffVsMult"), particle.pt(), percentile); ue.fill(HIST("Inclusive/hPtPrimReco"), track.pt()); + ue.fill(HIST("Inclusive/hPtPrimRecoVsMult"), track.pt(), percentile); - // Fill particle-specific efficiency numerator based on TRUE type if (pdgCode == PDGPion) { ue.fill(HIST("Pion/hPtNumEff"), particle.pt()); + ue.fill(HIST("Pion/hPtNumEffVsMult"), particle.pt(), percentile); } if (pdgCode == PDGKaon) { ue.fill(HIST("Kaon/hPtNumEff"), particle.pt()); + ue.fill(HIST("Kaon/hPtNumEffVsMult"), particle.pt(), percentile); } if (pdgCode == PDGProton) { ue.fill(HIST("Proton/hPtNumEff"), particle.pt()); + ue.fill(HIST("Proton/hPtNumEffVsMult"), particle.pt(), percentile); } } else { ue.fill(HIST("Inclusive/hPtSecReco"), track.pt()); + ue.fill(HIST("Inclusive/hPtSecRecoVsMult"), track.pt(), percentile); } } + // Identified particle analysis int bestSpecies = getBestPIDHypothesis(track); if (bestSpecies == kPion) { ue.fill(HIST("Pion/hPtMeasured"), track.pt()); + ue.fill(HIST("Pion/hPtMeasuredVsMult"), track.pt(), percentile); ue.fill(HIST("Pion/hPtAllReco"), track.pt()); + ue.fill(HIST("Pion/hPtAllRecoVsMult"), track.pt(), percentile); particleTracksIdentified[kPion]++; if (enablePIDHistograms) { @@ -775,15 +1256,20 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, const auto& particle = track.mcParticle(); if (particle.isPhysicalPrimary()) { ue.fill(HIST("Pion/hPtPrimReco"), track.pt()); + ue.fill(HIST("Pion/hPtPrimRecoVsMult"), track.pt(), percentile); particleTracksPrimary[kPion]++; } else { ue.fill(HIST("Pion/hPtSecReco"), track.pt()); + ue.fill(HIST("Pion/hPtSecRecoVsMult"), track.pt(), percentile); particleTracksSecondary[kPion]++; } } + } else if (bestSpecies == kKaon) { ue.fill(HIST("Kaon/hPtMeasured"), track.pt()); + ue.fill(HIST("Kaon/hPtMeasuredVsMult"), track.pt(), percentile); ue.fill(HIST("Kaon/hPtAllReco"), track.pt()); + ue.fill(HIST("Kaon/hPtAllRecoVsMult"), track.pt(), percentile); particleTracksIdentified[kKaon]++; if (enablePIDHistograms) { @@ -792,20 +1278,22 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, if (track.has_mcParticle()) { const auto& particle = track.mcParticle(); - - // KEY FIX: Primary fraction of identified kaons - // A misidentified pion that is primary still contributes to primary fraction if (particle.isPhysicalPrimary()) { ue.fill(HIST("Kaon/hPtPrimReco"), track.pt()); + ue.fill(HIST("Kaon/hPtPrimRecoVsMult"), track.pt(), percentile); particleTracksPrimary[kKaon]++; } else { ue.fill(HIST("Kaon/hPtSecReco"), track.pt()); + ue.fill(HIST("Kaon/hPtSecRecoVsMult"), track.pt(), percentile); particleTracksSecondary[kKaon]++; } } + } else if (bestSpecies == kProton) { ue.fill(HIST("Proton/hPtMeasured"), track.pt()); + ue.fill(HIST("Proton/hPtMeasuredVsMult"), track.pt(), percentile); ue.fill(HIST("Proton/hPtAllReco"), track.pt()); + ue.fill(HIST("Proton/hPtAllRecoVsMult"), track.pt(), percentile); particleTracksIdentified[kProton]++; if (enablePIDHistograms) { @@ -814,22 +1302,24 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, if (track.has_mcParticle()) { const auto& particle = track.mcParticle(); - - // KEY FIX: Primary fraction of identified protons if (particle.isPhysicalPrimary()) { ue.fill(HIST("Proton/hPtPrimReco"), track.pt()); + ue.fill(HIST("Proton/hPtPrimRecoVsMult"), track.pt(), percentile); particleTracksPrimary[kProton]++; } else { ue.fill(HIST("Proton/hPtSecReco"), track.pt()); + ue.fill(HIST("Proton/hPtSecRecoVsMult"), track.pt(), percentile); particleTracksSecondary[kProton]++; } } } } + LOG(info) << "=== DEBUG TRACK COUNTING ==="; LOG(info) << "Total tracks processed: " << totalTracksProcessed; LOG(info) << "Tracks from selected events: " << tracksFromSelectedEvents; LOG(info) << "Tracks passing selection: " << tracksPassingSelection; + LOG(info) << "Pions identified: " << particleTracksIdentified[kPion] << ", primary: " << particleTracksPrimary[kPion] << ", secondary: " << particleTracksSecondary[kPion]; @@ -840,125 +1330,152 @@ void MultiplicityPt::processMC(TrackTableMC const& tracks, << ", primary: " << particleTracksPrimary[kProton] << ", secondary: " << particleTracksSecondary[kProton]; - // Calculate and log primary fractions - if (particleTracksIdentified[kPion] > 0) { - float pionPrimFrac = static_cast(particleTracksPrimary[kPion]) / particleTracksIdentified[kPion]; - LOG(info) << "Pion primary fraction: " << pionPrimFrac * 100.0 << "%"; - } - if (particleTracksIdentified[kKaon] > 0) { - float kaonPrimFrac = static_cast(particleTracksPrimary[kKaon]) / particleTracksIdentified[kKaon]; - LOG(info) << "Kaon primary fraction: " << kaonPrimFrac * 100.0 << "%"; - } - if (particleTracksIdentified[kProton] > 0) { - float protonPrimFrac = static_cast(particleTracksPrimary[kProton]) / particleTracksIdentified[kProton]; - LOG(info) << "Proton primary fraction: " << protonPrimFrac * 100.0 << "%"; - } - LOG(info) << "=== DEBUG processMC END ==="; } // ======================================================================== -// TRUE MC PROCESSING - WITH PARTICLE-SPECIFIC SIGNAL LOSS +// TRUE MC PROCESSING - Using computed percentiles // ======================================================================== void MultiplicityPt::processTrue(CollisionTableMCTrue const& mcCollisions, ParticleTableMC const& particles) { + if (!percentilesComputed) { + LOG(warning) << "Percentiles not computed yet! Run processPercentileCalibration first!"; + } + LOG(info) << "=== DEBUG processTrue START ==="; LOG(info) << "Number of MC collisions: " << mcCollisions.size(); - int nPassPhysicsSelection = 0; - int nParticlesFilledAll = 0; - int nParticlesFilledAfterPS = 0; + int nAllGenerated = 0; + int nBadVertex = 0; + int nPhysicsSelected = 0; std::array particleCountAll = {0}; + std::array particleCountBadVertex = {0}; std::array particleCountAfterPS = {0}; for (const auto& mcCollision : mcCollisions) { - // Count EVERY generated event - ue.fill(HIST("hEventsAllGen"), 1.0); + nAllGenerated++; + + float mcMult = getMultiplicityMC(mcCollision, particles); + float percentile = multiplicityToPercentile(mcMult); ue.fill(HIST("hvtxZmc"), mcCollision.posZ()); auto particlesInCollision = particles.sliceBy(perMCCol, mcCollision.globalIndex()); - // ======================================================================== - // Fill ALL generated primaries BEFORE physics selection - // ======================================================================== + // Fill ALL generated primaries BEFORE any cuts for (const auto& particle : particlesInCollision) { if (isGoodPrimary(particle)) { ue.fill(HIST("Inclusive/hPtPrimGenAll"), particle.pt()); - nParticlesFilledAll++; + ue.fill(HIST("Inclusive/hPtPrimGenAllVsMult"), particle.pt(), percentile); } if (isGoodPrimarySpecies(particle)) { ue.fill(HIST("Pion/hPtPrimGenAll"), particle.pt()); + ue.fill(HIST("Pion/hPtPrimGenAllVsMult"), particle.pt(), percentile); particleCountAll[kPion]++; } if (isGoodPrimarySpecies(particle)) { ue.fill(HIST("Kaon/hPtPrimGenAll"), particle.pt()); + ue.fill(HIST("Kaon/hPtPrimGenAllVsMult"), particle.pt(), percentile); particleCountAll[kKaon]++; } if (isGoodPrimarySpecies(particle)) { ue.fill(HIST("Proton/hPtPrimGenAll"), particle.pt()); + ue.fill(HIST("Proton/hPtPrimGenAllVsMult"), particle.pt(), percentile); particleCountAll[kProton]++; } } - // ======================================================================== - // Apply physics selection - // ======================================================================== - if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) + // Apply vertex cut + if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) { + nBadVertex++; + + for (const auto& particle : particlesInCollision) { + if (isGoodPrimary(particle)) { + ue.fill(HIST("Inclusive/hPtPrimBadVertex"), particle.pt()); + ue.fill(HIST("Inclusive/hPtPrimBadVertexVsMult"), particle.pt(), percentile); + } + + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Pion/hPtPrimBadVertex"), particle.pt()); + ue.fill(HIST("Pion/hPtPrimBadVertexVsMult"), particle.pt(), percentile); + particleCountBadVertex[kPion]++; + } + + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Kaon/hPtPrimBadVertex"), particle.pt()); + ue.fill(HIST("Kaon/hPtPrimBadVertexVsMult"), particle.pt(), percentile); + particleCountBadVertex[kKaon]++; + } + + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Proton/hPtPrimBadVertex"), particle.pt()); + ue.fill(HIST("Proton/hPtPrimBadVertexVsMult"), particle.pt(), percentile); + particleCountBadVertex[kProton]++; + } + } continue; + } + // Apply INEL cuts if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) continue; if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) continue; - // Count physics-selected events - ue.fill(HIST("hEventsPassPhysicsSelection"), 1.0); - nPassPhysicsSelection++; + nPhysicsSelected++; - // Fill primaries AFTER physics selection + // Fill primaries AFTER physics selection (denominator for efficiency) for (const auto& particle : particlesInCollision) { if (isGoodPrimary(particle)) { ue.fill(HIST("Inclusive/hPtDenEff"), particle.pt()); + ue.fill(HIST("Inclusive/hPtDenEffVsMult"), particle.pt(), percentile); ue.fill(HIST("Inclusive/hPtPrimGen"), particle.pt()); - nParticlesFilledAfterPS++; + ue.fill(HIST("Inclusive/hPtPrimGenVsMult"), particle.pt(), percentile); } if (isGoodPrimarySpecies(particle)) { ue.fill(HIST("Pion/hPtDenEff"), particle.pt()); + ue.fill(HIST("Pion/hPtDenEffVsMult"), particle.pt(), percentile); ue.fill(HIST("Pion/hPtPrimGen"), particle.pt()); + ue.fill(HIST("Pion/hPtPrimGenVsMult"), particle.pt(), percentile); particleCountAfterPS[kPion]++; } if (isGoodPrimarySpecies(particle)) { ue.fill(HIST("Kaon/hPtDenEff"), particle.pt()); + ue.fill(HIST("Kaon/hPtDenEffVsMult"), particle.pt(), percentile); ue.fill(HIST("Kaon/hPtPrimGen"), particle.pt()); + ue.fill(HIST("Kaon/hPtPrimGenVsMult"), particle.pt(), percentile); particleCountAfterPS[kKaon]++; } if (isGoodPrimarySpecies(particle)) { ue.fill(HIST("Proton/hPtDenEff"), particle.pt()); + ue.fill(HIST("Proton/hPtDenEffVsMult"), particle.pt(), percentile); ue.fill(HIST("Proton/hPtPrimGen"), particle.pt()); + ue.fill(HIST("Proton/hPtPrimGenVsMult"), particle.pt(), percentile); particleCountAfterPS[kProton]++; } } } LOG(info) << "=== DEBUG processTrue END ==="; - LOG(info) << "All generated events: " << mcCollisions.size(); - LOG(info) << "Passing physics selection: " << nPassPhysicsSelection; - LOG(info) << "Total primaries (before PS): " << nParticlesFilledAll; - LOG(info) << "Total primaries (after PS): " << nParticlesFilledAfterPS; + LOG(info) << "All generated events: " << nAllGenerated; + LOG(info) << "Events with bad vertex: " << nBadVertex; + LOG(info) << "Passing physics selection: " << nPhysicsSelected; LOG(info) << "=== PARTICLE-SPECIFIC STATISTICS ==="; LOG(info) << "Pions - All: " << particleCountAll[kPion] + << ", Bad vertex: " << particleCountBadVertex[kPion] << ", After PS: " << particleCountAfterPS[kPion]; LOG(info) << "Kaons - All: " << particleCountAll[kKaon] + << ", Bad vertex: " << particleCountBadVertex[kKaon] << ", After PS: " << particleCountAfterPS[kKaon]; LOG(info) << "Protons - All: " << particleCountAll[kProton] + << ", Bad vertex: " << particleCountBadVertex[kProton] << ", After PS: " << particleCountAfterPS[kProton]; } From 0444202aacd153cac8fa1b72214969ab6d891cd3 Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Mon, 9 Mar 2026 17:29:25 -0600 Subject: [PATCH 14/16] Fix multiplicity issues --- PWGLF/Tasks/Nuspex/MultiplicityPt.cxx | 1605 ++++++++++--------------- 1 file changed, 620 insertions(+), 985 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx index 6d30693af1b..3254377c57c 100644 --- a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx +++ b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx @@ -10,6 +10,7 @@ // or submit itself to any jurisdiction. #include "PWGLF/DataModel/LFParticleIdentification.h" +#include "PWGLF/DataModel/mcCentrality.h" // For McCentFT0Ms #include "PWGLF/DataModel/spectraTOF.h" #include "PWGLF/Utils/inelGt.h" @@ -24,8 +25,6 @@ #include "Common/DataModel/PIDResponseTPC.h" #include "Common/DataModel/TrackSelectionTables.h" -#include "CCDB/BasicCCDBManager.h" -#include "DataFormatsParameters/GRPMagField.h" #include "Framework/ASoAHelpers.h" #include "Framework/AnalysisDataModel.h" #include "Framework/AnalysisTask.h" @@ -45,6 +44,7 @@ #include #include #include +#include // For std::accumulate #include #include #include @@ -56,14 +56,17 @@ using namespace o2::framework::expressions; using BCsRun3 = soa::Join; +//============================================================================= +// Main Analysis Struct +//============================================================================= struct MultiplicityPt { // Service Service pdg; - // Add CCDB service for magnetic field - Service ccdb; - + //=========================================================================== + // Configurable Parameters + //=========================================================================== Configurable isRun3{"isRun3", true, "is Run3 dataset"}; Configurable cfgCutVertex{"cfgCutVertex", 10.0f, "Accepted z-vertex range"}; Configurable cfgINELCut{"cfgINELCut", 0, "INEL event selection: 0 no sel, 1 INEL>0, 2 INEL>1"}; @@ -80,8 +83,6 @@ struct MultiplicityPt { Configurable lastRequiredTrdCluster{"lastRequiredTrdCluster", -1, "Last cluster to require in TRD"}; Configurable requireTrdOnly{"requireTrdOnly", false, "Require only tracks from TRD"}; Configurable requireNoTrd{"requireNoTrd", false, "Require tracks without TRD"}; - Configurable multiplicityEstimator{"multiplicityEstimator", 6, - "Multiplicity estimator: 0=NoMult, 1=MultFV0M, 2=MultFT0M, 3=MultFDDM, 4=MultTracklets, 5=MultTPC, 6=MultNTracksPV, 7=MultNTracksPVeta1, 8=CentFT0C, 9=CentFT0M, 10=CentFV0A"}; // Analysis switches Configurable enableDCAHistograms{"enableDCAHistograms", false, "Enable DCA histograms"}; @@ -98,77 +99,57 @@ struct MultiplicityPt { Configurable maxChi2PerClusterITS{"maxChi2PerClusterITS", 36.f, "Additional cut on the maximum value of the chi2 per cluster in the ITS"}; Configurable maxDcaXYFactor{"maxDcaXYFactor", 1.f, "Additional cut on the maximum value of the DCA xy (multiplicative factor)"}; Configurable maxDcaZ{"maxDcaZ", 0.1f, "Additional cut on the maximum value of the DCA z"}; - Configurable minTPCNClsFound{"minTPCNClsFound", 70.f, "Additional cut on the minimum value of the number of found clusters in the TPC"}; - Configurable min_ITS_nClusters{"min_ITS_nClusters", 5, "minimum number of found ITS clusters"}; + Configurable minTPCNClsFound{"minTPCNClsFound", 70.0f, "min number of found TPC clusters"}; + Configurable minTPCNClsPID{"minTPCNClsPID", 130.0f, "min number of PID TPC clusters"}; + Configurable nClTPCFoundCut{"nClTPCFoundCut", false, "Apply TPC found clusters cut"}; + Configurable nClTPCPIDCut{"nClTPCPIDCut", true, "Apply TPC clusters for PID cut"}; // Phi cut parameters - Configurable applyPhiCut{"applyPhiCut", true, "Apply phi sector cut to remove problematic TPC regions"}; - Configurable pTthresholdPhiCut{"pTthresholdPhiCut", 2.0f, "pT threshold above which to apply phi cut"}; - Configurable phiCutLowParam1{"phiCutLowParam1", 0.119297, "First parameter for low phi cut"}; - Configurable phiCutLowParam2{"phiCutLowParam2", 0.000379693, "Second parameter for low phi cut"}; - Configurable phiCutHighParam1{"phiCutHighParam1", 0.16685, "First parameter for high phi cut"}; - Configurable phiCutHighParam2{"phiCutHighParam2", 0.00981942, "Second parameter for high phi cut"}; + Configurable applyPhiCut{"applyPhiCut", false, "Apply phi sector cut"}; // Basic track cuts Configurable cfgTrkEtaCut{"cfgTrkEtaCut", 0.8f, "Eta range for tracks"}; Configurable cfgTrkLowPtCut{"cfgTrkLowPtCut", 0.15f, "Minimum constituent pT"}; + // PID selection - make them configurable per particle + Configurable cfgCutNsigmaPi{"cfgCutNsigmaPi", 3.0f, "nsigma cut for pions"}; + Configurable cfgCutNsigmaKa{"cfgCutNsigmaKa", 2.5f, "nsigma cut for kaons"}; + Configurable cfgCutNsigmaPr{"cfgCutNsigmaPr", 2.5f, "nsigma cut for protons"}; + // Custom track cuts matching spectraTOF TrackSelection customTrackCuts; - // TF1 pointers for phi cuts - TF1* fphiCutLow = nullptr; - TF1* fphiCutHigh = nullptr; - // Histogram Registry HistogramRegistry ue; - // ======================================================================== - // CENTRALITY/MULTIPLICITY CLASSES - Using same bins as before for consistency - // ======================================================================== - static constexpr int kCentralityClasses = 10; - static constexpr double CentClasses[kCentralityClasses + 1] = {0.0, 1.0, 5.0, 10.0, 15.0, 20.0, 30.0, 40.0, 50.0, 70.0, 100.0}; + //=========================================================================== + // Table Definitions - Using individual tables, not joined for MC + //=========================================================================== - // Multiplicity percentile boundaries (computed on first pass) - std::vector multPercentileboundaries; - bool percentilesComputed = false; + // Data collisions (not used but kept for completeness) + using CollisionTableData = soa::Join; - // Storage for multiplicity distribution (for percentile calculation) - std::vector multiplicityValues; - - // Table definitions - NO McCentFT0Ms dependency - using CollisionTableData = soa::Join; - using CollisionTableMC = soa::Join; - - // Track tables - TPC PID only + // Track tables using TrackTableData = soa::Join; using TrackTableMC = soa::Join; - // MC tables - NO McCentFT0Ms - using CollisionTableMCTrue = aod::McCollisions; - using ParticleTableMC = aod::McParticles; + // MC particles table + using ParticlesMC = aod::McParticles; + + // MC collisions table + using McCollisions = aod::McCollisions; + + // Reconstructed collisions (without joins that cause size mismatch) + using RecoCollisions = aod::Collisions; // Preslice for MC particles Preslice perMCCol = aod::mcparticle::mcCollisionId; - // Multiplicity estimator enum - enum MultCodes : int { - kNoMultiplicity = 0, - kMultFV0M = 1, - kMultFT0M = 2, - kMultFDDM = 3, - kMultTracklets = 4, - kMultTPC = 5, - kMultNTracksPV = 6, - kMultNTracksPVeta1 = 7, - kCentralityFT0C = 8, - kCentralityFT0M = 9, - kCentralityFV0A = 10 - }; - - // Particle species enum + //=========================================================================== + // Constants + //=========================================================================== enum ParticleSpecies : int { kPion = 0, kKaon = 1, @@ -176,248 +157,53 @@ struct MultiplicityPt { kNSpecies = 3 }; - // PDG codes static constexpr int PDGPion = 211; static constexpr int PDGKaon = 321; static constexpr int PDGProton = 2212; - void processData(CollisionTableData::iterator const& collision, - TrackTableData const& tracks, - BCsRun3 const& bcs); - PROCESS_SWITCH(MultiplicityPt, processData, "process data", false); - - // MC processing - First pass to build percentiles - void processPercentileCalibration(CollisionTableMCTrue const& mcCollisions, - ParticleTableMC const& particles); - PROCESS_SWITCH(MultiplicityPt, processPercentileCalibration, "Build multiplicity percentile calibration (run first)", false); - - // MC processing - Main analysis - void processMC(TrackTableMC const& tracks, - aod::McParticles const& particles, - CollisionTableMCTrue const& mcCollisions, - CollisionTableMC const& collisions, - BCsRun3 const& bcs); - PROCESS_SWITCH(MultiplicityPt, processMC, "process MC", true); - - // True MC processing - void processTrue(CollisionTableMCTrue const& mcCollisions, - ParticleTableMC const& particles); - PROCESS_SWITCH(MultiplicityPt, processTrue, "process true MC", true); - - // ======================================================================== - // MULTIPLICITY GETTER FUNCTIONS - Using raw charged particle count - // ======================================================================== + //=========================================================================== + // Helper Functions + //=========================================================================== - // Count charged primaries in |eta| < 1.0 - template - int countChargedPrimaries(const MCCollisionType& mcCollision, const ParticleTableMC& particles) const + template + int countGeneratedChargedPrimaries(const ParticleContainer& particles, float etaMax, float ptMin) const { - int nCharged = 0; - auto particlesInColl = particles.sliceBy(perMCCol, mcCollision.globalIndex()); - for (const auto& p : particlesInColl) { - if (!p.isPhysicalPrimary()) - continue; - auto pdgParticle = pdg->GetParticle(p.pdgCode()); + int count = 0; + for (const auto& particle : particles) { + auto pdgParticle = pdg->GetParticle(particle.pdgCode()); if (!pdgParticle || pdgParticle->Charge() == 0.) continue; - if (std::abs(p.eta()) < 1.0) - nCharged++; - } - return nCharged; - } - - // For reconstructed collisions - template - float getMultiplicity(const CollisionType& collision) const - { - switch (multiplicityEstimator.value) { - case kNoMultiplicity: - return 50.f; - case kMultFV0M: - return collision.multZeqFV0A(); - case kMultFT0M: - return collision.multZeqFT0A() + collision.multZeqFT0C(); - case kMultFDDM: - return collision.multZeqFDDA() + collision.multZeqFDDC(); - case kMultTracklets: - return 0.f; - case kMultTPC: - return collision.multTPC(); - case kMultNTracksPV: - return collision.multZeqNTracksPV(); - case kMultNTracksPVeta1: - return collision.multNTracksPVeta1(); - case kCentralityFT0C: - case kCentralityFT0M: - case kCentralityFV0A: - return collision.multZeqNTracksPV(); - default: - return 0.f; - } - } - - // For MC collisions - returns RAW multiplicity - template - float getMultiplicityMC(const MCCollisionType& mcCollision, const ParticleTableMC& particles) const - { - return static_cast(countChargedPrimaries(mcCollision, particles)); - } - // Convert raw multiplicity to percentile - float multiplicityToPercentile(float rawMult) const - { - if (!percentilesComputed || multPercentileboundaries.empty()) { - // If percentiles not computed, return raw multiplicity - return rawMult; - } - - // Find which percentile bin this multiplicity falls into - for (size_t i = 0; i < multPercentileboundaries.size() - 1; ++i) { - if (rawMult >= multPercentileboundaries[i] && rawMult < multPercentileboundaries[i + 1]) { - // Return the CENTER of the percentile bin - return CentClasses[i] + (CentClasses[i + 1] - CentClasses[i]) / 2.0; - } - } - - // Handle edge cases - if (rawMult < multPercentileboundaries[0]) { - return CentClasses[0]; - } - return CentClasses[kCentralityClasses]; - } - - // Get centrality class index from raw multiplicity - int getCentralityClass(float rawMult) const - { - if (!percentilesComputed || multPercentileboundaries.empty()) { - // Fallback: divide into equal bins - float maxMult = 150.0f; // Assumed maximum - int bin = static_cast((rawMult / maxMult) * kCentralityClasses); - return std::min(bin, kCentralityClasses - 1); - } - - // Use computed percentiles - for (int i = 0; i < kCentralityClasses; ++i) { - if (rawMult >= multPercentileboundaries[i] && rawMult < multPercentileboundaries[i + 1]) { - return i; - } - } - - // Outside range - if (rawMult < multPercentileboundaries[0]) - return 0; - return kCentralityClasses - 1; - } - - // ======================================================================== - // COMPUTE PERCENTILE BOUNDARIES - // ======================================================================== - void computePercentileBoundaries() - { - if (multiplicityValues.empty()) { - LOG(warning) << "No multiplicity values to compute percentiles from!"; - return; - } - - // Sort multiplicity values - std::sort(multiplicityValues.begin(), multiplicityValues.end()); - - LOG(info) << "Computing percentile boundaries from " << multiplicityValues.size() << " events"; - - // Compute percentile boundaries - multPercentileboundaries.clear(); - multPercentileboundaries.reserve(kCentralityClasses + 1); - - for (int i = 0; i <= kCentralityClasses; ++i) { - float percentile = CentClasses[i]; - size_t index = static_cast(percentile / 100.0 * multiplicityValues.size()); - if (index >= multiplicityValues.size()) { - index = multiplicityValues.size() - 1; - } - float boundary = multiplicityValues[index]; - multPercentileboundaries.push_back(boundary); - LOG(info) << "Percentile " << percentile << "% -> Multiplicity >= " << boundary; - } + if (!particle.isPhysicalPrimary()) + continue; - percentilesComputed = true; + if (std::abs(particle.eta()) > etaMax) + continue; - LOG(info) << "=== Percentile Boundaries Computed ==="; - for (int i = 0; i < kCentralityClasses; ++i) { - LOG(info) << "Class " << i << ": [" << CentClasses[i] << "%-" << CentClasses[i + 1] - << "%] = Mult [" << multPercentileboundaries[i] << "-" << multPercentileboundaries[i + 1] << ")"; - } - } + if (particle.pt() < ptMin) + continue; - // ======================================================================== - // MAGNETIC FIELD FUNCTION - // ======================================================================== - int getMagneticField(uint64_t timestamp) - { - static o2::parameters::GRPMagField* grpo = nullptr; - if (grpo == nullptr) { - grpo = ccdb->getForTimeStamp("GLO/Config/GRPMagField", timestamp); - if (grpo == nullptr) { - LOGF(fatal, "GRP object not found for timestamp %llu", timestamp); - return 0; - } - LOGF(info, "Retrieved GRP for timestamp %llu with magnetic field of %d kG", timestamp, grpo->getNominalL3Field()); + count++; } - return grpo->getNominalL3Field(); + return count; } - // ======================================================================== - // PHI CUT FUNCTION - // ======================================================================== - template - bool passedPhiCut(const TrackType& track, float magField) const + template + bool passedNClTPCFoundCut(const T& trk) const { - if (!applyPhiCut.value) { - return true; - } - - if (track.pt() < pTthresholdPhiCut.value) { + if (!nClTPCFoundCut.value) return true; - } - - float pt = track.pt(); - float phi = track.phi(); - int charge = track.sign(); - - if (magField < 0) { - phi = o2::constants::math::TwoPI - phi; - } - if (charge < 0) { - phi = o2::constants::math::TwoPI - phi; - } - - phi += o2::constants::math::PI / 18.0f; - phi = std::fmod(phi, o2::constants::math::PI / 9.0f); - - if (phi < fphiCutHigh->Eval(pt) && phi > fphiCutLow->Eval(pt)) { - return false; - } - - return true; + return trk.tpcNClsFound() >= minTPCNClsFound.value; } - float getTransformedPhi(const float phi, const int charge, const float magField) const + template + bool passedNClTPCPIDCut(const T& trk) const { - float transformedPhi = phi; - if (magField < 0) { - transformedPhi = o2::constants::math::TwoPI - transformedPhi; - } - if (charge < 0) { - transformedPhi = o2::constants::math::TwoPI - transformedPhi; - } - transformedPhi += o2::constants::math::PI / 18.0f; - transformedPhi = std::fmod(transformedPhi, o2::constants::math::PI / 9.0f); - return transformedPhi; + if (!nClTPCPIDCut.value) + return true; + return trk.tpcNClsPID() >= minTPCNClsPID.value; } - // ======================================================================== - // TRACK SELECTION FUNCTIONS - // ======================================================================== - template bool passesCutWoDCA(TrackType const& track) const { @@ -447,16 +233,13 @@ struct MultiplicityPt { constexpr float dcaXYPtScale = 0.0350f; constexpr float dcaXYPtPower = 1.1f; const float maxDcaXY = maxDcaXYFactor.value * (dcaXYConst + dcaXYPtScale / std::pow(track.pt(), dcaXYPtPower)); - if (std::abs(track.dcaXY()) > maxDcaXY) { - return false; - } - return true; + return std::abs(track.dcaXY()) <= maxDcaXY; } return track.isGlobalTrack(); } template - bool passesTrackSelection(TrackType const& track, float magField = 0) const + bool passesTrackSelection(TrackType const& track) const { if (track.eta() < cfgCutEtaMin.value || track.eta() > cfgCutEtaMax.value) return false; @@ -467,15 +250,17 @@ struct MultiplicityPt { if (!passesCutWoDCA(track)) return false; - if (applyPhiCut.value && !passedPhiCut(track, magField)) + if (!passesDCAxyCut(track)) return false; - return passesDCAxyCut(track); - } + if (!passedNClTPCFoundCut(track)) + return false; + + if (!passedNClTPCPIDCut(track)) + return false; - // ======================================================================== - // PID SELECTION FUNCTIONS - // ======================================================================== + return true; + } template bool passesPIDSelection(TrackType const& track) const @@ -490,7 +275,15 @@ struct MultiplicityPt { nsigmaTPC = track.tpcNSigmaPr(); } - return (std::abs(nsigmaTPC) < cfgCutNsigma.value); + float cutValue = cfgCutNsigma.value; + if constexpr (species == kPion) + cutValue = cfgCutNsigmaPi.value; + if constexpr (species == kKaon) + cutValue = cfgCutNsigmaKa.value; + if constexpr (species == kProton) + cutValue = cfgCutNsigmaPr.value; + + return (std::abs(nsigmaTPC) < cutValue); } template @@ -500,19 +293,18 @@ struct MultiplicityPt { float nsigmaKa = std::abs(track.tpcNSigmaKa()); float nsigmaPr = std::abs(track.tpcNSigmaPr()); - constexpr float largeNSigmaValue = 999.0f; - float minNSigma = largeNSigmaValue; + float minNSigma = 999.0f; int bestSpecies = -1; - if (nsigmaPi < cfgCutNsigma.value && nsigmaPi < minNSigma) { + if (nsigmaPi < cfgCutNsigmaPi.value && nsigmaPi < minNSigma) { minNSigma = nsigmaPi; bestSpecies = kPion; } - if (nsigmaKa < cfgCutNsigma.value && nsigmaKa < minNSigma) { + if (nsigmaKa < cfgCutNsigmaKa.value && nsigmaKa < minNSigma) { minNSigma = nsigmaKa; bestSpecies = kKaon; } - if (nsigmaPr < cfgCutNsigma.value && nsigmaPr < minNSigma) { + if (nsigmaPr < cfgCutNsigmaPr.value && nsigmaPr < minNSigma) { minNSigma = nsigmaPr; bestSpecies = kProton; } @@ -520,80 +312,6 @@ struct MultiplicityPt { return bestSpecies; } - // ======================================================================== - // EVENT SELECTION FUNCTION - // ======================================================================== - - template - bool isEventSelected(CollisionType const& collision) - { - if constexpr (fillHistograms) { - ue.fill(HIST("evsel"), 1.f); - if (collision.isInelGt0()) - ue.fill(HIST("evsel"), 2.f); - if (collision.isInelGt1()) - ue.fill(HIST("evsel"), 3.f); - } - - if (askForCustomTVX.value) { - if (!collision.selection_bit(aod::evsel::kIsTriggerTVX)) - return false; - } else { - if (!collision.sel8()) - return false; - } - - if constexpr (fillHistograms) - ue.fill(HIST("evsel"), 4.f); - - if (removeITSROFrameBorder.value && !collision.selection_bit(aod::evsel::kNoITSROFrameBorder)) - return false; - if constexpr (fillHistograms) - ue.fill(HIST("evsel"), 5.f); - - if (removeNoSameBunchPileup.value && !collision.selection_bit(aod::evsel::kNoSameBunchPileup)) - return false; - if constexpr (fillHistograms) - ue.fill(HIST("evsel"), 6.f); - - if (requireIsGoodZvtxFT0vsPV.value && !collision.selection_bit(aod::evsel::kIsGoodZvtxFT0vsPV)) - return false; - if constexpr (fillHistograms) - ue.fill(HIST("evsel"), 7.f); - - if (requireIsVertexITSTPC.value && !collision.selection_bit(aod::evsel::kIsVertexITSTPC)) - return false; - if constexpr (fillHistograms) - ue.fill(HIST("evsel"), 8.f); - - if (removeNoTimeFrameBorder.value && !collision.selection_bit(aod::evsel::kNoTimeFrameBorder)) - return false; - if constexpr (fillHistograms) - ue.fill(HIST("evsel"), 9.f); - - if (std::abs(collision.posZ()) > cfgCutVertex.value) - return false; - - if constexpr (fillHistograms) { - ue.fill(HIST("evsel"), 13.f); - if (collision.isInelGt0()) - ue.fill(HIST("evsel"), 14.f); - if (collision.isInelGt1()) - ue.fill(HIST("evsel"), 15.f); - } - - if (cfgINELCut.value == 1 && !collision.isInelGt0()) - return false; - if (cfgINELCut.value == 2 && !collision.isInelGt1()) - return false; - - return true; - } - - // ======================================================================== - // PRIMARY SELECTION - // ======================================================================== - template bool isGoodPrimary(ParticleType const& particle) const { @@ -609,44 +327,60 @@ struct MultiplicityPt { if (particle.pt() < cfgTrkLowPtCut.value) return false; - if (std::abs(particle.y()) > cfgCutY.value) - return false; - return true; } - template - bool isGoodPrimarySpecies(ParticleType const& particle) const - { - int pdgCode = std::abs(particle.pdgCode()); - int expectedPDG = 0; + //=========================================================================== + // Process Switches + //=========================================================================== + void processData(CollisionTableData::iterator const& collision, + TrackTableData const& tracks, + BCsRun3 const& bcs); + PROCESS_SWITCH(MultiplicityPt, processData, "process data", false); - if constexpr (species == kPion) - expectedPDG = PDGPion; - else if constexpr (species == kKaon) - expectedPDG = PDGKaon; - else if constexpr (species == kProton) - expectedPDG = PDGProton; + void processMC(TrackTableMC const& tracks, + aod::McParticles const& particles, + aod::McCollisions const& mcCollisions, + RecoCollisions const& collisions, + aod::McCollisionLabels const& labels, + aod::McCentFT0Ms const& centTable, + BCsRun3 const& bcs); + PROCESS_SWITCH(MultiplicityPt, processMC, "process MC", true); - if (pdgCode != expectedPDG) - return false; + //=========================================================================== + // Standard Framework Functions + //=========================================================================== + void init(InitContext const&); - return isGoodPrimary(particle); + void endOfStream(EndOfStreamContext& /*eos*/) + { + LOG(info) << "\n=== END OF STREAM: Writing histograms to output ==="; + auto hGenMult = ue.get(HIST("MC/EventLoss/GenMultVsCent")); + if (hGenMult) { + LOG(info) << "GenMultVsCent: Entries=" << hGenMult->GetEntries() + << ", Integral=" << hGenMult->Integral(); + } + LOG(info) << "=== END OF STREAM COMPLETE ==="; } - - void init(InitContext const&); }; +//============================================================================= +// Workflow Definition +//============================================================================= WorkflowSpec defineDataProcessing(ConfigContext const& cfgc) { return WorkflowSpec{adaptAnalysisTask(cfgc)}; } +//============================================================================= +// Implementation of Member Functions +//============================================================================= + void MultiplicityPt::init(InitContext const&) { - // ======================================================================== - // CUSTOM TRACK CUTS INITIALIZATION - // ======================================================================== + LOG(info) << "=================================================="; + LOG(info) << "Initializing MultiplicityPt task with full centrality diagnostics"; + LOG(info) << "=================================================="; if (useCustomTrackCuts.value) { LOG(info) << "Using custom track cuts matching spectraTOF approach"; @@ -654,7 +388,6 @@ void MultiplicityPt::init(InitContext const&) customTrackCuts.SetRequireITSRefit(requireITS.value); customTrackCuts.SetRequireTPCRefit(requireTPC.value); - customTrackCuts.SetMinNClustersITS(min_ITS_nClusters.value); customTrackCuts.SetRequireGoldenChi2(requireGoldenChi2.value); customTrackCuts.SetMaxChi2PerClusterTPC(maxChi2PerClusterTPC.value); customTrackCuts.SetMaxChi2PerClusterITS(maxChi2PerClusterITS.value); @@ -667,32 +400,7 @@ void MultiplicityPt::init(InitContext const&) customTrackCuts.print(); } - // ======================================================================== - // PHI CUT INITIALIZATION - // ======================================================================== - - if (applyPhiCut.value) { - fphiCutLow = new TF1("StandardPhiCutLow", - Form("%f/x/x+pi/18.0-%f", - phiCutLowParam1.value, phiCutLowParam2.value), - 0, 50); - fphiCutHigh = new TF1("StandardPhiCutHigh", - Form("%f/x+pi/18.0+%f", - phiCutHighParam1.value, phiCutHighParam2.value), - 0, 50); - - LOGF(info, "=== Phi Cut Parameters ==="); - LOGF(info, "Low cut: %.6f/x² + pi/18 - %.6f", - phiCutLowParam1.value, phiCutLowParam2.value); - LOGF(info, "High cut: %.6f/x + pi/18 + %.6f", - phiCutHighParam1.value, phiCutHighParam2.value); - LOGF(info, "Applied for pT > %.1f GeV/c", pTthresholdPhiCut.value); - } - - // ======================================================================== - // AXIS DEFINITIONS - // ======================================================================== - + // Axis definitions ConfigurableAxis ptBinning{ "ptBinning", {0.0, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, @@ -703,109 +411,162 @@ void MultiplicityPt::init(InitContext const&) "pT bin limits"}; AxisSpec ptAxis = {ptBinning, "#it{p}_{T} (GeV/#it{c})"}; - // Multiplicity axis - initially raw multiplicity, will represent percentiles after calibration - std::vector centBins(CentClasses, CentClasses + kCentralityClasses + 1); - AxisSpec multAxis = {centBins, "Centrality/Multiplicity Class (%)"}; + std::vector centBinningStd = {0., 1., 5., 10., 15., 20., 30., 40., 50., 60., 70., 80., 90., 100.}; - // Raw multiplicity axis for calibration - AxisSpec rawMultAxis = {150, 0, 150, "N_{ch} (|#eta| < 1.0)"}; + // Fine centrality binning for diagnostics (100 bins, guaranteed increasing) + std::vector centBinningFine; + for (int i = 0; i <= 100; i++) { + centBinningFine.push_back(static_cast(i)); + } - // ======================================================================== - // HISTOGRAM REGISTRY - // ======================================================================== + AxisSpec centAxis = {centBinningStd, "FT0M Centrality (%)"}; + AxisSpec centFineAxis = {centBinningFine, "FT0M Centrality (%)"}; - // Multiplicity distribution for percentile calibration - ue.add("Calibration/hRawMultiplicity", "Raw multiplicity distribution;N_{ch};Events", - HistType::kTH1D, {rawMultAxis}); + // Multiplicity axes - properly defined + std::vector multBins; + for (int i = 0; i <= 200; i++) { + multBins.push_back(static_cast(i)); + } + AxisSpec multAxis = {multBins, "N_{ch}^{gen} (|#eta|<0.8)"}; - // Event counting - ue.add("MC/GenRecoCollisions", "Generated and Reconstructed MC Collisions", HistType::kTH1D, {{10, 0.5, 10.5}}); + // Reconstructed multiplicity axis - properly defined with explicit bin edges + std::vector recoMultBins; + for (int i = 0; i <= 100; i++) { + recoMultBins.push_back(static_cast(i)); + } + AxisSpec recoMultAxis = {recoMultBins, "N_{ch}^{reco}"}; + + //=========================================================================== + // Comprehensive Histogram Registration + //=========================================================================== + + // Centrality diagnostic histograms - USE FINE BINNING + ue.add("Centrality/hCentRaw", "Raw FT0M Centrality (no cuts);Centrality (%);Counts", + HistType::kTH1D, {centFineAxis}); + ue.add("Centrality/hCentAfterVtx", "Centrality after vertex cut;Centrality (%);Counts", + HistType::kTH1D, {centFineAxis}); + ue.add("Centrality/hCentAfterINEL", "Centrality after INEL cut;Centrality (%);Counts", + HistType::kTH1D, {centFineAxis}); + ue.add("Centrality/hCentAfterAll", "Centrality after all cuts;Centrality (%);Counts", + HistType::kTH1D, {centFineAxis}); + + // 2D correlations - USE FINE BINNING FOR DIAGNOSTICS + ue.add("Centrality/hCentVsMult", "Centrality vs Generated Multiplicity;Centrality (%);N_{ch}^{gen}", + HistType::kTH2D, {centFineAxis, multAxis}); + ue.add("Centrality/hMultVsCent", "Generated Multiplicity vs Centrality;N_{ch}^{gen};Centrality (%)", + HistType::kTH2D, {multAxis, centFineAxis}); + ue.add("Centrality/hCentVsVz", "Centrality vs Vertex Z;Centrality (%);V_{z} (cm)", + HistType::kTH2D, {centFineAxis, {40, -20, 20}}); + ue.add("Centrality/hRecoMultVsCent", "Reconstructed Track Multiplicity vs Centrality;Centrality (%);N_{tracks}^{reco}", + HistType::kTH2D, {centFineAxis, recoMultAxis}); + ue.add("Centrality/hGenMultPerCent", "Generated Multiplicity Distribution per Centrality Bin;Centrality (%);", + HistType::kTH2D, {centFineAxis, multAxis}); + + // Vertex resolution vs centrality + ue.add("Centrality/hVertexResVsCent", "Vertex Resolution vs Centrality;Centrality (%);V_{z} resolution (cm)", + HistType::kTH2D, {centFineAxis, {100, -1, 1}}); + + // INEL class distributions + ue.add("INEL/hINELClass", "INEL Class for MC Collisions;INEL Class;Counts", + HistType::kTH1D, {{3, 0.5, 3.5}}); + auto hINEL = ue.get(HIST("INEL/hINELClass")); + hINEL->GetXaxis()->SetBinLabel(1, "INEL0"); + hINEL->GetXaxis()->SetBinLabel(2, "INEL>0"); + hINEL->GetXaxis()->SetBinLabel(3, "INEL>1"); + + ue.add("INEL/hINELVsCent", "INEL Class vs Centrality;Centrality (%);INEL Class", + HistType::kTH2D, {centFineAxis, {3, 0.5, 3.5}}); + + // Cut flow + ue.add("CutFlow/hCutStats", "Cut Statistics;Cut Stage;Counts", + HistType::kTH1D, {{6, 0.5, 6.5}}); + auto hCut = ue.get(HIST("CutFlow/hCutStats")); + hCut->GetXaxis()->SetBinLabel(1, "All reco events"); + hCut->GetXaxis()->SetBinLabel(2, "Has MC match"); + hCut->GetXaxis()->SetBinLabel(3, "Has centrality"); + hCut->GetXaxis()->SetBinLabel(4, "Pass vertex"); + hCut->GetXaxis()->SetBinLabel(5, "Pass INEL"); + hCut->GetXaxis()->SetBinLabel(6, "Selected"); + + ue.add("CutFlow/hCentPerCut", "Centrality Distribution at Each Cut;Cut Stage;Centrality (%)", + HistType::kTH2D, {{6, 0.5, 6.5}, centFineAxis}); + + ue.add("MC/GenRecoCollisions", "Generated and Reconstructed MC Collisions", + HistType::kTH1D, {{10, 0.5, 10.5}}); auto hColl = ue.get(HIST("MC/GenRecoCollisions")); hColl->GetXaxis()->SetBinLabel(1, "Collisions generated"); hColl->GetXaxis()->SetBinLabel(2, "Collisions reconstructed"); + hColl->GetXaxis()->SetBinLabel(3, "INEL>0"); + hColl->GetXaxis()->SetBinLabel(4, "INEL>1"); - // Event loss histograms - ue.add("MC/EventLoss/MultGenerated", "Generated events vs multiplicity", - HistType::kTH1D, {multAxis}); - ue.add("MC/EventLoss/MultBadVertex", "Events with bad vertex vs multiplicity", - HistType::kTH1D, {multAxis}); - ue.add("MC/EventLoss/MultPhysicsSelected", "Physics-selected events vs multiplicity", - HistType::kTH1D, {multAxis}); - ue.add("MC/EventLoss/MultReconstructed", "Reconstructed events vs multiplicity", - HistType::kTH1D, {multAxis}); - ue.add("MC/EventLoss/MultRecoSelected", "Reconstructed+selected events vs multiplicity", - HistType::kTH1D, {multAxis}); - - ue.add("hEventLossBreakdown", "Event loss breakdown", HistType::kTH1D, {{4, 0.5, 4.5}}); + ue.add("hEventLossBreakdown", "Event loss breakdown", + HistType::kTH1D, {{4, 0.5, 4.5}}); auto hLoss = ue.get(HIST("hEventLossBreakdown")); hLoss->GetXaxis()->SetBinLabel(1, "Physics selected"); hLoss->GetXaxis()->SetBinLabel(2, "Reconstructed"); hLoss->GetXaxis()->SetBinLabel(3, "Selected"); hLoss->GetXaxis()->SetBinLabel(4, "Final efficiency"); - // ======================================================================== - // INCLUSIVE CHARGED PARTICLE HISTOGRAMS - // ======================================================================== - + // Multiplicity histograms + ue.add("MC/EventLoss/NchGenerated", "Generated charged multiplicity;N_{ch}^{gen} (|#eta|<0.8);Counts", + HistType::kTH1D, {{200, 0, 200}}); + ue.add("MC/EventLoss/NchGenerated_PhysicsSelected", "Generated charged multiplicity (physics selected);N_{ch}^{gen} (|#eta|<0.8);Counts", + HistType::kTH1D, {{200, 0, 200}}); + ue.add("MC/EventLoss/NchGenerated_Reconstructed", "Generated charged multiplicity (reconstructed);N_{ch}^{gen} (|#eta|<0.8);Counts", + HistType::kTH1D, {{200, 0, 200}}); + + // pT vs Multiplicity + ue.add("MC/GenPtVsNch", "Generated pT vs Multiplicity;#it{p}_{T} (GeV/#it{c});N_{ch}^{gen}", + HistType::kTH2D, {ptAxis, {200, 0, 200}}); + ue.add("MC/GenPtVsNch_PhysicsSelected", "Generated pT vs Multiplicity (physics selected);#it{p}_{T} (GeV/#it{c});N_{ch}^{gen}", + HistType::kTH2D, {ptAxis, {200, 0, 200}}); + + // Centrality vs Multiplicity correlations - USE STANDARD BINNING FOR THESE + ue.add("MC/EventLoss/GenMultVsCent", "Generated charged particles vs FT0M centrality;FT0M Centrality (%);N_{ch}^{gen} (|#eta|<0.8)", + HistType::kTH2D, {centAxis, multAxis}); + ue.add("MC/EventLoss/GenMultVsCent_Selected", "Generated vs FT0M centrality (selected events);FT0M Centrality (%);N_{ch}^{gen}", + HistType::kTH2D, {centAxis, multAxis}); + ue.add("MC/EventLoss/GenMultVsCent_Rejected", "Generated vs FT0M centrality (rejected events);FT0M Centrality (%);N_{ch}^{gen}", + HistType::kTH2D, {centAxis, multAxis}); + + // TPC cluster histograms + ue.add("hNclFoundTPC", "Number of TPC found clusters", + HistType::kTH1D, {{200, 0, 200, "N_{cl, found}"}}); + ue.add("hNclPIDTPC", "Number of TPC PID clusters", + HistType::kTH1D, {{200, 0, 200, "N_{cl, PID}"}}); + ue.add("hNclFoundTPCvsPt", "TPC found clusters vs pT;#it{p}_{T} (GeV/#it{c});N_{cl,found}", + HistType::kTH2D, {ptAxis, {200, 0., 200.}}); + ue.add("hNclPIDTPCvsPt", "TPC PID clusters vs pT;#it{p}_{T} (GeV/#it{c});N_{cl,PID}", + HistType::kTH2D, {ptAxis, {200, 0., 200.}}); + + // Inclusive histograms ue.add("Inclusive/hPtPrimGenAll", "All generated primaries (no cuts);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimGenAllVsMult", "All generated primaries vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - ue.add("Inclusive/hPtPrimBadVertex", "Generated primaries (bad vertex);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimBadVertexVsMult", "Generated primaries (bad vertex) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - ue.add("Inclusive/hPtPrimGen", "Generated primaries (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimGenVsMult", "Generated primaries (after phys sel) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - ue.add("Inclusive/hPtPrimRecoEv", "Generated primaries (reco events);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimRecoEvVsMult", "Generated primaries (reco events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - ue.add("Inclusive/hPtPrimGoodEv", "Generated primaries (good events);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimGoodEvVsMult", "Generated primaries (good events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); ue.add("Inclusive/hPtNumEff", "Tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtNumEffVsMult", "Tracking efficiency numerator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - ue.add("Inclusive/hPtDenEff", "Tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtDenEffVsMult", "Tracking efficiency denominator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); ue.add("Inclusive/hPtAllReco", "All reconstructed tracks;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtAllRecoVsMult", "All reconstructed tracks vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - ue.add("Inclusive/hPtPrimReco", "Reconstructed primaries;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimRecoVsMult", "Reconstructed primaries vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - ue.add("Inclusive/hPtSecReco", "Reconstructed secondaries;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtSecRecoVsMult", "Reconstructed secondaries vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - ue.add("Inclusive/hPtMeasured", "All measured tracks;#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtMeasuredVsMult", "All measured tracks vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - - // ======================================================================== - // PARTICLE-SPECIFIC HISTOGRAMS - // ======================================================================== + ue.add("Inclusive/hPtMeasuredVsCent", "All measured tracks (PID) vs centrality;#it{p}_{T} (GeV/#it{c});FT0M Centrality (%)", + HistType::kTH2D, {ptAxis, centAxis}); + // Particle-specific histograms const std::array particleNames = {"Pion", "Kaon", "Proton"}; const std::array particleSymbols = {"#pi^{#pm}", "K^{#pm}", "p+#bar{p}"}; @@ -813,94 +574,43 @@ void MultiplicityPt::init(InitContext const&) const auto& name = particleNames[iSpecies]; const auto& symbol = particleSymbols[iSpecies]; - // 1D versions ue.add(Form("%s/hPtPrimGenAll", name.c_str()), Form("All generated %s (no cuts);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtPrimBadVertex", name.c_str()), Form("Generated %s (bad vertex);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtPrimGen", name.c_str()), Form("Generated %s (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtPrimRecoEv", name.c_str()), Form("Generated %s (reco events);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtPrimGoodEv", name.c_str()), Form("Generated %s (good events);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - // 2D versions (vs multiplicity class) - ue.add(Form("%s/hPtPrimGenAllVsMult", name.c_str()), - Form("All generated %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add(Form("%s/hPtPrimBadVertexVsMult", name.c_str()), - Form("Generated %s (bad vertex) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add(Form("%s/hPtPrimGenVsMult", name.c_str()), - Form("Generated %s (after phys sel) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add(Form("%s/hPtPrimRecoEvVsMult", name.c_str()), - Form("Generated %s (reco events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add(Form("%s/hPtPrimGoodEvVsMult", name.c_str()), - Form("Generated %s (good events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), - HistType::kTH2D, {ptAxis, multAxis}); - - // Tracking efficiency ue.add(Form("%s/hPtNumEff", name.c_str()), Form("%s tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtNumEffVsMult", name.c_str()), - Form("%s tracking eff numerator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), - HistType::kTH2D, {ptAxis, multAxis}); - ue.add(Form("%s/hPtDenEff", name.c_str()), Form("%s tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtDenEffVsMult", name.c_str()), - Form("%s tracking eff denominator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), - HistType::kTH2D, {ptAxis, multAxis}); - // Primary fraction ue.add(Form("%s/hPtAllReco", name.c_str()), Form("All reconstructed %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtAllRecoVsMult", name.c_str()), - Form("All reconstructed %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), - HistType::kTH2D, {ptAxis, multAxis}); - ue.add(Form("%s/hPtPrimReco", name.c_str()), Form("Reconstructed primary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtPrimRecoVsMult", name.c_str()), - Form("Reconstructed primary %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), - HistType::kTH2D, {ptAxis, multAxis}); - ue.add(Form("%s/hPtSecReco", name.c_str()), Form("Reconstructed secondary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtSecRecoVsMult", name.c_str()), - Form("Reconstructed secondary %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), - HistType::kTH2D, {ptAxis, multAxis}); - // Measured spectra - ue.add(Form("%s/hPtMeasured", name.c_str()), - Form("Measured %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), - HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtMeasuredVsMult", name.c_str()), - Form("Measured %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), - HistType::kTH2D, {ptAxis, multAxis}); + ue.add(Form("%s/hPtMeasuredVsCent", name.c_str()), + Form("Measured %s (PID) vs centrality;#it{p}_{T} (GeV/#it{c});FT0M Centrality (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, centAxis}); - // PID quality if (enablePIDHistograms) { ue.add(Form("%s/hNsigmaTPC", name.c_str()), Form("TPC n#sigma %s;#it{p}_{T} (GeV/#it{c});n#sigma_{TPC}", symbol.c_str()), @@ -908,31 +618,13 @@ void MultiplicityPt::init(InitContext const&) } } - // ======================================================================== - // PHI CUT MONITORING - // ======================================================================== - - if (applyPhiCut.value) { - ue.add("PhiCut/hPtVsPhiPrimeBefore", "pT vs φ' before cut;p_{T} (GeV/c);φ'", - HistType::kTH2F, {{100, 0, 10}, {100, 0, 0.4}}); - ue.add("PhiCut/hPtVsPhiPrimeAfter", "pT vs φ' after cut;p_{T} (GeV/c);φ'", - HistType::kTH2F, {{100, 0, 10}, {100, 0, 0.4}}); - ue.add("PhiCut/hRejectionRate", "Track rejection rate by phi cut;p_{T} (GeV/c);Rejection Rate", - HistType::kTProfile, {{100, 0, 10}}); - } - - // ======================================================================== - // EVENT SELECTION HISTOGRAM - // ======================================================================== - + // Event selection histogram constexpr int nEvSelBins = 20; constexpr float evSelMin = 0.5f; constexpr float evSelMax = 20.5f; ue.add("evsel", "Event selection", HistType::kTH1D, {{nEvSelBins, evSelMin, evSelMax}}); auto h = ue.get(HIST("evsel")); h->GetXaxis()->SetBinLabel(1, "Events read"); - h->GetXaxis()->SetBinLabel(2, "INEL>0"); - h->GetXaxis()->SetBinLabel(3, "INEL>1"); h->GetXaxis()->SetBinLabel(4, "Trigger passed"); h->GetXaxis()->SetBinLabel(5, "NoITSROFrameBorder"); h->GetXaxis()->SetBinLabel(6, "NoSameBunchPileup"); @@ -940,542 +632,485 @@ void MultiplicityPt::init(InitContext const&) h->GetXaxis()->SetBinLabel(8, "IsVertexITSTPC"); h->GetXaxis()->SetBinLabel(9, "NoTimeFrameBorder"); h->GetXaxis()->SetBinLabel(13, "posZ passed"); - h->GetXaxis()->SetBinLabel(14, "INEL>0 (final)"); - h->GetXaxis()->SetBinLabel(15, "INEL>1 (final)"); + // Basic tracking histograms ue.add("hEta", "Track eta;#eta;Counts", HistType::kTH1D, {{20, -0.8, 0.8}}); ue.add("hPhi", "Track phi;#varphi (rad);Counts", HistType::kTH1D, {{64, 0, 2.0 * M_PI}}); ue.add("hvtxZ", "Vertex Z (data);Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); ue.add("hvtxZmc", "MC vertex Z;Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); - LOG(info) << "=== Initialized MultiplicityPt task with ON-THE-FLY PERCENTILE COMPUTATION ==="; - LOG(info) << "Centrality classes: " << kCentralityClasses; - LOG(info) << "Multiplicity estimator: " << multiplicityEstimator.value; - LOG(info) << "IMPORTANT: Run processPercentileCalibration FIRST to build percentile boundaries!"; - if (applyPhiCut.value) { - LOG(info) << "Phi cut ENABLED for pT > " << pTthresholdPhiCut.value << " GeV/c"; - } + LOG(info) << "=== Initialized MultiplicityPt task with full centrality diagnostics ==="; + LOG(info) << "Standard centrality binning: " << centBinningStd.size() - 1 << " bins (0-100%)"; + LOG(info) << "Fine centrality binning: " << centBinningFine.size() - 1 << " bins (0-100%)"; } -// ======================================================================== -// PERCENTILE CALIBRATION PASS -// ======================================================================== -void MultiplicityPt::processPercentileCalibration(CollisionTableMCTrue const& mcCollisions, - ParticleTableMC const& particles) -{ - LOG(info) << "=== PERCENTILE CALIBRATION PASS ==="; - LOG(info) << "Processing " << mcCollisions.size() << " MC collisions"; - - multiplicityValues.clear(); - multiplicityValues.reserve(mcCollisions.size()); +//============================================================================= +// Process Functions +//============================================================================= - for (const auto& mcCollision : mcCollisions) { - // Apply basic cuts - if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) - continue; - - auto particlesInCollision = particles.sliceBy(perMCCol, mcCollision.globalIndex()); - - // Apply INEL cuts - if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) - continue; - if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) - continue; - - // Calculate multiplicity - float mcMult = getMultiplicityMC(mcCollision, particles); - multiplicityValues.push_back(mcMult); - - ue.fill(HIST("Calibration/hRawMultiplicity"), mcMult); - } - - // Compute percentile boundaries - computePercentileBoundaries(); - - LOG(info) << "=== PERCENTILE CALIBRATION COMPLETE ==="; - LOG(info) << "Processed " << multiplicityValues.size() << " events"; - LOG(info) << "Now run processMC and processTrue with these percentiles"; -} - -// ======================================================================== -// DATA PROCESSING -// ======================================================================== -void MultiplicityPt::processData(CollisionTableData::iterator const& collision, - TrackTableData const& tracks, +void MultiplicityPt::processData(CollisionTableData::iterator const& /*collision*/, + TrackTableData const& /*tracks*/, BCsRun3 const& /*bcs*/) { - if (!isEventSelected(collision)) { - return; - } - ue.fill(HIST("hvtxZ"), collision.posZ()); - - float magField = 0; - if (applyPhiCut.value) { - const auto& bc = collision.bc_as(); - magField = getMagneticField(bc.timestamp()); - } - - for (const auto& track : tracks) { - if (applyPhiCut.value && track.pt() >= pTthresholdPhiCut.value) { - float phiPrime = getTransformedPhi(track.phi(), track.sign(), magField); - ue.fill(HIST("PhiCut/hPtVsPhiPrimeBefore"), track.pt(), phiPrime); - } - - if (!passesTrackSelection(track, magField)) { - continue; - } - - if (applyPhiCut.value && track.pt() >= pTthresholdPhiCut.value) { - float phiPrime = getTransformedPhi(track.phi(), track.sign(), magField); - ue.fill(HIST("PhiCut/hPtVsPhiPrimeAfter"), track.pt(), phiPrime); - } - - ue.fill(HIST("Inclusive/hPtMeasured"), track.pt()); - ue.fill(HIST("hEta"), track.eta()); - ue.fill(HIST("hPhi"), track.phi()); - - int bestSpecies = getBestPIDHypothesis(track); - - if (bestSpecies == kPion) { - ue.fill(HIST("Pion/hPtMeasured"), track.pt()); - if (enablePIDHistograms) { - ue.fill(HIST("Pion/hNsigmaTPC"), track.pt(), track.tpcNSigmaPi()); - } - } else if (bestSpecies == kKaon) { - ue.fill(HIST("Kaon/hPtMeasured"), track.pt()); - if (enablePIDHistograms) { - ue.fill(HIST("Kaon/hNsigmaTPC"), track.pt(), track.tpcNSigmaKa()); - } - } else if (bestSpecies == kProton) { - ue.fill(HIST("Proton/hPtMeasured"), track.pt()); - if (enablePIDHistograms) { - ue.fill(HIST("Proton/hNsigmaTPC"), track.pt(), track.tpcNSigmaPr()); - } - } - } + // Intentionally empty - data processing disabled } -// ======================================================================== -// MC PROCESSING - Using computed percentiles -// ======================================================================== void MultiplicityPt::processMC(TrackTableMC const& tracks, aod::McParticles const& particles, - CollisionTableMCTrue const& mcCollisions, - CollisionTableMC const& collisions, + aod::McCollisions const& mcCollisions, + RecoCollisions const& collisions, + aod::McCollisionLabels const& labels, + aod::McCentFT0Ms const& centTable, BCsRun3 const& /*bcs*/) { - if (!percentilesComputed) { - LOG(warning) << "Percentiles not computed yet! Run processPercentileCalibration first!"; - LOG(warning) << "Using fallback linear binning for now..."; + LOG(info) << "\n=== processMC START ==="; + LOG(info) << "Total MC collisions (generated): " << mcCollisions.size(); + LOG(info) << "Total reconstructed collisions: " << collisions.size(); + LOG(info) << "Total collision labels: " << labels.size(); + LOG(info) << "Total centrality entries: " << centTable.size(); + + //=========================================================================== + // DEBUG: Print raw centrality information first + //=========================================================================== + LOG(info) << "\n=== CENTRALITY DEBUG - RAW DATA ==="; + LOG(info) << "First 20 centrality values from centTable:"; + int debugCount = 0; + float minCent = 999.0f, maxCent = -999.0f; + std::map centDistribution; + + for (const auto& cent : centTable) { + float c = cent.centFT0M(); + if (debugCount < 20) { + LOG(info) << " Cent entry " << debugCount << ": " << c; + } + minCent = std::min(minCent, c); + maxCent = std::max(maxCent, c); + + int bin10 = static_cast(c / 10) * 10; + centDistribution[bin10]++; + debugCount++; + } + + LOG(info) << "Centrality range: [" << minCent << ", " << maxCent << "]"; + LOG(info) << "Distribution by 10% bins:"; + for (int i = 0; i < 100; i += 10) { + LOG(info) << " " << i << "-" << i + 10 << "%: " << centDistribution[i]; } - LOG(info) << "=== DEBUG processMC START ==="; - LOG(info) << "MC collisions: " << mcCollisions.size(); - LOG(info) << "Reconstructed collisions: " << collisions.size(); + // Check if centrality is inverted (0 = peripheral, 100 = central) + // If minCent is near 0 and maxCent near 100, check correlation with multiplicity + LOG(info) << "Checking if centrality might be inverted..."; + LOG(info) << "Will check correlation with multiplicity in the next step."; + + //=========================================================================== + // FIRST PASS: Build maps of MC collision ID to generated particle counts + //=========================================================================== + std::map mcCollisionToNch; + std::map mcCollisionVz; + std::set physicsSelectedMCCollisions; + std::map mcCollisionToINELClass; // 0=INEL0, 1=INEL>0, 2=INEL>1 ue.fill(HIST("MC/GenRecoCollisions"), 1.f, mcCollisions.size()); ue.fill(HIST("MC/GenRecoCollisions"), 2.f, collisions.size()); - std::set physicsSelectedMCCollisions; - std::set reconstructedMCCollisions; - std::set selectedMCCollisions; + LOG(info) << "\n--- FIRST PASS: Building MC collision maps ---"; - std::map mcCollisionMultiplicity; - std::map mcCollisionPercentile; + int mcWithParticles = 0; + int mcINEL0 = 0, mcINELgt0 = 0, mcINELgt1 = 0; - // First pass: classify MC collisions for (const auto& mcCollision : mcCollisions) { int64_t mcCollId = mcCollision.globalIndex(); + auto particlesInCollision = particles.sliceBy(perMCCol, mcCollId); - float mcMult = getMultiplicityMC(mcCollision, particles); - mcCollisionMultiplicity[mcCollId] = mcMult; + int nGenCharged = countGeneratedChargedPrimaries(particlesInCollision, cfgCutEtaMax.value, cfgTrkLowPtCut.value); - // Convert to percentile - float percentile = multiplicityToPercentile(mcMult); - mcCollisionPercentile[mcCollId] = percentile; + mcCollisionToNch[mcCollId] = nGenCharged; + mcCollisionVz[mcCollId] = mcCollision.posZ(); - ue.fill(HIST("MC/EventLoss/MultGenerated"), percentile); + // Determine INEL class + bool inel0 = o2::pwglf::isINELgt0mc(particlesInCollision, pdg); + bool inel1 = o2::pwglf::isINELgt1mc(particlesInCollision, pdg); - auto particlesInCollision = particles.sliceBy(perMCCol, mcCollId); + int inelClass = 0; + if (inel1) + inelClass = 2; + else if (inel0) + inelClass = 1; + mcCollisionToINELClass[mcCollId] = inelClass; + + ue.fill(HIST("INEL/hINELClass"), inelClass); + + if (inel0) + mcINELgt0++; + if (inel1) + mcINELgt1++; + if (nGenCharged > 0) + mcWithParticles++; + + ue.fill(HIST("MC/EventLoss/NchGenerated"), nGenCharged); + + // Physics selection based on vertex and INEL cuts + bool physicsSelected = true; if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) { - ue.fill(HIST("MC/EventLoss/MultBadVertex"), percentile); - continue; + physicsSelected = false; } - if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) { - continue; + // Apply INEL cut based on configuration + if (cfgINELCut.value == 1 && !inel0) { + physicsSelected = false; } - if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) { - continue; + if (cfgINELCut.value == 2 && !inel1) { + physicsSelected = false; } - physicsSelectedMCCollisions.insert(mcCollId); - ue.fill(HIST("MC/EventLoss/MultPhysicsSelected"), percentile); + if (physicsSelected) { + physicsSelectedMCCollisions.insert(mcCollId); + ue.fill(HIST("MC/EventLoss/NchGenerated_PhysicsSelected"), nGenCharged); + + if (inel0) { + ue.fill(HIST("MC/GenRecoCollisions"), 3.f); + } + if (inel1) { + ue.fill(HIST("MC/GenRecoCollisions"), 4.f); + } + } } + LOG(info) << "\n--- FIRST PASS SUMMARY ---"; + LOG(info) << "Total MC collisions processed: " << mcCollisions.size(); + LOG(info) << "MC collisions with particles: " << mcWithParticles; + LOG(info) << "INEL0: " << (mcCollisions.size() - mcINELgt0); + LOG(info) << "INEL>0: " << mcINELgt0; + LOG(info) << "INEL>1: " << mcINELgt1; LOG(info) << "Physics-selected MC collisions: " << physicsSelectedMCCollisions.size(); - // Second pass: track reconstructed events - std::set selectedCollisionIndices; + //=========================================================================== + // Build maps for labels and centrality + //=========================================================================== + std::map recoToMcMap; + std::map recoToCentMap; + + size_t nCollisions = collisions.size(); + + // Associate labels with collisions by index + size_t iLabel = 0; + for (const auto& label : labels) { + if (iLabel < nCollisions) { + const auto& collision = collisions.iteratorAt(iLabel); + int64_t recoCollId = collision.globalIndex(); + int64_t mcCollId = label.mcCollisionId(); + recoToMcMap[recoCollId] = mcCollId; + } + iLabel++; + } + + // Associate centrality with collisions by index + size_t iCent = 0; + for (const auto& cent : centTable) { + if (iCent < nCollisions) { + const auto& collision = collisions.iteratorAt(iCent); + int64_t recoCollId = collision.globalIndex(); + float centValue = cent.centFT0M(); + + // Fill raw centrality histogram + ue.fill(HIST("Centrality/hCentRaw"), centValue); + + recoToCentMap[recoCollId] = centValue; + } + iCent++; + } + + LOG(info) << "\n--- MAP SIZES ---"; + LOG(info) << "recoToMcMap size: " << recoToMcMap.size(); + LOG(info) << "recoToCentMap size: " << recoToCentMap.size(); + + //=========================================================================== + // DEBUG: Check correlation between centrality and multiplicity + //=========================================================================== + LOG(info) << "\n=== CENTRALITY VS MULTIPLICITY DEBUG ==="; + // Create temporary vectors to check correlation + std::vector> centMultPairs; for (const auto& collision : collisions) { - if (!collision.has_mcCollision()) { + int64_t collId = collision.globalIndex(); + + auto mcIt = recoToMcMap.find(collId); + if (mcIt == recoToMcMap.end()) continue; - } - const auto& mcCollision = collision.mcCollision_as(); - int64_t mcCollId = mcCollision.globalIndex(); + auto centIt = recoToCentMap.find(collId); + if (centIt == recoToCentMap.end()) + continue; - if (physicsSelectedMCCollisions.find(mcCollId) == physicsSelectedMCCollisions.end()) { + auto nchIt = mcCollisionToNch.find(mcIt->second); + if (nchIt == mcCollisionToNch.end()) continue; - } - float percentile = mcCollisionPercentile[mcCollId]; + centMultPairs.push_back({centIt->second, nchIt->second}); + } - if (reconstructedMCCollisions.find(mcCollId) == reconstructedMCCollisions.end()) { - reconstructedMCCollisions.insert(mcCollId); - ue.fill(HIST("MC/EventLoss/MultReconstructed"), percentile); - } + // Sort by centrality + std::sort(centMultPairs.begin(), centMultPairs.end()); - if (isEventSelected(collision)) { - if (selectedMCCollisions.find(mcCollId) == selectedMCCollisions.end()) { - selectedMCCollisions.insert(mcCollId); - ue.fill(HIST("MC/EventLoss/MultRecoSelected"), percentile); - } - selectedCollisionIndices.insert(collision.globalIndex()); - ue.fill(HIST("hvtxZ"), collision.posZ()); + LOG(info) << "Correlation between centrality and multiplicity:"; + LOG(info) << " If centrality is normal (0=central, 100=peripheral), multiplicity should decrease with centrality"; + LOG(info) << " If inverted (0=peripheral, 100=central), multiplicity should increase with centrality"; + + // Print a few samples across the range + if (centMultPairs.size() > 10) { + for (size_t i = 0; i < centMultPairs.size(); i += centMultPairs.size() / 10) { + LOG(info) << " Cent: " << centMultPairs[i].first + << "%, Mult: " << centMultPairs[i].second; } } - LOG(info) << "Reconstructed MC collisions: " << reconstructedMCCollisions.size(); - LOG(info) << "Selected MC collisions: " << selectedMCCollisions.size(); + //=========================================================================== + // SECOND PASS: Process reconstructed collisions with detailed cut accounting + //=========================================================================== - int nPhysicsSelected = physicsSelectedMCCollisions.size(); - int nReconstructed = reconstructedMCCollisions.size(); - int nSelected = selectedMCCollisions.size(); + LOG(info) << "\n--- SECOND PASS: Processing reconstructed collisions ---"; - if (nPhysicsSelected > 0) { - ue.fill(HIST("hEventLossBreakdown"), 1, nPhysicsSelected); - ue.fill(HIST("hEventLossBreakdown"), 2, nReconstructed); - ue.fill(HIST("hEventLossBreakdown"), 3, nSelected); - ue.fill(HIST("hEventLossBreakdown"), 4, (nSelected * 100.0 / nPhysicsSelected)); - } + std::set reconstructedMCCollisions; + std::set selectedMCCollisions; - // Process tracks - int totalTracksProcessed = 0; - int tracksFromSelectedEvents = 0; - int tracksPassingSelection = 0; + int nRecoCollisions = 0; + int nSelectedEvents = 0; + int nRejectedEvents = 0; + int nNoMCMatch = 0; + int nNoCent = 0; + int nInvalidCent = 0; - std::array particleTracksIdentified = {0}; - std::array particleTracksPrimary = {0}; - std::array particleTracksSecondary = {0}; + // Cut counters + int nPassVertex = 0; + int nPassINEL = 0; + int nPassAll = 0; - for (const auto& track : tracks) { - totalTracksProcessed++; + // For mean calculations + std::vector centAll, centVertex, centINEL, centSelected; - if (!track.has_collision()) - continue; + for (const auto& collision : collisions) { + nRecoCollisions++; + + int64_t collId = collision.globalIndex(); - const auto& collision = track.collision_as(); + // Fill cut flow + ue.fill(HIST("CutFlow/hCutStats"), 1); - if (selectedCollisionIndices.find(collision.globalIndex()) == selectedCollisionIndices.end()) { + // Get MC collision ID from labels map + auto mcIt = recoToMcMap.find(collId); + if (mcIt == recoToMcMap.end()) { + nNoMCMatch++; continue; } - tracksFromSelectedEvents++; + ue.fill(HIST("CutFlow/hCutStats"), 2); + + int64_t mcCollId = mcIt->second; - if (!collision.has_mcCollision()) + // Get generated multiplicity for this MC collision + auto nchIt = mcCollisionToNch.find(mcCollId); + if (nchIt == mcCollisionToNch.end()) { continue; + } + + int nGenCharged = nchIt->second; - const auto& mcCollision = collision.mcCollision_as(); - float percentile = mcCollisionPercentile[mcCollision.globalIndex()]; + // Get INEL class + auto inelIt = mcCollisionToINELClass.find(mcCollId); + int inelClass = (inelIt != mcCollisionToINELClass.end()) ? inelIt->second : 0; - float magField = 0; - if (applyPhiCut.value) { - const auto& bc = collision.bc_as(); - magField = getMagneticField(bc.timestamp()); + // Get centrality from cent map + auto centIt = recoToCentMap.find(collId); + if (centIt == recoToCentMap.end()) { + nNoCent++; + continue; } + ue.fill(HIST("CutFlow/hCutStats"), 3); - if (!passesTrackSelection(track, magField)) { + float cent = centIt->second; + if (cent < 0 || cent > 100) { + nInvalidCent++; continue; } - tracksPassingSelection++; - - // Inclusive charged particle - ue.fill(HIST("Inclusive/hPtMeasured"), track.pt()); - ue.fill(HIST("Inclusive/hPtMeasuredVsMult"), track.pt(), percentile); - ue.fill(HIST("Inclusive/hPtAllReco"), track.pt()); - ue.fill(HIST("Inclusive/hPtAllRecoVsMult"), track.pt(), percentile); - ue.fill(HIST("hEta"), track.eta()); - ue.fill(HIST("hPhi"), track.phi()); - - // Efficiency numerator - if (track.has_mcParticle()) { - const auto& particle = track.mcParticle(); - int pdgCode = std::abs(particle.pdgCode()); - - if (particle.isPhysicalPrimary()) { - ue.fill(HIST("Inclusive/hPtNumEff"), particle.pt()); - ue.fill(HIST("Inclusive/hPtNumEffVsMult"), particle.pt(), percentile); - ue.fill(HIST("Inclusive/hPtPrimReco"), track.pt()); - ue.fill(HIST("Inclusive/hPtPrimRecoVsMult"), track.pt(), percentile); - - if (pdgCode == PDGPion) { - ue.fill(HIST("Pion/hPtNumEff"), particle.pt()); - ue.fill(HIST("Pion/hPtNumEffVsMult"), particle.pt(), percentile); - } - if (pdgCode == PDGKaon) { - ue.fill(HIST("Kaon/hPtNumEff"), particle.pt()); - ue.fill(HIST("Kaon/hPtNumEffVsMult"), particle.pt(), percentile); - } - if (pdgCode == PDGProton) { - ue.fill(HIST("Proton/hPtNumEff"), particle.pt()); - ue.fill(HIST("Proton/hPtNumEffVsMult"), particle.pt(), percentile); - } - } else { - ue.fill(HIST("Inclusive/hPtSecReco"), track.pt()); - ue.fill(HIST("Inclusive/hPtSecRecoVsMult"), track.pt(), percentile); - } + + // Store all events with valid info + centAll.push_back(cent); + ue.fill(HIST("Centrality/hCentVsMult"), cent, nGenCharged); + ue.fill(HIST("Centrality/hMultVsCent"), nGenCharged, cent); + ue.fill(HIST("Centrality/hCentVsVz"), cent, collision.posZ()); + ue.fill(HIST("INEL/hINELVsCent"), cent, inelClass); + + // Track cuts progressively + bool passVertex = std::abs(collision.posZ()) <= cfgCutVertex.value; + if (passVertex) { + centVertex.push_back(cent); + ue.fill(HIST("Centrality/hCentAfterVtx"), cent); + ue.fill(HIST("CutFlow/hCutStats"), 4); + ue.fill(HIST("CutFlow/hCentPerCut"), 4, cent); + nPassVertex++; } - // Identified particle analysis - int bestSpecies = getBestPIDHypothesis(track); + // Check INEL selection at generator level + bool passINEL = true; + if (cfgINELCut.value == 1 && inelClass < 1) + passINEL = false; + if (cfgINELCut.value == 2 && inelClass < 2) + passINEL = false; + + if (passINEL) { + centINEL.push_back(cent); + ue.fill(HIST("Centrality/hCentAfterINEL"), cent); + ue.fill(HIST("CutFlow/hCutStats"), 5); + ue.fill(HIST("CutFlow/hCentPerCut"), 5, cent); + nPassINEL++; + } - if (bestSpecies == kPion) { - ue.fill(HIST("Pion/hPtMeasured"), track.pt()); - ue.fill(HIST("Pion/hPtMeasuredVsMult"), track.pt(), percentile); - ue.fill(HIST("Pion/hPtAllReco"), track.pt()); - ue.fill(HIST("Pion/hPtAllRecoVsMult"), track.pt(), percentile); - particleTracksIdentified[kPion]++; + // Fill GenMultVsCent for all reconstructed events + ue.fill(HIST("MC/EventLoss/GenMultVsCent"), cent, nGenCharged); + ue.fill(HIST("MC/EventLoss/NchGenerated_Reconstructed"), nGenCharged); - if (enablePIDHistograms) { - ue.fill(HIST("Pion/hNsigmaTPC"), track.pt(), track.tpcNSigmaPi()); - } + reconstructedMCCollisions.insert(mcCollId); - if (track.has_mcParticle()) { - const auto& particle = track.mcParticle(); - if (particle.isPhysicalPrimary()) { - ue.fill(HIST("Pion/hPtPrimReco"), track.pt()); - ue.fill(HIST("Pion/hPtPrimRecoVsMult"), track.pt(), percentile); - particleTracksPrimary[kPion]++; - } else { - ue.fill(HIST("Pion/hPtSecReco"), track.pt()); - ue.fill(HIST("Pion/hPtSecRecoVsMult"), track.pt(), percentile); - particleTracksSecondary[kPion]++; - } - } + // Apply all cuts + bool passedAll = passVertex && passINEL; - } else if (bestSpecies == kKaon) { - ue.fill(HIST("Kaon/hPtMeasured"), track.pt()); - ue.fill(HIST("Kaon/hPtMeasuredVsMult"), track.pt(), percentile); - ue.fill(HIST("Kaon/hPtAllReco"), track.pt()); - ue.fill(HIST("Kaon/hPtAllRecoVsMult"), track.pt(), percentile); - particleTracksIdentified[kKaon]++; + if (!passedAll) { + ue.fill(HIST("MC/EventLoss/GenMultVsCent_Rejected"), cent, nGenCharged); + nRejectedEvents++; + continue; + } - if (enablePIDHistograms) { - ue.fill(HIST("Kaon/hNsigmaTPC"), track.pt(), track.tpcNSigmaKa()); - } + // Event passed all selections + centSelected.push_back(cent); + ue.fill(HIST("Centrality/hCentAfterAll"), cent); + ue.fill(HIST("CutFlow/hCutStats"), 6); + ue.fill(HIST("CutFlow/hCentPerCut"), 6, cent); + ue.fill(HIST("MC/EventLoss/GenMultVsCent_Selected"), cent, nGenCharged); + ue.fill(HIST("hvtxZ"), collision.posZ()); + selectedMCCollisions.insert(mcCollId); + nSelectedEvents++; + nPassAll++; + + // Process tracks in selected events + int nTracksInEvent = 0; + for (const auto& track : tracks) { + if (!track.has_collision()) + continue; + if (track.collisionId() != collId) + continue; - if (track.has_mcParticle()) { - const auto& particle = track.mcParticle(); - if (particle.isPhysicalPrimary()) { - ue.fill(HIST("Kaon/hPtPrimReco"), track.pt()); - ue.fill(HIST("Kaon/hPtPrimRecoVsMult"), track.pt(), percentile); - particleTracksPrimary[kKaon]++; - } else { - ue.fill(HIST("Kaon/hPtSecReco"), track.pt()); - ue.fill(HIST("Kaon/hPtSecRecoVsMult"), track.pt(), percentile); - particleTracksSecondary[kKaon]++; - } + if (!passesTrackSelection(track)) { + continue; } + nTracksInEvent++; - } else if (bestSpecies == kProton) { - ue.fill(HIST("Proton/hPtMeasured"), track.pt()); - ue.fill(HIST("Proton/hPtMeasuredVsMult"), track.pt(), percentile); - ue.fill(HIST("Proton/hPtAllReco"), track.pt()); - ue.fill(HIST("Proton/hPtAllRecoVsMult"), track.pt(), percentile); - particleTracksIdentified[kProton]++; + // Fill TPC cluster histograms + ue.fill(HIST("hNclFoundTPC"), track.tpcNClsFound()); + ue.fill(HIST("hNclPIDTPC"), track.tpcNClsPID()); + ue.fill(HIST("hNclFoundTPCvsPt"), track.pt(), track.tpcNClsFound()); + ue.fill(HIST("hNclPIDTPCvsPt"), track.pt(), track.tpcNClsPID()); - if (enablePIDHistograms) { - ue.fill(HIST("Proton/hNsigmaTPC"), track.pt(), track.tpcNSigmaPr()); - } + ue.fill(HIST("Inclusive/hPtAllReco"), track.pt()); + ue.fill(HIST("Inclusive/hPtMeasuredVsCent"), track.pt(), cent); + ue.fill(HIST("hEta"), track.eta()); + ue.fill(HIST("hPhi"), track.phi()); if (track.has_mcParticle()) { const auto& particle = track.mcParticle(); + int pdgCode = std::abs(particle.pdgCode()); + if (particle.isPhysicalPrimary()) { - ue.fill(HIST("Proton/hPtPrimReco"), track.pt()); - ue.fill(HIST("Proton/hPtPrimRecoVsMult"), track.pt(), percentile); - particleTracksPrimary[kProton]++; + ue.fill(HIST("Inclusive/hPtNumEff"), particle.pt()); + ue.fill(HIST("Inclusive/hPtPrimReco"), track.pt()); + + if (pdgCode == PDGPion) { + ue.fill(HIST("Pion/hPtNumEff"), particle.pt()); + ue.fill(HIST("Pion/hPtPrimReco"), track.pt()); + } else if (pdgCode == PDGKaon) { + ue.fill(HIST("Kaon/hPtNumEff"), particle.pt()); + ue.fill(HIST("Kaon/hPtPrimReco"), track.pt()); + } else if (pdgCode == PDGProton) { + ue.fill(HIST("Proton/hPtNumEff"), particle.pt()); + ue.fill(HIST("Proton/hPtPrimReco"), track.pt()); + } } else { - ue.fill(HIST("Proton/hPtSecReco"), track.pt()); - ue.fill(HIST("Proton/hPtSecRecoVsMult"), track.pt(), percentile); - particleTracksSecondary[kProton]++; + ue.fill(HIST("Inclusive/hPtSecReco"), track.pt()); + + if (pdgCode == PDGPion) { + ue.fill(HIST("Pion/hPtSecReco"), track.pt()); + } else if (pdgCode == PDGKaon) { + ue.fill(HIST("Kaon/hPtSecReco"), track.pt()); + } else if (pdgCode == PDGProton) { + ue.fill(HIST("Proton/hPtSecReco"), track.pt()); + } } } - } - } - - LOG(info) << "=== DEBUG TRACK COUNTING ==="; - LOG(info) << "Total tracks processed: " << totalTracksProcessed; - LOG(info) << "Tracks from selected events: " << tracksFromSelectedEvents; - LOG(info) << "Tracks passing selection: " << tracksPassingSelection; - - LOG(info) << "Pions identified: " << particleTracksIdentified[kPion] - << ", primary: " << particleTracksPrimary[kPion] - << ", secondary: " << particleTracksSecondary[kPion]; - LOG(info) << "Kaons identified: " << particleTracksIdentified[kKaon] - << ", primary: " << particleTracksPrimary[kKaon] - << ", secondary: " << particleTracksSecondary[kKaon]; - LOG(info) << "Protons identified: " << particleTracksIdentified[kProton] - << ", primary: " << particleTracksPrimary[kProton] - << ", secondary: " << particleTracksSecondary[kProton]; - - LOG(info) << "=== DEBUG processMC END ==="; -} - -// ======================================================================== -// TRUE MC PROCESSING - Using computed percentiles -// ======================================================================== -void MultiplicityPt::processTrue(CollisionTableMCTrue const& mcCollisions, - ParticleTableMC const& particles) -{ - if (!percentilesComputed) { - LOG(warning) << "Percentiles not computed yet! Run processPercentileCalibration first!"; - } - - LOG(info) << "=== DEBUG processTrue START ==="; - LOG(info) << "Number of MC collisions: " << mcCollisions.size(); - - int nAllGenerated = 0; - int nBadVertex = 0; - int nPhysicsSelected = 0; - - std::array particleCountAll = {0}; - std::array particleCountBadVertex = {0}; - std::array particleCountAfterPS = {0}; - - for (const auto& mcCollision : mcCollisions) { - nAllGenerated++; - - float mcMult = getMultiplicityMC(mcCollision, particles); - float percentile = multiplicityToPercentile(mcMult); - - ue.fill(HIST("hvtxZmc"), mcCollision.posZ()); - auto particlesInCollision = particles.sliceBy(perMCCol, mcCollision.globalIndex()); - - // Fill ALL generated primaries BEFORE any cuts - for (const auto& particle : particlesInCollision) { - if (isGoodPrimary(particle)) { - ue.fill(HIST("Inclusive/hPtPrimGenAll"), particle.pt()); - ue.fill(HIST("Inclusive/hPtPrimGenAllVsMult"), particle.pt(), percentile); - } - - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Pion/hPtPrimGenAll"), particle.pt()); - ue.fill(HIST("Pion/hPtPrimGenAllVsMult"), particle.pt(), percentile); - particleCountAll[kPion]++; - } - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Kaon/hPtPrimGenAll"), particle.pt()); - ue.fill(HIST("Kaon/hPtPrimGenAllVsMult"), particle.pt(), percentile); - particleCountAll[kKaon]++; - } - - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Proton/hPtPrimGenAll"), particle.pt()); - ue.fill(HIST("Proton/hPtPrimGenAllVsMult"), particle.pt(), percentile); - particleCountAll[kProton]++; - } - } - - // Apply vertex cut - if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) { - nBadVertex++; + int bestSpecies = getBestPIDHypothesis(track); - for (const auto& particle : particlesInCollision) { - if (isGoodPrimary(particle)) { - ue.fill(HIST("Inclusive/hPtPrimBadVertex"), particle.pt()); - ue.fill(HIST("Inclusive/hPtPrimBadVertexVsMult"), particle.pt(), percentile); - } + if (bestSpecies == kPion) { + ue.fill(HIST("Pion/hPtMeasuredVsCent"), track.pt(), cent); + ue.fill(HIST("Pion/hPtAllReco"), track.pt()); - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Pion/hPtPrimBadVertex"), particle.pt()); - ue.fill(HIST("Pion/hPtPrimBadVertexVsMult"), particle.pt(), percentile); - particleCountBadVertex[kPion]++; + if (enablePIDHistograms) { + ue.fill(HIST("Pion/hNsigmaTPC"), track.pt(), track.tpcNSigmaPi()); } + } else if (bestSpecies == kKaon) { + ue.fill(HIST("Kaon/hPtMeasuredVsCent"), track.pt(), cent); + ue.fill(HIST("Kaon/hPtAllReco"), track.pt()); - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Kaon/hPtPrimBadVertex"), particle.pt()); - ue.fill(HIST("Kaon/hPtPrimBadVertexVsMult"), particle.pt(), percentile); - particleCountBadVertex[kKaon]++; + if (enablePIDHistograms) { + ue.fill(HIST("Kaon/hNsigmaTPC"), track.pt(), track.tpcNSigmaKa()); } + } else if (bestSpecies == kProton) { + ue.fill(HIST("Proton/hPtMeasuredVsCent"), track.pt(), cent); + ue.fill(HIST("Proton/hPtAllReco"), track.pt()); - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Proton/hPtPrimBadVertex"), particle.pt()); - ue.fill(HIST("Proton/hPtPrimBadVertexVsMult"), particle.pt(), percentile); - particleCountBadVertex[kProton]++; + if (enablePIDHistograms) { + ue.fill(HIST("Proton/hNsigmaTPC"), track.pt(), track.tpcNSigmaPr()); } } - continue; } - // Apply INEL cuts - if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) - continue; - if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) - continue; - - nPhysicsSelected++; - - // Fill primaries AFTER physics selection (denominator for efficiency) - for (const auto& particle : particlesInCollision) { - if (isGoodPrimary(particle)) { - ue.fill(HIST("Inclusive/hPtDenEff"), particle.pt()); - ue.fill(HIST("Inclusive/hPtDenEffVsMult"), particle.pt(), percentile); - ue.fill(HIST("Inclusive/hPtPrimGen"), particle.pt()); - ue.fill(HIST("Inclusive/hPtPrimGenVsMult"), particle.pt(), percentile); - } + // Fill event-level track multiplicity + ue.fill(HIST("Centrality/hRecoMultVsCent"), cent, nTracksInEvent); + } - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Pion/hPtDenEff"), particle.pt()); - ue.fill(HIST("Pion/hPtDenEffVsMult"), particle.pt(), percentile); - ue.fill(HIST("Pion/hPtPrimGen"), particle.pt()); - ue.fill(HIST("Pion/hPtPrimGenVsMult"), particle.pt(), percentile); - particleCountAfterPS[kPion]++; - } + // Calculate and display cut statistics + LOG(info) << "\n=== CUT STATISTICS ==="; + LOG(info) << "Total collisions with valid info: " << centAll.size(); + LOG(info) << "Pass vertex cut: " << nPassVertex << " (" + << (centAll.size() > 0 ? 100.0 * nPassVertex / centAll.size() : 0.0) << "%)"; + LOG(info) << "Pass INEL cut: " << nPassINEL << " (" + << (centAll.size() > 0 ? 100.0 * nPassINEL / centAll.size() : 0.0) << "%)"; + LOG(info) << "Pass all cuts: " << nPassAll << " (" + << (centAll.size() > 0 ? 100.0 * nPassAll / centAll.size() : 0.0) << "%)"; + + // Calculate mean centrality at each stage + if (!centAll.empty()) { + float meanAll = std::accumulate(centAll.begin(), centAll.end(), 0.0) / centAll.size(); + float meanVertex = centVertex.empty() ? 0 : std::accumulate(centVertex.begin(), centVertex.end(), 0.0) / centVertex.size(); + float meanINEL = centINEL.empty() ? 0 : std::accumulate(centINEL.begin(), centINEL.end(), 0.0) / centINEL.size(); + float meanSelected = centSelected.empty() ? 0 : std::accumulate(centSelected.begin(), centSelected.end(), 0.0) / centSelected.size(); + + LOG(info) << "\n=== CENTRALITY MEANS ==="; + LOG(info) << "Mean centrality (all): " << meanAll; + LOG(info) << "Mean centrality (after vertex): " << meanVertex; + LOG(info) << "Mean centrality (after INEL): " << meanINEL; + LOG(info) << "Mean centrality (selected): " << meanSelected; + } - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Kaon/hPtDenEff"), particle.pt()); - ue.fill(HIST("Kaon/hPtDenEffVsMult"), particle.pt(), percentile); - ue.fill(HIST("Kaon/hPtPrimGen"), particle.pt()); - ue.fill(HIST("Kaon/hPtPrimGenVsMult"), particle.pt(), percentile); - particleCountAfterPS[kKaon]++; - } + ue.fill(HIST("hEventLossBreakdown"), 1.f, physicsSelectedMCCollisions.size()); + ue.fill(HIST("hEventLossBreakdown"), 2.f, reconstructedMCCollisions.size()); + ue.fill(HIST("hEventLossBreakdown"), 3.f, selectedMCCollisions.size()); - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Proton/hPtDenEff"), particle.pt()); - ue.fill(HIST("Proton/hPtDenEffVsMult"), particle.pt(), percentile); - ue.fill(HIST("Proton/hPtPrimGen"), particle.pt()); - ue.fill(HIST("Proton/hPtPrimGenVsMult"), particle.pt(), percentile); - particleCountAfterPS[kProton]++; - } - } - } + float efficiency = physicsSelectedMCCollisions.size() > 0 ? 100.f * selectedMCCollisions.size() / physicsSelectedMCCollisions.size() : 0; + ue.fill(HIST("hEventLossBreakdown"), 4.f, efficiency); - LOG(info) << "=== DEBUG processTrue END ==="; - LOG(info) << "All generated events: " << nAllGenerated; - LOG(info) << "Events with bad vertex: " << nBadVertex; - LOG(info) << "Passing physics selection: " << nPhysicsSelected; - - LOG(info) << "=== PARTICLE-SPECIFIC STATISTICS ==="; - LOG(info) << "Pions - All: " << particleCountAll[kPion] - << ", Bad vertex: " << particleCountBadVertex[kPion] - << ", After PS: " << particleCountAfterPS[kPion]; - LOG(info) << "Kaons - All: " << particleCountAll[kKaon] - << ", Bad vertex: " << particleCountBadVertex[kKaon] - << ", After PS: " << particleCountAfterPS[kKaon]; - LOG(info) << "Protons - All: " << particleCountAll[kProton] - << ", Bad vertex: " << particleCountBadVertex[kProton] - << ", After PS: " << particleCountAfterPS[kProton]; + LOG(info) << "\n=== FINAL EFFICIENCY ==="; + LOG(info) << "Physics selected: " << physicsSelectedMCCollisions.size(); + LOG(info) << "Reconstructed: " << reconstructedMCCollisions.size(); + LOG(info) << "Selected: " << selectedMCCollisions.size(); + LOG(info) << "Efficiency: " << efficiency << "%"; + LOG(info) << "=== processMC END ==="; } From 9ed0885fd27b4797e71bf0196c8350e7b0364445 Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Mon, 9 Mar 2026 17:52:00 -0600 Subject: [PATCH 15/16] Fix multiplicity issues --- PWGLF/Tasks/Nuspex/MultiplicityPt.cxx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx index dfe3d7b67de..8dff2a01237 100644 --- a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx +++ b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx @@ -66,10 +66,7 @@ struct MultiplicityPt { // Service Service pdg; - //=========================================================================== - // Configurable Parameters - //=========================================================================== - // Add CCDB service for magnetic field + Service ccdb; Configurable isRun3{"isRun3", true, "is Run3 dataset"}; From adefd3e73b3cbb2d3baf6b5abb55c026f9d9a3f0 Mon Sep 17 00:00:00 2001 From: Dushmanta Sahu Date: Mon, 9 Mar 2026 18:13:14 -0600 Subject: [PATCH 16/16] Fix multiplicity issues --- PWGLF/Tasks/Nuspex/MultiplicityPt.cxx | 2638 ++++++++++++------------- 1 file changed, 1257 insertions(+), 1381 deletions(-) diff --git a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx index 8dff2a01237..fc01e732256 100644 --- a/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx +++ b/PWGLF/Tasks/Nuspex/MultiplicityPt.cxx @@ -58,15 +58,11 @@ using namespace o2::framework::expressions; using BCsRun3 = soa::Join; -//============================================================================= -// Main Analysis Struct -//============================================================================= struct MultiplicityPt { // Service Service pdg; - Service ccdb; Configurable isRun3{"isRun3", true, "is Run3 dataset"}; @@ -136,10 +132,6 @@ struct MultiplicityPt { // Histogram Registry HistogramRegistry ue; - //=========================================================================== - // Table Definitions - Using individual tables, not joined for MC - //=========================================================================== - // Data collisions (not used but kept for completeness) using CollisionTableData = soa::Join; @@ -154,9 +146,6 @@ struct MultiplicityPt { // Histogram Registry HistogramRegistry ue; - // ======================================================================== - // CENTRALITY/MULTIPLICITY CLASSES - Using same bins as before for consistency - // ======================================================================== static constexpr int kCentralityClasses = 10; static constexpr double CentClasses[kCentralityClasses + 1] = {0.0, 1.0, 5.0, 10.0, 15.0, 20.0, 30.0, 40.0, 50.0, 70.0, 100.0}; @@ -192,9 +181,6 @@ struct MultiplicityPt { // Preslice for MC particles Preslice perMCCol = aod::mcparticle::mcCollisionId; - //=========================================================================== - // Constants - //=========================================================================== // Multiplicity estimator enum enum MultCodes : int { kNoMultiplicity = 0, @@ -223,10 +209,6 @@ struct MultiplicityPt { static constexpr int PDGKaon = 321; static constexpr int PDGProton = 2212; - //=========================================================================== - // Helper Functions - //=========================================================================== - template int countGeneratedChargedPrimaries(const ParticleContainer& particles, float etaMax, float ptMin) const { @@ -289,10 +271,6 @@ struct MultiplicityPt { ParticleTableMC const& particles); PROCESS_SWITCH(MultiplicityPt, processTrue, "process true MC", true); - // ======================================================================== - // MULTIPLICITY GETTER FUNCTIONS - Using raw charged particle count - // ======================================================================== - // Count charged primaries in |eta| < 1.0 template int countChargedPrimaries(const MCCollisionType& mcCollision, const ParticleTableMC& particles) const @@ -394,9 +372,6 @@ struct MultiplicityPt { return kCentralityClasses - 1; } - // ======================================================================== - // COMPUTE PERCENTILE BOUNDARIES - // ======================================================================== void computePercentileBoundaries() { if (multiplicityValues.empty()) { @@ -433,9 +408,6 @@ struct MultiplicityPt { } } - // ======================================================================== - // MAGNETIC FIELD FUNCTION - // ======================================================================== int getMagneticField(uint64_t timestamp) { static o2::parameters::GRPMagField* grpo = nullptr; @@ -450,9 +422,6 @@ struct MultiplicityPt { return grpo->getNominalL3Field(); } - // ======================================================================== - // PHI CUT FUNCTION - // ======================================================================== template bool passedPhiCut(const TrackType& track, float magField) const { @@ -499,10 +468,6 @@ struct MultiplicityPt { return transformedPhi; } - // ======================================================================== - // TRACK SELECTION FUNCTIONS - // ======================================================================== - template bool passesCutWoDCA(TrackType const& track) const { @@ -543,7 +508,7 @@ struct MultiplicityPt { template bool passesTrackSelection(TrackType const& track) const - bool passesTrackSelection(TrackType const& track, float magField = 0) const + bool passesTrackSelection(TrackType const& track, float magField = 0) const { if (track.eta() < cfgCutEtaMin.value || track.eta() > cfgCutEtaMax.value) return false; @@ -566,60 +531,56 @@ struct MultiplicityPt { return true; } - if (applyPhiCut.value && !passedPhiCut(track, magField)) - return false; + if (applyPhiCut.value && !passedPhiCut(track, magField)) + return false; - return passesDCAxyCut(track); - } - - // ======================================================================== - // PID SELECTION FUNCTIONS - // ======================================================================== + return passesDCAxyCut(track); +} - template - bool passesPIDSelection(TrackType const& track) const - { - float nsigmaTPC = 0.f; - - if constexpr (species == kPion) { - nsigmaTPC = track.tpcNSigmaPi(); - } else if constexpr (species == kKaon) { - nsigmaTPC = track.tpcNSigmaKa(); - } else if constexpr (species == kProton) { - nsigmaTPC = track.tpcNSigmaPr(); - } +template +bool passesPIDSelection(TrackType const& track) const +{ + float nsigmaTPC = 0.f; + + if constexpr (species == kPion) { + nsigmaTPC = track.tpcNSigmaPi(); + } else if constexpr (species == kKaon) { + nsigmaTPC = track.tpcNSigmaKa(); + } else if constexpr (species == kProton) { + nsigmaTPC = track.tpcNSigmaPr(); + } - float cutValue = cfgCutNsigma.value; - if constexpr (species == kPion) - cutValue = cfgCutNsigmaPi.value; - if constexpr (species == kKaon) - cutValue = cfgCutNsigmaKa.value; - if constexpr (species == kProton) - cutValue = cfgCutNsigmaPr.value; + float cutValue = cfgCutNsigma.value; + if constexpr (species == kPion) + cutValue = cfgCutNsigmaPi.value; + if constexpr (species == kKaon) + cutValue = cfgCutNsigmaKa.value; + if constexpr (species == kProton) + cutValue = cfgCutNsigmaPr.value; - return (std::abs(nsigmaTPC) < cutValue); - return (std::abs(nsigmaTPC) < cfgCutNsigma.value); - } + return (std::abs(nsigmaTPC) < cutValue); + return (std::abs(nsigmaTPC) < cfgCutNsigma.value); +} - template - int getBestPIDHypothesis(TrackType const& track) const - { - float nsigmaPi = std::abs(track.tpcNSigmaPi()); - float nsigmaKa = std::abs(track.tpcNSigmaKa()); - float nsigmaPr = std::abs(track.tpcNSigmaPr()); +template +int getBestPIDHypothesis(TrackType const& track) const +{ + float nsigmaPi = std::abs(track.tpcNSigmaPi()); + float nsigmaKa = std::abs(track.tpcNSigmaKa()); + float nsigmaPr = std::abs(track.tpcNSigmaPr()); - float minNSigma = 999.0f; - int bestSpecies = -1; + float minNSigma = 999.0f; + int bestSpecies = -1; - if (nsigmaPi < cfgCutNsigmaPi.value && nsigmaPi < minNSigma) { - minNSigma = nsigmaPi; - bestSpecies = kPion; - } - if (nsigmaKa < cfgCutNsigmaKa.value && nsigmaKa < minNSigma) { - minNSigma = nsigmaKa; - bestSpecies = kKaon; - } - if (nsigmaPr < cfgCutNsigmaPr.value && nsigmaPr < minNSigma) { + if (nsigmaPi < cfgCutNsigmaPi.value && nsigmaPi < minNSigma) { + minNSigma = nsigmaPi; + bestSpecies = kPion; + } + if (nsigmaKa < cfgCutNsigmaKa.value && nsigmaKa < minNSigma) { + minNSigma = nsigmaKa; + bestSpecies = kKaon; + } + if (nsigmaPr < cfgCutNsigmaPr.value && nsigmaPr < minNSigma) { constexpr float largeNSigmaValue = 999.0f; float minNSigma = largeNSigmaValue; int bestSpecies = -1; @@ -640,10 +601,6 @@ struct MultiplicityPt { return bestSpecies; } - // ======================================================================== - // EVENT SELECTION FUNCTION - // ======================================================================== - template bool isEventSelected(CollisionType const& collision) { @@ -710,10 +667,6 @@ struct MultiplicityPt { return true; } - // ======================================================================== - // PRIMARY SELECTION - // ======================================================================== - template bool isGoodPrimary(ParticleType const& particle) const { @@ -732,9 +685,6 @@ struct MultiplicityPt { return true; } - //=========================================================================== - // Process Switches - //=========================================================================== void processData(CollisionTableData::iterator const& collision, TrackTableData const& tracks, BCsRun3 const& bcs); @@ -749,9 +699,6 @@ struct MultiplicityPt { BCsRun3 const& bcs); PROCESS_SWITCH(MultiplicityPt, processMC, "process MC", true); - //=========================================================================== - // Standard Framework Functions - //=========================================================================== void init(InitContext const&); void endOfStream(EndOfStreamContext& /*eos*/) @@ -766,1546 +713,1475 @@ struct MultiplicityPt { } }; -//============================================================================= -// Workflow Definition -//============================================================================= - if (std::abs(particle.y()) > cfgCutY.value) - return false; +if (std::abs(particle.y()) > cfgCutY.value) + return false; - return true; - } +return true; +} - template - bool isGoodPrimarySpecies(ParticleType const& particle) const - { - int pdgCode = std::abs(particle.pdgCode()); - int expectedPDG = 0; +template +bool isGoodPrimarySpecies(ParticleType const& particle) const +{ + int pdgCode = std::abs(particle.pdgCode()); + int expectedPDG = 0; - if constexpr (species == kPion) - expectedPDG = PDGPion; - else if constexpr (species == kKaon) - expectedPDG = PDGKaon; - else if constexpr (species == kProton) - expectedPDG = PDGProton; + if constexpr (species == kPion) + expectedPDG = PDGPion; + else if constexpr (species == kKaon) + expectedPDG = PDGKaon; + else if constexpr (species == kProton) + expectedPDG = PDGProton; - if (pdgCode != expectedPDG) - return false; + if (pdgCode != expectedPDG) + return false; - return isGoodPrimary(particle); - } + return isGoodPrimary(particle); +} - void init(InitContext const&); -}; +void init(InitContext const&); +} +; WorkflowSpec defineDataProcessing(ConfigContext const& cfgc) { return WorkflowSpec{adaptAnalysisTask(cfgc)}; } -//============================================================================= -// Implementation of Member Functions -//============================================================================= - void MultiplicityPt::init(InitContext const&) { LOG(info) << "=================================================="; LOG(info) << "Initializing MultiplicityPt task with full centrality diagnostics"; LOG(info) << "=================================================="; -void MultiplicityPt::init(InitContext const&) -{ - // ======================================================================== - // CUSTOM TRACK CUTS INITIALIZATION - // ======================================================================== - - if (useCustomTrackCuts.value) { - LOG(info) << "Using custom track cuts matching spectraTOF approach"; - customTrackCuts = getGlobalTrackSelectionRun3ITSMatch(itsPattern.value); - - customTrackCuts.SetRequireITSRefit(requireITS.value); - customTrackCuts.SetRequireTPCRefit(requireTPC.value); - customTrackCuts.SetMinNClustersITS(min_ITS_nClusters.value); - customTrackCuts.SetRequireGoldenChi2(requireGoldenChi2.value); - customTrackCuts.SetMaxChi2PerClusterTPC(maxChi2PerClusterTPC.value); - customTrackCuts.SetMaxChi2PerClusterITS(maxChi2PerClusterITS.value); - customTrackCuts.SetMinNCrossedRowsTPC(minNCrossedRowsTPC.value); - customTrackCuts.SetMinNClustersTPC(minTPCNClsFound.value); - customTrackCuts.SetMinNCrossedRowsOverFindableClustersTPC(minNCrossedRowsOverFindableClustersTPC.value); - customTrackCuts.SetMaxDcaXYPtDep([](float /*pt*/) { return 10000.f; }); - customTrackCuts.SetMaxDcaZ(maxDcaZ.value); - - customTrackCuts.print(); - } - - // Axis definitions - // ======================================================================== - // PHI CUT INITIALIZATION - // ======================================================================== - - if (applyPhiCut.value) { - fphiCutLow = new TF1("StandardPhiCutLow", - Form("%f/x/x+pi/18.0-%f", - phiCutLowParam1.value, phiCutLowParam2.value), - 0, 50); - fphiCutHigh = new TF1("StandardPhiCutHigh", - Form("%f/x+pi/18.0+%f", - phiCutHighParam1.value, phiCutHighParam2.value), - 0, 50); - - LOGF(info, "=== Phi Cut Parameters ==="); - LOGF(info, "Low cut: %.6f/x² + pi/18 - %.6f", - phiCutLowParam1.value, phiCutLowParam2.value); - LOGF(info, "High cut: %.6f/x + pi/18 + %.6f", - phiCutHighParam1.value, phiCutHighParam2.value); - LOGF(info, "Applied for pT > %.1f GeV/c", pTthresholdPhiCut.value); - } + void MultiplicityPt::init(InitContext const&) + { - // ======================================================================== - // AXIS DEFINITIONS - // ======================================================================== - - ConfigurableAxis ptBinning{ - "ptBinning", - {0.0, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, - 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, - 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.2, 2.4, 2.6, 2.8, - 3.0, 3.5, 4.0, 4.5, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, - 12.0, 14.0, 16.0, 18.0, 20.0, 25.0, 30.0, 40.0, 50.0}, - "pT bin limits"}; - AxisSpec ptAxis = {ptBinning, "#it{p}_{T} (GeV/#it{c})"}; - - std::vector centBinningStd = {0., 1., 5., 10., 15., 20., 30., 40., 50., 60., 70., 80., 90., 100.}; - - // Fine centrality binning for diagnostics (100 bins, guaranteed increasing) - std::vector centBinningFine; - for (int i = 0; i <= 100; i++) { - centBinningFine.push_back(static_cast(i)); - } + if (useCustomTrackCuts.value) { + LOG(info) << "Using custom track cuts matching spectraTOF approach"; + customTrackCuts = getGlobalTrackSelectionRun3ITSMatch(itsPattern.value); - AxisSpec centAxis = {centBinningStd, "FT0M Centrality (%)"}; - AxisSpec centFineAxis = {centBinningFine, "FT0M Centrality (%)"}; + customTrackCuts.SetRequireITSRefit(requireITS.value); + customTrackCuts.SetRequireTPCRefit(requireTPC.value); + customTrackCuts.SetMinNClustersITS(min_ITS_nClusters.value); + customTrackCuts.SetRequireGoldenChi2(requireGoldenChi2.value); + customTrackCuts.SetMaxChi2PerClusterTPC(maxChi2PerClusterTPC.value); + customTrackCuts.SetMaxChi2PerClusterITS(maxChi2PerClusterITS.value); + customTrackCuts.SetMinNCrossedRowsTPC(minNCrossedRowsTPC.value); + customTrackCuts.SetMinNClustersTPC(minTPCNClsFound.value); + customTrackCuts.SetMinNCrossedRowsOverFindableClustersTPC(minNCrossedRowsOverFindableClustersTPC.value); + customTrackCuts.SetMaxDcaXYPtDep([](float /*pt*/) { return 10000.f; }); + customTrackCuts.SetMaxDcaZ(maxDcaZ.value); - // Multiplicity axes - properly defined - std::vector multBins; - for (int i = 0; i <= 200; i++) { - multBins.push_back(static_cast(i)); - } - AxisSpec multAxis = {multBins, "N_{ch}^{gen} (|#eta|<0.8)"}; + customTrackCuts.print(); + } - // Reconstructed multiplicity axis - properly defined with explicit bin edges - std::vector recoMultBins; - for (int i = 0; i <= 100; i++) { - recoMultBins.push_back(static_cast(i)); - } - AxisSpec recoMultAxis = {recoMultBins, "N_{ch}^{reco}"}; - - //=========================================================================== - // Comprehensive Histogram Registration - //=========================================================================== - - // Centrality diagnostic histograms - USE FINE BINNING - ue.add("Centrality/hCentRaw", "Raw FT0M Centrality (no cuts);Centrality (%);Counts", - HistType::kTH1D, {centFineAxis}); - ue.add("Centrality/hCentAfterVtx", "Centrality after vertex cut;Centrality (%);Counts", - HistType::kTH1D, {centFineAxis}); - ue.add("Centrality/hCentAfterINEL", "Centrality after INEL cut;Centrality (%);Counts", - HistType::kTH1D, {centFineAxis}); - ue.add("Centrality/hCentAfterAll", "Centrality after all cuts;Centrality (%);Counts", - HistType::kTH1D, {centFineAxis}); - - // 2D correlations - USE FINE BINNING FOR DIAGNOSTICS - ue.add("Centrality/hCentVsMult", "Centrality vs Generated Multiplicity;Centrality (%);N_{ch}^{gen}", - HistType::kTH2D, {centFineAxis, multAxis}); - ue.add("Centrality/hMultVsCent", "Generated Multiplicity vs Centrality;N_{ch}^{gen};Centrality (%)", - HistType::kTH2D, {multAxis, centFineAxis}); - ue.add("Centrality/hCentVsVz", "Centrality vs Vertex Z;Centrality (%);V_{z} (cm)", - HistType::kTH2D, {centFineAxis, {40, -20, 20}}); - ue.add("Centrality/hRecoMultVsCent", "Reconstructed Track Multiplicity vs Centrality;Centrality (%);N_{tracks}^{reco}", - HistType::kTH2D, {centFineAxis, recoMultAxis}); - ue.add("Centrality/hGenMultPerCent", "Generated Multiplicity Distribution per Centrality Bin;Centrality (%);", - HistType::kTH2D, {centFineAxis, multAxis}); - - // Vertex resolution vs centrality - ue.add("Centrality/hVertexResVsCent", "Vertex Resolution vs Centrality;Centrality (%);V_{z} resolution (cm)", - HistType::kTH2D, {centFineAxis, {100, -1, 1}}); - - // INEL class distributions - ue.add("INEL/hINELClass", "INEL Class for MC Collisions;INEL Class;Counts", - HistType::kTH1D, {{3, 0.5, 3.5}}); - auto hINEL = ue.get(HIST("INEL/hINELClass")); - hINEL->GetXaxis()->SetBinLabel(1, "INEL0"); - hINEL->GetXaxis()->SetBinLabel(2, "INEL>0"); - hINEL->GetXaxis()->SetBinLabel(3, "INEL>1"); - - ue.add("INEL/hINELVsCent", "INEL Class vs Centrality;Centrality (%);INEL Class", - HistType::kTH2D, {centFineAxis, {3, 0.5, 3.5}}); - - // Cut flow - ue.add("CutFlow/hCutStats", "Cut Statistics;Cut Stage;Counts", - HistType::kTH1D, {{6, 0.5, 6.5}}); - auto hCut = ue.get(HIST("CutFlow/hCutStats")); - hCut->GetXaxis()->SetBinLabel(1, "All reco events"); - hCut->GetXaxis()->SetBinLabel(2, "Has MC match"); - hCut->GetXaxis()->SetBinLabel(3, "Has centrality"); - hCut->GetXaxis()->SetBinLabel(4, "Pass vertex"); - hCut->GetXaxis()->SetBinLabel(5, "Pass INEL"); - hCut->GetXaxis()->SetBinLabel(6, "Selected"); - - ue.add("CutFlow/hCentPerCut", "Centrality Distribution at Each Cut;Cut Stage;Centrality (%)", - HistType::kTH2D, {{6, 0.5, 6.5}, centFineAxis}); - - ue.add("MC/GenRecoCollisions", "Generated and Reconstructed MC Collisions", - HistType::kTH1D, {{10, 0.5, 10.5}}); - auto hColl = ue.get(HIST("MC/GenRecoCollisions")); - hColl->GetXaxis()->SetBinLabel(1, "Collisions generated"); - hColl->GetXaxis()->SetBinLabel(2, "Collisions reconstructed"); - hColl->GetXaxis()->SetBinLabel(3, "INEL>0"); - hColl->GetXaxis()->SetBinLabel(4, "INEL>1"); - - ue.add("hEventLossBreakdown", "Event loss breakdown", - HistType::kTH1D, {{4, 0.5, 4.5}}); - // Multiplicity axis - initially raw multiplicity, will represent percentiles after calibration - std::vector centBins(CentClasses, CentClasses + kCentralityClasses + 1); - AxisSpec multAxis = {centBins, "Centrality/Multiplicity Class (%)"}; - - // Raw multiplicity axis for calibration - AxisSpec rawMultAxis = {150, 0, 150, "N_{ch} (|#eta| < 1.0)"}; - - // ======================================================================== - // HISTOGRAM REGISTRY - // ======================================================================== - - // Multiplicity distribution for percentile calibration - ue.add("Calibration/hRawMultiplicity", "Raw multiplicity distribution;N_{ch};Events", - HistType::kTH1D, {rawMultAxis}); - - // Event counting - ue.add("MC/GenRecoCollisions", "Generated and Reconstructed MC Collisions", HistType::kTH1D, {{10, 0.5, 10.5}}); - auto hColl = ue.get(HIST("MC/GenRecoCollisions")); - hColl->GetXaxis()->SetBinLabel(1, "Collisions generated"); - hColl->GetXaxis()->SetBinLabel(2, "Collisions reconstructed"); - - // Event loss histograms - ue.add("MC/EventLoss/MultGenerated", "Generated events vs multiplicity", - HistType::kTH1D, {multAxis}); - ue.add("MC/EventLoss/MultBadVertex", "Events with bad vertex vs multiplicity", - HistType::kTH1D, {multAxis}); - ue.add("MC/EventLoss/MultPhysicsSelected", "Physics-selected events vs multiplicity", - HistType::kTH1D, {multAxis}); - ue.add("MC/EventLoss/MultReconstructed", "Reconstructed events vs multiplicity", - HistType::kTH1D, {multAxis}); - ue.add("MC/EventLoss/MultRecoSelected", "Reconstructed+selected events vs multiplicity", - HistType::kTH1D, {multAxis}); - - ue.add("hEventLossBreakdown", "Event loss breakdown", HistType::kTH1D, {{4, 0.5, 4.5}}); - auto hLoss = ue.get(HIST("hEventLossBreakdown")); - hLoss->GetXaxis()->SetBinLabel(1, "Physics selected"); - hLoss->GetXaxis()->SetBinLabel(2, "Reconstructed"); - hLoss->GetXaxis()->SetBinLabel(3, "Selected"); - hLoss->GetXaxis()->SetBinLabel(4, "Final efficiency"); - - // Multiplicity histograms - ue.add("MC/EventLoss/NchGenerated", "Generated charged multiplicity;N_{ch}^{gen} (|#eta|<0.8);Counts", - HistType::kTH1D, {{200, 0, 200}}); - ue.add("MC/EventLoss/NchGenerated_PhysicsSelected", "Generated charged multiplicity (physics selected);N_{ch}^{gen} (|#eta|<0.8);Counts", - HistType::kTH1D, {{200, 0, 200}}); - ue.add("MC/EventLoss/NchGenerated_Reconstructed", "Generated charged multiplicity (reconstructed);N_{ch}^{gen} (|#eta|<0.8);Counts", - HistType::kTH1D, {{200, 0, 200}}); - - // pT vs Multiplicity - ue.add("MC/GenPtVsNch", "Generated pT vs Multiplicity;#it{p}_{T} (GeV/#it{c});N_{ch}^{gen}", - HistType::kTH2D, {ptAxis, {200, 0, 200}}); - ue.add("MC/GenPtVsNch_PhysicsSelected", "Generated pT vs Multiplicity (physics selected);#it{p}_{T} (GeV/#it{c});N_{ch}^{gen}", - HistType::kTH2D, {ptAxis, {200, 0, 200}}); - - // Centrality vs Multiplicity correlations - USE STANDARD BINNING FOR THESE - ue.add("MC/EventLoss/GenMultVsCent", "Generated charged particles vs FT0M centrality;FT0M Centrality (%);N_{ch}^{gen} (|#eta|<0.8)", - HistType::kTH2D, {centAxis, multAxis}); - ue.add("MC/EventLoss/GenMultVsCent_Selected", "Generated vs FT0M centrality (selected events);FT0M Centrality (%);N_{ch}^{gen}", - HistType::kTH2D, {centAxis, multAxis}); - ue.add("MC/EventLoss/GenMultVsCent_Rejected", "Generated vs FT0M centrality (rejected events);FT0M Centrality (%);N_{ch}^{gen}", - HistType::kTH2D, {centAxis, multAxis}); - - // TPC cluster histograms - ue.add("hNclFoundTPC", "Number of TPC found clusters", - HistType::kTH1D, {{200, 0, 200, "N_{cl, found}"}}); - ue.add("hNclPIDTPC", "Number of TPC PID clusters", - HistType::kTH1D, {{200, 0, 200, "N_{cl, PID}"}}); - ue.add("hNclFoundTPCvsPt", "TPC found clusters vs pT;#it{p}_{T} (GeV/#it{c});N_{cl,found}", - HistType::kTH2D, {ptAxis, {200, 0., 200.}}); - ue.add("hNclPIDTPCvsPt", "TPC PID clusters vs pT;#it{p}_{T} (GeV/#it{c});N_{cl,PID}", - HistType::kTH2D, {ptAxis, {200, 0., 200.}}); - - // Inclusive histograms - ue.add("Inclusive/hPtPrimGenAll", "All generated primaries (no cuts);#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimBadVertex", "Generated primaries (bad vertex);#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimGen", "Generated primaries (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimRecoEv", "Generated primaries (reco events);#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimGoodEv", "Generated primaries (good events);#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - - ue.add("Inclusive/hPtNumEff", "Tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtDenEff", "Tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - - ue.add("Inclusive/hPtAllReco", "All reconstructed tracks;#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimReco", "Reconstructed primaries;#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtSecReco", "Reconstructed secondaries;#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - - ue.add("Inclusive/hPtMeasuredVsCent", "All measured tracks (PID) vs centrality;#it{p}_{T} (GeV/#it{c});FT0M Centrality (%)", - HistType::kTH2D, {ptAxis, centAxis}); - - // Particle-specific histograms - // ======================================================================== - // INCLUSIVE CHARGED PARTICLE HISTOGRAMS - // ======================================================================== - - ue.add("Inclusive/hPtPrimGenAll", "All generated primaries (no cuts);#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimGenAllVsMult", "All generated primaries vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add("Inclusive/hPtPrimBadVertex", "Generated primaries (bad vertex);#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimBadVertexVsMult", "Generated primaries (bad vertex) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add("Inclusive/hPtPrimGen", "Generated primaries (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimGenVsMult", "Generated primaries (after phys sel) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add("Inclusive/hPtPrimRecoEv", "Generated primaries (reco events);#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimRecoEvVsMult", "Generated primaries (reco events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add("Inclusive/hPtPrimGoodEv", "Generated primaries (good events);#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimGoodEvVsMult", "Generated primaries (good events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add("Inclusive/hPtNumEff", "Tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtNumEffVsMult", "Tracking efficiency numerator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add("Inclusive/hPtDenEff", "Tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtDenEffVsMult", "Tracking efficiency denominator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add("Inclusive/hPtAllReco", "All reconstructed tracks;#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtAllRecoVsMult", "All reconstructed tracks vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add("Inclusive/hPtPrimReco", "Reconstructed primaries;#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtPrimRecoVsMult", "Reconstructed primaries vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add("Inclusive/hPtSecReco", "Reconstructed secondaries;#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtSecRecoVsMult", "Reconstructed secondaries vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - - ue.add("Inclusive/hPtMeasured", "All measured tracks;#it{p}_{T} (GeV/#it{c});Counts", - HistType::kTH1D, {ptAxis}); - ue.add("Inclusive/hPtMeasuredVsMult", "All measured tracks vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", - HistType::kTH2D, {ptAxis, multAxis}); - - // ======================================================================== - // PARTICLE-SPECIFIC HISTOGRAMS - // ======================================================================== - - const std::array particleNames = {"Pion", "Kaon", "Proton"}; - const std::array particleSymbols = {"#pi^{#pm}", "K^{#pm}", "p+#bar{p}"}; - - for (int iSpecies = 0; iSpecies < kNSpecies; ++iSpecies) { - const auto& name = particleNames[iSpecies]; - const auto& symbol = particleSymbols[iSpecies]; - - ue.add(Form("%s/hPtPrimGenAll", name.c_str()), - Form("All generated %s (no cuts);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), - HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtPrimBadVertex", name.c_str()), - Form("Generated %s (bad vertex);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), - HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtPrimGen", name.c_str()), - Form("Generated %s (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), - HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtPrimRecoEv", name.c_str()), - Form("Generated %s (reco events);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), - HistType::kTH1D, {ptAxis}); - // 1D versions - ue.add(Form("%s/hPtPrimGenAll", name.c_str()), - Form("All generated %s (no cuts);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + if (applyPhiCut.value) { + fphiCutLow = new TF1("StandardPhiCutLow", + Form("%f/x/x+pi/18.0-%f", + phiCutLowParam1.value, phiCutLowParam2.value), + 0, 50); + fphiCutHigh = new TF1("StandardPhiCutHigh", + Form("%f/x+pi/18.0+%f", + phiCutHighParam1.value, phiCutHighParam2.value), + 0, 50); + + LOGF(info, "=== Phi Cut Parameters ==="); + LOGF(info, "Low cut: %.6f/x² + pi/18 - %.6f", + phiCutLowParam1.value, phiCutLowParam2.value); + LOGF(info, "High cut: %.6f/x + pi/18 + %.6f", + phiCutHighParam1.value, phiCutHighParam2.value); + LOGF(info, "Applied for pT > %.1f GeV/c", pTthresholdPhiCut.value); + } + + ConfigurableAxis ptBinning{ + "ptBinning", + {0.0, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, + 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, + 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.2, 2.4, 2.6, 2.8, + 3.0, 3.5, 4.0, 4.5, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, + 12.0, 14.0, 16.0, 18.0, 20.0, 25.0, 30.0, 40.0, 50.0}, + "pT bin limits"}; + AxisSpec ptAxis = {ptBinning, "#it{p}_{T} (GeV/#it{c})"}; + + std::vector centBinningStd = {0., 1., 5., 10., 15., 20., 30., 40., 50., 60., 70., 80., 90., 100.}; + + // Fine centrality binning for diagnostics (100 bins, guaranteed increasing) + std::vector centBinningFine; + for (int i = 0; i <= 100; i++) { + centBinningFine.push_back(static_cast(i)); + } + + AxisSpec centAxis = {centBinningStd, "FT0M Centrality (%)"}; + AxisSpec centFineAxis = {centBinningFine, "FT0M Centrality (%)"}; + + // Multiplicity axes - properly defined + std::vector multBins; + for (int i = 0; i <= 200; i++) { + multBins.push_back(static_cast(i)); + } + AxisSpec multAxis = {multBins, "N_{ch}^{gen} (|#eta|<0.8)"}; + + // Reconstructed multiplicity axis - properly defined with explicit bin edges + std::vector recoMultBins; + for (int i = 0; i <= 100; i++) { + recoMultBins.push_back(static_cast(i)); + } + AxisSpec recoMultAxis = {recoMultBins, "N_{ch}^{reco}"}; + + // Centrality diagnostic histograms - USE FINE BINNING + ue.add("Centrality/hCentRaw", "Raw FT0M Centrality (no cuts);Centrality (%);Counts", + HistType::kTH1D, {centFineAxis}); + ue.add("Centrality/hCentAfterVtx", "Centrality after vertex cut;Centrality (%);Counts", + HistType::kTH1D, {centFineAxis}); + ue.add("Centrality/hCentAfterINEL", "Centrality after INEL cut;Centrality (%);Counts", + HistType::kTH1D, {centFineAxis}); + ue.add("Centrality/hCentAfterAll", "Centrality after all cuts;Centrality (%);Counts", + HistType::kTH1D, {centFineAxis}); + + // 2D correlations - USE FINE BINNING FOR DIAGNOSTICS + ue.add("Centrality/hCentVsMult", "Centrality vs Generated Multiplicity;Centrality (%);N_{ch}^{gen}", + HistType::kTH2D, {centFineAxis, multAxis}); + ue.add("Centrality/hMultVsCent", "Generated Multiplicity vs Centrality;N_{ch}^{gen};Centrality (%)", + HistType::kTH2D, {multAxis, centFineAxis}); + ue.add("Centrality/hCentVsVz", "Centrality vs Vertex Z;Centrality (%);V_{z} (cm)", + HistType::kTH2D, {centFineAxis, {40, -20, 20}}); + ue.add("Centrality/hRecoMultVsCent", "Reconstructed Track Multiplicity vs Centrality;Centrality (%);N_{tracks}^{reco}", + HistType::kTH2D, {centFineAxis, recoMultAxis}); + ue.add("Centrality/hGenMultPerCent", "Generated Multiplicity Distribution per Centrality Bin;Centrality (%);", + HistType::kTH2D, {centFineAxis, multAxis}); + + // Vertex resolution vs centrality + ue.add("Centrality/hVertexResVsCent", "Vertex Resolution vs Centrality;Centrality (%);V_{z} resolution (cm)", + HistType::kTH2D, {centFineAxis, {100, -1, 1}}); + + // INEL class distributions + ue.add("INEL/hINELClass", "INEL Class for MC Collisions;INEL Class;Counts", + HistType::kTH1D, {{3, 0.5, 3.5}}); + auto hINEL = ue.get(HIST("INEL/hINELClass")); + hINEL->GetXaxis()->SetBinLabel(1, "INEL0"); + hINEL->GetXaxis()->SetBinLabel(2, "INEL>0"); + hINEL->GetXaxis()->SetBinLabel(3, "INEL>1"); + + ue.add("INEL/hINELVsCent", "INEL Class vs Centrality;Centrality (%);INEL Class", + HistType::kTH2D, {centFineAxis, {3, 0.5, 3.5}}); + + // Cut flow + ue.add("CutFlow/hCutStats", "Cut Statistics;Cut Stage;Counts", + HistType::kTH1D, {{6, 0.5, 6.5}}); + auto hCut = ue.get(HIST("CutFlow/hCutStats")); + hCut->GetXaxis()->SetBinLabel(1, "All reco events"); + hCut->GetXaxis()->SetBinLabel(2, "Has MC match"); + hCut->GetXaxis()->SetBinLabel(3, "Has centrality"); + hCut->GetXaxis()->SetBinLabel(4, "Pass vertex"); + hCut->GetXaxis()->SetBinLabel(5, "Pass INEL"); + hCut->GetXaxis()->SetBinLabel(6, "Selected"); + + ue.add("CutFlow/hCentPerCut", "Centrality Distribution at Each Cut;Cut Stage;Centrality (%)", + HistType::kTH2D, {{6, 0.5, 6.5}, centFineAxis}); + + ue.add("MC/GenRecoCollisions", "Generated and Reconstructed MC Collisions", + HistType::kTH1D, {{10, 0.5, 10.5}}); + auto hColl = ue.get(HIST("MC/GenRecoCollisions")); + hColl->GetXaxis()->SetBinLabel(1, "Collisions generated"); + hColl->GetXaxis()->SetBinLabel(2, "Collisions reconstructed"); + hColl->GetXaxis()->SetBinLabel(3, "INEL>0"); + hColl->GetXaxis()->SetBinLabel(4, "INEL>1"); + + ue.add("hEventLossBreakdown", "Event loss breakdown", + HistType::kTH1D, {{4, 0.5, 4.5}}); + // Multiplicity axis - initially raw multiplicity, will represent percentiles after calibration + std::vector centBins(CentClasses, CentClasses + kCentralityClasses + 1); + AxisSpec multAxis = {centBins, "Centrality/Multiplicity Class (%)"}; + + // Raw multiplicity axis for calibration + AxisSpec rawMultAxis = {150, 0, 150, "N_{ch} (|#eta| < 1.0)"}; + + // Multiplicity distribution for percentile calibration + ue.add("Calibration/hRawMultiplicity", "Raw multiplicity distribution;N_{ch};Events", + HistType::kTH1D, {rawMultAxis}); + + // Event counting + ue.add("MC/GenRecoCollisions", "Generated and Reconstructed MC Collisions", HistType::kTH1D, {{10, 0.5, 10.5}}); + auto hColl = ue.get(HIST("MC/GenRecoCollisions")); + hColl->GetXaxis()->SetBinLabel(1, "Collisions generated"); + hColl->GetXaxis()->SetBinLabel(2, "Collisions reconstructed"); + + // Event loss histograms + ue.add("MC/EventLoss/MultGenerated", "Generated events vs multiplicity", + HistType::kTH1D, {multAxis}); + ue.add("MC/EventLoss/MultBadVertex", "Events with bad vertex vs multiplicity", + HistType::kTH1D, {multAxis}); + ue.add("MC/EventLoss/MultPhysicsSelected", "Physics-selected events vs multiplicity", + HistType::kTH1D, {multAxis}); + ue.add("MC/EventLoss/MultReconstructed", "Reconstructed events vs multiplicity", + HistType::kTH1D, {multAxis}); + ue.add("MC/EventLoss/MultRecoSelected", "Reconstructed+selected events vs multiplicity", + HistType::kTH1D, {multAxis}); + + ue.add("hEventLossBreakdown", "Event loss breakdown", HistType::kTH1D, {{4, 0.5, 4.5}}); + auto hLoss = ue.get(HIST("hEventLossBreakdown")); + hLoss->GetXaxis()->SetBinLabel(1, "Physics selected"); + hLoss->GetXaxis()->SetBinLabel(2, "Reconstructed"); + hLoss->GetXaxis()->SetBinLabel(3, "Selected"); + hLoss->GetXaxis()->SetBinLabel(4, "Final efficiency"); + + // Multiplicity histograms + ue.add("MC/EventLoss/NchGenerated", "Generated charged multiplicity;N_{ch}^{gen} (|#eta|<0.8);Counts", + HistType::kTH1D, {{200, 0, 200}}); + ue.add("MC/EventLoss/NchGenerated_PhysicsSelected", "Generated charged multiplicity (physics selected);N_{ch}^{gen} (|#eta|<0.8);Counts", + HistType::kTH1D, {{200, 0, 200}}); + ue.add("MC/EventLoss/NchGenerated_Reconstructed", "Generated charged multiplicity (reconstructed);N_{ch}^{gen} (|#eta|<0.8);Counts", + HistType::kTH1D, {{200, 0, 200}}); + + // pT vs Multiplicity + ue.add("MC/GenPtVsNch", "Generated pT vs Multiplicity;#it{p}_{T} (GeV/#it{c});N_{ch}^{gen}", + HistType::kTH2D, {ptAxis, {200, 0, 200}}); + ue.add("MC/GenPtVsNch_PhysicsSelected", "Generated pT vs Multiplicity (physics selected);#it{p}_{T} (GeV/#it{c});N_{ch}^{gen}", + HistType::kTH2D, {ptAxis, {200, 0, 200}}); + + // Centrality vs Multiplicity correlations - USE STANDARD BINNING FOR THESE + ue.add("MC/EventLoss/GenMultVsCent", "Generated charged particles vs FT0M centrality;FT0M Centrality (%);N_{ch}^{gen} (|#eta|<0.8)", + HistType::kTH2D, {centAxis, multAxis}); + ue.add("MC/EventLoss/GenMultVsCent_Selected", "Generated vs FT0M centrality (selected events);FT0M Centrality (%);N_{ch}^{gen}", + HistType::kTH2D, {centAxis, multAxis}); + ue.add("MC/EventLoss/GenMultVsCent_Rejected", "Generated vs FT0M centrality (rejected events);FT0M Centrality (%);N_{ch}^{gen}", + HistType::kTH2D, {centAxis, multAxis}); + + // TPC cluster histograms + ue.add("hNclFoundTPC", "Number of TPC found clusters", + HistType::kTH1D, {{200, 0, 200, "N_{cl, found}"}}); + ue.add("hNclPIDTPC", "Number of TPC PID clusters", + HistType::kTH1D, {{200, 0, 200, "N_{cl, PID}"}}); + ue.add("hNclFoundTPCvsPt", "TPC found clusters vs pT;#it{p}_{T} (GeV/#it{c});N_{cl,found}", + HistType::kTH2D, {ptAxis, {200, 0., 200.}}); + ue.add("hNclPIDTPCvsPt", "TPC PID clusters vs pT;#it{p}_{T} (GeV/#it{c});N_{cl,PID}", + HistType::kTH2D, {ptAxis, {200, 0., 200.}}); + + // Inclusive histograms + ue.add("Inclusive/hPtPrimGenAll", "All generated primaries (no cuts);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - - ue.add(Form("%s/hPtPrimBadVertex", name.c_str()), - Form("Generated %s (bad vertex);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtPrimBadVertex", "Generated primaries (bad vertex);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - - ue.add(Form("%s/hPtPrimGen", name.c_str()), - Form("Generated %s (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtPrimGen", "Generated primaries (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - - ue.add(Form("%s/hPtPrimRecoEv", name.c_str()), - Form("Generated %s (reco events);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtPrimRecoEv", "Generated primaries (reco events);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - - ue.add(Form("%s/hPtPrimGoodEv", name.c_str()), - Form("Generated %s (good events);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtPrimGoodEv", "Generated primaries (good events);#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtNumEff", name.c_str()), - Form("%s tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtNumEff", "Tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtDenEff", name.c_str()), - Form("%s tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtDenEff", "Tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtAllReco", name.c_str()), - Form("All reconstructed %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtAllReco", "All reconstructed tracks;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtPrimReco", name.c_str()), - Form("Reconstructed primary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtPrimReco", "Reconstructed primaries;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtSecReco", name.c_str()), - Form("Reconstructed secondary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtSecReco", "Reconstructed secondaries;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtMeasuredVsCent", name.c_str()), - Form("Measured %s (PID) vs centrality;#it{p}_{T} (GeV/#it{c});FT0M Centrality (%%)", symbol.c_str()), + ue.add("Inclusive/hPtMeasuredVsCent", "All measured tracks (PID) vs centrality;#it{p}_{T} (GeV/#it{c});FT0M Centrality (%)", HistType::kTH2D, {ptAxis, centAxis}); - // 2D versions (vs multiplicity class) - ue.add(Form("%s/hPtPrimGenAllVsMult", name.c_str()), - Form("All generated %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + ue.add("Inclusive/hPtPrimGenAll", "All generated primaries (no cuts);#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtPrimGenAllVsMult", "All generated primaries vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", HistType::kTH2D, {ptAxis, multAxis}); - ue.add(Form("%s/hPtPrimBadVertexVsMult", name.c_str()), - Form("Generated %s (bad vertex) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + ue.add("Inclusive/hPtPrimBadVertex", "Generated primaries (bad vertex);#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtPrimBadVertexVsMult", "Generated primaries (bad vertex) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", HistType::kTH2D, {ptAxis, multAxis}); - ue.add(Form("%s/hPtPrimGenVsMult", name.c_str()), - Form("Generated %s (after phys sel) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + ue.add("Inclusive/hPtPrimGen", "Generated primaries (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtPrimGenVsMult", "Generated primaries (after phys sel) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", HistType::kTH2D, {ptAxis, multAxis}); - ue.add(Form("%s/hPtPrimRecoEvVsMult", name.c_str()), - Form("Generated %s (reco events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + ue.add("Inclusive/hPtPrimRecoEv", "Generated primaries (reco events);#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtPrimRecoEvVsMult", "Generated primaries (reco events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", HistType::kTH2D, {ptAxis, multAxis}); - ue.add(Form("%s/hPtPrimGoodEvVsMult", name.c_str()), - Form("Generated %s (good events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + ue.add("Inclusive/hPtPrimGoodEv", "Generated primaries (good events);#it{p}_{T} (GeV/#it{c});Counts", + HistType::kTH1D, {ptAxis}); + ue.add("Inclusive/hPtPrimGoodEvVsMult", "Generated primaries (good events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", HistType::kTH2D, {ptAxis, multAxis}); - // Tracking efficiency - ue.add(Form("%s/hPtNumEff", name.c_str()), - Form("%s tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtNumEff", "Tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtNumEffVsMult", name.c_str()), - Form("%s tracking eff numerator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + ue.add("Inclusive/hPtNumEffVsMult", "Tracking efficiency numerator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", HistType::kTH2D, {ptAxis, multAxis}); - ue.add(Form("%s/hPtDenEff", name.c_str()), - Form("%s tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtDenEff", "Tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtDenEffVsMult", name.c_str()), - Form("%s tracking eff denominator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + ue.add("Inclusive/hPtDenEffVsMult", "Tracking efficiency denominator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", HistType::kTH2D, {ptAxis, multAxis}); - // Primary fraction - ue.add(Form("%s/hPtAllReco", name.c_str()), - Form("All reconstructed %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtAllReco", "All reconstructed tracks;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtAllRecoVsMult", name.c_str()), - Form("All reconstructed %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + ue.add("Inclusive/hPtAllRecoVsMult", "All reconstructed tracks vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", HistType::kTH2D, {ptAxis, multAxis}); - ue.add(Form("%s/hPtPrimReco", name.c_str()), - Form("Reconstructed primary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtPrimReco", "Reconstructed primaries;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtPrimRecoVsMult", name.c_str()), - Form("Reconstructed primary %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + ue.add("Inclusive/hPtPrimRecoVsMult", "Reconstructed primaries vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", HistType::kTH2D, {ptAxis, multAxis}); - ue.add(Form("%s/hPtSecReco", name.c_str()), - Form("Reconstructed secondary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtSecReco", "Reconstructed secondaries;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtSecRecoVsMult", name.c_str()), - Form("Reconstructed secondary %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + ue.add("Inclusive/hPtSecRecoVsMult", "Reconstructed secondaries vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", HistType::kTH2D, {ptAxis, multAxis}); - // Measured spectra - ue.add(Form("%s/hPtMeasured", name.c_str()), - Form("Measured %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + ue.add("Inclusive/hPtMeasured", "All measured tracks;#it{p}_{T} (GeV/#it{c});Counts", HistType::kTH1D, {ptAxis}); - ue.add(Form("%s/hPtMeasuredVsMult", name.c_str()), - Form("Measured %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + ue.add("Inclusive/hPtMeasuredVsMult", "All measured tracks vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%)", HistType::kTH2D, {ptAxis, multAxis}); - // PID quality - if (enablePIDHistograms) { - ue.add(Form("%s/hNsigmaTPC", name.c_str()), - Form("TPC n#sigma %s;#it{p}_{T} (GeV/#it{c});n#sigma_{TPC}", symbol.c_str()), - HistType::kTH2D, {ptAxis, {200, -10, 10}}); + const std::array particleNames = {"Pion", "Kaon", "Proton"}; + const std::array particleSymbols = {"#pi^{#pm}", "K^{#pm}", "p+#bar{p}"}; + + for (int iSpecies = 0; iSpecies < kNSpecies; ++iSpecies) { + const auto& name = particleNames[iSpecies]; + const auto& symbol = particleSymbols[iSpecies]; + + ue.add(Form("%s/hPtPrimGenAll", name.c_str()), + Form("All generated %s (no cuts);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtPrimBadVertex", name.c_str()), + Form("Generated %s (bad vertex);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtPrimGen", name.c_str()), + Form("Generated %s (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtPrimRecoEv", name.c_str()), + Form("Generated %s (reco events);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + // 1D versions + ue.add(Form("%s/hPtPrimGenAll", name.c_str()), + Form("All generated %s (no cuts);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + ue.add(Form("%s/hPtPrimBadVertex", name.c_str()), + Form("Generated %s (bad vertex);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + ue.add(Form("%s/hPtPrimGen", name.c_str()), + Form("Generated %s (after physics selection);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + ue.add(Form("%s/hPtPrimRecoEv", name.c_str()), + Form("Generated %s (reco events);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + ue.add(Form("%s/hPtPrimGoodEv", name.c_str()), + Form("Generated %s (good events);#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + ue.add(Form("%s/hPtNumEff", name.c_str()), + Form("%s tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtDenEff", name.c_str()), + Form("%s tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + ue.add(Form("%s/hPtAllReco", name.c_str()), + Form("All reconstructed %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtPrimReco", name.c_str()), + Form("Reconstructed primary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtSecReco", name.c_str()), + Form("Reconstructed secondary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + + ue.add(Form("%s/hPtMeasuredVsCent", name.c_str()), + Form("Measured %s (PID) vs centrality;#it{p}_{T} (GeV/#it{c});FT0M Centrality (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, centAxis}); + + // 2D versions (vs multiplicity class) + ue.add(Form("%s/hPtPrimGenAllVsMult", name.c_str()), + Form("All generated %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add(Form("%s/hPtPrimBadVertexVsMult", name.c_str()), + Form("Generated %s (bad vertex) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add(Form("%s/hPtPrimGenVsMult", name.c_str()), + Form("Generated %s (after phys sel) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add(Form("%s/hPtPrimRecoEvVsMult", name.c_str()), + Form("Generated %s (reco events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add(Form("%s/hPtPrimGoodEvVsMult", name.c_str()), + Form("Generated %s (good events) vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + // Tracking efficiency + ue.add(Form("%s/hPtNumEff", name.c_str()), + Form("%s tracking efficiency numerator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtNumEffVsMult", name.c_str()), + Form("%s tracking eff numerator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add(Form("%s/hPtDenEff", name.c_str()), + Form("%s tracking efficiency denominator;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtDenEffVsMult", name.c_str()), + Form("%s tracking eff denominator vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + // Primary fraction + ue.add(Form("%s/hPtAllReco", name.c_str()), + Form("All reconstructed %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtAllRecoVsMult", name.c_str()), + Form("All reconstructed %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add(Form("%s/hPtPrimReco", name.c_str()), + Form("Reconstructed primary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtPrimRecoVsMult", name.c_str()), + Form("Reconstructed primary %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + ue.add(Form("%s/hPtSecReco", name.c_str()), + Form("Reconstructed secondary %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtSecRecoVsMult", name.c_str()), + Form("Reconstructed secondary %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + // Measured spectra + ue.add(Form("%s/hPtMeasured", name.c_str()), + Form("Measured %s;#it{p}_{T} (GeV/#it{c});Counts", symbol.c_str()), + HistType::kTH1D, {ptAxis}); + ue.add(Form("%s/hPtMeasuredVsMult", name.c_str()), + Form("Measured %s vs mult;#it{p}_{T} (GeV/#it{c});Mult Class (%%)", symbol.c_str()), + HistType::kTH2D, {ptAxis, multAxis}); + + // PID quality + if (enablePIDHistograms) { + ue.add(Form("%s/hNsigmaTPC", name.c_str()), + Form("TPC n#sigma %s;#it{p}_{T} (GeV/#it{c});n#sigma_{TPC}", symbol.c_str()), + HistType::kTH2D, {ptAxis, {200, -10, 10}}); + } } - } - // Event selection histogram - // ======================================================================== - // PHI CUT MONITORING - // ======================================================================== - - if (applyPhiCut.value) { - ue.add("PhiCut/hPtVsPhiPrimeBefore", "pT vs φ' before cut;p_{T} (GeV/c);φ'", - HistType::kTH2F, {{100, 0, 10}, {100, 0, 0.4}}); - ue.add("PhiCut/hPtVsPhiPrimeAfter", "pT vs φ' after cut;p_{T} (GeV/c);φ'", - HistType::kTH2F, {{100, 0, 10}, {100, 0, 0.4}}); - ue.add("PhiCut/hRejectionRate", "Track rejection rate by phi cut;p_{T} (GeV/c);Rejection Rate", - HistType::kTProfile, {{100, 0, 10}}); - } + // Event selection histogram + // ======================================================================== + // PHI CUT MONITORING + // ======================================================================== - // ======================================================================== - // EVENT SELECTION HISTOGRAM - // ======================================================================== - - constexpr int nEvSelBins = 20; - constexpr float evSelMin = 0.5f; - constexpr float evSelMax = 20.5f; - ue.add("evsel", "Event selection", HistType::kTH1D, {{nEvSelBins, evSelMin, evSelMax}}); - auto h = ue.get(HIST("evsel")); - h->GetXaxis()->SetBinLabel(1, "Events read"); - h->GetXaxis()->SetBinLabel(2, "INEL>0"); - h->GetXaxis()->SetBinLabel(3, "INEL>1"); - h->GetXaxis()->SetBinLabel(4, "Trigger passed"); - h->GetXaxis()->SetBinLabel(5, "NoITSROFrameBorder"); - h->GetXaxis()->SetBinLabel(6, "NoSameBunchPileup"); - h->GetXaxis()->SetBinLabel(7, "IsGoodZvtxFT0vsPV"); - h->GetXaxis()->SetBinLabel(8, "IsVertexITSTPC"); - h->GetXaxis()->SetBinLabel(9, "NoTimeFrameBorder"); - h->GetXaxis()->SetBinLabel(13, "posZ passed"); - - // Basic tracking histograms - h->GetXaxis()->SetBinLabel(14, "INEL>0 (final)"); - h->GetXaxis()->SetBinLabel(15, "INEL>1 (final)"); - - ue.add("hEta", "Track eta;#eta;Counts", HistType::kTH1D, {{20, -0.8, 0.8}}); - ue.add("hPhi", "Track phi;#varphi (rad);Counts", HistType::kTH1D, {{64, 0, 2.0 * M_PI}}); - ue.add("hvtxZ", "Vertex Z (data);Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); - ue.add("hvtxZmc", "MC vertex Z;Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); - - LOG(info) << "=== Initialized MultiplicityPt task with full centrality diagnostics ==="; - LOG(info) << "Standard centrality binning: " << centBinningStd.size() - 1 << " bins (0-100%)"; - LOG(info) << "Fine centrality binning: " << centBinningFine.size() - 1 << " bins (0-100%)"; -} + if (applyPhiCut.value) { + ue.add("PhiCut/hPtVsPhiPrimeBefore", "pT vs φ' before cut;p_{T} (GeV/c);φ'", + HistType::kTH2F, {{100, 0, 10}, {100, 0, 0.4}}); + ue.add("PhiCut/hPtVsPhiPrimeAfter", "pT vs φ' after cut;p_{T} (GeV/c);φ'", + HistType::kTH2F, {{100, 0, 10}, {100, 0, 0.4}}); + ue.add("PhiCut/hRejectionRate", "Track rejection rate by phi cut;p_{T} (GeV/c);Rejection Rate", + HistType::kTProfile, {{100, 0, 10}}); + } + + constexpr int nEvSelBins = 20; + constexpr float evSelMin = 0.5f; + constexpr float evSelMax = 20.5f; + ue.add("evsel", "Event selection", HistType::kTH1D, {{nEvSelBins, evSelMin, evSelMax}}); + auto h = ue.get(HIST("evsel")); + h->GetXaxis()->SetBinLabel(1, "Events read"); + h->GetXaxis()->SetBinLabel(2, "INEL>0"); + h->GetXaxis()->SetBinLabel(3, "INEL>1"); + h->GetXaxis()->SetBinLabel(4, "Trigger passed"); + h->GetXaxis()->SetBinLabel(5, "NoITSROFrameBorder"); + h->GetXaxis()->SetBinLabel(6, "NoSameBunchPileup"); + h->GetXaxis()->SetBinLabel(7, "IsGoodZvtxFT0vsPV"); + h->GetXaxis()->SetBinLabel(8, "IsVertexITSTPC"); + h->GetXaxis()->SetBinLabel(9, "NoTimeFrameBorder"); + h->GetXaxis()->SetBinLabel(13, "posZ passed"); + + // Basic tracking histograms + h->GetXaxis()->SetBinLabel(14, "INEL>0 (final)"); + h->GetXaxis()->SetBinLabel(15, "INEL>1 (final)"); + + ue.add("hEta", "Track eta;#eta;Counts", HistType::kTH1D, {{20, -0.8, 0.8}}); + ue.add("hPhi", "Track phi;#varphi (rad);Counts", HistType::kTH1D, {{64, 0, 2.0 * M_PI}}); + ue.add("hvtxZ", "Vertex Z (data);Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); + ue.add("hvtxZmc", "MC vertex Z;Vertex Z (cm);Events", HistType::kTH1F, {{40, -20.0, 20.0}}); + + LOG(info) << "=== Initialized MultiplicityPt task with full centrality diagnostics ==="; + LOG(info) << "Standard centrality binning: " << centBinningStd.size() - 1 << " bins (0-100%)"; + LOG(info) << "Fine centrality binning: " << centBinningFine.size() - 1 << " bins (0-100%)"; + } -//============================================================================= -// Process Functions -//============================================================================= + void MultiplicityPt::processData(CollisionTableData::iterator const& /*collision*/, + TrackTableData const& /*tracks*/, + BCsRun3 const& /*bcs*/) + { + // Intentionally empty - data processing disabled + } -void MultiplicityPt::processData(CollisionTableData::iterator const& /*collision*/, - TrackTableData const& /*tracks*/, + void MultiplicityPt::processMC(TrackTableMC const& tracks, + aod::McParticles const& particles, + aod::McCollisions const& mcCollisions, + RecoCollisions const& collisions, + aod::McCollisionLabels const& labels, + aod::McCentFT0Ms const& centTable, BCsRun3 const& /*bcs*/) -{ - // Intentionally empty - data processing disabled -} + { + LOG(info) << "\n=== processMC START ==="; + LOG(info) << "Total MC collisions (generated): " << mcCollisions.size(); + LOG(info) << "Total reconstructed collisions: " << collisions.size(); + LOG(info) << "Total collision labels: " << labels.size(); + LOG(info) << "Total centrality entries: " << centTable.size(); + + LOG(info) << "\n=== CENTRALITY DEBUG - RAW DATA ==="; + LOG(info) << "First 20 centrality values from centTable:"; + int debugCount = 0; + float minCent = 999.0f, maxCent = -999.0f; + std::map centDistribution; + + for (const auto& cent : centTable) { + float c = cent.centFT0M(); + if (debugCount < 20) { + LOG(info) << " Cent entry " << debugCount << ": " << c; + } + minCent = std::min(minCent, c); + maxCent = std::max(maxCent, c); -void MultiplicityPt::processMC(TrackTableMC const& tracks, - aod::McParticles const& particles, - aod::McCollisions const& mcCollisions, - RecoCollisions const& collisions, - aod::McCollisionLabels const& labels, - aod::McCentFT0Ms const& centTable, - BCsRun3 const& /*bcs*/) -{ - LOG(info) << "\n=== processMC START ==="; - LOG(info) << "Total MC collisions (generated): " << mcCollisions.size(); - LOG(info) << "Total reconstructed collisions: " << collisions.size(); - LOG(info) << "Total collision labels: " << labels.size(); - LOG(info) << "Total centrality entries: " << centTable.size(); - - //=========================================================================== - // DEBUG: Print raw centrality information first - //=========================================================================== - LOG(info) << "\n=== CENTRALITY DEBUG - RAW DATA ==="; - LOG(info) << "First 20 centrality values from centTable:"; - int debugCount = 0; - float minCent = 999.0f, maxCent = -999.0f; - std::map centDistribution; - - for (const auto& cent : centTable) { - float c = cent.centFT0M(); - if (debugCount < 20) { - LOG(info) << " Cent entry " << debugCount << ": " << c; + int bin10 = static_cast(c / 10) * 10; + centDistribution[bin10]++; + debugCount++; } - minCent = std::min(minCent, c); - maxCent = std::max(maxCent, c); - int bin10 = static_cast(c / 10) * 10; - centDistribution[bin10]++; - debugCount++; - } + LOG(info) << "Centrality range: [" << minCent << ", " << maxCent << "]"; + LOG(info) << "Distribution by 10% bins:"; + for (int i = 0; i < 100; i += 10) { + LOG(info) << " " << i << "-" << i + 10 << "%: " << centDistribution[i]; + } - LOG(info) << "Centrality range: [" << minCent << ", " << maxCent << "]"; - LOG(info) << "Distribution by 10% bins:"; - for (int i = 0; i < 100; i += 10) { - LOG(info) << " " << i << "-" << i + 10 << "%: " << centDistribution[i]; - } + // Check if centrality is inverted (0 = peripheral, 100 = central) + // If minCent is near 0 and maxCent near 100, check correlation with multiplicity + LOG(info) << "Checking if centrality might be inverted..."; + LOG(info) << "Will check correlation with multiplicity in the next step."; - // Check if centrality is inverted (0 = peripheral, 100 = central) - // If minCent is near 0 and maxCent near 100, check correlation with multiplicity - LOG(info) << "Checking if centrality might be inverted..."; - LOG(info) << "Will check correlation with multiplicity in the next step."; + std::map mcCollisionToNch; + std::map mcCollisionVz; + std::set physicsSelectedMCCollisions; + std::map mcCollisionToINELClass; // 0=INEL0, 1=INEL>0, 2=INEL>1 - //=========================================================================== - // FIRST PASS: Build maps of MC collision ID to generated particle counts - //=========================================================================== - std::map mcCollisionToNch; - std::map mcCollisionVz; - std::set physicsSelectedMCCollisions; - std::map mcCollisionToINELClass; // 0=INEL0, 1=INEL>0, 2=INEL>1 + ue.fill(HIST("MC/GenRecoCollisions"), 1.f, mcCollisions.size()); + ue.fill(HIST("MC/GenRecoCollisions"), 2.f, collisions.size()); - ue.fill(HIST("MC/GenRecoCollisions"), 1.f, mcCollisions.size()); - ue.fill(HIST("MC/GenRecoCollisions"), 2.f, collisions.size()); + LOG(info) << "\n--- FIRST PASS: Building MC collision maps ---"; - LOG(info) << "\n--- FIRST PASS: Building MC collision maps ---"; + int mcWithParticles = 0; + int mcINEL0 = 0, mcINELgt0 = 0, mcINELgt1 = 0; - int mcWithParticles = 0; - int mcINEL0 = 0, mcINELgt0 = 0, mcINELgt1 = 0; + for (const auto& mcCollision : mcCollisions) { + int64_t mcCollId = mcCollision.globalIndex(); + auto particlesInCollision = particles.sliceBy(perMCCol, mcCollId); - for (const auto& mcCollision : mcCollisions) { - int64_t mcCollId = mcCollision.globalIndex(); - auto particlesInCollision = particles.sliceBy(perMCCol, mcCollId); + int nGenCharged = countGeneratedChargedPrimaries(particlesInCollision, cfgCutEtaMax.value, cfgTrkLowPtCut.value); - int nGenCharged = countGeneratedChargedPrimaries(particlesInCollision, cfgCutEtaMax.value, cfgTrkLowPtCut.value); + mcCollisionToNch[mcCollId] = nGenCharged; + mcCollisionVz[mcCollId] = mcCollision.posZ(); - mcCollisionToNch[mcCollId] = nGenCharged; - mcCollisionVz[mcCollId] = mcCollision.posZ(); + // Determine INEL class + bool inel0 = o2::pwglf::isINELgt0mc(particlesInCollision, pdg); + bool inel1 = o2::pwglf::isINELgt1mc(particlesInCollision, pdg); - // Determine INEL class - bool inel0 = o2::pwglf::isINELgt0mc(particlesInCollision, pdg); - bool inel1 = o2::pwglf::isINELgt1mc(particlesInCollision, pdg); + int inelClass = 0; + if (inel1) + inelClass = 2; + else if (inel0) + inelClass = 1; + mcCollisionToINELClass[mcCollId] = inelClass; - int inelClass = 0; - if (inel1) - inelClass = 2; - else if (inel0) - inelClass = 1; - mcCollisionToINELClass[mcCollId] = inelClass; + ue.fill(HIST("INEL/hINELClass"), inelClass); - ue.fill(HIST("INEL/hINELClass"), inelClass); + if (inel0) + mcINELgt0++; + if (inel1) + mcINELgt1++; + if (nGenCharged > 0) + mcWithParticles++; - if (inel0) - mcINELgt0++; - if (inel1) - mcINELgt1++; - if (nGenCharged > 0) - mcWithParticles++; + ue.fill(HIST("MC/EventLoss/NchGenerated"), nGenCharged); - ue.fill(HIST("MC/EventLoss/NchGenerated"), nGenCharged); + // Physics selection based on vertex and INEL cuts + bool physicsSelected = true; - // Physics selection based on vertex and INEL cuts - bool physicsSelected = true; + if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) { + physicsSelected = false; + } - if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) { - physicsSelected = false; - } + // Apply INEL cut based on configuration + if (cfgINELCut.value == 1 && !inel0) { + physicsSelected = false; + } + if (cfgINELCut.value == 2 && !inel1) { + physicsSelected = false; + } - // Apply INEL cut based on configuration - if (cfgINELCut.value == 1 && !inel0) { - physicsSelected = false; - } - if (cfgINELCut.value == 2 && !inel1) { - physicsSelected = false; + if (physicsSelected) { + physicsSelectedMCCollisions.insert(mcCollId); + ue.fill(HIST("MC/EventLoss/NchGenerated_PhysicsSelected"), nGenCharged); + + if (inel0) { + ue.fill(HIST("MC/GenRecoCollisions"), 3.f); + } + if (inel1) { + ue.fill(HIST("MC/GenRecoCollisions"), 4.f); + } + } } - if (physicsSelected) { - physicsSelectedMCCollisions.insert(mcCollId); - ue.fill(HIST("MC/EventLoss/NchGenerated_PhysicsSelected"), nGenCharged); + LOG(info) << "\n--- FIRST PASS SUMMARY ---"; + LOG(info) << "Total MC collisions processed: " << mcCollisions.size(); + LOG(info) << "MC collisions with particles: " << mcWithParticles; + LOG(info) << "INEL0: " << (mcCollisions.size() - mcINELgt0); + LOG(info) << "INEL>0: " << mcINELgt0; + LOG(info) << "INEL>1: " << mcINELgt1; + LOG(info) << "Physics-selected MC collisions: " << physicsSelectedMCCollisions.size(); + + std::map recoToMcMap; + std::map recoToCentMap; + + size_t nCollisions = collisions.size(); - if (inel0) { - ue.fill(HIST("MC/GenRecoCollisions"), 3.f); + // Associate labels with collisions by index + size_t iLabel = 0; + for (const auto& label : labels) { + if (iLabel < nCollisions) { + const auto& collision = collisions.iteratorAt(iLabel); + int64_t recoCollId = collision.globalIndex(); + int64_t mcCollId = label.mcCollisionId(); + recoToMcMap[recoCollId] = mcCollId; } - if (inel1) { - ue.fill(HIST("MC/GenRecoCollisions"), 4.f); + iLabel++; + } + + // Associate centrality with collisions by index + size_t iCent = 0; + for (const auto& cent : centTable) { + if (iCent < nCollisions) { + const auto& collision = collisions.iteratorAt(iCent); + int64_t recoCollId = collision.globalIndex(); + float centValue = cent.centFT0M(); + + // Fill raw centrality histogram + ue.fill(HIST("Centrality/hCentRaw"), centValue); + + recoToCentMap[recoCollId] = centValue; } + iCent++; } - } - LOG(info) << "\n--- FIRST PASS SUMMARY ---"; - LOG(info) << "Total MC collisions processed: " << mcCollisions.size(); - LOG(info) << "MC collisions with particles: " << mcWithParticles; - LOG(info) << "INEL0: " << (mcCollisions.size() - mcINELgt0); - LOG(info) << "INEL>0: " << mcINELgt0; - LOG(info) << "INEL>1: " << mcINELgt1; - LOG(info) << "Physics-selected MC collisions: " << physicsSelectedMCCollisions.size(); - - //=========================================================================== - // Build maps for labels and centrality - //=========================================================================== - std::map recoToMcMap; - std::map recoToCentMap; - - size_t nCollisions = collisions.size(); - - // Associate labels with collisions by index - size_t iLabel = 0; - for (const auto& label : labels) { - if (iLabel < nCollisions) { - const auto& collision = collisions.iteratorAt(iLabel); - int64_t recoCollId = collision.globalIndex(); - int64_t mcCollId = label.mcCollisionId(); - recoToMcMap[recoCollId] = mcCollId; + LOG(info) << "\n--- MAP SIZES ---"; + LOG(info) << "recoToMcMap size: " << recoToMcMap.size(); + LOG(info) << "recoToCentMap size: " << recoToCentMap.size(); + + LOG(info) << "\n=== CENTRALITY VS MULTIPLICITY DEBUG ==="; + + // Create temporary vectors to check correlation + std::vector> centMultPairs; + for (const auto& collision : collisions) { + int64_t collId = collision.globalIndex(); + + auto mcIt = recoToMcMap.find(collId); + if (mcIt == recoToMcMap.end()) + continue; + + auto centIt = recoToCentMap.find(collId); + if (centIt == recoToCentMap.end()) + continue; + + auto nchIt = mcCollisionToNch.find(mcIt->second); + if (nchIt == mcCollisionToNch.end()) + continue; + + centMultPairs.push_back({centIt->second, nchIt->second}); } - iLabel++; - } - // Associate centrality with collisions by index - size_t iCent = 0; - for (const auto& cent : centTable) { - if (iCent < nCollisions) { - const auto& collision = collisions.iteratorAt(iCent); - int64_t recoCollId = collision.globalIndex(); - float centValue = cent.centFT0M(); + // Sort by centrality + std::sort(centMultPairs.begin(), centMultPairs.end()); - // Fill raw centrality histogram - ue.fill(HIST("Centrality/hCentRaw"), centValue); + LOG(info) << "Correlation between centrality and multiplicity:"; + LOG(info) << " If centrality is normal (0=central, 100=peripheral), multiplicity should decrease with centrality"; + LOG(info) << " If inverted (0=peripheral, 100=central), multiplicity should increase with centrality"; - recoToCentMap[recoCollId] = centValue; + // Print a few samples across the range + if (centMultPairs.size() > 10) { + for (size_t i = 0; i < centMultPairs.size(); i += centMultPairs.size() / 10) { + LOG(info) << " Cent: " << centMultPairs[i].first + << "%, Mult: " << centMultPairs[i].second; + } } - iCent++; - } - LOG(info) << "\n--- MAP SIZES ---"; - LOG(info) << "recoToMcMap size: " << recoToMcMap.size(); - LOG(info) << "recoToCentMap size: " << recoToCentMap.size(); + LOG(info) << "\n--- SECOND PASS: Processing reconstructed collisions ---"; - //=========================================================================== - // DEBUG: Check correlation between centrality and multiplicity - //=========================================================================== - LOG(info) << "\n=== CENTRALITY VS MULTIPLICITY DEBUG ==="; + std::set reconstructedMCCollisions; + std::set selectedMCCollisions; - // Create temporary vectors to check correlation - std::vector> centMultPairs; - for (const auto& collision : collisions) { - int64_t collId = collision.globalIndex(); + int nRecoCollisions = 0; + int nSelectedEvents = 0; + int nRejectedEvents = 0; + int nNoMCMatch = 0; + int nNoCent = 0; + int nInvalidCent = 0; - auto mcIt = recoToMcMap.find(collId); - if (mcIt == recoToMcMap.end()) - continue; + // Cut counters + int nPassVertex = 0; + int nPassINEL = 0; + int nPassAll = 0; - auto centIt = recoToCentMap.find(collId); - if (centIt == recoToCentMap.end()) - continue; + // For mean calculations + std::vector centAll, centVertex, centINEL, centSelected; - auto nchIt = mcCollisionToNch.find(mcIt->second); - if (nchIt == mcCollisionToNch.end()) - continue; + for (const auto& collision : collisions) { + nRecoCollisions++; - centMultPairs.push_back({centIt->second, nchIt->second}); - } + int64_t collId = collision.globalIndex(); - // Sort by centrality - std::sort(centMultPairs.begin(), centMultPairs.end()); + // Fill cut flow + ue.fill(HIST("CutFlow/hCutStats"), 1); - LOG(info) << "Correlation between centrality and multiplicity:"; - LOG(info) << " If centrality is normal (0=central, 100=peripheral), multiplicity should decrease with centrality"; - LOG(info) << " If inverted (0=peripheral, 100=central), multiplicity should increase with centrality"; + // Get MC collision ID from labels map + auto mcIt = recoToMcMap.find(collId); + if (mcIt == recoToMcMap.end()) { + nNoMCMatch++; + continue; + } + ue.fill(HIST("CutFlow/hCutStats"), 2); - // Print a few samples across the range - if (centMultPairs.size() > 10) { - for (size_t i = 0; i < centMultPairs.size(); i += centMultPairs.size() / 10) { - LOG(info) << " Cent: " << centMultPairs[i].first - << "%, Mult: " << centMultPairs[i].second; - } - } + int64_t mcCollId = mcIt->second; - //=========================================================================== - // SECOND PASS: Process reconstructed collisions with detailed cut accounting - //=========================================================================== + // Get generated multiplicity for this MC collision + auto nchIt = mcCollisionToNch.find(mcCollId); + if (nchIt == mcCollisionToNch.end()) { + continue; + } - LOG(info) << "\n--- SECOND PASS: Processing reconstructed collisions ---"; + int nGenCharged = nchIt->second; - std::set reconstructedMCCollisions; - std::set selectedMCCollisions; + // Get INEL class + auto inelIt = mcCollisionToINELClass.find(mcCollId); + int inelClass = (inelIt != mcCollisionToINELClass.end()) ? inelIt->second : 0; - int nRecoCollisions = 0; - int nSelectedEvents = 0; - int nRejectedEvents = 0; - int nNoMCMatch = 0; - int nNoCent = 0; - int nInvalidCent = 0; + // Get centrality from cent map + auto centIt = recoToCentMap.find(collId); + if (centIt == recoToCentMap.end()) { + nNoCent++; + continue; + } + ue.fill(HIST("CutFlow/hCutStats"), 3); - // Cut counters - int nPassVertex = 0; - int nPassINEL = 0; - int nPassAll = 0; + float cent = centIt->second; + if (cent < 0 || cent > 100) { + nInvalidCent++; + continue; + } - // For mean calculations - std::vector centAll, centVertex, centINEL, centSelected; + // Store all events with valid info + centAll.push_back(cent); + ue.fill(HIST("Centrality/hCentVsMult"), cent, nGenCharged); + ue.fill(HIST("Centrality/hMultVsCent"), nGenCharged, cent); + ue.fill(HIST("Centrality/hCentVsVz"), cent, collision.posZ()); + ue.fill(HIST("INEL/hINELVsCent"), cent, inelClass); + + // Track cuts progressively + bool passVertex = std::abs(collision.posZ()) <= cfgCutVertex.value; + if (passVertex) { + centVertex.push_back(cent); + ue.fill(HIST("Centrality/hCentAfterVtx"), cent); + ue.fill(HIST("CutFlow/hCutStats"), 4); + ue.fill(HIST("CutFlow/hCentPerCut"), 4, cent); + nPassVertex++; + } + + // Check INEL selection at generator level + bool passINEL = true; + if (cfgINELCut.value == 1 && inelClass < 1) + passINEL = false; + if (cfgINELCut.value == 2 && inelClass < 2) + passINEL = false; + + if (passINEL) { + centINEL.push_back(cent); + ue.fill(HIST("Centrality/hCentAfterINEL"), cent); + ue.fill(HIST("CutFlow/hCutStats"), 5); + ue.fill(HIST("CutFlow/hCentPerCut"), 5, cent); + nPassINEL++; + } - for (const auto& collision : collisions) { - nRecoCollisions++; + // Fill GenMultVsCent for all reconstructed events + ue.fill(HIST("MC/EventLoss/GenMultVsCent"), cent, nGenCharged); + ue.fill(HIST("MC/EventLoss/NchGenerated_Reconstructed"), nGenCharged); - int64_t collId = collision.globalIndex(); + reconstructedMCCollisions.insert(mcCollId); - // Fill cut flow - ue.fill(HIST("CutFlow/hCutStats"), 1); + // Apply all cuts + bool passedAll = passVertex && passINEL; - // Get MC collision ID from labels map - auto mcIt = recoToMcMap.find(collId); - if (mcIt == recoToMcMap.end()) { - nNoMCMatch++; - continue; - } - ue.fill(HIST("CutFlow/hCutStats"), 2); + if (!passedAll) { + ue.fill(HIST("MC/EventLoss/GenMultVsCent_Rejected"), cent, nGenCharged); + nRejectedEvents++; + continue; + } - int64_t mcCollId = mcIt->second; + // Event passed all selections + centSelected.push_back(cent); + ue.fill(HIST("Centrality/hCentAfterAll"), cent); + ue.fill(HIST("CutFlow/hCutStats"), 6); + ue.fill(HIST("CutFlow/hCentPerCut"), 6, cent); + ue.fill(HIST("MC/EventLoss/GenMultVsCent_Selected"), cent, nGenCharged); + ue.fill(HIST("hvtxZ"), collision.posZ()); + selectedMCCollisions.insert(mcCollId); + nSelectedEvents++; + nPassAll++; + + // Process tracks in selected events + int nTracksInEvent = 0; + for (const auto& track : tracks) { + if (!track.has_collision()) + continue; + if (track.collisionId() != collId) + continue; - // Get generated multiplicity for this MC collision - auto nchIt = mcCollisionToNch.find(mcCollId); - if (nchIt == mcCollisionToNch.end()) { - continue; - } + if (!passesTrackSelection(track)) { + continue; + } + nTracksInEvent++; + + // Fill TPC cluster histograms + ue.fill(HIST("hNclFoundTPC"), track.tpcNClsFound()); + ue.fill(HIST("hNclPIDTPC"), track.tpcNClsPID()); + ue.fill(HIST("hNclFoundTPCvsPt"), track.pt(), track.tpcNClsFound()); + ue.fill(HIST("hNclPIDTPCvsPt"), track.pt(), track.tpcNClsPID()); + + ue.fill(HIST("Inclusive/hPtAllReco"), track.pt()); + ue.fill(HIST("Inclusive/hPtMeasuredVsCent"), track.pt(), cent); + ue.fill(HIST("hEta"), track.eta()); + ue.fill(HIST("hPhi"), track.phi()); + + if (track.has_mcParticle()) { + const auto& particle = track.mcParticle(); + int pdgCode = std::abs(particle.pdgCode()); + + if (particle.isPhysicalPrimary()) { + ue.fill(HIST("Inclusive/hPtNumEff"), particle.pt()); + ue.fill(HIST("Inclusive/hPtPrimReco"), track.pt()); + + if (pdgCode == PDGPion) { + ue.fill(HIST("Pion/hPtNumEff"), particle.pt()); + ue.fill(HIST("Pion/hPtPrimReco"), track.pt()); + } else if (pdgCode == PDGKaon) { + ue.fill(HIST("Kaon/hPtNumEff"), particle.pt()); + ue.fill(HIST("Kaon/hPtPrimReco"), track.pt()); + } else if (pdgCode == PDGProton) { + ue.fill(HIST("Proton/hPtNumEff"), particle.pt()); + ue.fill(HIST("Proton/hPtPrimReco"), track.pt()); + } + } else { + ue.fill(HIST("Inclusive/hPtSecReco"), track.pt()); + + if (pdgCode == PDGPion) { + ue.fill(HIST("Pion/hPtSecReco"), track.pt()); + } else if (pdgCode == PDGKaon) { + ue.fill(HIST("Kaon/hPtSecReco"), track.pt()); + } else if (pdgCode == PDGProton) { + ue.fill(HIST("Proton/hPtSecReco"), track.pt()); + } + } + } - int nGenCharged = nchIt->second; + int bestSpecies = getBestPIDHypothesis(track); - // Get INEL class - auto inelIt = mcCollisionToINELClass.find(mcCollId); - int inelClass = (inelIt != mcCollisionToINELClass.end()) ? inelIt->second : 0; + if (bestSpecies == kPion) { + ue.fill(HIST("Pion/hPtMeasuredVsCent"), track.pt(), cent); + ue.fill(HIST("Pion/hPtAllReco"), track.pt()); - // Get centrality from cent map - auto centIt = recoToCentMap.find(collId); - if (centIt == recoToCentMap.end()) { - nNoCent++; - continue; - } - ue.fill(HIST("CutFlow/hCutStats"), 3); + if (enablePIDHistograms) { + ue.fill(HIST("Pion/hNsigmaTPC"), track.pt(), track.tpcNSigmaPi()); + } + } else if (bestSpecies == kKaon) { + ue.fill(HIST("Kaon/hPtMeasuredVsCent"), track.pt(), cent); + ue.fill(HIST("Kaon/hPtAllReco"), track.pt()); - float cent = centIt->second; - if (cent < 0 || cent > 100) { - nInvalidCent++; - continue; - } + if (enablePIDHistograms) { + ue.fill(HIST("Kaon/hNsigmaTPC"), track.pt(), track.tpcNSigmaKa()); + } + } else if (bestSpecies == kProton) { + ue.fill(HIST("Proton/hPtMeasuredVsCent"), track.pt(), cent); + ue.fill(HIST("Proton/hPtAllReco"), track.pt()); - // Store all events with valid info - centAll.push_back(cent); - ue.fill(HIST("Centrality/hCentVsMult"), cent, nGenCharged); - ue.fill(HIST("Centrality/hMultVsCent"), nGenCharged, cent); - ue.fill(HIST("Centrality/hCentVsVz"), cent, collision.posZ()); - ue.fill(HIST("INEL/hINELVsCent"), cent, inelClass); - - // Track cuts progressively - bool passVertex = std::abs(collision.posZ()) <= cfgCutVertex.value; - if (passVertex) { - centVertex.push_back(cent); - ue.fill(HIST("Centrality/hCentAfterVtx"), cent); - ue.fill(HIST("CutFlow/hCutStats"), 4); - ue.fill(HIST("CutFlow/hCentPerCut"), 4, cent); - nPassVertex++; - } + if (enablePIDHistograms) { + ue.fill(HIST("Proton/hNsigmaTPC"), track.pt(), track.tpcNSigmaPr()); + } + } + } - // Check INEL selection at generator level - bool passINEL = true; - if (cfgINELCut.value == 1 && inelClass < 1) - passINEL = false; - if (cfgINELCut.value == 2 && inelClass < 2) - passINEL = false; - - if (passINEL) { - centINEL.push_back(cent); - ue.fill(HIST("Centrality/hCentAfterINEL"), cent); - ue.fill(HIST("CutFlow/hCutStats"), 5); - ue.fill(HIST("CutFlow/hCentPerCut"), 5, cent); - nPassINEL++; + // Fill event-level track multiplicity + ue.fill(HIST("Centrality/hRecoMultVsCent"), cent, nTracksInEvent); + } + + // Calculate and display cut statistics + LOG(info) << "\n=== CUT STATISTICS ==="; + LOG(info) << "Total collisions with valid info: " << centAll.size(); + LOG(info) << "Pass vertex cut: " << nPassVertex << " (" + << (centAll.size() > 0 ? 100.0 * nPassVertex / centAll.size() : 0.0) << "%)"; + LOG(info) << "Pass INEL cut: " << nPassINEL << " (" + << (centAll.size() > 0 ? 100.0 * nPassINEL / centAll.size() : 0.0) << "%)"; + LOG(info) << "Pass all cuts: " << nPassAll << " (" + << (centAll.size() > 0 ? 100.0 * nPassAll / centAll.size() : 0.0) << "%)"; + + // Calculate mean centrality at each stage + if (!centAll.empty()) { + float meanAll = std::accumulate(centAll.begin(), centAll.end(), 0.0) / centAll.size(); + float meanVertex = centVertex.empty() ? 0 : std::accumulate(centVertex.begin(), centVertex.end(), 0.0) / centVertex.size(); + float meanINEL = centINEL.empty() ? 0 : std::accumulate(centINEL.begin(), centINEL.end(), 0.0) / centINEL.size(); + float meanSelected = centSelected.empty() ? 0 : std::accumulate(centSelected.begin(), centSelected.end(), 0.0) / centSelected.size(); + + LOG(info) << "\n=== CENTRALITY MEANS ==="; + LOG(info) << "Mean centrality (all): " << meanAll; + LOG(info) << "Mean centrality (after vertex): " << meanVertex; + LOG(info) << "Mean centrality (after INEL): " << meanINEL; + LOG(info) << "Mean centrality (selected): " << meanSelected; + } + + ue.fill(HIST("hEventLossBreakdown"), 1.f, physicsSelectedMCCollisions.size()); + ue.fill(HIST("hEventLossBreakdown"), 2.f, reconstructedMCCollisions.size()); + ue.fill(HIST("hEventLossBreakdown"), 3.f, selectedMCCollisions.size()); + + float efficiency = physicsSelectedMCCollisions.size() > 0 ? 100.f * selectedMCCollisions.size() / physicsSelectedMCCollisions.size() : 0; + ue.fill(HIST("hEventLossBreakdown"), 4.f, efficiency); + + LOG(info) << "\n=== FINAL EFFICIENCY ==="; + LOG(info) << "Physics selected: " << physicsSelectedMCCollisions.size(); + LOG(info) << "Reconstructed: " << reconstructedMCCollisions.size(); + LOG(info) << "Selected: " << selectedMCCollisions.size(); + LOG(info) << "Efficiency: " << efficiency << "%"; + LOG(info) << "=== processMC END ==="; + LOG(info) << "=== Initialized MultiplicityPt task with ON-THE-FLY PERCENTILE COMPUTATION ==="; + LOG(info) << "Centrality classes: " << kCentralityClasses; + LOG(info) << "Multiplicity estimator: " << multiplicityEstimator.value; + LOG(info) << "IMPORTANT: Run processPercentileCalibration FIRST to build percentile boundaries!"; + if (applyPhiCut.value) { + LOG(info) << "Phi cut ENABLED for pT > " << pTthresholdPhiCut.value << " GeV/c"; } + } + + void MultiplicityPt::processPercentileCalibration(CollisionTableMCTrue const& mcCollisions, + ParticleTableMC const& particles) + { + LOG(info) << "=== PERCENTILE CALIBRATION PASS ==="; + LOG(info) << "Processing " << mcCollisions.size() << " MC collisions"; - // Fill GenMultVsCent for all reconstructed events - ue.fill(HIST("MC/EventLoss/GenMultVsCent"), cent, nGenCharged); - ue.fill(HIST("MC/EventLoss/NchGenerated_Reconstructed"), nGenCharged); + multiplicityValues.clear(); + multiplicityValues.reserve(mcCollisions.size()); - reconstructedMCCollisions.insert(mcCollId); + for (const auto& mcCollision : mcCollisions) { + // Apply basic cuts + if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) + continue; - // Apply all cuts - bool passedAll = passVertex && passINEL; + auto particlesInCollision = particles.sliceBy(perMCCol, mcCollision.globalIndex()); + + // Apply INEL cuts + if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) + continue; + if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) + continue; - if (!passedAll) { - ue.fill(HIST("MC/EventLoss/GenMultVsCent_Rejected"), cent, nGenCharged); - nRejectedEvents++; - continue; + // Calculate multiplicity + float mcMult = getMultiplicityMC(mcCollision, particles); + multiplicityValues.push_back(mcMult); + + ue.fill(HIST("Calibration/hRawMultiplicity"), mcMult); } - // Event passed all selections - centSelected.push_back(cent); - ue.fill(HIST("Centrality/hCentAfterAll"), cent); - ue.fill(HIST("CutFlow/hCutStats"), 6); - ue.fill(HIST("CutFlow/hCentPerCut"), 6, cent); - ue.fill(HIST("MC/EventLoss/GenMultVsCent_Selected"), cent, nGenCharged); + // Compute percentile boundaries + computePercentileBoundaries(); + + LOG(info) << "=== PERCENTILE CALIBRATION COMPLETE ==="; + LOG(info) << "Processed " << multiplicityValues.size() << " events"; + LOG(info) << "Now run processMC and processTrue with these percentiles"; + } + + void MultiplicityPt::processData(CollisionTableData::iterator const& collision, + TrackTableData const& tracks, + BCsRun3 const& /*bcs*/) + { + if (!isEventSelected(collision)) { + return; + } ue.fill(HIST("hvtxZ"), collision.posZ()); - selectedMCCollisions.insert(mcCollId); - nSelectedEvents++; - nPassAll++; - // Process tracks in selected events - int nTracksInEvent = 0; + float magField = 0; + if (applyPhiCut.value) { + const auto& bc = collision.bc_as(); + magField = getMagneticField(bc.timestamp()); + } + for (const auto& track : tracks) { - if (!track.has_collision()) - continue; - if (track.collisionId() != collId) - continue; + if (applyPhiCut.value && track.pt() >= pTthresholdPhiCut.value) { + float phiPrime = getTransformedPhi(track.phi(), track.sign(), magField); + ue.fill(HIST("PhiCut/hPtVsPhiPrimeBefore"), track.pt(), phiPrime); + } - if (!passesTrackSelection(track)) { + if (!passesTrackSelection(track, magField)) { continue; } - nTracksInEvent++; - // Fill TPC cluster histograms - ue.fill(HIST("hNclFoundTPC"), track.tpcNClsFound()); - ue.fill(HIST("hNclPIDTPC"), track.tpcNClsPID()); - ue.fill(HIST("hNclFoundTPCvsPt"), track.pt(), track.tpcNClsFound()); - ue.fill(HIST("hNclPIDTPCvsPt"), track.pt(), track.tpcNClsPID()); + if (applyPhiCut.value && track.pt() >= pTthresholdPhiCut.value) { + float phiPrime = getTransformedPhi(track.phi(), track.sign(), magField); + ue.fill(HIST("PhiCut/hPtVsPhiPrimeAfter"), track.pt(), phiPrime); + } - ue.fill(HIST("Inclusive/hPtAllReco"), track.pt()); - ue.fill(HIST("Inclusive/hPtMeasuredVsCent"), track.pt(), cent); + ue.fill(HIST("Inclusive/hPtMeasured"), track.pt()); ue.fill(HIST("hEta"), track.eta()); ue.fill(HIST("hPhi"), track.phi()); - if (track.has_mcParticle()) { - const auto& particle = track.mcParticle(); - int pdgCode = std::abs(particle.pdgCode()); - - if (particle.isPhysicalPrimary()) { - ue.fill(HIST("Inclusive/hPtNumEff"), particle.pt()); - ue.fill(HIST("Inclusive/hPtPrimReco"), track.pt()); - - if (pdgCode == PDGPion) { - ue.fill(HIST("Pion/hPtNumEff"), particle.pt()); - ue.fill(HIST("Pion/hPtPrimReco"), track.pt()); - } else if (pdgCode == PDGKaon) { - ue.fill(HIST("Kaon/hPtNumEff"), particle.pt()); - ue.fill(HIST("Kaon/hPtPrimReco"), track.pt()); - } else if (pdgCode == PDGProton) { - ue.fill(HIST("Proton/hPtNumEff"), particle.pt()); - ue.fill(HIST("Proton/hPtPrimReco"), track.pt()); - } - } else { - ue.fill(HIST("Inclusive/hPtSecReco"), track.pt()); - - if (pdgCode == PDGPion) { - ue.fill(HIST("Pion/hPtSecReco"), track.pt()); - } else if (pdgCode == PDGKaon) { - ue.fill(HIST("Kaon/hPtSecReco"), track.pt()); - } else if (pdgCode == PDGProton) { - ue.fill(HIST("Proton/hPtSecReco"), track.pt()); - } - } - } - int bestSpecies = getBestPIDHypothesis(track); if (bestSpecies == kPion) { - ue.fill(HIST("Pion/hPtMeasuredVsCent"), track.pt(), cent); - ue.fill(HIST("Pion/hPtAllReco"), track.pt()); - + ue.fill(HIST("Pion/hPtMeasured"), track.pt()); if (enablePIDHistograms) { ue.fill(HIST("Pion/hNsigmaTPC"), track.pt(), track.tpcNSigmaPi()); } } else if (bestSpecies == kKaon) { - ue.fill(HIST("Kaon/hPtMeasuredVsCent"), track.pt(), cent); - ue.fill(HIST("Kaon/hPtAllReco"), track.pt()); - + ue.fill(HIST("Kaon/hPtMeasured"), track.pt()); if (enablePIDHistograms) { ue.fill(HIST("Kaon/hNsigmaTPC"), track.pt(), track.tpcNSigmaKa()); } } else if (bestSpecies == kProton) { - ue.fill(HIST("Proton/hPtMeasuredVsCent"), track.pt(), cent); - ue.fill(HIST("Proton/hPtAllReco"), track.pt()); - + ue.fill(HIST("Proton/hPtMeasured"), track.pt()); if (enablePIDHistograms) { ue.fill(HIST("Proton/hNsigmaTPC"), track.pt(), track.tpcNSigmaPr()); } } } - - // Fill event-level track multiplicity - ue.fill(HIST("Centrality/hRecoMultVsCent"), cent, nTracksInEvent); } - // Calculate and display cut statistics - LOG(info) << "\n=== CUT STATISTICS ==="; - LOG(info) << "Total collisions with valid info: " << centAll.size(); - LOG(info) << "Pass vertex cut: " << nPassVertex << " (" - << (centAll.size() > 0 ? 100.0 * nPassVertex / centAll.size() : 0.0) << "%)"; - LOG(info) << "Pass INEL cut: " << nPassINEL << " (" - << (centAll.size() > 0 ? 100.0 * nPassINEL / centAll.size() : 0.0) << "%)"; - LOG(info) << "Pass all cuts: " << nPassAll << " (" - << (centAll.size() > 0 ? 100.0 * nPassAll / centAll.size() : 0.0) << "%)"; - - // Calculate mean centrality at each stage - if (!centAll.empty()) { - float meanAll = std::accumulate(centAll.begin(), centAll.end(), 0.0) / centAll.size(); - float meanVertex = centVertex.empty() ? 0 : std::accumulate(centVertex.begin(), centVertex.end(), 0.0) / centVertex.size(); - float meanINEL = centINEL.empty() ? 0 : std::accumulate(centINEL.begin(), centINEL.end(), 0.0) / centINEL.size(); - float meanSelected = centSelected.empty() ? 0 : std::accumulate(centSelected.begin(), centSelected.end(), 0.0) / centSelected.size(); - - LOG(info) << "\n=== CENTRALITY MEANS ==="; - LOG(info) << "Mean centrality (all): " << meanAll; - LOG(info) << "Mean centrality (after vertex): " << meanVertex; - LOG(info) << "Mean centrality (after INEL): " << meanINEL; - LOG(info) << "Mean centrality (selected): " << meanSelected; - } - - ue.fill(HIST("hEventLossBreakdown"), 1.f, physicsSelectedMCCollisions.size()); - ue.fill(HIST("hEventLossBreakdown"), 2.f, reconstructedMCCollisions.size()); - ue.fill(HIST("hEventLossBreakdown"), 3.f, selectedMCCollisions.size()); - - float efficiency = physicsSelectedMCCollisions.size() > 0 ? 100.f * selectedMCCollisions.size() / physicsSelectedMCCollisions.size() : 0; - ue.fill(HIST("hEventLossBreakdown"), 4.f, efficiency); - - LOG(info) << "\n=== FINAL EFFICIENCY ==="; - LOG(info) << "Physics selected: " << physicsSelectedMCCollisions.size(); - LOG(info) << "Reconstructed: " << reconstructedMCCollisions.size(); - LOG(info) << "Selected: " << selectedMCCollisions.size(); - LOG(info) << "Efficiency: " << efficiency << "%"; - LOG(info) << "=== processMC END ==="; - LOG(info) << "=== Initialized MultiplicityPt task with ON-THE-FLY PERCENTILE COMPUTATION ==="; - LOG(info) << "Centrality classes: " << kCentralityClasses; - LOG(info) << "Multiplicity estimator: " << multiplicityEstimator.value; - LOG(info) << "IMPORTANT: Run processPercentileCalibration FIRST to build percentile boundaries!"; - if (applyPhiCut.value) { - LOG(info) << "Phi cut ENABLED for pT > " << pTthresholdPhiCut.value << " GeV/c"; - } -} - -// ======================================================================== -// PERCENTILE CALIBRATION PASS -// ======================================================================== -void MultiplicityPt::processPercentileCalibration(CollisionTableMCTrue const& mcCollisions, - ParticleTableMC const& particles) -{ - LOG(info) << "=== PERCENTILE CALIBRATION PASS ==="; - LOG(info) << "Processing " << mcCollisions.size() << " MC collisions"; - - multiplicityValues.clear(); - multiplicityValues.reserve(mcCollisions.size()); - - for (const auto& mcCollision : mcCollisions) { - // Apply basic cuts - if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) - continue; - - auto particlesInCollision = particles.sliceBy(perMCCol, mcCollision.globalIndex()); - - // Apply INEL cuts - if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) - continue; - if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) - continue; - - // Calculate multiplicity - float mcMult = getMultiplicityMC(mcCollision, particles); - multiplicityValues.push_back(mcMult); - - ue.fill(HIST("Calibration/hRawMultiplicity"), mcMult); - } + void MultiplicityPt::processMC(TrackTableMC const& tracks, + aod::McParticles const& particles, + CollisionTableMCTrue const& mcCollisions, + CollisionTableMC const& collisions, + BCsRun3 const& /*bcs*/) + { + if (!percentilesComputed) { + LOG(warning) << "Percentiles not computed yet! Run processPercentileCalibration first!"; + LOG(warning) << "Using fallback linear binning for now..."; + } - // Compute percentile boundaries - computePercentileBoundaries(); + LOG(info) << "=== DEBUG processMC START ==="; + LOG(info) << "MC collisions: " << mcCollisions.size(); + LOG(info) << "Reconstructed collisions: " << collisions.size(); - LOG(info) << "=== PERCENTILE CALIBRATION COMPLETE ==="; - LOG(info) << "Processed " << multiplicityValues.size() << " events"; - LOG(info) << "Now run processMC and processTrue with these percentiles"; -} + ue.fill(HIST("MC/GenRecoCollisions"), 1.f, mcCollisions.size()); + ue.fill(HIST("MC/GenRecoCollisions"), 2.f, collisions.size()); -// ======================================================================== -// DATA PROCESSING -// ======================================================================== -void MultiplicityPt::processData(CollisionTableData::iterator const& collision, - TrackTableData const& tracks, - BCsRun3 const& /*bcs*/) -{ - if (!isEventSelected(collision)) { - return; - } - ue.fill(HIST("hvtxZ"), collision.posZ()); + std::set physicsSelectedMCCollisions; + std::set reconstructedMCCollisions; + std::set selectedMCCollisions; - float magField = 0; - if (applyPhiCut.value) { - const auto& bc = collision.bc_as(); - magField = getMagneticField(bc.timestamp()); - } + std::map mcCollisionMultiplicity; + std::map mcCollisionPercentile; - for (const auto& track : tracks) { - if (applyPhiCut.value && track.pt() >= pTthresholdPhiCut.value) { - float phiPrime = getTransformedPhi(track.phi(), track.sign(), magField); - ue.fill(HIST("PhiCut/hPtVsPhiPrimeBefore"), track.pt(), phiPrime); - } + // First pass: classify MC collisions + for (const auto& mcCollision : mcCollisions) { + int64_t mcCollId = mcCollision.globalIndex(); - if (!passesTrackSelection(track, magField)) { - continue; - } + float mcMult = getMultiplicityMC(mcCollision, particles); + mcCollisionMultiplicity[mcCollId] = mcMult; - if (applyPhiCut.value && track.pt() >= pTthresholdPhiCut.value) { - float phiPrime = getTransformedPhi(track.phi(), track.sign(), magField); - ue.fill(HIST("PhiCut/hPtVsPhiPrimeAfter"), track.pt(), phiPrime); - } + // Convert to percentile + float percentile = multiplicityToPercentile(mcMult); + mcCollisionPercentile[mcCollId] = percentile; - ue.fill(HIST("Inclusive/hPtMeasured"), track.pt()); - ue.fill(HIST("hEta"), track.eta()); - ue.fill(HIST("hPhi"), track.phi()); + ue.fill(HIST("MC/EventLoss/MultGenerated"), percentile); - int bestSpecies = getBestPIDHypothesis(track); + auto particlesInCollision = particles.sliceBy(perMCCol, mcCollId); - if (bestSpecies == kPion) { - ue.fill(HIST("Pion/hPtMeasured"), track.pt()); - if (enablePIDHistograms) { - ue.fill(HIST("Pion/hNsigmaTPC"), track.pt(), track.tpcNSigmaPi()); + if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) { + ue.fill(HIST("MC/EventLoss/MultBadVertex"), percentile); + continue; } - } else if (bestSpecies == kKaon) { - ue.fill(HIST("Kaon/hPtMeasured"), track.pt()); - if (enablePIDHistograms) { - ue.fill(HIST("Kaon/hNsigmaTPC"), track.pt(), track.tpcNSigmaKa()); + + if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) { + continue; } - } else if (bestSpecies == kProton) { - ue.fill(HIST("Proton/hPtMeasured"), track.pt()); - if (enablePIDHistograms) { - ue.fill(HIST("Proton/hNsigmaTPC"), track.pt(), track.tpcNSigmaPr()); + if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) { + continue; } + + physicsSelectedMCCollisions.insert(mcCollId); + ue.fill(HIST("MC/EventLoss/MultPhysicsSelected"), percentile); } - } -} -// ======================================================================== -// MC PROCESSING - Using computed percentiles -// ======================================================================== -void MultiplicityPt::processMC(TrackTableMC const& tracks, - aod::McParticles const& particles, - CollisionTableMCTrue const& mcCollisions, - CollisionTableMC const& collisions, - BCsRun3 const& /*bcs*/) -{ - if (!percentilesComputed) { - LOG(warning) << "Percentiles not computed yet! Run processPercentileCalibration first!"; - LOG(warning) << "Using fallback linear binning for now..."; - } + LOG(info) << "Physics-selected MC collisions: " << physicsSelectedMCCollisions.size(); - LOG(info) << "=== DEBUG processMC START ==="; - LOG(info) << "MC collisions: " << mcCollisions.size(); - LOG(info) << "Reconstructed collisions: " << collisions.size(); + // Second pass: track reconstructed events + std::set selectedCollisionIndices; - ue.fill(HIST("MC/GenRecoCollisions"), 1.f, mcCollisions.size()); - ue.fill(HIST("MC/GenRecoCollisions"), 2.f, collisions.size()); + for (const auto& collision : collisions) { + if (!collision.has_mcCollision()) { + continue; + } - std::set physicsSelectedMCCollisions; - std::set reconstructedMCCollisions; - std::set selectedMCCollisions; + const auto& mcCollision = collision.mcCollision_as(); + int64_t mcCollId = mcCollision.globalIndex(); - std::map mcCollisionMultiplicity; - std::map mcCollisionPercentile; + if (physicsSelectedMCCollisions.find(mcCollId) == physicsSelectedMCCollisions.end()) { + continue; + } - // First pass: classify MC collisions - for (const auto& mcCollision : mcCollisions) { - int64_t mcCollId = mcCollision.globalIndex(); + float percentile = mcCollisionPercentile[mcCollId]; - float mcMult = getMultiplicityMC(mcCollision, particles); - mcCollisionMultiplicity[mcCollId] = mcMult; + if (reconstructedMCCollisions.find(mcCollId) == reconstructedMCCollisions.end()) { + reconstructedMCCollisions.insert(mcCollId); + ue.fill(HIST("MC/EventLoss/MultReconstructed"), percentile); + } - // Convert to percentile - float percentile = multiplicityToPercentile(mcMult); - mcCollisionPercentile[mcCollId] = percentile; + if (isEventSelected(collision)) { + if (selectedMCCollisions.find(mcCollId) == selectedMCCollisions.end()) { + selectedMCCollisions.insert(mcCollId); + ue.fill(HIST("MC/EventLoss/MultRecoSelected"), percentile); + } + selectedCollisionIndices.insert(collision.globalIndex()); + ue.fill(HIST("hvtxZ"), collision.posZ()); + } + } - ue.fill(HIST("MC/EventLoss/MultGenerated"), percentile); + LOG(info) << "Reconstructed MC collisions: " << reconstructedMCCollisions.size(); + LOG(info) << "Selected MC collisions: " << selectedMCCollisions.size(); - auto particlesInCollision = particles.sliceBy(perMCCol, mcCollId); + int nPhysicsSelected = physicsSelectedMCCollisions.size(); + int nReconstructed = reconstructedMCCollisions.size(); + int nSelected = selectedMCCollisions.size(); - if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) { - ue.fill(HIST("MC/EventLoss/MultBadVertex"), percentile); - continue; + if (nPhysicsSelected > 0) { + ue.fill(HIST("hEventLossBreakdown"), 1, nPhysicsSelected); + ue.fill(HIST("hEventLossBreakdown"), 2, nReconstructed); + ue.fill(HIST("hEventLossBreakdown"), 3, nSelected); + ue.fill(HIST("hEventLossBreakdown"), 4, (nSelected * 100.0 / nPhysicsSelected)); } - if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) { - continue; - } - if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) { - continue; - } + // Process tracks + int totalTracksProcessed = 0; + int tracksFromSelectedEvents = 0; + int tracksPassingSelection = 0; - physicsSelectedMCCollisions.insert(mcCollId); - ue.fill(HIST("MC/EventLoss/MultPhysicsSelected"), percentile); - } + std::array particleTracksIdentified = {0}; + std::array particleTracksPrimary = {0}; + std::array particleTracksSecondary = {0}; - LOG(info) << "Physics-selected MC collisions: " << physicsSelectedMCCollisions.size(); + for (const auto& track : tracks) { + totalTracksProcessed++; - // Second pass: track reconstructed events - std::set selectedCollisionIndices; + if (!track.has_collision()) + continue; - for (const auto& collision : collisions) { - if (!collision.has_mcCollision()) { - continue; - } + const auto& collision = track.collision_as(); - const auto& mcCollision = collision.mcCollision_as(); - int64_t mcCollId = mcCollision.globalIndex(); + if (selectedCollisionIndices.find(collision.globalIndex()) == selectedCollisionIndices.end()) { + continue; + } + tracksFromSelectedEvents++; - if (physicsSelectedMCCollisions.find(mcCollId) == physicsSelectedMCCollisions.end()) { - continue; - } + if (!collision.has_mcCollision()) + continue; - float percentile = mcCollisionPercentile[mcCollId]; + const auto& mcCollision = collision.mcCollision_as(); + float percentile = mcCollisionPercentile[mcCollision.globalIndex()]; - if (reconstructedMCCollisions.find(mcCollId) == reconstructedMCCollisions.end()) { - reconstructedMCCollisions.insert(mcCollId); - ue.fill(HIST("MC/EventLoss/MultReconstructed"), percentile); - } + float magField = 0; + if (applyPhiCut.value) { + const auto& bc = collision.bc_as(); + magField = getMagneticField(bc.timestamp()); + } - if (isEventSelected(collision)) { - if (selectedMCCollisions.find(mcCollId) == selectedMCCollisions.end()) { - selectedMCCollisions.insert(mcCollId); - ue.fill(HIST("MC/EventLoss/MultRecoSelected"), percentile); + if (!passesTrackSelection(track, magField)) { + continue; } - selectedCollisionIndices.insert(collision.globalIndex()); - ue.fill(HIST("hvtxZ"), collision.posZ()); - } - } + tracksPassingSelection++; - LOG(info) << "Reconstructed MC collisions: " << reconstructedMCCollisions.size(); - LOG(info) << "Selected MC collisions: " << selectedMCCollisions.size(); + // Inclusive charged particle + ue.fill(HIST("Inclusive/hPtMeasured"), track.pt()); + ue.fill(HIST("Inclusive/hPtMeasuredVsMult"), track.pt(), percentile); + ue.fill(HIST("Inclusive/hPtAllReco"), track.pt()); + ue.fill(HIST("Inclusive/hPtAllRecoVsMult"), track.pt(), percentile); + ue.fill(HIST("hEta"), track.eta()); + ue.fill(HIST("hPhi"), track.phi()); - int nPhysicsSelected = physicsSelectedMCCollisions.size(); - int nReconstructed = reconstructedMCCollisions.size(); - int nSelected = selectedMCCollisions.size(); + // Efficiency numerator + if (track.has_mcParticle()) { + const auto& particle = track.mcParticle(); + int pdgCode = std::abs(particle.pdgCode()); - if (nPhysicsSelected > 0) { - ue.fill(HIST("hEventLossBreakdown"), 1, nPhysicsSelected); - ue.fill(HIST("hEventLossBreakdown"), 2, nReconstructed); - ue.fill(HIST("hEventLossBreakdown"), 3, nSelected); - ue.fill(HIST("hEventLossBreakdown"), 4, (nSelected * 100.0 / nPhysicsSelected)); - } + if (particle.isPhysicalPrimary()) { + ue.fill(HIST("Inclusive/hPtNumEff"), particle.pt()); + ue.fill(HIST("Inclusive/hPtNumEffVsMult"), particle.pt(), percentile); + ue.fill(HIST("Inclusive/hPtPrimReco"), track.pt()); + ue.fill(HIST("Inclusive/hPtPrimRecoVsMult"), track.pt(), percentile); - // Process tracks - int totalTracksProcessed = 0; - int tracksFromSelectedEvents = 0; - int tracksPassingSelection = 0; + if (pdgCode == PDGPion) { + ue.fill(HIST("Pion/hPtNumEff"), particle.pt()); + ue.fill(HIST("Pion/hPtNumEffVsMult"), particle.pt(), percentile); + } + if (pdgCode == PDGKaon) { + ue.fill(HIST("Kaon/hPtNumEff"), particle.pt()); + ue.fill(HIST("Kaon/hPtNumEffVsMult"), particle.pt(), percentile); + } + if (pdgCode == PDGProton) { + ue.fill(HIST("Proton/hPtNumEff"), particle.pt()); + ue.fill(HIST("Proton/hPtNumEffVsMult"), particle.pt(), percentile); + } + } else { + ue.fill(HIST("Inclusive/hPtSecReco"), track.pt()); + ue.fill(HIST("Inclusive/hPtSecRecoVsMult"), track.pt(), percentile); + } + } - std::array particleTracksIdentified = {0}; - std::array particleTracksPrimary = {0}; - std::array particleTracksSecondary = {0}; + // Identified particle analysis + int bestSpecies = getBestPIDHypothesis(track); - for (const auto& track : tracks) { - totalTracksProcessed++; + if (bestSpecies == kPion) { + ue.fill(HIST("Pion/hPtMeasured"), track.pt()); + ue.fill(HIST("Pion/hPtMeasuredVsMult"), track.pt(), percentile); + ue.fill(HIST("Pion/hPtAllReco"), track.pt()); + ue.fill(HIST("Pion/hPtAllRecoVsMult"), track.pt(), percentile); + particleTracksIdentified[kPion]++; - if (!track.has_collision()) - continue; + if (enablePIDHistograms) { + ue.fill(HIST("Pion/hNsigmaTPC"), track.pt(), track.tpcNSigmaPi()); + } - const auto& collision = track.collision_as(); + if (track.has_mcParticle()) { + const auto& particle = track.mcParticle(); + if (particle.isPhysicalPrimary()) { + ue.fill(HIST("Pion/hPtPrimReco"), track.pt()); + ue.fill(HIST("Pion/hPtPrimRecoVsMult"), track.pt(), percentile); + particleTracksPrimary[kPion]++; + } else { + ue.fill(HIST("Pion/hPtSecReco"), track.pt()); + ue.fill(HIST("Pion/hPtSecRecoVsMult"), track.pt(), percentile); + particleTracksSecondary[kPion]++; + } + } - if (selectedCollisionIndices.find(collision.globalIndex()) == selectedCollisionIndices.end()) { - continue; - } - tracksFromSelectedEvents++; + } else if (bestSpecies == kKaon) { + ue.fill(HIST("Kaon/hPtMeasured"), track.pt()); + ue.fill(HIST("Kaon/hPtMeasuredVsMult"), track.pt(), percentile); + ue.fill(HIST("Kaon/hPtAllReco"), track.pt()); + ue.fill(HIST("Kaon/hPtAllRecoVsMult"), track.pt(), percentile); + particleTracksIdentified[kKaon]++; - if (!collision.has_mcCollision()) - continue; + if (enablePIDHistograms) { + ue.fill(HIST("Kaon/hNsigmaTPC"), track.pt(), track.tpcNSigmaKa()); + } - const auto& mcCollision = collision.mcCollision_as(); - float percentile = mcCollisionPercentile[mcCollision.globalIndex()]; + if (track.has_mcParticle()) { + const auto& particle = track.mcParticle(); + if (particle.isPhysicalPrimary()) { + ue.fill(HIST("Kaon/hPtPrimReco"), track.pt()); + ue.fill(HIST("Kaon/hPtPrimRecoVsMult"), track.pt(), percentile); + particleTracksPrimary[kKaon]++; + } else { + ue.fill(HIST("Kaon/hPtSecReco"), track.pt()); + ue.fill(HIST("Kaon/hPtSecRecoVsMult"), track.pt(), percentile); + particleTracksSecondary[kKaon]++; + } + } - float magField = 0; - if (applyPhiCut.value) { - const auto& bc = collision.bc_as(); - magField = getMagneticField(bc.timestamp()); - } + } else if (bestSpecies == kProton) { + ue.fill(HIST("Proton/hPtMeasured"), track.pt()); + ue.fill(HIST("Proton/hPtMeasuredVsMult"), track.pt(), percentile); + ue.fill(HIST("Proton/hPtAllReco"), track.pt()); + ue.fill(HIST("Proton/hPtAllRecoVsMult"), track.pt(), percentile); + particleTracksIdentified[kProton]++; - if (!passesTrackSelection(track, magField)) { - continue; - } - tracksPassingSelection++; - - // Inclusive charged particle - ue.fill(HIST("Inclusive/hPtMeasured"), track.pt()); - ue.fill(HIST("Inclusive/hPtMeasuredVsMult"), track.pt(), percentile); - ue.fill(HIST("Inclusive/hPtAllReco"), track.pt()); - ue.fill(HIST("Inclusive/hPtAllRecoVsMult"), track.pt(), percentile); - ue.fill(HIST("hEta"), track.eta()); - ue.fill(HIST("hPhi"), track.phi()); - - // Efficiency numerator - if (track.has_mcParticle()) { - const auto& particle = track.mcParticle(); - int pdgCode = std::abs(particle.pdgCode()); - - if (particle.isPhysicalPrimary()) { - ue.fill(HIST("Inclusive/hPtNumEff"), particle.pt()); - ue.fill(HIST("Inclusive/hPtNumEffVsMult"), particle.pt(), percentile); - ue.fill(HIST("Inclusive/hPtPrimReco"), track.pt()); - ue.fill(HIST("Inclusive/hPtPrimRecoVsMult"), track.pt(), percentile); - - if (pdgCode == PDGPion) { - ue.fill(HIST("Pion/hPtNumEff"), particle.pt()); - ue.fill(HIST("Pion/hPtNumEffVsMult"), particle.pt(), percentile); - } - if (pdgCode == PDGKaon) { - ue.fill(HIST("Kaon/hPtNumEff"), particle.pt()); - ue.fill(HIST("Kaon/hPtNumEffVsMult"), particle.pt(), percentile); + if (enablePIDHistograms) { + ue.fill(HIST("Proton/hNsigmaTPC"), track.pt(), track.tpcNSigmaPr()); } - if (pdgCode == PDGProton) { - ue.fill(HIST("Proton/hPtNumEff"), particle.pt()); - ue.fill(HIST("Proton/hPtNumEffVsMult"), particle.pt(), percentile); + + if (track.has_mcParticle()) { + const auto& particle = track.mcParticle(); + if (particle.isPhysicalPrimary()) { + ue.fill(HIST("Proton/hPtPrimReco"), track.pt()); + ue.fill(HIST("Proton/hPtPrimRecoVsMult"), track.pt(), percentile); + particleTracksPrimary[kProton]++; + } else { + ue.fill(HIST("Proton/hPtSecReco"), track.pt()); + ue.fill(HIST("Proton/hPtSecRecoVsMult"), track.pt(), percentile); + particleTracksSecondary[kProton]++; + } } - } else { - ue.fill(HIST("Inclusive/hPtSecReco"), track.pt()); - ue.fill(HIST("Inclusive/hPtSecRecoVsMult"), track.pt(), percentile); } } - // Identified particle analysis - int bestSpecies = getBestPIDHypothesis(track); - - if (bestSpecies == kPion) { - ue.fill(HIST("Pion/hPtMeasured"), track.pt()); - ue.fill(HIST("Pion/hPtMeasuredVsMult"), track.pt(), percentile); - ue.fill(HIST("Pion/hPtAllReco"), track.pt()); - ue.fill(HIST("Pion/hPtAllRecoVsMult"), track.pt(), percentile); - particleTracksIdentified[kPion]++; - - if (enablePIDHistograms) { - ue.fill(HIST("Pion/hNsigmaTPC"), track.pt(), track.tpcNSigmaPi()); - } + LOG(info) << "=== DEBUG TRACK COUNTING ==="; + LOG(info) << "Total tracks processed: " << totalTracksProcessed; + LOG(info) << "Tracks from selected events: " << tracksFromSelectedEvents; + LOG(info) << "Tracks passing selection: " << tracksPassingSelection; - if (track.has_mcParticle()) { - const auto& particle = track.mcParticle(); - if (particle.isPhysicalPrimary()) { - ue.fill(HIST("Pion/hPtPrimReco"), track.pt()); - ue.fill(HIST("Pion/hPtPrimRecoVsMult"), track.pt(), percentile); - particleTracksPrimary[kPion]++; - } else { - ue.fill(HIST("Pion/hPtSecReco"), track.pt()); - ue.fill(HIST("Pion/hPtSecRecoVsMult"), track.pt(), percentile); - particleTracksSecondary[kPion]++; - } - } + LOG(info) << "Pions identified: " << particleTracksIdentified[kPion] + << ", primary: " << particleTracksPrimary[kPion] + << ", secondary: " << particleTracksSecondary[kPion]; + LOG(info) << "Kaons identified: " << particleTracksIdentified[kKaon] + << ", primary: " << particleTracksPrimary[kKaon] + << ", secondary: " << particleTracksSecondary[kKaon]; + LOG(info) << "Protons identified: " << particleTracksIdentified[kProton] + << ", primary: " << particleTracksPrimary[kProton] + << ", secondary: " << particleTracksSecondary[kProton]; - } else if (bestSpecies == kKaon) { - ue.fill(HIST("Kaon/hPtMeasured"), track.pt()); - ue.fill(HIST("Kaon/hPtMeasuredVsMult"), track.pt(), percentile); - ue.fill(HIST("Kaon/hPtAllReco"), track.pt()); - ue.fill(HIST("Kaon/hPtAllRecoVsMult"), track.pt(), percentile); - particleTracksIdentified[kKaon]++; + LOG(info) << "=== DEBUG processMC END ==="; + } - if (enablePIDHistograms) { - ue.fill(HIST("Kaon/hNsigmaTPC"), track.pt(), track.tpcNSigmaKa()); - } + void MultiplicityPt::processTrue(CollisionTableMCTrue const& mcCollisions, + ParticleTableMC const& particles) + { + if (!percentilesComputed) { + LOG(warning) << "Percentiles not computed yet! Run processPercentileCalibration first!"; + } - if (track.has_mcParticle()) { - const auto& particle = track.mcParticle(); - if (particle.isPhysicalPrimary()) { - ue.fill(HIST("Kaon/hPtPrimReco"), track.pt()); - ue.fill(HIST("Kaon/hPtPrimRecoVsMult"), track.pt(), percentile); - particleTracksPrimary[kKaon]++; - } else { - ue.fill(HIST("Kaon/hPtSecReco"), track.pt()); - ue.fill(HIST("Kaon/hPtSecRecoVsMult"), track.pt(), percentile); - particleTracksSecondary[kKaon]++; - } - } + LOG(info) << "=== DEBUG processTrue START ==="; + LOG(info) << "Number of MC collisions: " << mcCollisions.size(); - } else if (bestSpecies == kProton) { - ue.fill(HIST("Proton/hPtMeasured"), track.pt()); - ue.fill(HIST("Proton/hPtMeasuredVsMult"), track.pt(), percentile); - ue.fill(HIST("Proton/hPtAllReco"), track.pt()); - ue.fill(HIST("Proton/hPtAllRecoVsMult"), track.pt(), percentile); - particleTracksIdentified[kProton]++; + int nAllGenerated = 0; + int nBadVertex = 0; + int nPhysicsSelected = 0; - if (enablePIDHistograms) { - ue.fill(HIST("Proton/hNsigmaTPC"), track.pt(), track.tpcNSigmaPr()); - } + std::array particleCountAll = {0}; + std::array particleCountBadVertex = {0}; + std::array particleCountAfterPS = {0}; - if (track.has_mcParticle()) { - const auto& particle = track.mcParticle(); - if (particle.isPhysicalPrimary()) { - ue.fill(HIST("Proton/hPtPrimReco"), track.pt()); - ue.fill(HIST("Proton/hPtPrimRecoVsMult"), track.pt(), percentile); - particleTracksPrimary[kProton]++; - } else { - ue.fill(HIST("Proton/hPtSecReco"), track.pt()); - ue.fill(HIST("Proton/hPtSecRecoVsMult"), track.pt(), percentile); - particleTracksSecondary[kProton]++; - } - } - } - } + for (const auto& mcCollision : mcCollisions) { + nAllGenerated++; - LOG(info) << "=== DEBUG TRACK COUNTING ==="; - LOG(info) << "Total tracks processed: " << totalTracksProcessed; - LOG(info) << "Tracks from selected events: " << tracksFromSelectedEvents; - LOG(info) << "Tracks passing selection: " << tracksPassingSelection; - - LOG(info) << "Pions identified: " << particleTracksIdentified[kPion] - << ", primary: " << particleTracksPrimary[kPion] - << ", secondary: " << particleTracksSecondary[kPion]; - LOG(info) << "Kaons identified: " << particleTracksIdentified[kKaon] - << ", primary: " << particleTracksPrimary[kKaon] - << ", secondary: " << particleTracksSecondary[kKaon]; - LOG(info) << "Protons identified: " << particleTracksIdentified[kProton] - << ", primary: " << particleTracksPrimary[kProton] - << ", secondary: " << particleTracksSecondary[kProton]; - - LOG(info) << "=== DEBUG processMC END ==="; -} + float mcMult = getMultiplicityMC(mcCollision, particles); + float percentile = multiplicityToPercentile(mcMult); -// ======================================================================== -// TRUE MC PROCESSING - Using computed percentiles -// ======================================================================== -void MultiplicityPt::processTrue(CollisionTableMCTrue const& mcCollisions, - ParticleTableMC const& particles) -{ - if (!percentilesComputed) { - LOG(warning) << "Percentiles not computed yet! Run processPercentileCalibration first!"; - } + ue.fill(HIST("hvtxZmc"), mcCollision.posZ()); + auto particlesInCollision = particles.sliceBy(perMCCol, mcCollision.globalIndex()); - LOG(info) << "=== DEBUG processTrue START ==="; - LOG(info) << "Number of MC collisions: " << mcCollisions.size(); + // Fill ALL generated primaries BEFORE any cuts + for (const auto& particle : particlesInCollision) { + if (isGoodPrimary(particle)) { + ue.fill(HIST("Inclusive/hPtPrimGenAll"), particle.pt()); + ue.fill(HIST("Inclusive/hPtPrimGenAllVsMult"), particle.pt(), percentile); + } - int nAllGenerated = 0; - int nBadVertex = 0; - int nPhysicsSelected = 0; + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Pion/hPtPrimGenAll"), particle.pt()); + ue.fill(HIST("Pion/hPtPrimGenAllVsMult"), particle.pt(), percentile); + particleCountAll[kPion]++; + } - std::array particleCountAll = {0}; - std::array particleCountBadVertex = {0}; - std::array particleCountAfterPS = {0}; + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Kaon/hPtPrimGenAll"), particle.pt()); + ue.fill(HIST("Kaon/hPtPrimGenAllVsMult"), particle.pt(), percentile); + particleCountAll[kKaon]++; + } - for (const auto& mcCollision : mcCollisions) { - nAllGenerated++; + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Proton/hPtPrimGenAll"), particle.pt()); + ue.fill(HIST("Proton/hPtPrimGenAllVsMult"), particle.pt(), percentile); + particleCountAll[kProton]++; + } + } - float mcMult = getMultiplicityMC(mcCollision, particles); - float percentile = multiplicityToPercentile(mcMult); + // Apply vertex cut + if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) { + nBadVertex++; - ue.fill(HIST("hvtxZmc"), mcCollision.posZ()); - auto particlesInCollision = particles.sliceBy(perMCCol, mcCollision.globalIndex()); + for (const auto& particle : particlesInCollision) { + if (isGoodPrimary(particle)) { + ue.fill(HIST("Inclusive/hPtPrimBadVertex"), particle.pt()); + ue.fill(HIST("Inclusive/hPtPrimBadVertexVsMult"), particle.pt(), percentile); + } - // Fill ALL generated primaries BEFORE any cuts - for (const auto& particle : particlesInCollision) { - if (isGoodPrimary(particle)) { - ue.fill(HIST("Inclusive/hPtPrimGenAll"), particle.pt()); - ue.fill(HIST("Inclusive/hPtPrimGenAllVsMult"), particle.pt(), percentile); - } + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Pion/hPtPrimBadVertex"), particle.pt()); + ue.fill(HIST("Pion/hPtPrimBadVertexVsMult"), particle.pt(), percentile); + particleCountBadVertex[kPion]++; + } - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Pion/hPtPrimGenAll"), particle.pt()); - ue.fill(HIST("Pion/hPtPrimGenAllVsMult"), particle.pt(), percentile); - particleCountAll[kPion]++; - } + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Kaon/hPtPrimBadVertex"), particle.pt()); + ue.fill(HIST("Kaon/hPtPrimBadVertexVsMult"), particle.pt(), percentile); + particleCountBadVertex[kKaon]++; + } - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Kaon/hPtPrimGenAll"), particle.pt()); - ue.fill(HIST("Kaon/hPtPrimGenAllVsMult"), particle.pt(), percentile); - particleCountAll[kKaon]++; + if (isGoodPrimarySpecies(particle)) { + ue.fill(HIST("Proton/hPtPrimBadVertex"), particle.pt()); + ue.fill(HIST("Proton/hPtPrimBadVertexVsMult"), particle.pt(), percentile); + particleCountBadVertex[kProton]++; + } + } + continue; } - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Proton/hPtPrimGenAll"), particle.pt()); - ue.fill(HIST("Proton/hPtPrimGenAllVsMult"), particle.pt(), percentile); - particleCountAll[kProton]++; - } - } + // Apply INEL cuts + if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) + continue; + if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) + continue; - // Apply vertex cut - if (std::abs(mcCollision.posZ()) > cfgCutVertex.value) { - nBadVertex++; + nPhysicsSelected++; + // Fill primaries AFTER physics selection (denominator for efficiency) for (const auto& particle : particlesInCollision) { if (isGoodPrimary(particle)) { - ue.fill(HIST("Inclusive/hPtPrimBadVertex"), particle.pt()); - ue.fill(HIST("Inclusive/hPtPrimBadVertexVsMult"), particle.pt(), percentile); + ue.fill(HIST("Inclusive/hPtDenEff"), particle.pt()); + ue.fill(HIST("Inclusive/hPtDenEffVsMult"), particle.pt(), percentile); + ue.fill(HIST("Inclusive/hPtPrimGen"), particle.pt()); + ue.fill(HIST("Inclusive/hPtPrimGenVsMult"), particle.pt(), percentile); } if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Pion/hPtPrimBadVertex"), particle.pt()); - ue.fill(HIST("Pion/hPtPrimBadVertexVsMult"), particle.pt(), percentile); - particleCountBadVertex[kPion]++; + ue.fill(HIST("Pion/hPtDenEff"), particle.pt()); + ue.fill(HIST("Pion/hPtDenEffVsMult"), particle.pt(), percentile); + ue.fill(HIST("Pion/hPtPrimGen"), particle.pt()); + ue.fill(HIST("Pion/hPtPrimGenVsMult"), particle.pt(), percentile); + particleCountAfterPS[kPion]++; } if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Kaon/hPtPrimBadVertex"), particle.pt()); - ue.fill(HIST("Kaon/hPtPrimBadVertexVsMult"), particle.pt(), percentile); - particleCountBadVertex[kKaon]++; + ue.fill(HIST("Kaon/hPtDenEff"), particle.pt()); + ue.fill(HIST("Kaon/hPtDenEffVsMult"), particle.pt(), percentile); + ue.fill(HIST("Kaon/hPtPrimGen"), particle.pt()); + ue.fill(HIST("Kaon/hPtPrimGenVsMult"), particle.pt(), percentile); + particleCountAfterPS[kKaon]++; } if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Proton/hPtPrimBadVertex"), particle.pt()); - ue.fill(HIST("Proton/hPtPrimBadVertexVsMult"), particle.pt(), percentile); - particleCountBadVertex[kProton]++; + ue.fill(HIST("Proton/hPtDenEff"), particle.pt()); + ue.fill(HIST("Proton/hPtDenEffVsMult"), particle.pt(), percentile); + ue.fill(HIST("Proton/hPtPrimGen"), particle.pt()); + ue.fill(HIST("Proton/hPtPrimGenVsMult"), particle.pt(), percentile); + particleCountAfterPS[kProton]++; } } - continue; } - // Apply INEL cuts - if (cfgINELCut.value == 1 && !o2::pwglf::isINELgt0mc(particlesInCollision, pdg)) - continue; - if (cfgINELCut.value == 2 && !o2::pwglf::isINELgt1mc(particlesInCollision, pdg)) - continue; - - nPhysicsSelected++; - - // Fill primaries AFTER physics selection (denominator for efficiency) - for (const auto& particle : particlesInCollision) { - if (isGoodPrimary(particle)) { - ue.fill(HIST("Inclusive/hPtDenEff"), particle.pt()); - ue.fill(HIST("Inclusive/hPtDenEffVsMult"), particle.pt(), percentile); - ue.fill(HIST("Inclusive/hPtPrimGen"), particle.pt()); - ue.fill(HIST("Inclusive/hPtPrimGenVsMult"), particle.pt(), percentile); - } - - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Pion/hPtDenEff"), particle.pt()); - ue.fill(HIST("Pion/hPtDenEffVsMult"), particle.pt(), percentile); - ue.fill(HIST("Pion/hPtPrimGen"), particle.pt()); - ue.fill(HIST("Pion/hPtPrimGenVsMult"), particle.pt(), percentile); - particleCountAfterPS[kPion]++; - } - - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Kaon/hPtDenEff"), particle.pt()); - ue.fill(HIST("Kaon/hPtDenEffVsMult"), particle.pt(), percentile); - ue.fill(HIST("Kaon/hPtPrimGen"), particle.pt()); - ue.fill(HIST("Kaon/hPtPrimGenVsMult"), particle.pt(), percentile); - particleCountAfterPS[kKaon]++; - } - - if (isGoodPrimarySpecies(particle)) { - ue.fill(HIST("Proton/hPtDenEff"), particle.pt()); - ue.fill(HIST("Proton/hPtDenEffVsMult"), particle.pt(), percentile); - ue.fill(HIST("Proton/hPtPrimGen"), particle.pt()); - ue.fill(HIST("Proton/hPtPrimGenVsMult"), particle.pt(), percentile); - particleCountAfterPS[kProton]++; - } - } + LOG(info) << "=== DEBUG processTrue END ==="; + LOG(info) << "All generated events: " << nAllGenerated; + LOG(info) << "Events with bad vertex: " << nBadVertex; + LOG(info) << "Passing physics selection: " << nPhysicsSelected; + + LOG(info) << "=== PARTICLE-SPECIFIC STATISTICS ==="; + LOG(info) << "Pions - All: " << particleCountAll[kPion] + << ", Bad vertex: " << particleCountBadVertex[kPion] + << ", After PS: " << particleCountAfterPS[kPion]; + LOG(info) << "Kaons - All: " << particleCountAll[kKaon] + << ", Bad vertex: " << particleCountBadVertex[kKaon] + << ", After PS: " << particleCountAfterPS[kKaon]; + LOG(info) << "Protons - All: " << particleCountAll[kProton] + << ", Bad vertex: " << particleCountBadVertex[kProton] + << ", After PS: " << particleCountAfterPS[kProton]; } - - LOG(info) << "=== DEBUG processTrue END ==="; - LOG(info) << "All generated events: " << nAllGenerated; - LOG(info) << "Events with bad vertex: " << nBadVertex; - LOG(info) << "Passing physics selection: " << nPhysicsSelected; - - LOG(info) << "=== PARTICLE-SPECIFIC STATISTICS ==="; - LOG(info) << "Pions - All: " << particleCountAll[kPion] - << ", Bad vertex: " << particleCountBadVertex[kPion] - << ", After PS: " << particleCountAfterPS[kPion]; - LOG(info) << "Kaons - All: " << particleCountAll[kKaon] - << ", Bad vertex: " << particleCountBadVertex[kKaon] - << ", After PS: " << particleCountAfterPS[kKaon]; - LOG(info) << "Protons - All: " << particleCountAll[kProton] - << ", Bad vertex: " << particleCountBadVertex[kProton] - << ", After PS: " << particleCountAfterPS[kProton]; -}