2626#include < DataFormatsFIT/Triggers.h>
2727#include < DataFormatsParameters/AggregatedRunInfo.h>
2828#include < DataFormatsParameters/GRPLHCIFData.h>
29+ #include < Framework/ASoA.h>
2930#include < Framework/AnalysisDataModel.h>
3031#include < Framework/AnalysisHelpers.h>
3132#include < Framework/AnalysisTask.h>
3940#include < TH1.h>
4041#include < TH2.h>
4142
42- #include < Rtypes.h>
43-
4443#include < algorithm>
4544#include < bitset>
4645#include < cmath>
4746#include < cstddef>
4847#include < cstdint>
4948#include < memory>
49+ #include < unordered_map>
50+ #include < unordered_set>
51+ #include < utility>
5052#include < vector>
5153
5254using namespace o2 ;
@@ -59,19 +61,37 @@ constexpr int NBCsPerOrbit = o2::constants::lhc::LHCMaxBunches;
5961constexpr int MaxStoredDistance = 15 ;
6062constexpr float MaxFITTime = 30 .f;
6163
64+ constexpr int NoSignalMCRef = -1 ;
65+ constexpr int NoRecoCollision = -1 ;
66+ constexpr int MultipleRecoTracks = -2 ;
67+
68+ constexpr uint16_t MCTrackFakeMask = 1u << 15 ;
69+
70+ enum MCParticleFlag : uint8_t {
71+ kNoFlags = 0 ,
72+ kPhysicalPrimary = 1u << 0 , // generated physical primary
73+ kHasRecoTrack = 1u << 1 , // has at least one non-fake reconstructed track
74+ kHighMultiplicityCollision = 1u << 2 , // matched track belongs to a collision with extra contributors
75+ kHasPVContributor = 1u << 3 , // has a track contributing to the PV
76+ kHasAmbiguousTrack = 1u << 4 // has an ambiguous track
77+ };
78+
79+ using SignalRef = std::pair<int32_t , int16_t >;
6280using DistanceMap = std::vector<std::vector<uint32_t >>;
6381
6482DistanceMap buildMinimumDistanceMap (std::vector<uint32_t > const & tfIDs,
6583 int64_t bcSOR, int64_t nBCsPerTF,
66- std::vector<int64_t >& activeBCs, uint32_t maxDistance)
84+ std::vector<int64_t >& activeBCs, uint32_t maxDistance,
85+ bool skipFirstExactMatch = false )
6786{
6887 const uint32_t overflowDistance = maxDistance + 1 ;
6988 DistanceMap distances (tfIDs.size (), std::vector<uint32_t >(nBCsPerTF, overflowDistance));
7089 if (activeBCs.empty ()) {
7190 return distances;
7291 }
92+ // Keep duplicate BCs so collisions at the same BC have distance zero
7393 std::sort (activeBCs.begin (), activeBCs.end ());
74- activeBCs. erase ( std::unique (activeBCs. begin (), activeBCs. end ()), activeBCs. end ());
94+ // The forward-only cursor makes the scan linear after sorting
7595 size_t nextIndex = 0 ;
7696 for (size_t iTF = 0 ; iTF < tfIDs.size (); ++iTF) {
7797 const int64_t tfStartBC = bcSOR + tfIDs[iTF] * nBCsPerTF;
@@ -80,9 +100,14 @@ DistanceMap buildMinimumDistanceMap(std::vector<uint32_t> const& tfIDs,
80100 while (nextIndex < activeBCs.size () && activeBCs[nextIndex] < currentBC) {
81101 ++nextIndex;
82102 }
103+ size_t rightIndex = nextIndex;
104+ // MC maps contain the queried collision itself, so skip one exact self-match
105+ if (skipFirstExactMatch && rightIndex < activeBCs.size () && activeBCs[rightIndex] == currentBC) {
106+ ++rightIndex;
107+ }
83108 int64_t distance = overflowDistance;
84- if (nextIndex < activeBCs.size ()) {
85- distance = std::min (distance, activeBCs[nextIndex ] - currentBC);
109+ if (rightIndex < activeBCs.size ()) {
110+ distance = std::min (distance, activeBCs[rightIndex ] - currentBC);
86111 }
87112 if (nextIndex > 0 ) {
88113 distance = std::min (distance, currentBC - activeBCs[nextIndex - 1 ]);
@@ -143,6 +168,13 @@ DECLARE_SOA_COLUMN(Sign, sign, std::vector<int8_t>);
143168DECLARE_SOA_COLUMN (MinimumDistanceFT0, minimumDistanceFT0, int8_t );
144169DECLARE_SOA_COLUMN (MinimumDistanceFV0, minimumDistanceFV0, int8_t );
145170DECLARE_SOA_COLUMN (MinimumDistanceFDD, minimumDistanceFDD, int8_t );
171+ DECLARE_SOA_COLUMN (Pdg, pdg, std::vector<int32_t >);
172+ DECLARE_SOA_COLUMN (MinimumDistanceMCCol, minimumDistanceMCCol, int8_t );
173+ DECLARE_SOA_COLUMN (RecoFlags, recoFlags, std::vector<uint8_t >); // See MCParticleFlag for the bit definitions
174+ DECLARE_SOA_COLUMN (RecoCollisionIds, recoCollisionIds, std::vector<int32_t >); // -1: no collision, -2: multiple tracks
175+ DECLARE_SOA_COLUMN (ParticleIds, particleIds, std::vector<int16_t >);
176+ DECLARE_SOA_COLUMN (Mult, mult, int32_t ); // number of physical-primary MC particles in the collision
177+ DECLARE_SOA_COLUMN (IsSignal, isSignal, bool ); // whether the collision belongs to the configured signal source
146178} // namespace o2::aod::upc_cand_prod_bar
147179
148180namespace o2 ::aod
@@ -173,10 +205,39 @@ DECLARE_SOA_TABLE(UPCBarrelCands, "AOD", "UPCBARRELCANDS",
173205 upc_cand_prod_bar::MinimumDistanceFT0,
174206 upc_cand_prod_bar::MinimumDistanceFV0,
175207 upc_cand_prod_bar::MinimumDistanceFDD);
208+
209+ DECLARE_SOA_TABLE (UPCBarrelMcColls, " AOD" , " UPCBMCCOLL" ,
210+ upc_cand_prod_bar::TfId,
211+ upc_cand_prod_bar::GlobalBC,
212+ upc_cand_prod_bar::PosX,
213+ upc_cand_prod_bar::PosY,
214+ upc_cand_prod_bar::PosZ,
215+ upc_cand_prod_bar::Px,
216+ upc_cand_prod_bar::Py,
217+ upc_cand_prod_bar::Pz,
218+ upc_cand_prod_bar::Pdg,
219+ upc_cand_prod_bar::Mult,
220+ upc_cand_prod_bar::MinimumDistanceMCCol,
221+ upc_cand_prod_bar::RecoFlags,
222+ upc_cand_prod_bar::RecoCollisionIds,
223+ upc_cand_prod_bar::IsSignal);
224+
225+ namespace upc_cand_prod_bar
226+ {
227+ // Array index into UPCBarrelMcColls
228+ DECLARE_SOA_ARRAY_INDEX_COLUMN_FULL_CUSTOM (McCollision, mcCollision, int32_t , UPCBarrelMcColls, " UPCBMCColls" , " " );
229+ } // namespace upc_cand_prod_bar
230+
231+ // Both IDs are -1 if the track has no valid stored signal reference
232+ DECLARE_SOA_TABLE (UPCBarrelMCLabels, " AOD" , " UPCBMCLABEL" ,
233+ upc_cand_prod_bar::McCollisionIds,
234+ upc_cand_prod_bar::ParticleIds);
176235} // namespace o2::aod
177236
178237struct UpcCandProducerBarrel {
179238 Produces<aod::UPCBarrelCands> selectedCandidates;
239+ Produces<aod::UPCBarrelMcColls> selectedMCCollisions;
240+ Produces<aod::UPCBarrelMCLabels> selectedCandidateMCLabels;
180241 HistogramRegistry registry{" registry" , {}};
181242 Service<o2::ccdb::BasicCCDBManager> ccdb{};
182243
@@ -197,13 +258,21 @@ struct UpcCandProducerBarrel {
197258 Configurable<int > vetoFT0{" vetoFT0" , 1 , " FT0 veto: 0=off, 1=on" };
198259 Configurable<int > vetoFV0{" vetoFV0" , 0 , " FV0 veto: 0=off, 1=on" };
199260 Configurable<int > vetoFDD{" vetoFDD" , 0 , " FDD veto: 0=off, 1=on" };
261+ Configurable<int > signalSourceId{" signalSourceId" , 1 , " MC source ID stored as embedded signal" };
200262
201263 using CollisionsWithSels = soa::Join<aod::Collisions, aod::EvSels>;
202264 using BCsWithSels = soa::Join<aod::BCsWithTimestamps, aod::BcSels>;
203265 using TracksWithPID = soa::Join<aod::Tracks, aod::TracksExtra,
204266 aod::pidTPCEl, aod::pidTPCPi, aod::pidTPCKa, aod::pidTPCPr,
205267 aod::pidTOFEl, aod::pidTOFPi, aod::pidTOFKa, aod::pidTOFPr>;
268+ using TracksWithPIDMC = soa::Join<aod::Tracks, aod::TracksExtra,
269+ aod::pidTPCEl, aod::pidTPCPi, aod::pidTPCKa, aod::pidTPCPr,
270+ aod::pidTOFEl, aod::pidTOFPi, aod::pidTOFKa, aod::pidTOFPr,
271+ aod::McTrackLabels>;
206272 Preslice<TracksWithPID> tracksPerCollision = aod::track::collisionId;
273+ Preslice<TracksWithPIDMC> mcTracksPerCollision = aod::track::collisionId;
274+ PresliceUnsorted<TracksWithPIDMC> tracksPerMCParticle = aod::mctracklabel::mcParticleId;
275+ Preslice<aod::McParticles> particlesPerMCCollision = aod::mcparticle::mcCollisionId;
207276
208277 RCTFlagsChecker rctChecker{kFDDBad , kFT0Bad , kFV0Bad , kITSBad , kITSLimAccMCRepr , kTPCBadTracking , kTPCLimAccMCRepr , kTPCBadPID , kTOFBad , kTOFLimAccMCRepr , kCcdbObjectLoaded };
209278
@@ -250,13 +319,19 @@ struct UpcCandProducerBarrel {
250319 cachedRunNumber = runNumber;
251320 }
252321
253- void process (CollisionsWithSels const & collisions, TracksWithPID const & tracks, BCsWithSels const & bcs, aod::FT0s const & ft0s, aod::FV0As const & fv0s, aod::FDDs const & fdds)
322+ template <bool isMC, typename CollisionsT, typename TracksT, typename PresliceT>
323+ void processImpl (CollisionsT const & collisions, TracksT const & tracks, PresliceT const & tracksPerColl,
324+ BCsWithSels const & bcs, aod::FT0s const & ft0s, aod::FV0As const & fv0s,
325+ aod::FDDs const & fdds, aod::McCollisions const * mcCollisions = nullptr ,
326+ aod::McParticles const * mcParticles = nullptr ,
327+ aod::AmbiguousTracks const * ambiguousTracks = nullptr )
254328 {
255329 updateRunInfo (bcs.begin ().runNumber ());
256330 std::vector<uint32_t > tfIDs;
257331 std::vector<uint8_t > tfPassesRCT;
258332 std::vector<size_t > localTFIndex;
259333 localTFIndex.reserve (bcs.size ());
334+ // Cache the local TF slot for each BC row for collision and MC lookups
260335 for (const auto & bc : bcs) {
261336 const auto tfID = static_cast <uint32_t >((static_cast <int64_t >(bc.globalBC ()) - bcSOR) / nBCsPerTF);
262337 if (tfIDs.empty () || tfID != tfIDs.back ()) {
@@ -267,12 +342,84 @@ struct UpcCandProducerBarrel {
267342 }
268343 registry.fill (HIST (" hProcessedTFs" ), 0.5 , static_cast <double >(tfIDs.size ()));
269344
345+ std::unordered_map<int64_t , SignalRef> signalParticleRefs;
346+ if constexpr (isMC) {
347+ std::unordered_set<int64_t > ambiguousTrackIds;
348+ ambiguousTrackIds.reserve (ambiguousTracks->size ());
349+ for (const auto & ambiguousTrack : *ambiguousTracks) {
350+ ambiguousTrackIds.insert (ambiguousTrack.trackId ());
351+ }
352+
353+ std::vector<int64_t > bcsWithMCCollision;
354+ bcsWithMCCollision.reserve (mcCollisions->size ());
355+ // Use collisions from every source and retain same-BC pile-up
356+ for (const auto & mcCollision : *mcCollisions) {
357+ bcsWithMCCollision.push_back (static_cast <int64_t >(mcCollision.template bc_as <BCsWithSels>().globalBC ()));
358+ }
359+ const auto minDistanceMC = buildMinimumDistanceMap (tfIDs, bcSOR, nBCsPerTF, bcsWithMCCollision, MaxStoredDistance, true );
360+
361+ for (const auto & mcCollision : *mcCollisions) {
362+ const auto bc = mcCollision.template bc_as <BCsWithSels>();
363+ const auto globalBC = bc.globalBC ();
364+ const auto iTF = localTFIndex[bc.globalIndex ()];
365+ const auto bcInTF = (static_cast <int64_t >(globalBC) - bcSOR) % nBCsPerTF;
366+ const auto particles = mcParticles->sliceBy (particlesPerMCCollision, mcCollision.globalIndex ());
367+ const bool isSignal = mcCollision.getSourceId () == signalSourceId.value ;
368+
369+ std::vector<float > px, py, pz;
370+ std::vector<int32_t > pdg, recoCollisionIds;
371+ std::vector<uint8_t > recoFlags;
372+ int32_t mult = 0 ;
373+ // Output rows follow input MC collisions, making this the compact collision label
374+ const auto newMCCollisionId = static_cast <int32_t >(selectedMCCollisions.lastIndex () + 1 );
375+ for (const auto & particle : particles) {
376+ mult += particle.isPhysicalPrimary ();
377+ // Background rows keep only collision metadata and physical-primary multiplicity
378+ if (!isSignal) {
379+ continue ;
380+ }
381+ const auto newMCParticleId = static_cast <int16_t >(pdg.size ());
382+ px.push_back (particle.px ());
383+ py.push_back (particle.py ());
384+ pz.push_back (particle.pz ());
385+ pdg.push_back (particle.pdgCode ());
386+
387+ uint8_t flags = particle.isPhysicalPrimary () ? kPhysicalPrimary : kNoFlags ;
388+ int32_t recoCollisionId = NoRecoCollision;
389+ int nRecoTracks = 0 ;
390+ // Inspect only reconstructed tracks matched to this signal particle
391+ for (const auto & track : tracks.sliceBy (tracksPerMCParticle, particle.globalIndex ())) {
392+ if ((track.mcMask () & MCTrackFakeMask) != 0u ) {
393+ continue ;
394+ }
395+ ++nRecoTracks;
396+ flags |= kHasRecoTrack ;
397+ flags |= track.isPVContributor () ? kHasPVContributor : kNoFlags ;
398+ flags |= ambiguousTrackIds.contains (track.globalIndex ()) ? kHasAmbiguousTrack : kNoFlags ;
399+ if (track.has_collision ()) {
400+ recoCollisionId = track.collisionId ();
401+ flags |= collisions.iteratorAt (recoCollisionId).numContrib () > nTracks ? kHighMultiplicityCollision : kNoFlags ;
402+ }
403+ }
404+ // A single collision label is meaningful only for exactly one reconstructed track
405+ if (nRecoTracks > 1 ) {
406+ recoCollisionId = MultipleRecoTracks;
407+ }
408+ recoFlags.push_back (flags);
409+ recoCollisionIds.push_back (recoCollisionId);
410+ // Remap original particle IDs to output index for candidate labels
411+ signalParticleRefs.emplace (particle.globalIndex (), SignalRef{newMCCollisionId, newMCParticleId});
412+ }
413+ selectedMCCollisions (tfIDs[iTF], globalBC, mcCollision.posX (), mcCollision.posY (), mcCollision.posZ (),
414+ px, py, pz, pdg, mult, minDistanceMC[iTF][bcInTF],
415+ recoFlags, recoCollisionIds, isSignal);
416+ }
417+ }
418+
270419 auto hTVX = registry.get <TH1 >(HIST (" hTVX" ));
271420 auto hTVXRCT = registry.get <TH1 >(HIST (" hTVXRCT" ));
272421
273- std::vector<int64_t > bcsWithFT0;
274- std::vector<int64_t > bcsWithFV0;
275- std::vector<int64_t > bcsWithFDD;
422+ std::vector<int64_t > bcsWithFT0, bcsWithFV0, bcsWithFDD;
276423 bcsWithFT0.reserve (ft0s.size ());
277424 bcsWithFV0.reserve (fv0s.size ());
278425 bcsWithFDD.reserve (fdds.size ());
@@ -284,7 +431,7 @@ struct UpcCandProducerBarrel {
284431 if (ft0.timeA () < MaxFITTime || ft0.timeC () < MaxFITTime) {
285432 bcsWithFT0.push_back (gbc);
286433 }
287- if (!collidingBCs[bcInOrbit] || !TESTBIT (ft0.triggerMask (), o2::fit::Triggers::bitVertex)) {
434+ if (!collidingBCs[bcInOrbit] || !(ft0.triggerMask () & ( 1U << o2::fit::Triggers::bitVertex) )) {
288435 continue ;
289436 }
290437 hTVX->Fill (bcInOrbit);
@@ -337,7 +484,7 @@ struct UpcCandProducerBarrel {
337484 if (collision.numContrib () != nTracks || !collision.selection_bit (aod::evsel::kNoTimeFrameBorder ) || !rctChecker (collision)) {
338485 continue ;
339486 }
340- auto bc = collision.bc_as <BCsWithSels>();
487+ auto bc = collision.template bc_as <BCsWithSels>();
341488 auto gbc = bc.globalBC ();
342489 const auto iTF = localTFIndex[bc.globalIndex ()];
343490 const int64_t bcInTF = (static_cast <int64_t >(gbc) - bcSOR) % nBCsPerTF;
@@ -354,7 +501,9 @@ struct UpcCandProducerBarrel {
354501 std::vector<uint8_t > itsClusterMap;
355502 std::vector<uint8_t > nClusters;
356503 std::vector<int8_t > sign;
357- for (const auto & track : tracks.sliceBy (tracksPerCollision, collision.globalIndex ())) {
504+ std::vector<int32_t > newMCCollisionIds;
505+ std::vector<int16_t > newParticleIds;
506+ for (const auto & track : tracks.sliceBy (tracksPerColl, collision.globalIndex ())) {
358507 if (!track.isPVContributor () || !track.hasITS () || !track.hasTPC () || std::abs (track.eta ()) > maxAbsEta.value || track.pt () < minPt.value ) {
359508 continue ;
360509 }
@@ -373,6 +522,17 @@ struct UpcCandProducerBarrel {
373522 itsClusterMap.push_back (track.itsClusterMap ());
374523 nClusters.push_back (track.tpcNClsFound ());
375524 sign.push_back (track.sign ());
525+ if constexpr (isMC) {
526+ // Fake, background, and unmatched particles keep the -1 label
527+ SignalRef label{NoSignalMCRef, NoSignalMCRef};
528+ if (track.has_mcParticle () && (track.mcMask () & MCTrackFakeMask) == 0u ) {
529+ if (const auto ref = signalParticleRefs.find (track.mcParticleId ()); ref != signalParticleRefs.end ()) {
530+ label = ref->second ;
531+ }
532+ }
533+ newMCCollisionIds.push_back (label.first );
534+ newParticleIds.push_back (label.second );
535+ }
376536 }
377537 if (px.size () != static_cast <size_t >(nTracks)) {
378538 continue ;
@@ -383,8 +543,28 @@ struct UpcCandProducerBarrel {
383543 std::min<uint32_t >(distFT0, MaxStoredDistance + 1 ),
384544 std::min<uint32_t >(distFV0, MaxStoredDistance + 1 ),
385545 std::min<uint32_t >(distFDD, MaxStoredDistance + 1 ));
546+ if constexpr (isMC) {
547+ selectedCandidateMCLabels (newMCCollisionIds, newParticleIds);
548+ }
386549 }
387550 }
551+
552+ void processData (CollisionsWithSels const & collisions, TracksWithPID const & tracks, BCsWithSels const & bcs,
553+ aod::FT0s const & ft0s, aod::FV0As const & fv0s, aod::FDDs const & fdds)
554+ {
555+ processImpl<false >(collisions, tracks, tracksPerCollision, bcs, ft0s, fv0s, fdds);
556+ }
557+ PROCESS_SWITCH (UpcCandProducerBarrel, processData, " Process reconstructed data" , true );
558+
559+ void processMC (CollisionsWithSels const & collisions, TracksWithPIDMC const & tracks, BCsWithSels const & bcs,
560+ aod::FT0s const & ft0s, aod::FV0As const & fv0s, aod::FDDs const & fdds,
561+ aod::McCollisions const & mcCollisions, aod::McParticles const & mcParticles,
562+ aod::AmbiguousTracks const & ambiguousTracks)
563+ {
564+ processImpl<true >(collisions, tracks, mcTracksPerCollision, bcs, ft0s, fv0s, fdds,
565+ &mcCollisions, &mcParticles, &ambiguousTracks);
566+ }
567+ PROCESS_SWITCH (UpcCandProducerBarrel, processMC, " Process reconstructed MC and write compact MC truth" , false );
388568};
389569
390570WorkflowSpec defineDataProcessing (ConfigContext const & context) { return WorkflowSpec{adaptAnalysisTask<UpcCandProducerBarrel>(context)}; }
0 commit comments