diff --git a/PWGEM/PhotonMeson/Core/V0PhotonCut.h b/PWGEM/PhotonMeson/Core/V0PhotonCut.h index 1921a71c79b..d9c442f499b 100644 --- a/PWGEM/PhotonMeson/Core/V0PhotonCut.h +++ b/PWGEM/PhotonMeson/Core/V0PhotonCut.h @@ -246,8 +246,10 @@ class V0PhotonCut : public TNamed fRegistry->add("QA/V0Photon/before/hConvZR", "Conversion point ZR", o2::framework::HistType::kTH2F, {thAxisConvZ, thAxisConvR}, true); fRegistry->add("QA/V0Photon/before/hChi2", "Chi2/ndf from KFParticle;#chi^{2}_{KF}/ndf;counts", o2::framework::HistType::kTH1D, {thAxisChi2}, true); - // TODO: add psi_pair once available - // fRegistry->add("QA/V0Photon/before/hPsiPair", "Psi pair;#Psi_{pair};counts", o2::framework::HistType::kTH1D, {thAxisPsiPair}, true); + // psi_pair (V0PhotonsPhiVPsi) and the KF chi2 split (V0PhotonsKFChi2), filled only if the photon table joins them + fRegistry->add("QA/V0Photon/before/hPsiPair", "Psi pair;#Psi_{pair};counts", o2::framework::HistType::kTH1D, {thAxisPsiPair}, true); + fRegistry->add("QA/V0Photon/before/hChi2Vtx", "KF chi2/ndf of the conversion-vertex fit;#chi^{2}_{vtx}/ndf;counts", o2::framework::HistType::kTH1D, {thAxisChi2}, true); + fRegistry->add("QA/V0Photon/before/hChi2PV", "KF chi2 to the primary vertex;#chi^{2}_{PV};counts", o2::framework::HistType::kTH1D, {{100, 0., 200.}}, true); fRegistry->add("QA/V0Photon/before/Pos/NSigmaE", "NSigmaE of pos leg vs momentum", o2::framework::HistType::kTH2F, {thAxisMomentum, thAxisNSigmaE}, true); fRegistry->add("QA/V0Photon/before/Pos/NSigmaPi", "NSigmaE of pos leg vs momentum", o2::framework::HistType::kTH2F, {thAxisMomentum, thAxisNSigmaPi}, true); @@ -316,8 +318,13 @@ class V0PhotonCut : public TNamed fRegistry->fill(HIST("QA/V0Photon/before/hConvZR"), v0.vz(), v0.v0radius()); fRegistry->fill(HIST("QA/V0Photon/before/hChi2"), v0.chiSquareNDF()); - // TODO: add psi_pair once available - // fRegistry->fill(HIST("QA/V0Photon/before/hPsiPair"), v0.psiPair()); + if constexpr (requires { v0.psipair(); }) { + fRegistry->fill(HIST("QA/V0Photon/before/hPsiPair"), v0.psipair()); + } + if constexpr (requires { v0.chi2PV(); }) { + fRegistry->fill(HIST("QA/V0Photon/before/hChi2Vtx"), v0.chi2NDFVtx()); + fRegistry->fill(HIST("QA/V0Photon/before/hChi2PV"), v0.chi2PV()); + } fRegistry->fill(HIST("QA/V0Photon/before/Pos/NSigmaE"), pos.p(), pos.tpcNSigmaEl()); fRegistry->fill(HIST("QA/V0Photon/before/Pos/NSigmaPi"), pos.p(), pos.tpcNSigmaPi()); @@ -346,8 +353,13 @@ class V0PhotonCut : public TNamed fRegistry->fill(HIST("QA/V0Photon/after/hConvZR"), v0.vz(), v0.v0radius()); fRegistry->fill(HIST("QA/V0Photon/after/hChi2"), v0.chiSquareNDF()); - // TODO: add psi_pair once available - // fRegistry->fill(HIST("QA/V0Photon/after/hPsiPair"), v0.psiPair()); + if constexpr (requires { v0.psipair(); }) { + fRegistry->fill(HIST("QA/V0Photon/after/hPsiPair"), v0.psipair()); + } + if constexpr (requires { v0.chi2PV(); }) { + fRegistry->fill(HIST("QA/V0Photon/after/hChi2Vtx"), v0.chi2NDFVtx()); + fRegistry->fill(HIST("QA/V0Photon/after/hChi2PV"), v0.chi2PV()); + } fRegistry->fill(HIST("QA/V0Photon/after/Pos/NSigmaE"), pos.p(), pos.tpcNSigmaEl()); fRegistry->fill(HIST("QA/V0Photon/after/Pos/NSigmaPi"), pos.p(), pos.tpcNSigmaPi()); diff --git a/PWGEM/PhotonMeson/DataModel/gammaTables.h b/PWGEM/PhotonMeson/DataModel/gammaTables.h index f0e0bdf8d85..454bcea9740 100644 --- a/PWGEM/PhotonMeson/DataModel/gammaTables.h +++ b/PWGEM/PhotonMeson/DataModel/gammaTables.h @@ -292,6 +292,31 @@ DECLARE_SOA_TABLE_VERSIONED(V0LegsDeDxMCTmp, "AOD", "V0LEGDEDXMCTMP", 0, mcpidtp // iterators using V0LegDeDxMCTmp = V0LegsDeDxMCTmp::iterator; +namespace v0legcov +{ +DECLARE_SOA_COLUMN(X, x, float); //! x of the leg parameters given to the V0 fit (innermost update, cm) +DECLARE_SOA_COLUMN(Alpha, alpha, float); //! frame angle +DECLARE_SOA_COLUMN(Y, y, float); //! local y (cm) +DECLARE_SOA_COLUMN(Z, z, float); //! z (cm), TPC-only legs after the drift correction to the V0's collision +DECLARE_SOA_COLUMN(Snp, snp, float); //! sin(phi) in the local frame +DECLARE_SOA_COLUMN(Tgl, tgl, float); //! tan(lambda) +DECLARE_SOA_COLUMN(Signed1Pt, signed1Pt, float); //! q/pT (c/GeV) +DECLARE_SOA_COLUMN(Cov, cov, float[15]); // NOLINT(modernize-avoid-c-arrays) //! covariance matrix, o2::track::TrackParCov order +} // namespace v0legcov + +// leg track parameters as used in the V0 fit, one row per V0Legs row (optional builder output, joinable with V0Legs) +DECLARE_SOA_TABLE_VERSIONED(V0LegsCov_000, "AOD", "V0LEGCOV", 0, //! + v0legcov::X, v0legcov::Alpha, v0legcov::Y, v0legcov::Z, v0legcov::Snp, v0legcov::Tgl, v0legcov::Signed1Pt, v0legcov::Cov); +using V0LegsCov = V0LegsCov_000; +// iterators +using V0LegCov = V0LegsCov::iterator; + +// copy of V0LegsCov table +DECLARE_SOA_TABLE_VERSIONED(V0LegsCovTmp, "AOD", "V0LEGCOVTMP", 0, //! + v0legcov::X, v0legcov::Alpha, v0legcov::Y, v0legcov::Z, v0legcov::Snp, v0legcov::Tgl, v0legcov::Signed1Pt, v0legcov::Cov); +// iterators +using V0LegCovTmp = V0LegsCovTmp::iterator; + namespace emevent { DECLARE_SOA_COLUMN(NgPCM, ngpcm, int); @@ -540,6 +565,17 @@ DECLARE_SOA_INDEX_COLUMN(PMEvent, pmevent); //! DECLARE_SOA_COLUMN(PhiV, phiv, float); //! DECLARE_SOA_COLUMN(PsiPair, psipair, float); } // namespace v0photonsphivpsi +// KF quality of the photon: the stored v0photonkf::ChiSquareNDF is the chi2/ndf AFTER the topological constraint to the PV; +// these two separate the conversion-vertex fit from the pointing to the PV (row-by-row extension of the photon table, join by index) +namespace v0photonkfchi2 +{ +DECLARE_SOA_COLUMN(Chi2NDFVtx, chi2NDFVtx, float); //! chi2/ndf of the two-leg conversion-vertex fit (incl. the mass constraint if applied), before the PV constraint +DECLARE_SOA_COLUMN(Chi2PV, chi2PV, float); //! chi2 deviation of the photon from the primary vertex (KFParticle::GetDeviationFromVertex) +} // namespace v0photonkfchi2 +DECLARE_SOA_TABLE(V0PhotonsKFChi2, "AOD", "V0PHKFCHI2", o2::soa::Index<>, v0photonkfchi2::Chi2NDFVtx, v0photonkfchi2::Chi2PV); //! +DECLARE_SOA_TABLE(V0PhotonsKFChi2Tmp, "AOD", "V0PHKFCHI2TMP", o2::soa::Index<>, v0photonkfchi2::Chi2NDFVtx, v0photonkfchi2::Chi2PV); //! +DECLARE_SOA_TABLE(V0PhotonsKFChi2Mix, "AOD", "V0PHKFCHI2MIX", o2::soa::Index<>, v0photonkfchi2::Chi2NDFVtx, v0photonkfchi2::Chi2PV); //! builder run 2 + DECLARE_SOA_TABLE(V0PhotonsPhiVPsi, "AOD", "V0PHOTONPHIVPSI", //! o2::soa::Index<>, v0photonsphivpsi::PhiV, v0photonsphivpsi::PsiPair); // iterators @@ -551,6 +587,217 @@ DECLARE_SOA_TABLE(V0PhotonsPhiVPsiTmp, "AOD", "V0PHOTONPHIVTMP", //! // iterators using V0PhotonsPhiVPsiTmp = V0PhotonsPhiVPsiTmp; +// co-reconstructed mixed event: artificial events made by photon-mixed-event-producer from the photons of builder run 1, +// rebuilt from their legs by builder run 2 (photon-conversion-builder-mixed) +namespace pcmmixcol +{ +DECLARE_SOA_COLUMN(IsMixed, isMixed, bool); //! false: the photons of one event, true: N photons from N different events +DECLARE_SOA_COLUMN(SourceCollisionId, sourceCollisionId, int); //! collision (this DF) whose vertex the artificial event has +DECLARE_SOA_COLUMN(SourceEventUid, sourceEventUid, int64_t); //! mixer event counter of that collision +DECLARE_SOA_COLUMN(ZBin, zBin, int); //! pool z bin +DECLARE_SOA_COLUMN(OccBin, occBin, int); //! pool occupancy bin +DECLARE_SOA_COLUMN(Occupancy, occupancy, float); //! occupancy of the source collision +DECLARE_SOA_COLUMN(CentFT0M, centFT0M, float); //! centralities of the source collision (V0 fit input) +DECLARE_SOA_COLUMN(CentFT0A, centFT0A, float); //! +DECLARE_SOA_COLUMN(CentFT0C, centFT0C, float); //! +DECLARE_SOA_COLUMN(FirstLeg, firstLeg, int); //! first PCMMixLegs row of this artificial event +DECLARE_SOA_COLUMN(NLegs, nLegs, int); //! number of PCMMixLegs rows (2 per photon, contiguous) +DECLARE_SOA_COLUMN(RunNumber, runNumber, int); //! run of the source collision (CCDB: B field, TPC drift velocity) +DECLARE_SOA_COLUMN(Timestamp, timestamp, uint64_t); //! CCDB timestamp of the source collision (builder run 2 on derived data: no BCs) +DECLARE_SOA_COLUMN(MultNTracksPVeta1, multNTracksPVeta1, int); //! PV contributors with |eta| < 1 of the source collision +DECLARE_SOA_COLUMN(MultFT0M, multFT0M, float); //! FT0A + FT0C amplitude of the source collision +} // namespace pcmmixcol +DECLARE_SOA_TABLE(PCMMixCollisions, "AOD", "PCMMIXCOL", //! + o2::soa::Index<>, collision::PosX, collision::PosY, collision::PosZ, + collision::CovXX, collision::CovXY, collision::CovXZ, collision::CovYY, collision::CovYZ, collision::CovZZ, + collision::Chi2, collision::NumContrib, + pcmmixcol::CentFT0M, pcmmixcol::CentFT0A, pcmmixcol::CentFT0C, + pcmmixcol::IsMixed, pcmmixcol::SourceCollisionId, pcmmixcol::SourceEventUid, + pcmmixcol::ZBin, pcmmixcol::OccBin, pcmmixcol::Occupancy, pcmmixcol::FirstLeg, pcmmixcol::NLegs, + pcmmixcol::RunNumber, pcmmixcol::MultNTracksPVeta1, pcmmixcol::MultFT0M, pcmmixcol::Timestamp); +using PCMMixCollision = PCMMixCollisions::iterator; + +namespace pcmmixleg +{ +DECLARE_SOA_COLUMN(CollisionId, collisionId, int); //! PCMMixCollisions row +DECLARE_SOA_COLUMN(Pt, pt, float); //! pT of the AO2D track (track-like interface for the builder) +DECLARE_SOA_COLUMN(Phi, phi, float); //! phi of the AO2D track +DECLARE_SOA_COLUMN(CYY, cYY, float); //! covariance, o2::track::TrackParCov order (from V0LegsCov) +DECLARE_SOA_COLUMN(CZY, cZY, float); //! +DECLARE_SOA_COLUMN(CZZ, cZZ, float); //! +DECLARE_SOA_COLUMN(CSnpY, cSnpY, float); //! +DECLARE_SOA_COLUMN(CSnpZ, cSnpZ, float); //! +DECLARE_SOA_COLUMN(CSnpSnp, cSnpSnp, float); //! +DECLARE_SOA_COLUMN(CTglY, cTglY, float); //! +DECLARE_SOA_COLUMN(CTglZ, cTglZ, float); //! +DECLARE_SOA_COLUMN(CTglSnp, cTglSnp, float); //! +DECLARE_SOA_COLUMN(CTglTgl, cTglTgl, float); //! +DECLARE_SOA_COLUMN(C1PtY, c1PtY, float); //! +DECLARE_SOA_COLUMN(C1PtZ, c1PtZ, float); //! +DECLARE_SOA_COLUMN(C1PtSnp, c1PtSnp, float); //! +DECLARE_SOA_COLUMN(C1PtTgl, c1PtTgl, float); //! +DECLARE_SOA_COLUMN(C1Pt21Pt2, c1Pt21Pt2, float); //! +DECLARE_SOA_COLUMN(SourceEventUid, sourceEventUid, int64_t); //! mixer event counter of the event the leg comes from +DECLARE_SOA_COLUMN(SourcePhotonUid, sourcePhotonUid, int64_t); //! mixer photon counter of the run-1 photon the leg belonged to +DECLARE_SOA_COLUMN(SourceV0Id, sourceV0Id, int); //! v0Id of that photon in run 1 (< 0: not an SVertexer V0) +DECLARE_SOA_COLUMN(SourcePx, sourcePx, float); //! run-1 photon momentum at the PV +DECLARE_SOA_COLUMN(SourcePy, sourcePy, float); //! +DECLARE_SOA_COLUMN(SourcePz, sourcePz, float); //! +DECLARE_SOA_COLUMN(SourceVx, sourceVx, float); //! run-1 conversion point +DECLARE_SOA_COLUMN(SourceVy, sourceVy, float); //! +DECLARE_SOA_COLUMN(SourceVz, sourceVz, float); //! +DECLARE_SOA_COLUMN(SourcePvX, sourcePvX, float); //! primary vertex of the source event (run 2: refit of the original at its own vertex) +DECLARE_SOA_COLUMN(SourcePvY, sourcePvY, float); //! +DECLARE_SOA_COLUMN(SourcePvZ, sourcePvZ, float); //! +DECLARE_SOA_COLUMN(SourcePvCovXX, sourcePvCovXX, float); //! +DECLARE_SOA_COLUMN(SourcePvCovXY, sourcePvCovXY, float); //! +DECLARE_SOA_COLUMN(SourcePvCovXZ, sourcePvCovXZ, float); //! +DECLARE_SOA_COLUMN(SourcePvCovYY, sourcePvCovYY, float); //! +DECLARE_SOA_COLUMN(SourcePvCovYZ, sourcePvCovYZ, float); //! +DECLARE_SOA_COLUMN(SourcePvCovZZ, sourcePvCovZZ, float); //! +DECLARE_SOA_COLUMN(SourcePvChi2, sourcePvChi2, float); //! +DECLARE_SOA_COLUMN(SourcePvNContrib, sourcePvNContrib, uint16_t); //! +DECLARE_SOA_COLUMN(OrigTrackId, origTrackId, int); //! AO2D track (DF of the source event) +DECLARE_SOA_COLUMN(McParticleId, mcParticleId, int); //! MC: particle of the leg (DF of the source event), -1 without +DECLARE_SOA_COLUMN(McMotherId, mcMotherId, int); //! MC: first mother (DF of the source event), -1 without +DECLARE_SOA_COLUMN(McPdg, mcPdg, int); //! MC: PDG of the leg +DECLARE_SOA_COLUMN(McMotherPdg, mcMotherPdg, int); //! MC: PDG of the mother +DECLARE_SOA_COLUMN(McMotherPx, mcMotherPx, float); //! MC: true momentum of the mother +DECLARE_SOA_COLUMN(McMotherPy, mcMotherPy, float); //! +DECLARE_SOA_COLUMN(McMotherPz, mcMotherPz, float); //! +DECLARE_SOA_COLUMN(McGrandMotherId, mcGrandMotherId, int); //! MC: first mother of the mother (DF of the source event), -1 without +DECLARE_SOA_COLUMN(McGrandMotherPdg, mcGrandMotherPdg, int); //! MC: PDG of the grandmother +DECLARE_SOA_COLUMN(McAncestorId, mcAncestorId, int); //! MC: first physical primary along the mothers of the leg's mother (itself if primary), -1 without +DECLARE_SOA_COLUMN(McGGMotherId, mcGGMotherId, int); //! MC: mother of the grandmother (e.g. the eta of eta -> 3 pi0), -1 without +DECLARE_SOA_COLUMN(McGGMotherPdg, mcGGMotherPdg, int); //! MC: PDG of that particle +DECLARE_SOA_COLUMN(McRootId, mcRootId, int); //! MC: last particle along the first mothers of the leg's mother (no mother), -1 without +DECLARE_SOA_COLUMN(McRootPdg, mcRootPdg, int); //! MC: PDG of the root +DECLARE_SOA_COLUMN(McRootDepth, mcRootDepth, int); //! MC: generations from the leg's mother to the root +DECLARE_SOA_COLUMN(McVx, mcVx, float); //! MC: production vertex of the leg = true conversion point (lab frame of the source event; translated with the photon), 0 without +DECLARE_SOA_COLUMN(McVy, mcVy, float); //! +DECLARE_SOA_COLUMN(McVz, mcVz, float); //! +} // namespace pcmmixleg +// one row per leg of an artificial event; the columns the builder reads from a track carry the same names as in the AO2D +DECLARE_SOA_TABLE(PCMMixLegs, "AOD", "PCMMIXLEG", //! + o2::soa::Index<>, pcmmixleg::CollisionId, v0leg::Sign, + track::X, track::Alpha, track::Y, track::Z, track::Snp, track::Tgl, track::Signed1Pt, pcmmixleg::Pt, pcmmixleg::Phi, + pcmmixleg::CYY, pcmmixleg::CZY, pcmmixleg::CZZ, pcmmixleg::CSnpY, pcmmixleg::CSnpZ, pcmmixleg::CSnpSnp, + pcmmixleg::CTglY, pcmmixleg::CTglZ, pcmmixleg::CTglSnp, pcmmixleg::CTglTgl, + pcmmixleg::C1PtY, pcmmixleg::C1PtZ, pcmmixleg::C1PtSnp, pcmmixleg::C1PtTgl, pcmmixleg::C1Pt21Pt2, + track::TPCNClsFindable, track::TPCNClsFindableMinusFound, track::TPCNClsFindableMinusCrossedRows, track::TPCNClsShared, + track::TPCChi2NCl, track::TPCInnerParam, track::TPCSignal, pidtpc::TPCNSigmaEl, pidtpc::TPCNSigmaPi, + track::ITSClusterSizes, track::ITSChi2NCl, track::DetectorMap, + pcmmixleg::SourceEventUid, pcmmixleg::SourcePhotonUid, pcmmixleg::SourceV0Id, + pcmmixleg::SourcePx, pcmmixleg::SourcePy, pcmmixleg::SourcePz, pcmmixleg::SourceVx, pcmmixleg::SourceVy, pcmmixleg::SourceVz, + pcmmixleg::SourcePvX, pcmmixleg::SourcePvY, pcmmixleg::SourcePvZ, pcmmixleg::SourcePvCovXX, pcmmixleg::SourcePvCovXY, pcmmixleg::SourcePvCovXZ, + pcmmixleg::SourcePvCovYY, pcmmixleg::SourcePvCovYZ, pcmmixleg::SourcePvCovZZ, pcmmixleg::SourcePvChi2, pcmmixleg::SourcePvNContrib, + pcmmixleg::OrigTrackId, pcmmixleg::McParticleId, pcmmixleg::McMotherId, pcmmixleg::McPdg, pcmmixleg::McMotherPdg, + pcmmixleg::McMotherPx, pcmmixleg::McMotherPy, pcmmixleg::McMotherPz, pcmmixleg::McGrandMotherId, pcmmixleg::McGrandMotherPdg, pcmmixleg::McAncestorId, + pcmmixleg::McGGMotherId, pcmmixleg::McGGMotherPdg, pcmmixleg::McRootId, pcmmixleg::McRootPdg, pcmmixleg::McRootDepth, pcmmixleg::McVx, pcmmixleg::McVy, pcmmixleg::McVz, + + // dynamic column + track::TPCNClsFound, + track::TPCNClsCrossedRows, + track::TPCCrossedRowsOverFindableCls, + track::TPCFoundOverFindableCls, + track::TPCFractionSharedCls, + track::v001::ITSClusterMap, track::v001::ITSNCls, track::v001::ITSNClsInnerBarrel, + track::HasITS, track::HasTPC, track::HasTRD, track::HasTOF); +using PCMMixLeg = PCMMixLegs::iterator; + +// free electron legs of builder run 1 (fillFreeLegs): tracks that pass the leg cuts and are the leg of no stored photon; the +// parameters as the V0 fit would use them (TPC-only legs moved to their own collision). The mixer pools them with the photons +// (cfgPoolFreeLegs), so that run 2 can pair them in the same and the mixed event alike. +namespace pcmfreeleg +{ +DECLARE_SOA_COLUMN(CollisionId, collisionId, int); //! collision of the track +DECLARE_SOA_COLUMN(TrackId, trackId, int); //! AO2D track +} // namespace pcmfreeleg +DECLARE_SOA_TABLE(PCMFreeLegs, "AOD", "PCMFREELEG", //! + o2::soa::Index<>, pcmfreeleg::CollisionId, pcmfreeleg::TrackId, v0leg::Sign, + v0legcov::X, v0legcov::Alpha, v0legcov::Y, v0legcov::Z, v0legcov::Snp, v0legcov::Tgl, v0legcov::Signed1Pt, v0legcov::Cov, + track::TPCNClsFindable, track::TPCNClsFindableMinusFound, track::TPCNClsFindableMinusCrossedRows, track::TPCNClsShared, + track::TPCChi2NCl, track::TPCInnerParam, track::TPCSignal, pidtpc::TPCNSigmaEl, pidtpc::TPCNSigmaPi, + track::ITSClusterSizes, track::ITSChi2NCl, track::DetectorMap, + + // dynamic column + track::HasITS, track::HasTPC); +using PCMFreeLeg = PCMFreeLegs::iterator; + +// free legs as AO2D track indices only (linked derived data): run 1 writes (collision, track) of every free leg of a selected +// collision; associate-photon-to-emevent adds the derived event row. The mixer on linked derived data reads the tracks from the parent AO2D. +namespace pcmfreelegid +{ +DECLARE_SOA_INDEX_COLUMN(PMEvent, pmevent); //! derived event of the free leg (-1: collision not in PMEvents) +DECLARE_SOA_COLUMN(TrackIdDelta, trackIdDelta, int); //! AO2D track index minus the one of the previous row of this DF (first row: the index): small numbers, ~5x smaller on disk +} // namespace pcmfreelegid +DECLARE_SOA_TABLE(PCMFreeLegIds, "AOD", "PCMFREELEGID", //! rows in increasing track order; track index = running sum of TrackIdDelta over the DF + o2::soa::Index<>, pcmfreeleg::CollisionId, pcmfreelegid::TrackIdDelta); +using PCMFreeLegId = PCMFreeLegIds::iterator; +DECLARE_SOA_TABLE(PCMFreeLegEMEventIds, "AOD", "PCMFREELEGEVID", pcmfreelegid::PMEventId); //! to be joined with PCMFreeLegIds + +// builder run 2 output (same columns as the run-1 tables; collisionId = PCMMixCollisions row, trackId = PCMMixLegs row, +// v0Id >= 0: original pairing of run 1, -1: crossed pairing) +DECLARE_SOA_TABLE(V0LegsMix, "AOD", "V0LEGMIX", //! + o2::soa::Index<>, v0leg::CollisionId, v0leg::TrackId, v0leg::Sign, + v0leg::Px, v0leg::Py, v0leg::Pz, + track::DcaXY, track::DcaZ, + track::TPCNClsFindable, track::TPCNClsFindableMinusFound, track::TPCNClsFindableMinusCrossedRows, track::TPCNClsShared, + track::TPCChi2NCl, track::TPCInnerParam, + track::TPCSignal, pidtpc::TPCNSigmaEl, pidtpc::TPCNSigmaPi, + track::ITSClusterSizes, track::ITSChi2NCl, track::DetectorMap, + + // dynamic column + v0leg::P, + v0leg::Pt, + v0leg::Eta, + v0leg::Phi, + v0leg::Tgl, + + track::TPCNClsFound, + track::TPCNClsCrossedRows, + track::TPCCrossedRowsOverFindableCls, + track::TPCFoundOverFindableCls, + track::TPCFractionSharedCls, + track::v001::ITSClusterMap, track::v001::ITSNCls, track::v001::ITSNClsInnerBarrel, + track::HasITS, track::HasTPC, track::HasTRD, track::HasTOF, + v0leg::MeanClusterSizeITS, + v0leg::MeanClusterSizeITSib, + v0leg::MeanClusterSizeITSob); +using V0LegMix = V0LegsMix::iterator; + +namespace v0photonkfmix +{ +DECLARE_SOA_INDEX_COLUMN_FULL(PosTrack, posTrack, int, V0LegsMix, "_Pos"); //! +DECLARE_SOA_INDEX_COLUMN_FULL(NegTrack, negTrack, int, V0LegsMix, "_Neg"); //! +DECLARE_SOA_DYNAMIC_COLUMN(PMEventId, pmeventId, [](int collisionId) -> int { return collisionId; }); //! event index as in V0KFEMEventIds (= PCMMixCollisions row), for V0PhotonCut +} // namespace v0photonkfmix +DECLARE_SOA_TABLE(V0PhotonsKFMix, "AOD", "V0PHOTONKFMIX", //! + o2::soa::Index<>, v0photonkf::CollisionId, v0photonkf::V0Id, v0photonkfmix::PosTrackId, v0photonkfmix::NegTrackId, + v0photonkf::Vx, v0photonkf::Vy, v0photonkf::Vz, + v0photonkf::Px, v0photonkf::Py, v0photonkf::Pz, + v0photonkf::MGamma, + v0photonkf::DCAxyToPV, v0photonkf::DCAzToPV, + v0photonkf::CosPA, v0photonkf::CosPAXY, v0photonkf::CosPARZ, v0photonkf::PCA, + v0photonkf::Alpha, v0photonkf::QtArm, + v0photonkf::ChiSquareNDF, + + // dynamic column + v0photonkf::E, + v0photonkf::Pt, + v0photonkf::Eta, + v0photonkf::Phi, + v0photonkf::P, + v0photonkf::V0Radius, + v0photonkfmix::PMEventId); +using V0PhotonKFMix = V0PhotonsKFMix::iterator; + +DECLARE_SOA_TABLE(V0LegsXYZMix, "AOD", "V0LEGXYZMIX", track::X, track::Y, track::Z); //! +DECLARE_SOA_TABLE(V0LegsDeDxMCMix, "AOD", "V0LEGDEDXMCMIX", mcpidtpc::DeDxTunedMc, o2::soa::Marker<3>); //! stays empty (run 2 runs without MC) +DECLARE_SOA_TABLE(V0PhotonsPhiVPsiMix, "AOD", "V0PHOTONPHIVMIX", o2::soa::Index<>, v0photonsphivpsi::PhiV, v0photonsphivpsi::PsiPair); //! +DECLARE_SOA_TABLE(V0LegsCovMix, "AOD", "V0LEGCOVMIX", //! + v0legcov::X, v0legcov::Alpha, v0legcov::Y, v0legcov::Z, v0legcov::Snp, v0legcov::Tgl, v0legcov::Signed1Pt, v0legcov::Cov); + namespace dalitzee { DECLARE_SOA_INDEX_COLUMN(EMEvent, emevent); //! diff --git a/PWGEM/PhotonMeson/TableProducer/CMakeLists.txt b/PWGEM/PhotonMeson/TableProducer/CMakeLists.txt index b7eaafafcaf..9f787c4bb06 100644 --- a/PWGEM/PhotonMeson/TableProducer/CMakeLists.txt +++ b/PWGEM/PhotonMeson/TableProducer/CMakeLists.txt @@ -21,6 +21,16 @@ o2physics_add_dpl_workflow(photon-conversion-builder-tmptable PUBLIC_LINK_LIBRARIES O2::Framework O2::DCAFitter O2Physics::AnalysisCore O2Physics::MLCore KFParticle::KFParticle O2Physics::TPCDriftManager COMPONENT_NAME Analysis) +o2physics_add_dpl_workflow(photon-conversion-builder-mixed + SOURCES photonconversionbuilderMixed.cxx + PUBLIC_LINK_LIBRARIES O2::Framework O2::DCAFitter O2Physics::AnalysisCore O2Physics::MLCore KFParticle::KFParticle O2Physics::TPCDriftManager + COMPONENT_NAME Analysis) + +o2physics_add_dpl_workflow(photon-mixed-event-producer + SOURCES photonMixedEventProducer.cxx + PUBLIC_LINK_LIBRARIES O2::Framework O2Physics::AnalysisCore + COMPONENT_NAME Analysis) + o2physics_add_dpl_workflow(create-emevent-photon SOURCES createEMEventPhoton.cxx PUBLIC_LINK_LIBRARIES O2::Framework O2Physics::AnalysisCore O2Physics::PWGJECore diff --git a/PWGEM/PhotonMeson/TableProducer/createEMEventPhoton.cxx b/PWGEM/PhotonMeson/TableProducer/createEMEventPhoton.cxx index 03aba7421de..85bf97981e0 100644 --- a/PWGEM/PhotonMeson/TableProducer/createEMEventPhoton.cxx +++ b/PWGEM/PhotonMeson/TableProducer/createEMEventPhoton.cxx @@ -41,6 +41,7 @@ #include #include #include +#include using namespace o2; using namespace o2::framework; @@ -67,6 +68,8 @@ struct CreateEMEventPhoton { Produces emswtbit; Produces event_norm_info; Produces eventWeights; + Produces eventXY; // filled only with fillVertex + Produces eventCov; // filled only with fillVertex enum class EMEventType : int { kEvent = 0, @@ -75,6 +78,7 @@ struct CreateEMEventPhoton { kEvent_JJ = 3, }; + Configurable fillVertex{"fillVertex", false, "fill EMEventsXY and EMEventsCov (primary vertex x, y, covariance, chi2; the tables are written in any case, empty if false), joinable to PMEvents: needed by the co-reconstructed mixed event on derived data (photon-mixed-event-producer processDerived)"}; Configurable needEMCTrigger{"needEMCTrigger", false, "flag to only save events which have kTVXinEMC trigger bit. To reduce PbPb derived data size"}; Configurable needPHSTrigger{"needPHSTrigger", false, "flag to only save events which have kTVXinPHOS trigger bit. To reduce PbPb derived data size"}; Configurable enableJJHistograms{"enableJJHistograms", false, "flag to fill JJ QA histograms for outlier rejection"}; @@ -149,7 +153,10 @@ struct CreateEMEventPhoton { collision.numContrib(), collision.trackOccupancyInTimeRange(), collision.ft0cOccupancyInTimeRange()); eventalias(collision.alias_raw()); - // eventCov(collision.covXX(), collision.covXY(), collision.covXZ(), collision.covYY(), collision.covYZ(), collision.covZZ(), collision.chi2()); + if (fillVertex) { + eventXY(collision.posX(), collision.posY()); + eventCov(collision.covXX(), collision.covXY(), collision.covXZ(), collision.covYY(), collision.covYZ(), collision.covZZ(), collision.chi2()); + } eventMult(collision.multFT0A(), collision.multFT0C(), collision.multNTracksPV(), collision.multNTracksPVeta1(), collision.multNTracksPVetaHalf()); @@ -279,6 +286,7 @@ struct CreateEMEventPhoton { }; struct AssociatePhotonToEMEvent { Produces v0kfeventid; + Produces freelegeventid; Produces prmeleventid; Produces phoseventid; Produces emceventid; @@ -312,6 +320,19 @@ struct AssociatePhotonToEMEvent { fillEventId(collisions, photons, v0kfeventid, perCollisionPCM); } + // free legs of run 1 (PCMFreeLegIds, linked derived data): one row per free leg, in its order; -1 when the collision has no PMEvent + void processPCMFreeLegs(aod::PMEvents const& collisions, aod::PCMFreeLegIds const& freeLegs) + { + std::unordered_map eventOf; + for (const auto& collision : collisions) { + eventOf[collision.collisionId()] = collision.globalIndex(); + } + for (const auto& leg : freeLegs) { + const auto it = eventOf.find(leg.collisionId()); + freelegeventid(it == eventOf.end() ? -1 : it->second); + } + } + void processElectronFromDalitz(aod::PMEvents const& collisions, aod::EMPrimaryElectronsFromDalitz const& tracks) { fillEventId(collisions, tracks, prmeleventid, perCollisionEl); @@ -335,6 +356,7 @@ struct AssociatePhotonToEMEvent { void processDummy(aod::PMEvents const&) {} PROCESS_SWITCH(AssociatePhotonToEMEvent, processPCM, "process pcm-event indexing", false); + PROCESS_SWITCH(AssociatePhotonToEMEvent, processPCMFreeLegs, "free legs of run 1 (PCMFreeLegIds) -> derived event index, for the mixer on linked derived data", false); PROCESS_SWITCH(AssociatePhotonToEMEvent, processElectronFromDalitz, "process dalitzee-event indexing", false); PROCESS_SWITCH(AssociatePhotonToEMEvent, processPHOS, "process phos-event indexing", false); PROCESS_SWITCH(AssociatePhotonToEMEvent, processEMC, "process emc-event indexing", false); diff --git a/PWGEM/PhotonMeson/TableProducer/photonMixedEventProducer.cxx b/PWGEM/PhotonMeson/TableProducer/photonMixedEventProducer.cxx new file mode 100644 index 00000000000..5e873e46102 --- /dev/null +++ b/PWGEM/PhotonMeson/TableProducer/photonMixedEventProducer.cxx @@ -0,0 +1,787 @@ +// Copyright 2019-2025 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. + +/// \file photonMixedEventProducer.cxx +/// \brief This task implements a new mixed event method and is the necessary step between builder run 1 and run 2. +// At first it pools of the run-1 events (photons and free legs,copies with their legs) in z x occupancy bins; +// per event one same-event artificial event (the complete event) and cfgOverlay mixed artificial events +// (the complete event + one complete partner event of its pool, translated to the +/// current vertex), written as PCMMixCollisions / PCMMixLegs for photon-conversion-builder-mixed. Needs builder run 1 with +/// fillLegCov = true and fillFreeLegs = true. +/// \author Stefanie Mrozinski + +#include "PWGEM/PhotonMeson/DataModel/EventTables.h" +#include "PWGEM/PhotonMeson/DataModel/gammaTables.h" + +#include "Common/Core/RecoDecay.h" +#include "Common/DataModel/Centrality.h" +#include "Common/DataModel/EventSelection.h" +#include "Common/DataModel/Multiplicity.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace o2; +using namespace o2::framework; + +namespace +{ +constexpr int kMaxAncestorDepth = 100; // mother-chain steps searched for the physical primary + +// copy of one leg +struct LegCopy { + int8_t sign = 0; + float x = 0.f, alpha = 0.f, y = 0.f, z = 0.f, snp = 0.f, tgl = 0.f, signed1Pt = 0.f, pt = 0.f, phi = 0.f; + std::array cov{}; + uint8_t tpcNClsFindable = 0; + int8_t tpcNClsFindableMinusFound = 0, tpcNClsFindableMinusCrossedRows = 0; + uint8_t tpcNClsShared = 0; + float tpcChi2NCl = 0.f, tpcInnerParam = 0.f, tpcSignal = 0.f, tpcNSigmaEl = 0.f, tpcNSigmaPi = 0.f; + uint32_t itsClusterSizes = 0; + float itsChi2NCl = 0.f; + uint8_t detectorMap = 0; + int origTrackId = -1, mcParticleId = -1, mcMotherId = -1, mcPdg = 0, mcMotherPdg = 0, mcGrandMotherId = -1, mcGrandMotherPdg = 0, mcAncestorId = -1; + int mcGGMotherId = -1, mcGGMotherPdg = 0, mcRootId = -1, mcRootPdg = 0, mcRootDepth = -1; + float mcMotherPx = 0.f, mcMotherPy = 0.f, mcMotherPz = 0.f; + float mcVx = 0.f, mcVy = 0.f, mcVz = 0.f; // MC production vertex of the leg = true conversion point +}; + +struct PhotonCopy { + int64_t eventUid = -1, photonUid = -1; + int v0Id = -1; + float px = 0.f, py = 0.f, pz = 0.f, vx = 0.f, vy = 0.f, vz = 0.f; + float pvX = 0.f, pvY = 0.f, pvZ = 0.f, pvCovXX = 0.f, pvCovXY = 0.f, pvCovXZ = 0.f, pvCovYY = 0.f, pvCovYZ = 0.f, pvCovZZ = 0.f, pvChi2 = 0.f; // primary vertex of its event + uint16_t pvNContrib = 0; + std::vector legs; + int timesUsed = 0; // in mixed artificial events +}; + +struct EventCopy { + int64_t eventUid = -1; + float x = 0.f, y = 0.f, z = 0.f, occupancy = 0.f, cent = 0.f; + std::vector photons; + std::vector freeLegs; +}; + +// vertex and centralities of the event whose vertex an artificial event gets +struct VertexCopy { + float x = 0.f, y = 0.f, z = 0.f, covXX = 0.f, covXY = 0.f, covXZ = 0.f, covYY = 0.f, covYZ = 0.f, covZZ = 0.f, chi2 = 0.f; + uint16_t numContrib = 0; + float centFT0M = 0.f, centFT0A = 0.f, centFT0C = 0.f; + int runNumber = 0, multNTracksPVeta1 = 0; // run and multiplicities of the event whose vertex it is (photonhbt: CCDB, 2D CF axes) + float multFT0M = 0.f; + uint64_t timestamp = 0; // CCDB timestamp +}; +} // namespace + +struct PhotonMixedEventProducer { + using MyCollisions = soa::Join; + using MyTracksMC = soa::Join; + using MyEvents = soa::Join; + using MyPhotonsDerived = soa::Join; + using MyLegsMCDerived = soa::Join; + + Produces mixCollisions; + Produces mixLegs; + + Configurable cfgCentEstimator{"cfgCentEstimator", 2, "centrality estimator: 0 FT0M, 1 FT0A, 2 FT0C"}; + Configurable cfgCentMin{"cfgCentMin", 10.f, "min centrality (one class)"}; + Configurable cfgCentMax{"cfgCentMax", 20.f, "max centrality"}; + Configurable cfgZMax{"cfgZMax", 10.f, "max |z_vtx| (cm)"}; + Configurable cfgZBinWidth{"cfgZBinWidth", 2.f, "pool z bin width (cm)"}; + Configurable cfgOccupancyEstimator{"cfgOccupancyEstimator", 0, "occupancy: 0 trackOccupancyInTimeRange, 1 ft0cOccupancyInTimeRange"}; + Configurable> cfgOccupancyBins{"cfgOccupancyBins", {-1.f, 1.e9f}, "pool occupancy bin edges (events outside are not used)"}; + Configurable cfgPoolDepth{"cfgPoolDepth", 50, "events per pool (has to exceed the photon multiplicity N)"}; + Configurable cfgNMixPerEvent{"cfgNMixPerEvent", 1, "mixed artificial events per event"}; + Configurable cfgRequireFullPool{"cfgRequireFullPool", true, "skip an event completely (same and mixed) if its pool has fewer than N events; false: write the same-event one and no mixed one"}; + Configurable cfgSeed{"cfgSeed", 12345, "random seed (choice of partner events and photons)"}; + Configurable cfgAlignToVertex{"cfgAlignToVertex", true, "translate the legs and the conversion point of each partner photon by (current vertex - partner vertex): every photon of the mixed artificial event originates from its vertex, as in the same event"}; + Configurable cfgPoolGroups{"cfgPoolGroups", false, "pool conflict groups instead of photons (run 1 with deduplicationMode 3 = keep all): candidates sharing a track form one group with all its tracks once; the decision is taken only in run 2, after mixing, identically for the same and the mixed event"}; + Configurable cfgPhotonWeighted{"cfgPhotonWeighted", true, "partner events drawn with probability proportional to their photon number (each pooled photon equally likely, still N distinct events); false: events equally likely"}; + Configurable cfgInject{"cfgInject", 0, "injection mixed event: instead of N photons from N partner events, K mixed artificial events per event, each = the COMPLETE current event (its photons and, with cfgPoolFreeLegs, its free legs) + ONE photon of a distinct partner event (translated with cfgAlignToVertex). K = cfgInject, -1: K = N (mixed pairs = 2 x same-event pairs), -2: K = N/2 rounded up (mixed pairs = same-event pairs, half the CPU); the pool needs K partner events. Run 2 with mixed_group.skipSameSourceCrossed: the current event's pairings stay as run 1 decided them, only the injected legs form new candidates (also with the free legs, which the N-photon mixed event cannot offer)"}; + Configurable cfgOverlay{"cfgOverlay", 1, "two-event leg overlay: K mixed artificial events per event, each = the COMPLETE current event + the COMPLETE partner event k (all its photons and, with cfgPoolFreeLegs, all its free legs, translated with cfgAlignToVertex). Every leg configuration of the same event exists across the two events, incl. true pairings that failed a run-1 cut. Run 2 with mixed_group.skipSameSourceCrossed: pairings inside one source event stay as run 1 decided them, only cross-event pairings are new. 0 = off (not with cfgInject)"}; + Configurable cfgPoolFreeLegs{"cfgPoolFreeLegs", true, "pool the free legs of run 1 (PCMFreeLegs, builder fillFreeLegs) with the photons: the same artificial event gets all M free legs of the event, the mixed one M free legs drawn from the N partner events, so that run 2 pairs free legs identically in both"}; + + HistogramRegistry registry{"registry", {}, OutputObjHandlingPolicy::AnalysisObject, false, false}; + + std::map, std::deque> mPools; + std::mt19937 mRandom; + int mRunNumber = -1; + int64_t mEventUid = 0; + int64_t mPhotonUid = 0; + int64_t mNPooledPhotons = 0; + + enum CutFlow { kAll = 0, + kSelected, + kCent, + kZ, + kOccupancy, + kHasPhotons, + kPoolTooSmall, + kWritten }; + + void init(InitContext&) + { + mRandom.seed(cfgSeed.value); + if (cfgOccupancyBins->size() < 2) { + LOG(fatal) << "cfgOccupancyBins needs at least two edges"; + } + const AxisSpec axType{2, -0.5f, 1.5f, "artificial event (0 same, 1 mixed)"}; + const AxisSpec axN{101, -0.5f, 100.5f, "photons per event N"}; + const int nZ = static_cast(std::lround(2.f * cfgZMax / cfgZBinWidth)); + registry.add("Event/hCutFlow", "events;0 all, 1 selected, 2 centrality, 3 z, 4 occupancy, 5 with photons, 6 pool too small (skipped), 7 written", kTH1D, {{8, -0.5f, 7.5f}}); + registry.add("Event/hNPhotons", "accepted events;photons per event N", kTH1D, {axN}); + registry.add("Event/hNFreeLegs", "accepted events (cfgPoolFreeLegs);free legs per event M", kTH1D, {{300, -0.5f, 299.5f}}); + registry.add("Mix/hFreeLegsMixedVsCurrent", "cfgPoolFreeLegs;M of the current event;free legs in the mixed artificial event", kTH2D, {{300, -0.5f, 299.5f}, {300, -0.5f, 299.5f}}); + registry.add("Event/hNPhotonsMixedVsCurrent", "mixed artificial events (must be the diagonal);N of the current event;photons in the mixed event", kTH2D, {axN, axN}); + registry.add("Event/hZ", "accepted events;z_{vtx} (cm)", kTH1D, {{nZ, -cfgZMax, cfgZMax}}); + registry.add("Event/hOccupancy", "accepted events;occupancy", kTH1D, {{200, 0.f, 20000.f}}); + registry.add("Pool/hDepthVsZ", "pool size when used;z_{vtx} (cm);events in the pool", kTH2D, {{nZ, -cfgZMax, cfgZMax}, {cfgPoolDepth + 1, -0.5f, cfgPoolDepth + 0.5f}}); + registry.add("Pool/hDepthVsOccBin", "pool size when used;occupancy bin;events in the pool", kTH2D, {{static_cast(cfgOccupancyBins->size()) - 1, -0.5f, cfgOccupancyBins->size() - 1.5f}, {cfgPoolDepth + 1, -0.5f, cfgPoolDepth + 0.5f}}); + registry.add("Pool/hPartnersVsN", "partner events available vs needed;N of the current event;events in the pool", kTH2D, {axN, {cfgPoolDepth + 1, -0.5f, cfgPoolDepth + 0.5f}}); + registry.add("Pool/hNPooledPhotons", "photons in all pools (memory), per event;photons", kTH1D, {{500, 0.f, 5.e5f}}); + registry.add("Pool/hNPools", "filled pools, per event;pools", kTH1D, {{1000, 0.f, 10000.f}}); + registry.add("Pool/hResets", "pool resets (run change);resets", kTH1D, {{1, -0.5f, 0.5f}}); + registry.add("Pool/hTimesUsed", "pooled photons leaving the pool;times used in a mixed event", kTH1D, {{51, -0.5f, 50.5f}}); + registry.add("Mix/hDz", "mixed photons;z_{vtx}^{partner} - z_{vtx}^{current} (cm)", kTH1D, {{200, -0.5f, 0.5f}}); + registry.add("Mix/hDOccupancy", "mixed photons;occupancy^{partner} - occupancy^{current}", kTH1D, {{200, -5000.f, 5000.f}}); + registry.add("Mix/hDCent", "mixed photons;centrality^{partner} - centrality^{current} (%)", kTH1D, {{200, -10.f, 10.f}}); + registry.add("Mix/hPartnerAge", "mixed photons;position of the partner event in the pool (0 = newest)", kTH1D, {{cfgPoolDepth, -0.5f, cfgPoolDepth - 0.5f}}); + registry.add("Mix/hDistinctSourcesMinusN", "mixed artificial events (must be 0);distinct source events - N", kTH1D, {{21, -10.5f, 10.5f}}); + registry.add("Mix/hSourceIsCurrent", "mixed photons from the current event (must be 0);0 other event, 1 current event", kTH1D, {{2, -0.5f, 1.5f}}); + registry.add("Mix/hGroupLegs", "cfgPoolGroups: tracks per conflict group;tracks;groups", kTH1D, {{30, 0.5f, 30.5f}}); + registry.add("Event/hNCandidates", "cfgPoolGroups: run-1 candidates per event (before grouping);candidates;events", kTH1D, {{200, -0.5f, 199.5f}}); + registry.add("Mix/hLegsMinus2N", "artificial events (must be 0);artificial event;legs written - 2 N", kTH2D, {axType, {21, -10.5f, 10.5f}}); + registry.add("Mix/hPhotonPtEta", "photons of the artificial events;artificial event;p_{T} (GeV/c);#eta", kTH3D, {axType, {100, 0.f, 5.f}, {40, -1.f, 1.f}}); + registry.add("Mix/hLegTypes", "legs of the artificial events;artificial event;0 ITS-TPC, 1 TPC-only, 2 ITS-only, 3 other", kTH2D, {axType, {4, -0.5f, 3.5f}}); + // AO2D input: leg pT and phi of the track vs the values computed from V0LegsCov (what the derived-data mode uses), must be 0 + registry.add("Leg/hPtPhiFromCov", "legs (AO2D input);(p_{T}^{track} - p_{T}^{cov}) / p_{T}^{track};#varphi^{track} - #varphi^{cov} (rad)", kTH2D, {{100, -1.e-4f, 1.e-4f}, {100, -1.e-4f, 1.e-4f}}); + if (static_cast(doprocessRec.value) + static_cast(doprocessMC.value) + static_cast(doprocessDerived.value) + static_cast(doprocessDerivedMC.value) != 1) { + LOG(fatal) << "enable exactly one of processRec, processMC (AO2D input) and processDerived, processDerivedMC (derived data)"; + } + if (cfgOverlay != 0 && cfgInject != 0) { + LOG(fatal) << "cfgOverlay and cfgInject are two different mixed events, enable one"; + } + if (cfgOverlay != 0 && !cfgPoolFreeLegs) { + LOG(warning) << "cfgOverlay without cfgPoolFreeLegs: the partner event brings its stored photons only, not its free legs"; + } + if (cfgPoolFreeLegs && (doprocessDerived || doprocessDerivedMC)) { + LOG(fatal) << "cfgPoolFreeLegs needs PCMFreeLegs, which are not in the derived data"; + } + } + + template + float centrality(TCollision const& collision) const + { + switch (cfgCentEstimator.value) { + case 0: + return collision.centFT0M(); + case 1: + return collision.centFT0A(); + default: + return collision.centFT0C(); + } + } + + template + float occupancy(TCollision const& collision) const + { + return cfgOccupancyEstimator.value == 1 ? collision.ft0cOccupancyInTimeRange() : static_cast(collision.trackOccupancyInTimeRange()); + } + + int occupancyBin(const float occ) const + { + const auto& edges = cfgOccupancyBins.value; + for (size_t i = 0; i + 1 < edges.size(); ++i) { + if (edges[i] <= occ && occ < edges[i + 1]) { + return static_cast(i); + } + } + return -1; + } + + // leg pT and phi from the fit-input parameters + template + static std::pair ptPhiOf(TLegCov const& legCov) + { + const float pt = legCov.signed1Pt() != 0.f ? 1.f / std::fabs(legCov.signed1Pt()) : 0.f; + float phi = std::asin(std::clamp(legCov.snp(), -1.f, 1.f)) + legCov.alpha(); + phi = std::fmod(phi, o2::constants::math::TwoPI); + if (phi < 0.f) { + phi += o2::constants::math::TwoPI; + } + return {pt, phi}; + } + + // MC history of a leg + template + static void fillLegMC(LegCopy& c, TMCParticle const& mcLeg, TMCParticles const& mcParticles) + { + c.mcParticleId = mcLeg.globalIndex(); + c.mcPdg = mcLeg.pdgCode(); + c.mcVx = mcLeg.vx(); + c.mcVy = mcLeg.vy(); + c.mcVz = mcLeg.vz(); + if (!mcLeg.has_mothers()) { + return; + } + const auto mother = mcParticles.rawIteratorAt(mcLeg.mothersIds()[0]); + c.mcMotherId = mother.globalIndex(); + c.mcMotherPdg = mother.pdgCode(); + c.mcMotherPx = mother.px(); + c.mcMotherPy = mother.py(); + c.mcMotherPz = mother.pz(); + if (mother.has_mothers()) { // e.g. the e+- that radiated a bremsstrahlung photon + const auto grandMother = mcParticles.rawIteratorAt(mother.mothersIds()[0]); + c.mcGrandMotherId = grandMother.globalIndex(); + c.mcGrandMotherPdg = grandMother.pdgCode(); + if (grandMother.has_mothers()) { // e.g. the eta of eta -> 3 pi0 + const auto ggMother = mcParticles.rawIteratorAt(grandMother.mothersIds()[0]); + c.mcGGMotherId = ggMother.globalIndex(); + c.mcGGMotherPdg = ggMother.pdgCode(); + } + } + auto root = mother; + int rootDepth = 0; + for (; root.has_mothers() && rootDepth < kMaxAncestorDepth; ++rootDepth) { + root = mcParticles.rawIteratorAt(root.mothersIds()[0]); + } + c.mcRootId = static_cast(root.globalIndex()); + c.mcRootPdg = root.pdgCode(); + c.mcRootDepth = rootDepth; + auto anc = mother; + for (int depth = 0; !anc.isPhysicalPrimary() && anc.has_mothers() && depth < kMaxAncestorDepth; ++depth) { + anc = mcParticles.rawIteratorAt(anc.mothersIds()[0]); + } + c.mcAncestorId = anc.isPhysicalPrimary() ? static_cast(anc.globalIndex()) : -1; + } + + template + LegCopy copyLeg(TLeg const& leg, TLegCov const& legCov, [[maybe_unused]] TTracks const* tracks, [[maybe_unused]] TMCParticles const* mcParticles) + { + LegCopy c; + c.sign = leg.sign(); + c.x = legCov.x(); + c.alpha = legCov.alpha(); + c.y = legCov.y(); + c.z = legCov.z(); + c.snp = legCov.snp(); + c.tgl = legCov.tgl(); + c.signed1Pt = legCov.signed1Pt(); + const auto& cov = legCov.cov(); + for (size_t i = 0; i < c.cov.size(); ++i) { + c.cov[i] = cov[i]; + } + c.tpcNClsFindable = leg.tpcNClsFindable(); + c.tpcNClsFindableMinusFound = leg.tpcNClsFindableMinusFound(); + c.tpcNClsFindableMinusCrossedRows = leg.tpcNClsFindableMinusCrossedRows(); + c.tpcNClsShared = leg.tpcNClsShared(); + c.tpcChi2NCl = leg.tpcChi2NCl(); + c.tpcInnerParam = leg.tpcInnerParam(); + c.tpcSignal = leg.tpcSignal(); + c.tpcNSigmaEl = leg.tpcNSigmaEl(); + c.tpcNSigmaPi = leg.tpcNSigmaPi(); + c.itsClusterSizes = leg.itsClusterSizes(); + c.itsChi2NCl = leg.itsChi2NCl(); + c.detectorMap = leg.detectorMap(); + c.origTrackId = leg.trackId(); + const auto [ptCov, phiCov] = ptPhiOf(legCov); + if constexpr (isDerived) { + c.pt = ptCov; + c.phi = phiCov; + if constexpr (isMC) { + if (leg.has_emmcparticle()) { + fillLegMC(c, leg.template emmcparticle_as(), *mcParticles); + } + } + } else { + const auto track = tracks->rawIteratorAt(leg.trackId()); + c.pt = track.pt(); // as the builder reads them from the AO2D track + c.phi = track.phi(); + registry.fill(HIST("Leg/hPtPhiFromCov"), c.pt > 0.f ? (c.pt - ptCov) / c.pt : 0.f, RecoDecay::constrainAngle(c.phi - phiCov, -o2::constants::math::PI)); + if constexpr (isMC) { + if (track.has_mcParticle()) { + fillLegMC(c, track.template mcParticle_as(), *mcParticles); + } + } + } + return c; + } + + void writeLeg(LegCopy const& l, PhotonCopy const& p) + { + mixLegs(mixCollisions.lastIndex(), l.sign, + l.x, l.alpha, l.y, l.z, l.snp, l.tgl, l.signed1Pt, l.pt, l.phi, + l.cov[0], l.cov[1], l.cov[2], l.cov[3], l.cov[4], l.cov[5], l.cov[6], l.cov[7], l.cov[8], l.cov[9], l.cov[10], l.cov[11], l.cov[12], l.cov[13], l.cov[14], + l.tpcNClsFindable, l.tpcNClsFindableMinusFound, l.tpcNClsFindableMinusCrossedRows, l.tpcNClsShared, + l.tpcChi2NCl, l.tpcInnerParam, l.tpcSignal, l.tpcNSigmaEl, l.tpcNSigmaPi, + l.itsClusterSizes, l.itsChi2NCl, l.detectorMap, + p.eventUid, p.photonUid, p.v0Id, p.px, p.py, p.pz, p.vx, p.vy, p.vz, + p.pvX, p.pvY, p.pvZ, p.pvCovXX, p.pvCovXY, p.pvCovXZ, p.pvCovYY, p.pvCovYZ, p.pvCovZZ, p.pvChi2, p.pvNContrib, + l.origTrackId, l.mcParticleId, l.mcMotherId, l.mcPdg, l.mcMotherPdg, l.mcMotherPx, l.mcMotherPy, l.mcMotherPz, l.mcGrandMotherId, l.mcGrandMotherPdg, l.mcAncestorId, + l.mcGGMotherId, l.mcGGMotherPdg, l.mcRootId, l.mcRootPdg, l.mcRootDepth, l.mcVx, l.mcVy, l.mcVz); + } + + static PhotonCopy translated(PhotonCopy const& p, const float dx, const float dy, const float dz) + { + PhotonCopy t = p; + t.vx += dx; + t.vy += dy; + t.vz += dz; + t.pvX += dx; // its own vertex moves with it + t.pvY += dy; + t.pvZ += dz; + for (auto& l : t.legs) { + const float ca = std::cos(l.alpha), sa = std::sin(l.alpha); + l.x += dx * ca + dy * sa; + l.y += -dx * sa + dy * ca; + l.z += dz; + l.mcVx += dx; // the true conversion point moves with the photon + l.mcVy += dy; + l.mcVz += dz; + } + return t; + } + + void writeArtificialEvent(VertexCopy const& v, EventCopy const& current, const bool isMixed, const int zBin, const int occBin, + std::vector const& photons, const int currentCollisionId) + { + const float type = isMixed ? 1.f : 0.f; + const int firstLeg = mixLegs.lastIndex() + 1; + mixCollisions(v.x, v.y, v.z, v.covXX, v.covXY, v.covXZ, v.covYY, v.covYZ, v.covZZ, v.chi2, v.numContrib, + v.centFT0M, v.centFT0A, v.centFT0C, isMixed, currentCollisionId, current.eventUid, + zBin, occBin, current.occupancy, firstLeg, nLegsOf(photons), v.runNumber, v.multNTracksPVeta1, v.multFT0M, v.timestamp); + for (const auto* p : photons) { + if (p->legs.size() > 1) { // not a free-leg stub + registry.fill(HIST("Mix/hPhotonPtEta"), type, std::hypot(p->px, p->py), std::asinh(p->pz / std::hypot(p->px, p->py))); + } + for (const auto& l : p->legs) { + const bool hasITS = (l.detectorMap & o2::aod::track::ITS) != 0; + const bool hasTPC = (l.detectorMap & o2::aod::track::TPC) != 0; + registry.fill(HIST("Mix/hLegTypes"), type, hasITS && hasTPC ? 0.f : (hasTPC ? 1.f : (hasITS ? 2.f : 3.f))); + writeLeg(l, *p); + } + } + registry.fill(HIST("Mix/hLegsMinus2N"), type, (mixLegs.lastIndex() + 1 - firstLeg) - nLegsOf(photons)); + } + + static int nLegsOf(std::vector const& photons) + { + int n = 0; + for (const auto* p : photons) { + n += static_cast(p->legs.size()); + } + return n; + } + + std::vector toGroups(std::vector const& cands) + { + const int n = cands.size(); + std::vector parent(n); + std::iota(parent.begin(), parent.end(), 0); + std::function find = [&](int i) { return parent[i] == i ? i : parent[i] = find(parent[i]); }; + std::unordered_map firstOfTrack; + for (int i = 0; i < n; ++i) { + for (const auto& l : cands[i].legs) { + const auto [it, inserted] = firstOfTrack.emplace(l.origTrackId, i); + if (!inserted) { + parent[find(i)] = find(it->second); + } + } + } + std::map groups; // root -> group + std::map> tracksOfGroup; + for (int i = 0; i < n; ++i) { + const int r = find(i); + auto [it, inserted] = groups.try_emplace(r, cands[i]); + if (inserted) { + it->second.legs.clear(); + it->second.photonUid = mPhotonUid++; + } + for (const auto& l : cands[i].legs) { + if (tracksOfGroup[r].insert(l.origTrackId).second) { + it->second.legs.push_back(l); + } + } + } + std::vector out; + for (auto& [r, g] : groups) { + registry.fill(HIST("Mix/hGroupLegs"), static_cast(g.legs.size())); + out.push_back(std::move(g)); + } + return out; + } + + template + void runMixer(TCollisions const& collisions, TPhotons const& photons, TLegs const& legs, aod::V0LegsCov const& legsCov, + aod::PCMFreeLegs const* freeLegs, TTracks const* tracks, TMCParticles const* mcParticles) + { + if (legs.size() != 2 * photons.size() || legsCov.size() != legs.size()) { + LOG(fatal) << "V0LegsCov (" << legsCov.size() << ") / V0Legs (" << legs.size() << ") / V0PhotonsKF (" << photons.size() << ") do not match: run builder run 1 with fillLegCov = true"; + } + if (cfgPoolFreeLegs && (freeLegs == nullptr || (freeLegs->size() == 0 && photons.size() > 0))) { + LOG(fatal) << "cfgPoolFreeLegs: PCMFreeLegs is empty, run builder run 1 with fillFreeLegs = true"; + } + std::unordered_map> photonsOfCollision, freeLegsOfCollision; + for (const auto& photon : photons) { + if constexpr (isDerived) { + photonsOfCollision[photon.pmeventId()].push_back(photon.globalIndex()); + } else { + photonsOfCollision[photon.collisionId()].push_back(photon.globalIndex()); + } + } + if (cfgPoolFreeLegs) { + for (const auto& leg : *freeLegs) { + freeLegsOfCollision[leg.collisionId()].push_back(leg.globalIndex()); + } + } + + for (const auto& collision : collisions) { + registry.fill(HIST("Event/hCutFlow"), kAll); + int runNumber = 0; + uint64_t timestamp = 0; + if constexpr (isDerived) { + runNumber = collision.runNumber(); + timestamp = collision.timestamp(); + } else { + const auto bc = collision.template foundBC_as(); + runNumber = bc.runNumber(); + timestamp = bc.timestamp(); + } + if (runNumber != mRunNumber) { + if (mRunNumber >= 0) { + registry.fill(HIST("Pool/hResets"), 0.f); + } + mPools.clear(); + mNPooledPhotons = 0; + mRunNumber = runNumber; + } + if constexpr (!isDerived) { + if (!collision.isSelected()) { + continue; + } + } + registry.fill(HIST("Event/hCutFlow"), kSelected); + const float cent = centrality(collision); + if (cent < cfgCentMin || cfgCentMax <= cent) { + continue; + } + registry.fill(HIST("Event/hCutFlow"), kCent); + if (std::fabs(collision.posZ()) >= cfgZMax) { + continue; + } + registry.fill(HIST("Event/hCutFlow"), kZ); + const float occ = occupancy(collision); + const int occBin = occupancyBin(occ); + if (occBin < 0) { + continue; + } + registry.fill(HIST("Event/hCutFlow"), kOccupancy); + const auto it = photonsOfCollision.find(collision.globalIndex()); + if (it == photonsOfCollision.end()) { + continue; + } + registry.fill(HIST("Event/hCutFlow"), kHasPhotons); + const int zBin = static_cast(std::floor((collision.posZ() + cfgZMax) / cfgZBinWidth)); + registry.fill(HIST("Event/hZ"), collision.posZ()); + registry.fill(HIST("Event/hOccupancy"), occ); + + // copies of this event's photons + EventCopy current; + current.eventUid = mEventUid++; + current.x = collision.posX(); + current.y = collision.posY(); + current.z = collision.posZ(); + current.occupancy = occ; + current.cent = cent; + for (const auto& photonId : it->second) { + const auto photon = photons.rawIteratorAt(photonId); + PhotonCopy p; + p.eventUid = current.eventUid; + p.photonUid = mPhotonUid++; + p.v0Id = photon.v0Id(); + p.px = photon.px(); + p.py = photon.py(); + p.pz = photon.pz(); + p.vx = photon.vx(); + p.vy = photon.vy(); + p.vz = photon.vz(); + p.pvX = collision.posX(); + p.pvY = collision.posY(); + p.pvZ = collision.posZ(); + p.pvCovXX = collision.covXX(); + p.pvCovXY = collision.covXY(); + p.pvCovXZ = collision.covXZ(); + p.pvCovYY = collision.covYY(); + p.pvCovYZ = collision.covYZ(); + p.pvCovZZ = collision.covZZ(); + p.pvChi2 = collision.chi2(); + p.pvNContrib = collision.numContrib(); + p.legs.push_back(copyLeg(legs.rawIteratorAt(photon.posTrackId()), legsCov.rawIteratorAt(photon.posTrackId()), tracks, mcParticles)); + p.legs.push_back(copyLeg(legs.rawIteratorAt(photon.negTrackId()), legsCov.rawIteratorAt(photon.negTrackId()), tracks, mcParticles)); + current.photons.push_back(p); + } + if (cfgPoolGroups) { + registry.fill(HIST("Event/hNCandidates"), static_cast(current.photons.size())); + current.photons = toGroups(current.photons); + } + if constexpr (!isDerived) { + if (cfgPoolFreeLegs) { // one-leg stubs: event and vertex of this event, no photon momentum, v0Id -2 + if (const auto itF = freeLegsOfCollision.find(collision.globalIndex()); itF != freeLegsOfCollision.end()) { + for (const auto& legId : itF->second) { + const auto leg = freeLegs->rawIteratorAt(legId); + PhotonCopy f; + f.eventUid = current.eventUid; + f.photonUid = mPhotonUid++; + f.v0Id = -2; + f.pvX = collision.posX(); + f.pvY = collision.posY(); + f.pvZ = collision.posZ(); + f.pvCovXX = collision.covXX(); + f.pvCovXY = collision.covXY(); + f.pvCovXZ = collision.covXZ(); + f.pvCovYY = collision.covYY(); + f.pvCovYZ = collision.covYZ(); + f.pvCovZZ = collision.covZZ(); + f.pvChi2 = collision.chi2(); + f.pvNContrib = collision.numContrib(); + f.legs.push_back(copyLeg(leg, leg, tracks, mcParticles)); + current.freeLegs.push_back(f); + } + } + registry.fill(HIST("Event/hNFreeLegs"), static_cast(current.freeLegs.size())); + } + } + const int nPhotons = static_cast(current.photons.size()); + registry.fill(HIST("Event/hNPhotons"), nPhotons); + + auto& pool = mPools[{zBin, occBin}]; + const int depth = static_cast(pool.size()); + registry.fill(HIST("Pool/hPartnersVsN"), nPhotons, depth); + registry.fill(HIST("Pool/hDepthVsZ"), collision.posZ(), depth); + registry.fill(HIST("Pool/hDepthVsOccBin"), occBin, depth); + + VertexCopy v{collision.posX(), collision.posY(), collision.posZ(), collision.covXX(), collision.covXY(), collision.covXZ(), + collision.covYY(), collision.covYZ(), collision.covZZ(), collision.chi2(), collision.numContrib(), + collision.centFT0M(), collision.centFT0A(), collision.centFT0C(), runNumber, collision.multNTracksPVeta1(), collision.multFT0M(), timestamp}; + const int nPartnersNeeded = cfgOverlay > 0 ? cfgOverlay.value : (cfgInject > 0 ? cfgInject.value : (cfgInject == -2 ? std::max(1, (nPhotons + 1) / 2) : nPhotons)); // overlay / injection: K partner events + const bool poolFull = depth >= nPartnersNeeded; + if (!poolFull) { + registry.fill(HIST("Event/hCutFlow"), kPoolTooSmall); + } + if (poolFull || !cfgRequireFullPool) { + registry.fill(HIST("Event/hCutFlow"), kWritten); + } + std::vector host; // the complete current event: its photons and (cfgPoolFreeLegs) its free legs + host.reserve(current.photons.size() + current.freeLegs.size()); + for (const auto& p : current.photons) { + host.push_back(&p); + } + for (const auto& f : current.freeLegs) { + host.push_back(&f); + } + if (poolFull || !cfgRequireFullPool) { + writeArtificialEvent(v, current, false, zBin, occBin, host, collision.globalIndex()); + } + if (poolFull) { + std::vector order(depth); + auto drawOrder = [&]() { // partner events in random order (cfgPhotonWeighted: weighted sampling without replacement, largest log(u) / w first) + std::iota(order.begin(), order.end(), 0); + if (cfgPhotonWeighted) { + std::uniform_real_distribution uni(0., 1.); + std::vector key(depth); + for (int k = 0; k < depth; ++k) { + key[k] = std::log(std::max(uni(mRandom), 1e-300)) / static_cast(pool[k].photons.size()); // o2-linter: disable=magic-number (log floor) + } + std::sort(order.begin(), order.end(), [&](int x, int y) { return key[x] > key[y]; }); + } else { + std::shuffle(order.begin(), order.end(), mRandom); + } + }; + auto fillPartnerQA = [&](EventCopy const& partner, int age) { + registry.fill(HIST("Mix/hDz"), partner.z - current.z); + registry.fill(HIST("Mix/hDOccupancy"), partner.occupancy - current.occupancy); + registry.fill(HIST("Mix/hDCent"), partner.cent - current.cent); + registry.fill(HIST("Mix/hPartnerAge"), age); + registry.fill(HIST("Mix/hSourceIsCurrent"), partner.eventUid == current.eventUid ? 1.f : 0.f); + }; + if (cfgOverlay > 0) { // overlay: K artificial events = the complete current event + the complete partner event k + for (int iMix = 0; iMix < cfgNMixPerEvent; ++iMix) { + drawOrder(); + for (int k = 0; k < std::min(nPartnersNeeded, depth); ++k) { + auto& partner = pool[order[k]]; + fillPartnerQA(partner, order[k]); + std::vector overlay(host); + std::vector> aligned; // translated copies; reserved, so the pointers stay valid + aligned.reserve(partner.photons.size() + partner.freeLegs.size()); + for (auto* src : {&partner.photons, &partner.freeLegs}) { + for (auto& p : *src) { + if (src == &partner.photons) { + p.timesUsed++; + } + if (cfgAlignToVertex) { + aligned.push_back(std::make_unique(translated(p, current.x - partner.x, current.y - partner.y, current.z - partner.z))); + overlay.push_back(aligned.back().get()); + } else { + overlay.push_back(&p); + } + } + } + registry.fill(HIST("Mix/hDistinctSourcesMinusN"), 2 - nPhotons); // overlay: the current event + one partner + registry.fill(HIST("Event/hNPhotonsMixedVsCurrent"), nPhotons, nPhotons + static_cast(partner.photons.size())); + writeArtificialEvent(v, current, true, zBin, occBin, overlay, collision.globalIndex()); + } + } + } + if (cfgInject != 0) { + for (int iMix = 0; iMix < cfgNMixPerEvent; ++iMix) { + drawOrder(); + for (int k = 0; k < std::min(nPartnersNeeded, depth); ++k) { + auto& partner = pool[order[k]]; + std::uniform_int_distribution pick(0, partner.photons.size() - 1); + auto& p = partner.photons[pick(mRandom)]; + p.timesUsed++; + fillPartnerQA(partner, order[k]); + std::vector injected(host); + PhotonCopy alignedCopy; + if (cfgAlignToVertex) { + alignedCopy = translated(p, current.x - partner.x, current.y - partner.y, current.z - partner.z); + injected.push_back(&alignedCopy); + } else { + injected.push_back(&p); + } + registry.fill(HIST("Mix/hDistinctSourcesMinusN"), 2 - nPhotons); + registry.fill(HIST("Event/hNPhotonsMixedVsCurrent"), nPhotons, nPhotons + 1); + writeArtificialEvent(v, current, true, zBin, occBin, injected, collision.globalIndex()); + } + } + } + for (int iMix = 0; iMix < ((cfgInject != 0 || cfgOverlay > 0) ? 0 : cfgNMixPerEvent.value); ++iMix) { + drawOrder(); + std::vector mixedPhotons; + std::vector> aligned; // translated copies (cfgAlignToVertex); reserved, so the pointers stay valid + aligned.reserve(nPhotons + (cfgPoolFreeLegs ? static_cast(current.freeLegs.size()) : 0)); + std::set sources; + for (int k = 0; k < nPhotons; ++k) { // N partner events, one random photon each + auto& partner = pool[order[k]]; + std::uniform_int_distribution pick(0, partner.photons.size() - 1); + auto& p = partner.photons[pick(mRandom)]; + p.timesUsed++; + if (cfgAlignToVertex) { + aligned.push_back(std::make_unique(translated(p, current.x - partner.x, current.y - partner.y, current.z - partner.z))); + // cppcheck-suppress invalidContainer + mixedPhotons.push_back(aligned.back().get()); + } else { + mixedPhotons.push_back(&p); + } + sources.insert(partner.eventUid); + fillPartnerQA(partner, order[k]); + } + registry.fill(HIST("Mix/hDistinctSourcesMinusN"), static_cast(sources.size()) - nPhotons); + registry.fill(HIST("Event/hNPhotonsMixedVsCurrent"), nPhotons, mixedPhotons.size()); + if (cfgPoolFreeLegs) { // M free legs of the current event -> M drawn without replacement from the free legs of the N partner events + std::vector pooledFree; + for (int k = 0; k < nPhotons; ++k) { + for (const auto& f : pool[order[k]].freeLegs) { + pooledFree.push_back(&f); + } + } + std::shuffle(pooledFree.begin(), pooledFree.end(), mRandom); + const size_t nTake = std::min(pooledFree.size(), current.freeLegs.size()); + for (size_t k = 0; k < nTake; ++k) { + if (cfgAlignToVertex) { + const auto& partner = *pooledFree[k]; + aligned.push_back(std::make_unique(translated(partner, current.x - partner.pvX, current.y - partner.pvY, current.z - partner.pvZ))); + // cppcheck-suppress invalidContainer + mixedPhotons.push_back(aligned.back().get()); + } else { + mixedPhotons.push_back(pooledFree[k]); + } + } + registry.fill(HIST("Mix/hFreeLegsMixedVsCurrent"), static_cast(current.freeLegs.size()), static_cast(nTake)); + } + writeArtificialEvent(v, current, true, zBin, occBin, mixedPhotons, collision.globalIndex()); + } + } + + // this event joins its pool after it was mixed (never mixed with itself) + mNPooledPhotons += nPhotons; + pool.push_front(std::move(current)); + if (static_cast(pool.size()) > cfgPoolDepth) { + for (const auto& p : pool.back().photons) { + registry.fill(HIST("Pool/hTimesUsed"), p.timesUsed); + } + mNPooledPhotons -= static_cast(pool.back().photons.size()); + pool.pop_back(); + } + registry.fill(HIST("Pool/hNPooledPhotons"), mNPooledPhotons); + registry.fill(HIST("Pool/hNPools"), mPools.size()); + } + } + + // AO2D input (builder run 1 in the same workflow) + void processRec(MyCollisions const& collisions, aod::BCsWithTimestamps const&, aod::V0PhotonsKF const& photons, aod::V0Legs const& legs, aod::V0LegsCov const& legsCov, aod::PCMFreeLegs const& freeLegs, aod::TracksIU const& tracks) + { + runMixer(collisions, photons, legs, legsCov, &freeLegs, &tracks, static_cast(nullptr)); + } + PROCESS_SWITCH(PhotonMixedEventProducer, processRec, "data, AO2D input", true); + + void processMC(MyCollisions const& collisions, aod::BCsWithTimestamps const&, aod::V0PhotonsKF const& photons, aod::V0Legs const& legs, aod::V0LegsCov const& legsCov, aod::PCMFreeLegs const& freeLegs, MyTracksMC const& tracks, aod::McParticles const& mcParticles) + { + runMixer(collisions, photons, legs, legsCov, &freeLegs, &tracks, &mcParticles); + } + PROCESS_SWITCH(PhotonMixedEventProducer, processMC, "MC, AO2D input (leg truth into PCMMixLegs)", false); + + // derived data (EM skim with V0LegsCov and, from create-emevent-photon fillVertex, EMEventsXY + EMEventsCov) + void processDerived(MyEvents const& collisions, MyPhotonsDerived const& photons, aod::V0Legs const& legs, aod::V0LegsCov const& legsCov) + { + runMixer(collisions, photons, legs, legsCov, nullptr, static_cast(nullptr), static_cast(nullptr)); + } + PROCESS_SWITCH(PhotonMixedEventProducer, processDerived, "data, derived-data input", false); + + void processDerivedMC(MyEvents const& collisions, MyPhotonsDerived const& photons, MyLegsMCDerived const& legs, aod::V0LegsCov const& legsCov, aod::EMMCParticles const& mcParticles) + { + runMixer(collisions, photons, legs, legsCov, nullptr, static_cast(nullptr), &mcParticles); + } + PROCESS_SWITCH(PhotonMixedEventProducer, processDerivedMC, "MC, derived-data input (leg truth from V0LegMCLabels / EMMCParticles)", false); +}; + +WorkflowSpec defineDataProcessing(ConfigContext const& cfgc) +{ + return WorkflowSpec{adaptAnalysisTask(cfgc, TaskName{"photon-mixed-event-producer"})}; +} diff --git a/PWGEM/PhotonMeson/TableProducer/photonconversionbuilder.cxx b/PWGEM/PhotonMeson/TableProducer/photonconversionbuilder.cxx index 6ee033f2817..0f210615c7c 100644 --- a/PWGEM/PhotonMeson/TableProducer/photonconversionbuilder.cxx +++ b/PWGEM/PhotonMeson/TableProducer/photonconversionbuilder.cxx @@ -23,5 +23,5 @@ o2::framework::WorkflowSpec defineDataProcessing(o2::framework::ConfigContext const& context) { return o2::framework::WorkflowSpec{ - adaptAnalysisTask>(context, o2::framework::TaskName{"photon-conversion-builder"})}; + adaptAnalysisTask>(context, o2::framework::TaskName{"photon-conversion-builder"})}; } diff --git a/PWGEM/PhotonMeson/TableProducer/photonconversionbuilder.h b/PWGEM/PhotonMeson/TableProducer/photonconversionbuilder.h index d4edc620927..3c77efa0efd 100644 --- a/PWGEM/PhotonMeson/TableProducer/photonconversionbuilder.h +++ b/PWGEM/PhotonMeson/TableProducer/photonconversionbuilder.h @@ -61,8 +61,10 @@ #include #include #include +#include #include #include +#include #include // IWYU pragma: keep (do not replace with Math/Vector4Dfwd.h) #include @@ -76,15 +78,20 @@ #include #include +#include #include #include #include +#include #include #include #include +#include +#include #include #include #include +#include #include #include #include @@ -156,6 +163,9 @@ struct V0CandidateHelper { float svX = 0.f; float svY = 0.f; float svZ = 0.f; + float pxAtSV = 0.f; // momentum at the conversion point without the PV constraint (pointing residuals) + float pyAtSV = 0.f; + float pzAtSV = 0.f; float purity = -999.f; float cosPAxy = -2.f; // transverse pointing angle float cosPA3D = -2.f; // 3D pointing angle @@ -173,6 +183,7 @@ struct V0CandidateHelper { }; // (v0.globalIndex(), collision.globalIndex(), pos.globalIndex(), ele.globalIndex()) +// Identifies a V0 candidate by collision, positive-track and negative track IDs, and the SVertexer V0 index (or -1 for finder and resolver photons) struct CandKey { int64_t v0Id = -1; // SVertexer V0 index, -1 for finder and resolver photons int64_t collisionId = -1; @@ -181,37 +192,101 @@ struct CandKey { auto operator<=>(const CandKey&) const = default; }; -// result of a probe fit (FitOptions::probe): leg pair passed the cuts, and its dedup score +// Stores whether a trial fit succeded and the score of the candidate (for the ambiguity resolver, the score is used to decide which of two crossed candidates to keep) struct PairFit { bool ok = false; float score = 999.f; }; // options for fitting a leg pair that was not built by the S-Vertexer (finder, ambiguity resolver) +struct SourcePV { + float x = 0.f, y = 0.f, z = 0.f, cXX = 0.f, cXY = 0.f, cXZ = 0.f, cYY = 0.f, cYZ = 0.f, cZZ = 0.f, c2 = 0.f; + uint16_t nContrib = 0; + [[nodiscard]] float posX() const { return x; } + [[nodiscard]] float posY() const { return y; } + [[nodiscard]] float posZ() const { return z; } + [[nodiscard]] float covXX() const { return cXX; } + [[nodiscard]] float covXY() const { return cXY; } + [[nodiscard]] float covXZ() const { return cXZ; } + [[nodiscard]] float covYY() const { return cYY; } + [[nodiscard]] float covYZ() const { return cYZ; } + [[nodiscard]] float covZZ() const { return cZZ; } + [[nodiscard]] float chi2() const { return c2; } + [[nodiscard]] uint16_t numContrib() const { return nContrib; } +}; + +struct LegDcaKey { + int64_t trackId = -1, collisionId = -1; + float x = 0.f, y = 0.f, z = 0.f; + bool sideBased = false; + bool operator==(LegDcaKey const& o) const { return trackId == o.trackId && collisionId == o.collisionId && x == o.x && y == o.y && z == o.z && sideBased == o.sideBased; } +}; +struct LegDcaKeyHash { + size_t operator()(LegDcaKey const& k) const + { + size_t h = std::hash{}(k.trackId) * 1000003u ^ std::hash{}(k.collisionId); + h = h * 1000003u ^ std::hash{}(k.x); + h = h * 1000003u ^ std::hash{}(k.y); + return (h * 1000003u ^ std::hash{}(k.z)) * 2u + (k.sideBased ? 1u : 0u); + } +}; +// Caches a propagated leg (electron PID) at its DCA to the vertex, for reuse in the ambiguity resolver and the mixed-event builder +struct LegAtDca { + std::array dca{}; + o2::track::TrackParCov trk; // leg (electron PID) propagated to its DCA to the vertex +}; + struct FitOptions { - bool nonSVertexer = false; // finder PCA cut for mixed legs, side-based TPC drift correction - bool relaxAP = false; // no Armenteros-Podolanski cut - bool purityGate = false; // finder: require purity score > finderMinScore - PairFit* probe = nullptr; // only return the dedup score, do not add a candidate + bool nonSVertexer = false; // finder PCA cut for mixed legs, side-based TPC drift correction + bool relaxAP = false; // no Armenteros-Podolanski cut + bool purityGate = false; // finder: require purity score > finderMinScore + float minPurity = -999.f; // purity gate threshold for this fit; below -998 the configured finderMinScore + PairFit* probe = nullptr; // only return the dedup score, do not add a candidate + const SourcePV* pv = nullptr; // run 2: fit against this vertex instead of the collision's (original pairings at their own source vertex) }; +// twin of PCMFreeLegs for the mixed instance of the builder +namespace o2::aod +{ +DECLARE_SOA_TABLE(PCMFreeLegsMix, "AOD", "PCMFREELEGMIX", //! + o2::soa::Index<>, pcmfreeleg::CollisionId, pcmfreeleg::TrackId, v0leg::Sign, + v0legcov::X, v0legcov::Alpha, v0legcov::Y, v0legcov::Z, v0legcov::Snp, v0legcov::Tgl, v0legcov::Signed1Pt, v0legcov::Cov, + track::TPCNClsFindable, track::TPCNClsFindableMinusFound, track::TPCNClsFindableMinusCrossedRows, track::TPCNClsShared, + track::TPCChi2NCl, track::TPCInnerParam, track::TPCSignal, pidtpc::TPCNSigmaEl, pidtpc::TPCNSigmaPi, + track::ITSClusterSizes, track::ITSChi2NCl, track::DetectorMap); +DECLARE_SOA_TABLE(PCMFreeLegIdsMix, "AOD", "PCMFREELEGIDMIX", //! twin of PCMFreeLegIds for the mixed instance (never written) + o2::soa::Index<>, pcmfreeleg::CollisionId, pcmfreelegid::TrackIdDelta); +} // namespace o2::aod + // one tested pair of stored photons in the ambiguity resolver (also read by the MC diagnostics) struct SwapRecord { CandKey keyI{}, keyJ{}; float q = -1.f; bool complete = false; bool swapped = false; + float gain = -999.f; + int fail = 0; // bits: 1 stored I refit, 2 stored J refit, 4 crossed I prefilter, 8 crossed J prefilter, 16 crossed I fit, 32 crossed J fit + int line = 0; // rejection line in fitV0Candidate of the first crossed fit that failed +}; + +// MC truth of the legs of a genuine photon, and the SVertexer and finder results for this leg pair +struct TrueLegTrace { + int svLine = -1; // SVertexer V0 of these legs: -1 none, 0 accepted as candidate, > 0 source line of the rejection in fitV0Candidate + int finderStep = -9; // finder: -9 pair not tested (tgl window, leg not offered, finder off), -8 both legs with ITS, -7 pair already built, else evalFinderPair + int finderLine = -1; // finder fit: -1 not fitted, 0 accepted, > 0 source line of the rejection in fitV0Candidate }; struct DedupDiag { - std::map> blockersByKey; // rejected candidate -> candidates that took one of its legs - std::vector snapshot; // all candidates, before deduplication - std::vector stored; // surviving candidates + std::map> blockersByKey; // rejected candidate -> candidates that took one of its legs + std::set finderKeys; // candidates built by the run-1 finder (the other v0Id = -1 ones come from the swap resolver) + std::map, TrueLegTrace> trueLegTrace; // MC: leg pairs of genuine photons, SVertexer fit and finder result + std::vector snapshot; // all candidates, before deduplication + std::vector stored; // surviving candidates std::map> lossMargin; std::vector swaps; }; -template +template struct PhotonConversionBuilder { // using namespace o2; @@ -226,6 +301,10 @@ struct PhotonConversionBuilder { Produces v0legsXYZ; Produces v0legsDeDxMC; Produces v0photonsphivpsi; + Produces v0legsCov; + Produces v0photonskfchi2; // KF chi2 of the conversion-vertex fit and to the PV + Produces> pcmfreelegs; + Produces> pcmfreelegids; // linked derived data: free legs as track indices // Produces v0photonskfcov; // Produces events_ngpcm; @@ -241,6 +320,9 @@ struct PhotonConversionBuilder { Configurable modeTrackPropagation{"modeTrackPropagation", 0, "0: use real track propagation, including material, 1: use fast approximation using only geometry, 2: Use real track propagation and make comparison to fast propagation (only for debugging and testing)"}; Configurable propV0LegsRadius{"propV0LegsRadius", 60.f, "Radius to which the V0 legs are propagated to calculate psipair and phiV"}; o2::framework::Configurable useSideBasedCorrection{"useSideBasedCorrection", false, "when moving TPC-only tracks, use TPC side flags (with legacy fallback) instead of tgl sign"}; + Configurable fillLegCov{"fillLegCov", false, "fill V0LegsCov (the table is written in any case, empty if false): the leg track parameters and covariance given to the V0 fit (TPC-only legs moved to the collision), one row per V0Legs row"}; + Configurable fillFreeLegIds{"fillFreeLegIds", false, "fill PCMFreeLegIds (the table is written in any case, empty if false): the free legs (as for fillFreeLegs) as AO2D (collision, track) indices only, for the mixer on linked derived data (the leg parameters are read from the parent AO2D); only selected collisions"}; + Configurable fillFreeLegs{"fillFreeLegs", false, "fill PCMFreeLegs (the table is written in any case, empty if false): tracks that pass the leg cuts (as the finder takes them) and are the leg of no stored photon, with the parameters the V0 fit would use (TPC-only legs moved to their own collision); pooled by the mixer with cfgPoolFreeLegs"}; // deduplication settings struct : ConfigurableGroup { @@ -256,6 +338,7 @@ struct PhotonConversionBuilder { Configurable resolveSplitLegs{"resolveSplitLegs", false, "after the dedup, remove photons that share a split track with a better photon - avoids duplicates for photon candidates"}; Configurable splitLegMaxDTgl{"splitLegMaxDTgl", 0.03f, "split legs: max |tgl1 - tgl2|"}; Configurable splitLegMaxChi2{"splitLegMaxChi2", 15.f, "split legs: max chi2 of two track pieces"}; + Configurable dedupHalfSwapRule{"dedupHalfSwapRule", false, "dedup mode 2, after the choice of a conflict group: a non-selected candidate whose two legs went to two different winners replaces both if it scores better than either and the two remaining legs of the winners form no candidate (two fakes on the legs of one photon cannot outweigh it through the score floor); same rule in run 2"}; Configurable cfgExtendedDiag{"cfgExtendedDiag", false, "MC: truth of the stored photons, pairs and swaps"}; } dedup; @@ -274,11 +357,32 @@ struct PhotonConversionBuilder { Configurable finderTimeMarginFwd{"finderTimeMarginFwd", 3000.f, "finder: extra late margin for TPC-only legs (ns)"}; Configurable finderMaxPCAMixed{"finderMaxPCAMixed", -1.f, "finder: max PCA for an ITS and a non-ITS leg (cm), < 0 builder cuts"}; Configurable finderFreeCollinearLegs{"finderFreeCollinearLegs", 0.f, "finder: two candidates of one collision with q_inv below this (GeV/c) count as collinear; their legs stay available to the finder (leg pairs already built are not refitted). Recovers true photons next to a close partner photon. 0 = off"}; + Configurable finderUniformFit{"finderUniformFit", false, "finder candidates (run 1) and finder-origin originals (run 2) are fitted exactly like SVertexer candidates: no side-based drift correction, AP cut applied, no purity gate (symmetric contest between finder-built and SVertexer-built photons)"}; + Configurable svertexerLegRule{"svertexerLegRule", false, "run 1: an SVertexer V0 candidate with a leg without ITS is fitted like a finder candidate (no AP cut, purity gate; the TPC drift correction of moved legs is useSideBasedCorrection), so that run 1 and run 2 (uniformPairFit) apply one leg-based rule to every pairing"}; + Configurable finderFreeAllLegs{"finderFreeAllLegs", false, "finder: no leg blocking at all, legs of accepted candidates stay available (leg pairs already built are not refitted), so the candidate set does not depend on the partner photons"}; Configurable runSwapResolver{"runSwapResolver", false, "ambiguity resolver: for close stored photon pairs, fit the crossed leg pairing (no AP cut) and keep the pairing with the lower dedup score"}; Configurable swapMaxQ{"swapMaxQ", 0.1f, "swap resolver: max q_inv of the two photons (GeV/c)"}; Configurable swapMaxConvDist{"swapMaxConvDist", 20.f, "swap resolver: max distance of the conversion points (cm)"}; } recovery; + // builder run 2 of the co-reconstructed mixed event (processMixed) + struct : ConfigurableGroup { + std::string prefix = "mixed_group"; + Configurable prefilterCrossed{"prefilterCrossed", true, "crossed leg pairs (legs of two different run-1 photons): fit only if |dtgl| < recovery finderMaxDeltaTgl and |circle gap| < finderMaxCircleGap (CPU; validation: off)"}; + Configurable skipSameSourcePairs{"skipSameSourcePairs", true, "mixed event: do not count pairs whose 4 legs all come from the same source event (with pooled conflict groups a group can give two photons of one partner event: a same-event pair inside the mixed event). Data-safe (mixer event counter), pair counting only"}; + Configurable freeLegRuleMask{"freeLegRuleMask", 5, "with freeLegRun1Rules: which run-1 rules a crossed pairing with a pooled free leg gets: bit 0 AP cut, bit 1 purity gate (pairings with a leg without ITS), bit 2 finder fit options (side-based drift correction, finder PCA; pairings with a leg without ITS); 7 = all run-1 rules, 0 = the lenient crossed fit"}; + Configurable freeLegMinPurity{"freeLegMinPurity", -999.f, "with freeLegRun1Rules and bit 1 of freeLegRuleMask: purity threshold for crossed pairings with a pooled free leg; below -998 recovery.finderMinScore (as run 1)"}; + Configurable mixedDiagnostics{"mixedDiagnostics", false, "MC diagnostics of run 2 (hCrossedFit, four-leg decisions, matched configurations, mixed pair classes, fake classes, hQSym, swap/brems pointing, alternative pairings): CPU on data, no effect on the stored photons"}; + Configurable diagFreeLegFits{"diagFreeLegFits", false, "diagnostic: every crossed pairing with a pooled free leg is fitted twice as a trial, with the lenient crossed options and with the run-1 rules, and Mixed/Decision/hFreeLegFit records both results (accepted or the rejection line), split by MC truth; the trial candidates are removed again, the real fit follows the configuration (CPU and QA side effect: V0 QA histograms count the trial fits)"}; + Configurable freeLegRun1Rules{"freeLegRun1Rules", true, "crossed pairings of a pooled free leg (sourceV0Id -2) with a stored leg are fitted as run 1 would have fitted them: with a leg without ITS as a finder candidate (side-based drift correction, AP cut, purity gate), with both legs with ITS as an SVertexer V0 (AP cut, no purity gate); stored x stored pairings keep crossedRelaxAP (overlay mixed event)"}; + Configurable freeFreeFinderOnly{"freeFreeFinderOnly", false, "with skipFreeFreeCrossed off: a crossed pairing of two pooled free legs is formed only if at least one leg has no ITS (the run-1 finder domain); free x free with both legs with ITS could only have been an SVertexer V0 in run 1 and is skipped (overlay mixed event)"}; + Configurable skipFreeFreeCrossed{"skipFreeFreeCrossed", true, "crossed pairings of two pooled free legs (sourceV0Id -2 on both) are not formed: run 1 rejected these legs (finder purity gate, AP cut), the run-2 crossed fit would accept them as fakes (overlay mixed event)"}; + Configurable skipSameSourceCrossed{"skipSameSourceCrossed", true, "crossed pairings of two legs from one source event are not formed (injection mixed event, mixer cfgInject: the current event's pairings were decided by run 1, only the injected legs form new candidates; the same-event artificial event is then exactly the run-1 output). Candidate formation and the hMatched configurations of the mixed event"}; + Configurable crossedRelaxAP{"crossedRelaxAP", true, "crossed leg pairs are fitted without the AP cut, like the originals without an SVertexer V0 (symmetric fit options, control)"}; + Configurable refitOriginalsAtSourceVertex{"refitOriginalsAtSourceVertex", true, "the original pairings are refitted against the primary vertex of their own source event (position and covariance carried in PCMMixLegs), so a mixed photon enters the decision and the output exactly as it left run 1; the crossed pairings use the vertex of the artificial event"}; + Configurable uniformPairFit{"uniformPairFit", false, "decide in run 2 only: every leg pairing of an artificial event (original or crossed, inside or across pooled conflict groups) passes the same prefilter and is fitted with the same options, chosen by the legs as the run-1 finder does (a pair with a leg without ITS gets the finder options: side-based drift correction, no AP cut, purity gate); for run 1 keep-all + mixer cfgPoolGroups, so that the same and the mixed event are decided by identical code on identical kinds of candidates"}; + } mixed; + // event-level skim for derived data struct : ConfigurableGroup { std::string prefix = "skim_group"; @@ -312,7 +416,7 @@ struct PhotonConversionBuilder { struct : ConfigurableGroup { std::string prefix = "v0cut_group"; Configurable min_v0cospa_tpconly{"min_v0cospa_tpconly", 0.99, "min V0 CosPA to V0s with TPConly tracks"}; // double -> N.B. dcos(x)/dx = 0 at x=0) - Configurable min_v0cospa_its{"min_v0cospa_its", 0.99, "min V0 CosPA to V0s with ITs hits"}; // double -> N.B. dcos(x)/dx = 0 at x=0) + Configurable min_v0cospa_its{"min_v0cospa_its", 0.99, "min V0 CosPA to V0s with ITs hits"}; Configurable max_dcav0dau_tpconly{"max_dcav0dau_tpconly", 3.0, "max distance btween 2 legs to V0s with TPConly tracks"}; Configurable max_dcav0dau_its{"max_dcav0dau_its", 0.5, "max distance btween 2 legs to V0s with ITS hits"}; Configurable max_dcav0dau_itsibss{"max_dcav0dau_itsibss", 1.0, "max distance btween 2 legs to V0s with ITS hits on ITSib SS"}; @@ -373,11 +477,23 @@ struct PhotonConversionBuilder { o2::aod::common::TPCVDriftManager mVDriftMgr; uint64_t mLastCCDBTimestamp{0}; std::set mRelaxedKeys; + std::unordered_map mLegDcaCache; // leg DCA to the vertex per DF (fitV0Candidate) + std::map> mGroupOfKey; + std::vector mGroupSizeOfCand; + struct PermuteResult { + int colID = -1; + int nMembers = 0; + bool changed = false; + bool greedy = false; + }; + std::vector mPermuteResults; // run 2, mode 2: one entry per conflict group with >= 2 candidates + int mNAnomalyDump = 0; o2::framework::HistogramRegistry registry{ "registry", { {"hCollisionCounter", "hCollisionCounter", {HistType::kTH1F, {{1, 0.5f, 1.5f}}}}, + {"hNFreeLegs", "free legs filled per DF (fillFreeLegs);legs;DFs", {HistType::kTH1F, {{200, 0.f, 20000.f}}}}, {"V0/hAP", "Armenteros Podolanski;#alpha;q_{T} (GeV/c)", {HistType::kTH2F, {{200, -1.0f, 1.0f}, {250, 0, 0.25}}}}, {"V0/hConversionPointXY", "conversion point in XY;X (cm);Y (cm)", {HistType::kTH2F, {{400, -100.0f, 100.0f}, {400, -100.f, 100.f}}}}, {"V0/hConversionPointRZ", "conversion point in RZ;Z (cm);R_{xy} (cm)", {HistType::kTH2F, {{200, -100.0f, 100.0f}, {200, 0.f, 100.f}}}}, @@ -532,6 +648,7 @@ struct PhotonConversionBuilder { LOG(warning) << "deduplicationScoreWeight is smaller than zero which is not allowed"; } + registry.add("V0/hSVFitReject", "SVertexer V0 fits (run 1, dedup pass): source line of the rejecting cut, 0 = accepted (CPU order of the cuts);line;V0s", kTH1F, {{2400, -0.5f, 2399.5f}}); if (v0cuts.conversionPointMethod == 1) { registry.add("V0/hConvPointProfile", "conversionPointMethod 1;0 fallback to the touching point, 1 profile minimum used;candidates", kTH1F, {{2, -0.5f, 1.5f}}); registry.add("V0/hConvPointProfileShift", "conversionPointMethod 1;R_{profile} - R_{touching point} (cm);candidates", kTH1F, {{200, -10.f, 10.f}}); @@ -542,6 +659,9 @@ struct PhotonConversionBuilder { if (dedup.resolveSplitLegs) { registry.add("Dedup/hSplitLegRemoved", "split legs: photons removed per DF;photons;DFs", kTH1F, {{100, -0.5f, 99.5f}}); } + if (dedup.dedupHalfSwapRule) { + registry.add("Dedup/hHalfSwapRule", "half-swap rule: two winners replaced by the candidate on their legs;replacements per DF;DFs", kTH1F, {{100, -0.5f, 99.5f}}); + } if (recovery.runFinder) { registry.add("Finder/hSteps", "finder;step passed;pairs", kTH1D, {{FinderStep::kNFindStep, -0.5f, static_cast(FinderStep::kNFindStep) - 0.5f}}); registry.add("Finder/hScore", "finder;purity score;pairs", kTH1F, {{100, -20.f, 10.f}}); @@ -568,6 +688,17 @@ struct PhotonConversionBuilder { const AxisSpec axBlocker{4, -0.5f, 3.5f, "0 = true, 1 = cross-leg fake, 2 = other fake, 3 = no blocker (cut, not dedup)"}; registry.add("MCDedup/Photons/hFate", "fate of the true photons;0 = alive, 1 = lost (blocked), 2 = lost (no blocker);photons", kTH1F, {{3, -0.5f, 2.5f}}, true); + registry.add("MCDedup/Photons/hFateProps", "true photons with a candidate;0 alive, 1 lost to a cross-leg fake, 2 lost to another candidate, 3 lost without blocker;p_{T} (GeV/c);R_{xy} (cm);legs with ITS;pointing angle (rad);PCA (cm);other candidates sharing a leg", kTHnSparseF, {{4, -0.5f, 3.5f}, {20, 0.f, 2.f}, {18, 0.f, 90.f}, {3, -0.5f, 2.5f}, {16, 0.f, 0.08f}, {15, 0.f, 3.f}, {11, -0.5f, 10.5f}}); + registry.add("MCDedup/FourLeg/hRun1", "run 1: two true photon candidates whose crossed pairings are candidates too (MC);q_{inv} of the true photons (GeV/c);outcome: 0 both true stored, 1 both crossed stored (swap), 2 one true stored, 3 one crossed stored, 4 none, 5 other;isolated 4-candidate decision: 0 true, 1 crossed, 2 other;other candidates using one of the 4 legs;photons related (same mother, or mother e^{#pm});one true candidate is a resolver/finder photon (no SVertexer V0)", kTHnSparseF, {{10, 0.f, 0.05f}, {6, -0.5f, 5.5f}, {3, -0.5f, 2.5f}, {6, -0.5f, 5.5f}, {2, -0.5f, 1.5f}, {2, -0.5f, 1.5f}}); + registry.add("MCDedup/HalfSwap/hSource", "run 1, true photons with a candidate (MC);0 stored, 1 both legs in two other stored candidates (half-swap source), 2 one leg used, 3 no leg used, 4 both legs in one other stored candidate;score S of the best true candidate;objective: c(S_{w1}) + c(S_{w2}) - c(S_{true}), c = max(s_{0} - S, 0.001);(S_{w1} + S_{w2})/2 - S_{true};p_{T} of the true candidate (GeV/c)", kTHnSparseF, {{5, -0.5f, 4.5f}, {40, 0.f, 2.f}, {40, -0.5f, 0.5f}, {40, -1.f, 1.f}, {10, 0.f, 2.f}}); + registry.add("MCDedup/FourLeg/hRun1Origin", "run 1: as hRun1, with the origin of the candidates (MC);q_{inv} of the true photons (GeV/c);outcome: 0 both true stored, 1 swap, 2 one true stored, 3 one crossed stored, 4 none, 5 other;crossed candidates: 0 both SVertexer V0s, 1 one built by the swap resolver, 2 both built by the resolver;true candidates: 0 both SVertexer V0s, 1 one non-SVertexer, 2 both non-SVertexer;other candidates on the 4 legs (0, 1+);photons related", kTHnSparseF, {{10, 0.f, 0.05f}, {6, -0.5f, 5.5f}, {3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}, {2, -0.5f, 1.5f}, {2, -0.5f, 1.5f}}); + registry.add("MCDedup/Inventory/hPhoton", "eligible true photons (MC);#it{p} (GeV/c);R_{conv} (cm);legs with ITS;fate: 0 no true candidate, 1 candidate lost, 2 stored, 3 lost but a leg in a stored crossed candidate;q_{true} to the closest other eligible photon (GeV/c)", kTHnSparseF, {{20, 0.f, 2.f}, {18, 0.f, 90.f}, {3, -0.5f, 2.5f}, {4, -0.5f, 3.5f}, {20, 0.f, 0.1f}}); + registry.add("MCDedup/Inventory/hPair", "eligible pairs of true photons (MC);q_{true} (GeV/c);|E_{1} - E_{2}| / (E_{1} + E_{2});k_{T} (GeV/c);fate: 0 both stored, 1 one stored, 2 none stored and no crossed stored, 3 both crossed stored (swap), 4 one crossed stored, 5 other;true candidates of the pair (0, 1, 2);crossed candidates of the pair (0, 1, 2)", kTHnSparseF, {{20, 0.f, 0.1f}, {10, 0.f, 1.f}, {6, 0.f, 1.5f}, {6, -0.5f, 5.5f}, {3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}}); + registry.add("MCDedup/Inventory/hSwapOrigin", "eligible pairs of true photons with a stored crossed candidate (MC);q_{true} (GeV/c);fate: 0 both crossed stored (swap), 1 one crossed stored;true candidates of the pair (0, 1, 2);stored crossed: 0 SV+SV, 1 SV+finder, 2 finder+finder, 3 SV+resolver, 4 finder+resolver, 5 resolver+resolver;history of photon 1 (the higher code);history of photon 2", kTHnSparseF, {{20, 0.f, 0.1f}, {2, -0.5f, 1.5f}, {3, -0.5f, 2.5f}, {6, -0.5f, 5.5f}, {7, -0.5f, 6.5f}, {7, -0.5f, 6.5f}}); + registry.add("MCDedup/Inventory/hNeverDetail", "true photons without a true candidate in eligible pairs with q_{true} < 20 MeV (MC, entries = pairs);pair fate (as hPair);history (as hSwapOrigin);detail: SVertexer or finder fit rejection line (history 1, 5), finder step (history 2-4: -8 both ITS, -7 built, -9 not tested, else evalFinderPair)", kTH3F, {{6, -0.5f, 5.5f}, {7, -0.5f, 6.5f}, {2010, -10.5f, 1999.5f}}); + registry.add("MCDedup/Inventory/hSwapLoss", "same-event full swaps of two true photons (MC);q_{true} (GeV/c);true candidates of the pair (0, 1, 2);resolver: 0 crossed in different collisions, 1 not tested q_{rec} > swapMaxQ, 2 not tested conv. distance > swapMaxConvDist, 3 true pairing fails the prefilter, 4 true pairing rejected by a fit cut, 5 stored refit failed, 6 complete, crossed score better, 7 complete, gain > 0 but leg taken by another swap, 8 other;gain = S_{crossed} - S_{true} (resolver sign: S_{stored} - S_{alternative})", kTHnSparseF, {{20, 0.f, 0.1f}, {3, -0.5f, 2.5f}, {9, -0.5f, 8.5f}, {40, -2.f, 2.f}}); + registry.add("MCDedup/Inventory/hSwapLossCut", "same-event full swaps whose true pairing the resolver fit rejects (MC, q_{true} < 20 MeV);true candidates of the pair (0, 1, 2);rejection line in fitV0Candidate", kTH2F, {{3, -0.5f, 2.5f}, {2000, -0.5f, 1999.5f}}); + registry.add("MCDedup/Swap/hResolverTruth", "swap resolver, every tested pair of close stored photons (MC);true photons among the 2 stored;true photons among the 2 alternatives;outcome: 0 incomplete (prefilter), 1 incomplete (fit cut), 2 incomplete (stored refit), 3 complete kept, 4 swapped;gain S_{stored} - S_{alternative};q_{inv} of the stored pair (GeV/c)", kTHnSparseF, {{3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}, {5, -0.5f, 4.5f}, {40, -2.f, 2.f}, {20, 0.f, 0.1f}}); registry.add("MCDedup/Photons/hBlockerClass", "blockers of a killed true photon (ENTRIES = blockers, not photons);class of the blocker;blocker entries", kTH1F, {axBlocker}, true); registry.add("MCDedup/Photons/hKillMargin", "mode 0/1 only;S_{victim} - S_{winner};kills", kTH1F, {{200, 0.f, 1.f}}, true); registry.add("MCDedup/Photons/hVictimVsWinnerPCA", "PCA of the two;PCA_{victim} (cm);PCA_{winner} (cm)", kTH2F, {{60, 0.f, 3.f}, {60, 0.f, 3.f}}, true); @@ -587,6 +718,7 @@ struct PhotonConversionBuilder { const AxisSpec axQrec{50, 0.f, 0.1f, "q_{inv} (GeV/c)"}; registry.add("MCDedup/Stored/hClassSource", "stored photons;class;0 SVertexer, 1 finder or swap;p_{T,#gamma} (GeV/c)", kTH3F, {axClass, {2, -0.5f, 1.5f}, axPt}); registry.add("MCDedup/Stored/hPairClass", "stored photon pairs, same collision;0 true-true, 1 crossed doublet, 2 other;q_{inv} (GeV/c)", kTH2F, {{3, -0.5f, 2.5f}, axQrec}); + registry.add("MCDedup/Stored/hSwapTrueCands", "stored crossed doublets (full swaps);true pairings among the dedup candidates (0, 1, 2);q_{inv} (GeV/c)", kTH2F, {{3, -0.5f, 2.5f}, axQrec}); registry.add("MCDedup/Copies/hChoice", "true photons with a candidate in the dedup sample;collisions with a true candidate;0 = right coll., 1 = wrong (right available), 2 = wrong (no right copy), 3 = lost", kTH2F, {{5, 0.5f, 5.5f}, {4, -0.5f, 3.5f}}); registry.add("MCDedup/Copies/hRule", "true photons, >= 2 copies, right copy available;0 = current dedup choice, 1 = smallest |#Deltaz| of the photon line, 2 = largest xy cos(PA) (master), 3 = largest 3D cos(PA);0 = wrong, 1 = right", kTH2F, {{4, -0.5f, 3.5f}, {2, -0.5f, 1.5f}}); registry.add("MCDedup/Copies/hPAxy", "copies of true photons with >= 2 copies;0 = wrong coll., 1 = right coll.;transverse pointing angle (rad)", kTH2F, {{2, -0.5f, 1.5f}, {200, 0.f, 0.2f}}); @@ -595,15 +727,93 @@ struct PhotonConversionBuilder { } } + if (doprocessMixed && doprocessMixedDerived) { + LOG(fatal) << "enable processMixed (AO2D input) or processMixedDerived (derived data), not both"; + } + if (doprocessMixed || doprocessMixedDerived) { + if (doprocessRec || doprocessMC || doprocessRec_OnlyIfDielectron) { + LOG(fatal) << "processMixed (builder run 2) has to run in its own instance (photon-conversion-builder-mixed) without processRec / processMC"; + } + addMixedHistograms(); + } + mVDriftMgr.setUseSideBasedCorrection(useSideBasedCorrection.value); } + void addMixedHistograms() + { + const AxisSpec axType{2, -0.5f, 1.5f, "artificial event (0 same, 1 mixed)"}; + registry.add("Mixed/hEvents", "artificial events", kTH1F, {axType}); + registry.add("Mixed/hNPhotonsIn", "run-1 photons given to run 2;artificial event;photons per event", kTH2F, {axType, {201, -0.5f, 200.5f}}); + registry.add("Mixed/hCandidates", "candidate cut flow;artificial event;0 original pairs, 1 original refit ok, 2 crossed pairs, 3 crossed after pre-cut, 4 crossed fit ok", kTH2F, {axType, {5, -0.5f, 4.5f}}); + registry.add("Mixed/hStored", "stored photons;artificial event;0 original pairing, 1 crossed pairing", kTH2F, {axType, {2, -0.5f, 1.5f}}); + registry.add("Mixed/hNStoredVsIn", "photons per artificial event;artificial event;photons in (run 1);photons out (run 2)", kTH3F, {axType, {101, -0.5f, 100.5f}, {101, -0.5f, 100.5f}}); + registry.add("Mixed/hNOriginalLost", "run-1 photons not stored again;artificial event;lost originals per event", kTH2F, {axType, {21, -0.5f, 20.5f}}); + registry.add("Mixed/hNCrossedStored", "crossed photons stored;artificial event;crossed photons per event", kTH2F, {axType, {21, -0.5f, 20.5f}}); + registry.add("Mixed/hOrigRelDPt", "stored original pairing vs run 1;artificial event;p_{T}^{run 2} / p_{T}^{run 1} - 1", kTH2F, {axType, {400, -0.02f, 0.02f}}); + registry.add("Mixed/hOrigDPhi", "stored original pairing vs run 1;artificial event;#varphi^{run 2} - #varphi^{run 1} (rad)", kTH2F, {axType, {400, -0.02f, 0.02f}}); + registry.add("Mixed/hOrigDEta", "stored original pairing vs run 1;artificial event;#eta^{run 2} - #eta^{run 1}", kTH2F, {axType, {400, -0.02f, 0.02f}}); + registry.add("Mixed/hOrigExact", "stored original pairing vs run 1;artificial event;0 differs, 1 identical p (float)", kTH2F, {axType, {2, -0.5f, 1.5f}}); + registry.add("Mixed/hCrossedSourceQ", "stored crossed photons: the two run-1 photons of its legs;artificial event;q_{inv} (GeV/c)", kTH2F, {axType, {200, 0.f, 0.2f}}); + registry.add("Mixed/hCrossedSourceDist", "stored crossed photons: the two run-1 photons of its legs;artificial event;distance of the conversion points (cm)", kTH2F, {axType, {200, 0.f, 40.f}}); + registry.add("Mixed/hCrossedSourceEvents", "stored crossed photons;artificial event;legs from 0 the same event, 1 two events", kTH2F, {axType, {2, -0.5f, 1.5f}}); + registry.add("Mixed/hStoredTruth", "MC, stored photons;artificial event;0 original true, 1 original fake, 2 crossed true, 3 crossed fake, 4 no label", kTH2F, {axType, {5, -0.5f, 4.5f}}); + registry.add("Mixed/hTimeCandidates", "CPU of the candidate fits;artificial event;time per event (ms)", kTH2F, {axType, {1000, 0.f, 1000.f}}); + registry.add("Mixed/hTimeDF", "CPU of run 2 per DF;time (ms)", kTH1F, {{1000, 0.f, 100000.f}}); + const AxisSpec axPairTruth{5, -0.5f, 4.5f, "pair (0 all, 1 two true photons, 2 full swap of two true photons, 3 other with a fake, 4 no label)"}; + const AxisSpec axQ{500, 0.f, 0.5f, "q_{inv} (GeV/c)"}; + registry.add("Mixed/Pairs/hQBefore", "photon pairs before run 2 (run-1 photons);artificial event", kTH3F, {axType, axPairTruth, axQ}); + registry.add("Mixed/Pairs/hQAfter", "photon pairs after run 2;artificial event", kTH3F, {axType, axPairTruth, axQ}); + const AxisSpec axAlt{9, -0.5f, 8.5f, "3 #times alternatives passing the fit + (0 kept, 1 changed, 2 other)"}; + const AxisSpec axQLow{100, 0.f, 0.1f, "q_{inv} of the input pair (GeV/c)"}; + registry.add("Mixed/Pairs/hAltTrueTrue", "input pairs of two true photons;artificial event", kTH3F, {axType, axAlt, axQLow}); + registry.add("Mixed/Pairs/hFakeClass", "photon pairs by MC class (MC);artificial event;0 before / 1 after run 2;class (see mixFakeClass);q_{inv} (GeV/c);legs with ITS;crossed pairing passed the run-2 fit", kTHnSparseF, {axType, {2, -0.5f, 1.5f}, {18, -0.5f, 17.5f}, {200, 0.f, 0.2f}, {5, -0.5f, 4.5f}, {2, -0.5f, 1.5f}}); + const AxisSpec axDR{std::vector{0., 0.5, 1., 2., 3., 5., 10., 20., 50., 200.}, "|R_{A} - R_{B}| (cm)"}; + registry.add("Mixed/Pairs/hFakeClassOrder", "photon pairs by MC class and conversion order (MC);artificial event;0 before / 1 after run 2;class (see mixFakeClass);q_{inv} (GeV/c);0 softer photon at the smaller radius (kept), 1 softer photon at the larger radius;|R_{A} - R_{B}| (cm)", kTHnSparseF, {axType, {2, -0.5f, 1.5f}, {18, -0.5f, 17.5f}, {40, 0.f, 0.2f}, {2, -0.5f, 1.5f}, axDR}); + registry.add("Mixed/Pairs/hAltSwap", "input pairs = full swap of two true photons;artificial event", kTH3F, {axType, axAlt, axQLow}); + registry.add("Mixed/Pairs/hFakeClassGeo", "photon pairs by MC class and geometry (MC);artificial event;0 before / 1 after run 2;class (see mixFakeClass);q_{inv} (GeV/c);crossed pairing passed the run-2 fit;same physical-primary ancestor;conversion-point distance (cm);log_{10} closest same-sign legs #sqrt{#Delta#varphi^{2}+#Deltatan#lambda^{2}};max TPC shared-cluster fraction", kTHnSparseF, {axType, {2, -0.5f, 1.5f}, {18, -0.5f, 17.5f}, {50, 0.f, 0.1f}, {2, -0.5f, 1.5f}, {2, -0.5f, 1.5f}, {40, 0.f, 40.f}, {30, -5.f, 1.f}, {10, 0.f, 1.f}}); + registry.add("Mixed/Pairs/hGroupCtx", "photon pairs after run 2, conflict group of the run-2 dedup (MC);artificial event;class (see mixFakeClass);q_{inv} (GeV/c);crossed pairing passed the run-2 fit;same conflict group;candidates in the group;legs in the group;photons stored from the group", kTHnSparseF, {axType, {18, -0.5f, 17.5f}, {20, 0.f, 0.1f}, {2, -0.5f, 1.5f}, {2, -0.5f, 1.5f}, {20, 0.5f, 20.5f}, {15, 3.5f, 33.5f}, {10, 0.5f, 10.5f}}); + registry.add("Mixed/Decision/hFourLeg", "pairs of run-1 photons whose two crossed pairings also passed the fit (MC), selected before the decision;artificial event;MC: 0 original assignment = two conversions, 1 crossed assignment = two conversions, 2 neither;isolated 4-candidate decision: 0 original, 1 crossed, 2 other;actual decision: 0 original stored, 1 crossed stored, 2 other;q_{inv} original assignment (GeV/c);q_{inv} crossed assignment (GeV/c);candidates in the conflict group;S_{crossed} - S_{original};an original was fitted without the AP cut", kTHnSparseF, {axType, {3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}, {50, 0.f, 0.1f}, {50, 0.f, 0.1f}, {12, 3.5f, 15.5f}, {80, -1.f, 1.f}, {2, -0.5f, 1.5f}}); + registry.add("Mixed/Decision/hFourLegKin", "4-leg configurations with min(q_{orig}, q_{crossed}) < 50 MeV (MC);artificial event;MC: 0 original = two conversions, 1 crossed = two conversions, 2 neither;isolated decision: 0 original, 1 crossed, 2 other;#Sigma PCA_{crossed} - #Sigma PCA_{original} (cm);#Sigma #theta_{PA, crossed} - #Sigma #theta_{PA, original} (rad);|#DeltaR_{xy}| of the conversions (cm);|#Deltaz| of the conversions (cm);momentum asymmetry of the more similar same-sign legs;photons related by MC history (brems, siblings)", kTHnSparseF, {axType, {3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}, {40, -2.f, 2.f}, {40, -0.1f, 0.1f}, {20, 0.f, 20.f}, {20, 0.f, 20.f}, {10, 0.f, 1.f}, {2, -0.5f, 1.5f}}); + { + const AxisSpec axTruth2{2, -0.5f, 1.5f, "MC: 0 original = two conversions, 1 crossed = two conversions"}; + const AxisSpec axChoice{3, -0.5f, 2.5f, "isolated decision: 0 genuine assignment, 1 wrong assignment, 2 other"}; + const AxisSpec axRel2{2, -0.5f, 1.5f, "photons related by MC history"}; + registry.add("Mixed/Decision/hFourLegTruth", "4-leg configurations with a genuine assignment, min(q) < 50 MeV (MC);artificial event", kTHnSparseF, {axType, axTruth2, axChoice, {4, -0.5f, 3.5f, "MC relation: 0 none, 1 radiated by a leg, 2 siblings, 3 leg daughter of a leg"}, {10, 0.f, 0.05f, "q_{inv} of the genuine assignment (GeV/c)"}, {9, -0.5f, 8.5f, "3 #times (genuine photons with S #geq s_{0}) + wrong candidates with S #geq s_{0}"}, {24, -60.f, 60.f, "#Sigma#theta_{PA} wrong - genuine (mrad)"}, {20, -1.f, 1.f, "#Sigma PCA wrong - genuine (cm)"}, {std::vector{0., 0.005, 0.05, 0.1, 0.2, 0.4, 1.}, "max TPC shared-cluster fraction of the 4 legs"}}); + registry.add("Mixed/Decision/hFourLegTrack", "4-leg configurations with a genuine assignment, min(q) < 50 MeV (MC);artificial event", kTHnSparseF, {axType, axTruth2, axChoice, axRel2, {15, -2.f, 1.5f, "log_{10} min distance in the TPC of the legs the wrong assignment pairs (cm)"}, {15, -2.f, 1.5f, "log_{10} min distance in the TPC of the same-sign legs (cm)"}, {5, -0.5f, 4.5f, "legs with ITS"}, {std::vector{-10., 722., 2228., 1.e6}, "occupancy of the artificial event (terciles)"}}); + registry.add("Mixed/Decision/hFourLegResid", "4-leg configurations with a genuine assignment, min(q) < 50 MeV (MC), residuals signed toward the partner;artificial event", kTHnSparseF, {axType, axTruth2, axChoice, axRel2, {20, -0.01f, 0.01f, "#varphi residual photon 1 (rad)"}, {20, -0.01f, 0.01f, "#varphi residual photon 2 (rad)"}, {20, -0.01f, 0.01f, "#eta residual photon 1"}, {20, -0.01f, 0.01f, "#eta residual photon 2"}}); + registry.add("Mixed/Decision/hFourLegAlt", "4-leg configurations with a genuine assignment, min(q) < 50 MeV (MC), isolated decision with other scores;artificial event", kTHnSparseF, {axType, axTruth2, axRel2, {3, -0.5f, 2.5f, "baseline: 0 genuine, 1 wrong, 2 other"}, {3, -0.5f, 2.5f, "pointing only (w = 1)"}, {3, -0.5f, 2.5f, "PCA only (w = 0)"}, {3, -0.5f, 2.5f, "baseline score, raw-sum objective"}, {3, -0.5f, 2.5f, "baseline weight without gap term"}}); + registry.add("Mixed/Decision/hFourLegOrigin", "4-leg configurations with a genuine assignment, min(q) < 50 MeV (MC), common origin of the two genuine photons;artificial event", kTHnSparseF, {axType, axTruth2, axChoice, {4, -0.5f, 3.5f, "MC relation: 0 none, 1 radiated by a leg, 2 siblings, 3 leg daughter of a leg"}, {10, 0.f, 0.05f, "q_{inv} of the genuine assignment (GeV/c)"}, {5, -1.5f, 3.5f, "common origin: -1 n/a, 0 none, 1 same chain root, 2 same great-grandmother, 3 same grandmother"}, {8, -0.5f, 7.5f, "PDG of the chain root: 0 other, 1 pi0, 2 eta, 3 omega, 4 eta', 5 rho/K*, 6 K0, 7 baryon"}}); + registry.add("Mixed/Decision/hFourLegPoint", "4-leg configurations with a genuine assignment, min(q) < 50 MeV (MC), pointing to the PV split into transverse (#varphi) and longitudinal (#lambda) parts;artificial event", kTHnSparseF, {axType, axTruth2, axChoice, axRel2, {5, 0.f, 0.05f, "q_{inv} of the genuine assignment (GeV/c)"}, {24, -30.f, 30.f, "#Sigma|#Delta#varphi| wrong - genuine (mrad)"}, {24, -30.f, 30.f, "#Sigma|#Delta#lambda| wrong - genuine (mrad)"}, {20, -20.f, 20.f, "#Delta#lambda genuine photon 1 (mrad)"}, {20, -20.f, 20.f, "#Delta#lambda genuine photon 2 (mrad)"}, {20, -20.f, 20.f, "#Delta#varphi genuine photon 1 (mrad)"}, {20, -20.f, 20.f, "#Delta#varphi genuine photon 2 (mrad)"}}); + } + registry.add("Mixed/Decision/hFreeLegFit", "crossed pairings with a pooled free leg (run 2, mixed_group.diagFreeLegFits): trial fits with both option sets (MC);artificial event;truth: 0 legs of two genuine photons with q_{true} < 50 MeV (swap ingredient), 1 other;lenient crossed fit: 0 accepted, else rejection line;run-1 rules: 0 accepted, else rejection line;legs with ITS;free legs in the pairing (1, 2)", kTHnSparseF, {axType, {2, -0.5f, 1.5f}, {2000, -0.5f, 1999.5f}, {2000, -0.5f, 1999.5f}, {3, -0.5f, 2.5f}, {2, 0.5f, 2.5f}}); + registry.add("Mixed/Decision/hCrossedFit", "crossed leg pairings in run 2: result of the fit (MC);artificial event;two genuine photons, q_{true} < 50 MeV;0 accepted, 1 prefilter (only pairs inside the tgl window are visited), else source line of the rejection in fitV0Candidate;legs with ITS;q_{true} of the two photons (GeV/c)", kTHnSparseF, {axType, {2, -0.5f, 1.5f}, {1600, -0.5f, 1599.5f}, {3, -0.5f, 2.5f}, {5, 0.f, 0.05f}}); + registry.add("Mixed/Decision/hMatched", "two genuine MC photons whose 4 legs are all in the artificial event (same definition for SE and ME, q from the MC photons);artificial event;q_{true} (GeV/c);run-1 input pairing: 0 both genuine photons, 1 swapped (both crossed), 2 other;genuine pairings fitted in run 2 (0-2);crossed pairings fitted in run 2 (0-2);run-2 outcome: 0 both genuine stored, 1 both crossed stored, 2 other;photons related (same mother, or one radiated by a leg of the other);PV contributors of the event (charged multiplicity);photons in the artificial event;occupancy of the event;occupancy of the source event - current event (signed, the photon with the larger difference; 0 for SE);TPC-only legs among the 4", kTHnSparseF, {axType, {10, 0.f, 0.05f}, {3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}, {2, -0.5f, 1.5f}, {std::vector{0., 50., 100., 200., 400., 800., 1200., 1600., 2000., 2500., 3000., 5000.}, "PV contributors"}, {12, 0.f, 60.f, "photons in the artificial event"}, {std::vector{-10., 722., 2228., 1.e6}, "occupancy"}, {std::vector{-20000., -3000., -2000., -1000., -500., -250., 250., 500., 1000., 2000., 3000., 20000.}, "occupancy partner - current"}, {5, -0.5f, 4.5f, "TPC-only legs"}}); + registry.add("Mixed/Decision/hMatchedCE", "two genuine MC photons whose 4 legs are all in the artificial event, central-electrode variables;artificial event;q_{true} (GeV/c);run-1 input pairing: 0 both genuine photons, 1 swapped, 2 other;run-2 outcome: 0 both genuine stored, 1 both crossed stored, 2 other;photons related;min |#eta| of the two MC photons;min |z| of the two true conversion points (cm);legs crossing the central electrode in the TPC (of 4);TPC-only legs", kTHnSparseF, {axType, {5, 0.f, 0.05f}, {3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}, {2, -0.5f, 1.5f}, {std::vector{0., 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.6, 0.9, 2.}, "min |#eta|"}, {std::vector{0., 1., 2., 5., 10., 20., 50., 300.}, "min |z_{conv}| (cm)"}, {5, -0.5f, 4.5f}, {5, -0.5f, 4.5f}}); + registry.add("Mixed/Decision/hMatchedLegs", "two genuine MC photons whose 4 legs are all in the artificial event, partner of each leg in the stored photons;artificial event;q_{true} (GeV/c);run-1 input pairing: 0 both genuine photons, 1 swapped, 2 other;run-2 outcome: 0 both genuine stored, 1 both crossed stored, 2 other;photons related;legs not stored;legs stored with their own genuine partner;legs stored with the crossed partner (other photon of the pair);legs stored with a leg of a third genuine photon (both legs in the event);legs stored with a leg of an incomplete MC photon (arrived inside a stored fake);legs stored with a leg without photon mother", kTHnSparseF, {axType, {5, 0.f, 0.05f}, {3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}, {2, -0.5f, 1.5f}, {5, -0.5f, 4.5f}, {5, -0.5f, 4.5f}, {5, -0.5f, 4.5f}, {5, -0.5f, 4.5f}, {5, -0.5f, 4.5f}, {5, -0.5f, 4.5f}}); + registry.add("Mixed/Decision/hFourLegAll", "pairs of run-1 photons whose two crossed pairings are candidates INCLUDING those appended by the run-2 resolvers (MC);artificial event;MC: 0 original = two conversions, 1 crossed = two conversions, 2 neither;actual decision: 0 original stored, 1 crossed stored, 2 other;q_{inv} of the genuine (truth 0: original, 1: crossed) assignment (GeV/c);crossed candidates: 0 both from the run-2 fit, 1 one appended by a resolver, 2 both appended;original photons: 0 both SVertexer V0s in run 1, 1 one not, 2 both not", kTHnSparseF, {axType, {3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}, {10, 0.f, 0.05f}, {3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}}); + registry.add("Mixed/Decision/hPermute", "conflict groups of the run-2 dedup with >= 2 candidates;artificial event;candidates;reversed order selects other candidates;greedy (no enumeration)", kTHnSparseF, {axType, {30, 0.5f, 30.5f}, {2, -0.5f, 1.5f}, {2, -0.5f, 1.5f}}); + registry.add("Mixed/Pairs/hPairOrigin", "photon pairs after run 2 by MC class and common origin of the two MC photons (MC);artificial event;MC class (see mixFakeClass);q_{inv} (GeV/c);common origin: -1 same photon / no label, 0 none, 1 same chain root, 2 same great-grandmother, 3 same grandmother", kTHnSparseF, {axType, {18, -0.5f, 17.5f}, {50, 0.f, 0.1f}, {5, -1.5f, 3.5f}}); + registry.add("Mixed/Pairs/hSameSource", "mixed event after run 2: pairs whose 4 legs all come from one source event (same-event pairs inside the mixed event, from pooled groups);q_{inv} (GeV/c);MC class (see mixFakeClass)", kTH2F, {{200, 0.f, 0.2f}, {18, -0.5f, 17.5f}}); + registry.add("Mixed/Pairs/hQSym", "photon pairs after run 2, swap-symmetric counting: a stored pair whose two crossed pairings (same 4 legs) also passed the run-2 fit counts 1/2 at its q and 1/2 at q of the crossed pairing;artificial event;MC class of the stored pair (see mixFakeClass);q_{inv} (GeV/c)", kTH3F, {axType, {18, -0.5f, 17.5f}, {500, 0.f, 0.5f}}); + registry.add("Mixed/Pairs/hQClean", "photon pairs, all legs TPC shared fraction < 0.4;artificial event;0 before / 1 after run 2;q_{inv} (GeV/c)", kTH3F, {axType, {2, -0.5f, 1.5f}, {500, 0.f, 0.5f}}); + registry.add("Mixed/Pairs/hBrems", "photon pairs, bremsstrahlung pointing;artificial event;0 before / 1 after run 2;class (see mixFakeClass);MC relation (0 none, 1 radiated by a leg, 2 siblings, 3 leg daughter of a leg);q_{inv} (GeV/c);log_{10} d_{min} (cm);log_{10} angle to the leg (rad);radius of the closest point (cm);B converts further out;p_{T} of B (GeV/c);all legs TPC shared fraction < 0.4", kTHnSparseF, {axType, {2, -0.5f, 1.5f}, {18, -0.5f, 17.5f}, {4, -0.5f, 3.5f}, {50, 0.f, 0.1f}, {40, -3.f, 2.f}, {40, -4.f, 0.5f}, {45, 0.f, 90.f}, {2, -0.5f, 1.5f}, {20, 0.f, 1.f}, {2, -0.5f, 1.5f}}); + registry.add("Mixed/Pairs/hSwapDecision", "full swaps after run 2 (classes 14 and 17);artificial event;0 created in run 2, 1 carried from run 1, 2 one of each;0 true pairings scored better, 1 swap scored better, 2 a true pairing failed the fit;q_{inv} (GeV/c);all legs TPC shared fraction < 0.4", kTHnSparseF, {axType, {3, -0.5f, 2.5f}, {3, -0.5f, 2.5f}, {50, 0.f, 0.1f}, {2, -0.5f, 1.5f}}); + } + void initCCDB(MyBCs::iterator const& bc) { - if (mRunNumber == bc.runNumber()) { + initCCDBFor(bc.runNumber(), bc.timestamp(), bc.grpMagField()); + } + + // run number, timestamp and GRPMagField of a BC (AO2D) or of a PMEvents row with EmMagFields (derived data, builder run 2) + template + void initCCDBFor(const int runNumber, const uint64_t timestamp, TGrpMagField const& grpMagField) + { + if (mRunNumber == runNumber) { return; } - mRunNumber = bc.runNumber(); + mRunNumber = runNumber; // In case override, don't proceed, please - no CCDB access required if (d_bz_input > -990) { // o2-linter: disable=magic-number (override value) d_bz = d_bz_input; @@ -616,10 +826,11 @@ struct PhotonConversionBuilder { return; } - auto run3grp_timestamp = bc.timestamp(); - o2::base::Propagator::initFieldFromGRP(&bc.grpMagField()); + auto run3grp_timestamp = timestamp; + o2::parameters::GRPMagField grp = grpMagField; // copy: getNominalL3Field is not const + o2::base::Propagator::initFieldFromGRP(&grp); // Fetch magnetic field from ccdb for current collision - d_bz = bc.grpMagField().getNominalL3Field(); + d_bz = grp.getNominalL3Field(); LOG(info) << "Retrieved GRP for timestamp " << run3grp_timestamp << " with magnetic field of " << d_bz << " kZG"; if (useMatCorrType == 2) { // o2-linter: disable=magic-number (material budget correction) @@ -635,8 +846,11 @@ struct PhotonConversionBuilder { void updateCCDB(MyBCs::iterator const& bc) { - auto timestamp = bc.timestamp(); + updateCCDBAt(bc.timestamp()); + } + void updateCCDBAt(const uint64_t timestamp) + { mVDriftMgr.update(timestamp); mLastCCDBTimestamp = timestamp; } @@ -762,13 +976,76 @@ struct PhotonConversionBuilder { } } + void fillLegCovTable(o2::track::TrackParCov const& trackParCov) + { + const auto cov = trackParCov.getCov(); + v0legsCov(trackParCov.getX(), trackParCov.getAlpha(), trackParCov.getY(), trackParCov.getZ(), + trackParCov.getSnp(), trackParCov.getTgl(), trackParCov.getQ2Pt(), cov.data()); + } + + // free legs (fillFreeLegs): after the tables are written; a track that passes the leg cuts, has a collision and the TPC (as the + // finder takes them) and is the leg of no stored photon; TPC-only legs moved to their own collision as in the fit + template + void writeFreeLegs(TCollisions const& collisions, TTracks const& tracks, std::set const& skimRejected) + { + std::set usedLegs; + for (const auto& key : stored_fullv0Ids) { + usedLegs.insert(key.posId); + usedLegs.insert(key.eleId); + } + int nFree = 0; + int64_t lastFreeLegTrack = 0; // PCMFreeLegIds: track index stored as the difference to the previous row of this DF + for (const auto& track : tracks) { + if (track.collisionId() < 0 || !track.hasTPC() || usedLegs.contains(track.globalIndex()) || skimRejected.contains(track.collisionId()) || + track.pt() < trackcuts.min_pt_trackiu || !checkV0leg(track)) { + continue; + } + const auto collision = collisions.rawIteratorAt(track.collisionId()); + if (!collision.isSelected()) { // the mixer only takes selected events (PMEvents): free legs of other collisions are never used + continue; + } + auto par = getTrackParCov(track); + if constexpr (!std::is_void_v) { + if (trackcuts.moveTPCTracks && o2::pwgem::photonmeson::isTPConlyTrack(track) && !mVDriftMgr.moveTPCTrack(collision, track, par)) { + continue; + } + } + if (fillFreeLegIds) { // tracks are visited in increasing index order: store the difference to the previous free leg of this DF + pcmfreelegids(track.collisionId(), static_cast(track.globalIndex() - lastFreeLegTrack)); + lastFreeLegTrack = track.globalIndex(); + } + if (!fillFreeLegs) { + ++nFree; + continue; + } + const auto cov = par.getCov(); + pcmfreelegs(track.collisionId(), track.globalIndex(), track.sign(), + par.getX(), par.getAlpha(), par.getY(), par.getZ(), par.getSnp(), par.getTgl(), par.getQ2Pt(), cov.data(), + track.tpcNClsFindable(), track.tpcNClsFindableMinusFound(), track.tpcNClsFindableMinusCrossedRows(), track.tpcNClsShared(), + track.tpcChi2NCl(), track.tpcInnerParam(), track.tpcSignal(), track.tpcNSigmaEl(), track.tpcNSigmaPi(), + track.itsClusterSizes(), track.itsChi2NCl(), track.detectorMap()); + ++nFree; + } + registry.fill(HIST("hNFreeLegs"), static_cast(nFree)); + } + template void fillV0Table(TV0 const& v0, const bool filltable) { const auto& pos = v0.template posTrack_as(); const auto& ele = v0.template negTrack_as(); - const auto& collision = v0.template collision_as(); // collision where this v0 belongs to. - fitV0Candidate(collision, pos, ele, v0.globalIndex(), filltable); + const auto& collision = v0.template collision_as(); // collision where this v0 belongs to. + const bool finderLike = recovery.svertexerLegRule.value && !(pos.hasITS() && ele.hasITS()); // one leg-based rule for every pairing + fitV0Candidate(collision, pos, ele, v0.globalIndex(), filltable, + FitOptions{.nonSVertexer = finderLike, .relaxAP = finderLike, .purityGate = finderLike && recovery.runFinder.value}); + if (!filltable) { + registry.fill(HIST("V0/hSVFitReject"), static_cast(mLastFitReject)); + } + if constexpr (isMC) { + if (auto* t = filltable ? nullptr : trueLegTrace(pos.globalIndex(), ele.globalIndex()); t != nullptr) { + t->svLine = t->svLine == 0 ? 0 : mLastFitReject; // accepted in any collision wins + } + } } // the purity score (finder LLR tables) is only needed for the finder gate and the split-leg order @@ -792,35 +1069,42 @@ struct PhotonConversionBuilder { return score; } - // fit of one leg pair with the V0 cuts; filltable = false adds a dedup candidate, - // true writes the tables; opt.probe only returns the score + // fit of one leg pair with the V0 cuts template void fitV0Candidate(TCollision const& collision, TTrack const& pos, TTrack const& ele, const int64_t v0Id, const bool filltable, FitOptions const& opt = {}) { using namespace o2::pwgem::photonmeson; mVDriftMgr.setUseSideBasedCorrection(opt.nonSVertexer || useSideBasedCorrection.value); + mLastFitReject = 0; + const float pvX = opt.pv ? opt.pv->posX() : collision.posX(), pvY = opt.pv ? opt.pv->posY() : collision.posY(), pvZ = opt.pv ? opt.pv->posZ() : collision.posZ(); if (pos.sign() * ele.sign() > 0) { // reject same sign pair + mLastFitReject = __LINE__; // rejection point return; } if (pos.pt() < trackcuts.min_pt_trackiu || ele.pt() < trackcuts.min_pt_trackiu) { + mLastFitReject = __LINE__; // rejection point return; } if (pos.globalIndex() == ele.globalIndex()) { + mLastFitReject = __LINE__; // rejection point return; } if (isITSonlyTrack(pos) && !ele.hasITS()) { + mLastFitReject = __LINE__; // rejection point return; } if (isITSonlyTrack(ele) && !pos.hasITS()) { + mLastFitReject = __LINE__; // rejection point return; } if (!checkV0leg(pos) || !checkV0leg(ele)) { + mLastFitReject = __LINE__; // rejection point return; } @@ -835,25 +1119,44 @@ struct PhotonConversionBuilder { std::array dcaInfo{}; auto pTrack = getTrackParCov(pos); - if (trackcuts.moveTPCTracks && isTPConlyTrack(pos) && !mVDriftMgr.moveTPCTrack(collision, pos, pTrack)) { + bool failedMovePos = false; + if constexpr (!std::is_void_v) { // TBCs = void: legs of builder run 2, already moved in run 1 + failedMovePos = trackcuts.moveTPCTracks && isTPConlyTrack(pos) && !mVDriftMgr.moveTPCTrack(collision, pos, pTrack); + } + if (failedMovePos) { if (opt.probe == nullptr) { LOGP(error, "failed correction for positive tpc track"); } + mLastFitReject = __LINE__; // rejection point (diagnostic) return; } + const bool sideBased = opt.nonSVertexer || useSideBasedCorrection.value; + const bool cacheDca = modeTrackPropagation == TrackPropMode::kProper; + std::optional posAtDca, eleAtDca; // legs at their DCA to the vertex (reused for phi_V / psi_pair) + const LegDcaKey posKey{pos.globalIndex(), collision.globalIndex(), pvX, pvY, pvZ, sideBased}; + const LegAtDca* posCached = nullptr; + if (cacheDca) { + if (const auto it = mLegDcaCache.find(posKey); it != mLegDcaCache.end()) { + posCached = &it->second; + } + } auto pTrackC = pTrack; - o2::math_utils::Point3D vtxPrim{ - collision.posX(), - collision.posY(), - collision.posZ()}; + o2::math_utils::Point3D vtxPrim{pvX, pvY, pvZ}; if (modeTrackPropagation != TrackPropMode::kProper) { dcaInfo = CalculateDCAFast(pTrackC, vtxPrim, d_bz); } pTrackC.setPID(o2::track::PID::Electron); std::array dcaInfoFast = dcaInfo; - if (modeTrackPropagation != TrackPropMode::kFast) { - o2::base::Propagator::Instance()->propagateToDCABxByBz({collision.posX(), collision.posY(), collision.posZ()}, pTrackC, 2.f, matCorr, &dcaInfo); + if (posCached != nullptr) { + dcaInfo = posCached->dca; + posAtDca = posCached->trk; + } else if (modeTrackPropagation != TrackPropMode::kFast) { + const bool propOk = o2::base::Propagator::Instance()->propagateToDCABxByBz({pvX, pvY, pvZ}, pTrackC, 2.f, matCorr, &dcaInfo); + if (cacheDca && propOk) { // a failed propagation leaves dcaInfo as it was: not cached + mLegDcaCache.emplace(posKey, LegAtDca{dcaInfo, pTrackC}); + posAtDca = pTrackC; + } if (filltable && modeTrackPropagation == TrackPropMode::kBoth) { registry.fill(HIST("V0Leg/hDCAxyPropagationCompare"), dcaInfo[0], dcaInfoFast[0]); registry.fill(HIST("V0Leg/hDCAzPropagationCompare"), dcaInfo[1], dcaInfoFast[1]); @@ -863,12 +1166,24 @@ struct PhotonConversionBuilder { auto posdcaZ = dcaInfo[1]; auto nTrack = getTrackParCov(ele); - if (trackcuts.moveTPCTracks && isTPConlyTrack(ele) && !mVDriftMgr.moveTPCTrack(collision, ele, nTrack)) { + bool failedMoveEle = false; + if constexpr (!std::is_void_v) { // TBCs = void: legs of builder run 2, already moved in run 1 + failedMoveEle = trackcuts.moveTPCTracks && isTPConlyTrack(ele) && !mVDriftMgr.moveTPCTrack(collision, ele, nTrack); + } + if (failedMoveEle) { if (opt.probe == nullptr) { LOGP(error, "failed correction for negative tpc track"); } + mLastFitReject = __LINE__; // rejection point (diagnostic) return; } + const LegDcaKey eleKey{ele.globalIndex(), collision.globalIndex(), pvX, pvY, pvZ, sideBased}; + const LegAtDca* eleCached = nullptr; + if (cacheDca) { + if (const auto it = mLegDcaCache.find(eleKey); it != mLegDcaCache.end()) { + eleCached = &it->second; + } + } auto nTrackC = nTrack; if (modeTrackPropagation != TrackPropMode::kProper) { dcaInfo = CalculateDCAFast(nTrackC, vtxPrim, d_bz); @@ -876,8 +1191,15 @@ struct PhotonConversionBuilder { nTrackC.setPID(o2::track::PID::Electron); dcaInfoFast = dcaInfo; - if (modeTrackPropagation != TrackPropMode::kFast) { - o2::base::Propagator::Instance()->propagateToDCABxByBz({collision.posX(), collision.posY(), collision.posZ()}, nTrackC, 2.f, matCorr, &dcaInfo); + if (eleCached != nullptr) { + dcaInfo = eleCached->dca; + eleAtDca = eleCached->trk; + } else if (modeTrackPropagation != TrackPropMode::kFast) { + const bool propOk = o2::base::Propagator::Instance()->propagateToDCABxByBz({pvX, pvY, pvZ}, nTrackC, 2.f, matCorr, &dcaInfo); + if (cacheDca && propOk) { // a failed propagation leaves dcaInfo as it was: not cached + mLegDcaCache.emplace(eleKey, LegAtDca{dcaInfo, nTrackC}); + eleAtDca = nTrackC; + } if (filltable && modeTrackPropagation == TrackPropMode::kBoth) { registry.fill(HIST("V0Leg/hDCAxyPropagationCompare"), dcaInfo[0], dcaInfoFast[0]); registry.fill(HIST("V0Leg/hDCAzPropagationCompare"), dcaInfo[1], dcaInfoFast[1]); @@ -887,10 +1209,22 @@ struct PhotonConversionBuilder { auto eledcaZ = dcaInfo[1]; if (std::fabs(posdcaXY) < trackcuts.dcapostopv || std::fabs(eledcaXY) < trackcuts.dcanegtopv) { + mLastFitReject = __LINE__; // rejection point (diagnostic) return; } std::array xyz = {0.f, 0.f, 0.f}; + if (v0cuts.conversionPointMethod != 1) { + // Calculate the conversion point as the weighted average of the two legs' closest approach points -- avoids expensice KFParticle construction for every candidate + const float bzNominal = o2::base::Propagator::Instance()->getNominalBz(); + const o2::track::TrackAuxPar cP(pTrack, bzNominal), cN(nTrack, bzNominal); + const float xC = (cP.xC * cN.rC + cN.xC * cP.rC) / (cP.rC + cN.rC); + const float yC = (cP.yC * cN.rC + cN.yC * cP.rC) / (cP.rC + cN.rC); + if (RecoDecay::sqrtSumOfSquares(xC, yC) > trackcuts.maxX + v0cuts.margin_r_tpc) { + mLastFitReject = __LINE__; // rejection point + return; + } + } Vtx_recalculationParCov(o2::base::Propagator::Instance(), pTrack, nTrack, xyz, matCorr); if (v0cuts.conversionPointMethod == 1) { LegScan scanP, scanN; @@ -907,78 +1241,12 @@ struct PhotonConversionBuilder { } float rxy_tmp = RecoDecay::sqrtSumOfSquares(xyz[0], xyz[1]); if (rxy_tmp > trackcuts.maxX + v0cuts.margin_r_tpc) { + mLastFitReject = __LINE__; // rejection point (diagnostic) return; } if (rxy_tmp < std::fabs(xyz[2]) * std::tan(2 * std::atan(std::exp(-v0cuts.max_eta_v0))) - v0cuts.margin_z) { - return; // RZ line cut - } - - float phiv = 999.f; - float psipair = 999.f; - float phivFast = 999.f; - float psipairFast = 999.f; - float baseR = std::hypot(xyz[0], xyz[1]); - // This method uses the track Helix instead of the full propagation. - // Hence, it is only an approximation but much faster - if (modeTrackPropagation != TrackPropMode::kProper) { - - o2::track::TrackAuxPar helixPosEle(nTrack, d_bz); - o2::track::TrackAuxPar helixPosPos(pTrack, d_bz); - - float diffX = helixPosEle.xC - helixPosPos.xC; - float diffY = helixPosEle.yC - helixPosPos.yC; - auto phiHelix = RecoDecay::constrainAngle(std::atan2(diffY, diffX) - o2::constants::math::PI / 2.); - - // Electron - float arcLenghtEle = helixPosEle.rC * 0.9 > propV0LegsRadius ? std::asin(propV0LegsRadius / helixPosEle.rC) * helixPosEle.rC : o2::constants::math::PI / 2.2 * helixPosEle.rC; // This assumes that the photon momentum vector is a tangent of the circle // o2-linter: disable=magic-number (geometrical assumption for propagation) - auto propTrackEle = getPropMomentumFromTrackHelix(arcLenghtEle, ele, helixPosEle, d_bz / 10., phiHelix - ele.phi()); - // Positron - float arcLenghtPos = helixPosPos.rC * 0.9 > propV0LegsRadius ? std::asin(propV0LegsRadius / helixPosPos.rC) * helixPosPos.rC : o2::constants::math::PI / 2.2 * helixPosPos.rC; // This assumes that the photon momentum vector is a tangent of the circle // o2-linter: disable=magic-number (geometrical assumption for propagation) - auto propTrackPos = getPropMomentumFromTrackHelix(arcLenghtPos, pos, helixPosPos, d_bz / 10., phiHelix - pos.phi()); - - phiv = o2::aod::pwgem::dilepton::utils::pairutil::getPhivPair(propTrackPos[0], propTrackPos[1], propTrackPos[2], propTrackEle[0], propTrackEle[1], propTrackEle[2], pos.sign(), ele.sign(), d_bz); - psipair = o2::aod::pwgem::dilepton::utils::pairutil::getPsiPair(propTrackPos[0], propTrackPos[1], propTrackPos[2], propTrackEle[0], propTrackEle[1], propTrackEle[2]); - - // Store values for later comparison - phivFast = phiv; - psipairFast = psipair; - } - // This uses the full propagation including material effects - if (modeTrackPropagation != TrackPropMode::kFast) { - std::array offsetsR = {propV0LegsRadius, 30.f, 10.f}; - bool pPropagatedSuccess = false; - bool nPropagatedSuccess = false; - auto pTrackProp = pTrack; - auto nTrackProp = nTrack; - for (const auto& offsetR : offsetsR) { - pTrackProp = pTrack; - pTrackProp.setPID(o2::track::PID::Electron); - nTrackProp = nTrack; - nTrackProp.setPID(o2::track::PID::Electron); - - o2::base::Propagator::Instance()->propagateToDCABxByBz({collision.posX(), collision.posY(), collision.posZ()}, pTrackProp, 2.f, matCorr, &dcaInfo); - o2::base::Propagator::Instance()->propagateToDCABxByBz({collision.posX(), collision.posY(), collision.posZ()}, nTrackProp, 2.f, matCorr, &dcaInfo); - - pPropagatedSuccess = o2::base::Propagator::Instance()->propagateToR(pTrackProp, baseR + offsetR); - nPropagatedSuccess = o2::base::Propagator::Instance()->propagateToR(nTrackProp, baseR + offsetR); - - if (pPropagatedSuccess && nPropagatedSuccess) { - KFPTrack kfp_track_posProp = createKFPTrackFromTrackParCov(pTrackProp, pos.sign(), pos.tpcNClsFound(), pos.tpcChi2NCl()); - KFPTrack kfp_track_eleProp = createKFPTrackFromTrackParCov(nTrackProp, ele.sign(), ele.tpcNClsFound(), ele.tpcChi2NCl()); - phiv = o2::aod::pwgem::dilepton::utils::pairutil::getPhivPair(kfp_track_posProp.GetPx(), kfp_track_posProp.GetPy(), kfp_track_posProp.GetPz(), kfp_track_eleProp.GetPx(), kfp_track_eleProp.GetPy(), kfp_track_eleProp.GetPz(), pos.sign(), ele.sign(), d_bz); - psipair = o2::aod::pwgem::dilepton::utils::pairutil::getPsiPair(kfp_track_posProp.GetPx(), kfp_track_posProp.GetPy(), kfp_track_posProp.GetPz(), kfp_track_eleProp.GetPx(), kfp_track_eleProp.GetPy(), kfp_track_eleProp.GetPz()); - break; - } - LOG(debug) << "Propagation to offset" << offsetR << " cm failed for " << (pPropagatedSuccess ? "negative" : "positive") << " track. Trying smaller offset."; - } - if (filltable && modeTrackPropagation == TrackPropMode::kBoth) { - registry.fill(HIST("V0/hPhivPropagationCompare"), phiv, phivFast); - registry.fill(HIST("V0/hPsiPairPropagationCompare"), psipair, psipairFast); - } - } - - if (phiv == 999.f || psipair == 999.f) { // o2-linter: disable=magic-number (nonsensical default value) - LOG(debug) << "Propagation failed for all radii (" << propV0LegsRadius << ", 30, 10 cm). Using default values for phiv and psipair (999.f)."; + mLastFitReject = __LINE__; // rejection point (diagnostic) // RZ line cut + return; } KFPTrack kfp_track_pos = createKFPTrackFromTrackParCov(pTrack, pos.sign(), pos.tpcNClsFound(), pos.tpcChi2NCl()); @@ -993,8 +1261,10 @@ struct PhotonConversionBuilder { if (v0cuts.kfMassConstrain > -0.1) { // o2-linter: disable=magic-number (nonsensical default value) gammaKF.SetNonlinearMassConstraint(v0cuts.kfMassConstrain); } - KFPVertex kfpVertex = createKFPVertexFromCollision(collision); + KFPVertex kfpVertex = opt.pv ? createKFPVertexFromCollision(*opt.pv) : createKFPVertexFromCollision(collision); KFParticle KFPV(kfpVertex); + const float chi2NDFVtx = gammaKF.GetNDF() > 0 ? gammaKF.GetChi2() / gammaKF.GetNDF() : -1.f; // before the PV constraint + const float chi2PV = gammaKF.GetDeviationFromVertex(KFPV); // Transport the gamma to the recalculated decay vertex KFParticle gammaKF_DecayVtx = gammaKF; // with respect to (0,0,0) @@ -1003,22 +1273,27 @@ struct PhotonConversionBuilder { float cospa_kf = cpaFromKF(gammaKF_DecayVtx, KFPV); if (!ele.hasITS() && !pos.hasITS()) { if (cospa_kf < v0cuts.min_v0cospa_tpconly) { + mLastFitReject = __LINE__; // rejection point return; } } else { if (cospa_kf < v0cuts.min_v0cospa_its) { + mLastFitReject = __LINE__; // rejection point return; } } float rxy = RecoDecay::sqrtSumOfSquares(gammaKF_DecayVtx.GetX(), gammaKF_DecayVtx.GetY()); if (rxy > trackcuts.maxX + v0cuts.margin_r_tpc) { + mLastFitReject = __LINE__; // rejection point return; } if (rxy < std::fabs(gammaKF_DecayVtx.GetZ()) * std::tan(2 * std::atan(std::exp(-v0cuts.max_eta_v0))) - v0cuts.margin_z) { - return; // RZ line cut + mLastFitReject = __LINE__; // rejection point // RZ line cut + return; } if (rxy < v0cuts.min_v0radius || v0cuts.max_v0radius < rxy) { + mLastFitReject = __LINE__; // rejection point return; } @@ -1040,27 +1315,33 @@ struct PhotonConversionBuilder { if (pos.hasITS() && ele.hasITS()) { // ITSonly-ITSonly, ITSTPC-ITSTPC, ITSTPC-ITSonly if (rxy > std::min(pTrack.getX(), nTrack.getX()) + v0cuts.margin_r_its) { + mLastFitReject = __LINE__; // rejection point return; } } else if (!pos.hasITS() && ele.hasITS()) { // ITSTPC-TPC if (rxy > std::min(83.f, nTrack.getX()) + v0cuts.margin_r_itstpc_tpc) { + mLastFitReject = __LINE__; // rejection point return; } } else if (pos.hasITS() && !ele.hasITS()) { // ITSTPC-TPC if (rxy > std::min(pTrack.getX(), 83.f) + v0cuts.margin_r_itstpc_tpc) { + mLastFitReject = __LINE__; // rejection point return; } } else if (!pos.hasITS() && !ele.hasITS()) { // TPC-TPC if (rxy > std::min(83.f, 83.f) + v0cuts.margin_r_tpc) { + mLastFitReject = __LINE__; // rejection point return; } } if ((!pos.hasITS() || !ele.hasITS()) && rxy < v0cuts.max_r_req_its) { // conversion points smaller than v0cuts.max_r_req_its have to be detected with ITS hits. + mLastFitReject = __LINE__; // rejection point return; } if ((!pos.hasITS() && !ele.hasITS()) && rxy < v0cuts.min_r_tpconly) { // TPConly tracks can detect conversion points larger than v0cuts.min_r_tpconly. + mLastFitReject = __LINE__; // rejection point return; } @@ -1079,13 +1360,91 @@ struct PhotonConversionBuilder { // LOGF(info, "gammaKF_PV.GetPz() = %f, gammaKF_DecayVtx.GetPz() = %f, gammaKF_DecayVtx2.GetPz() = %f", gammaKF_PV.GetPz(), gammaKF_DecayVtx.GetPz(), gammaKF_DecayVtx2.GetPz()); if (std::fabs(v0eta) > v0cuts.max_eta_v0 || v0pt < v0cuts.min_pt_v0) { + mLastFitReject = __LINE__; // rejection point return; } if (isITSonlyTrack(ele) && isITSonlyTrack(pos) && v0pt > v0cuts.max_pt_v0_itsonly) { + mLastFitReject = __LINE__; // rejection point return; } + // phi_V and psi_pair (4 material propagations) are only stored with the candidate: computed after the cuts (CPU; identical values) + float phiv = 999.f; + float psipair = 999.f; + float phivFast = 999.f; + float psipairFast = 999.f; + float baseR = std::hypot(xyz[0], xyz[1]); + // This method uses the track Helix instead of the full propagation. + // Hence, it is only an approximation but much faster + if (modeTrackPropagation != TrackPropMode::kProper) { + + o2::track::TrackAuxPar helixPosEle(nTrack, d_bz); + o2::track::TrackAuxPar helixPosPos(pTrack, d_bz); + + float diffX = helixPosEle.xC - helixPosPos.xC; + float diffY = helixPosEle.yC - helixPosPos.yC; + auto phiHelix = RecoDecay::constrainAngle(std::atan2(diffY, diffX) - o2::constants::math::PI / 2.); + + // Electron + float arcLenghtEle = helixPosEle.rC * 0.9 > propV0LegsRadius ? std::asin(propV0LegsRadius / helixPosEle.rC) * helixPosEle.rC : o2::constants::math::PI / 2.2 * helixPosEle.rC; // This assumes that the photon momentum vector is a tangent of the circle // o2-linter: disable=magic-number (geometrical assumption for propagation) + auto propTrackEle = getPropMomentumFromTrackHelix(arcLenghtEle, ele, helixPosEle, d_bz / 10., phiHelix - ele.phi()); + // Positron + float arcLenghtPos = helixPosPos.rC * 0.9 > propV0LegsRadius ? std::asin(propV0LegsRadius / helixPosPos.rC) * helixPosPos.rC : o2::constants::math::PI / 2.2 * helixPosPos.rC; // This assumes that the photon momentum vector is a tangent of the circle // o2-linter: disable=magic-number (geometrical assumption for propagation) + auto propTrackPos = getPropMomentumFromTrackHelix(arcLenghtPos, pos, helixPosPos, d_bz / 10., phiHelix - pos.phi()); + + phiv = o2::aod::pwgem::dilepton::utils::pairutil::getPhivPair(propTrackPos[0], propTrackPos[1], propTrackPos[2], propTrackEle[0], propTrackEle[1], propTrackEle[2], pos.sign(), ele.sign(), d_bz); + psipair = o2::aod::pwgem::dilepton::utils::pairutil::getPsiPair(propTrackPos[0], propTrackPos[1], propTrackPos[2], propTrackEle[0], propTrackEle[1], propTrackEle[2]); + + // Store values for later comparison + phivFast = phiv; + psipairFast = psipair; + } + // This uses the full propagation including material effects + if (modeTrackPropagation != TrackPropMode::kFast) { + std::array offsetsR = {propV0LegsRadius, 30.f, 10.f}; + bool pPropagatedSuccess = false; + bool nPropagatedSuccess = false; + auto pTrackProp = pTrack; + auto nTrackProp = nTrack; + for (const auto& offsetR : offsetsR) { + // the legs at their DCA to the vertex: the same propagation as for the DCA cut above (reused when it succeeded there) + if (posAtDca) { + pTrackProp = *posAtDca; + } else { + pTrackProp = pTrack; + pTrackProp.setPID(o2::track::PID::Electron); + o2::base::Propagator::Instance()->propagateToDCABxByBz({pvX, pvY, pvZ}, pTrackProp, 2.f, matCorr, &dcaInfo); + } + if (eleAtDca) { + nTrackProp = *eleAtDca; + } else { + nTrackProp = nTrack; + nTrackProp.setPID(o2::track::PID::Electron); + o2::base::Propagator::Instance()->propagateToDCABxByBz({pvX, pvY, pvZ}, nTrackProp, 2.f, matCorr, &dcaInfo); + } + + pPropagatedSuccess = o2::base::Propagator::Instance()->propagateToR(pTrackProp, baseR + offsetR); + nPropagatedSuccess = o2::base::Propagator::Instance()->propagateToR(nTrackProp, baseR + offsetR); + + if (pPropagatedSuccess && nPropagatedSuccess) { + KFPTrack kfp_track_posProp = createKFPTrackFromTrackParCov(pTrackProp, pos.sign(), pos.tpcNClsFound(), pos.tpcChi2NCl()); + KFPTrack kfp_track_eleProp = createKFPTrackFromTrackParCov(nTrackProp, ele.sign(), ele.tpcNClsFound(), ele.tpcChi2NCl()); + phiv = o2::aod::pwgem::dilepton::utils::pairutil::getPhivPair(kfp_track_posProp.GetPx(), kfp_track_posProp.GetPy(), kfp_track_posProp.GetPz(), kfp_track_eleProp.GetPx(), kfp_track_eleProp.GetPy(), kfp_track_eleProp.GetPz(), pos.sign(), ele.sign(), d_bz); + psipair = o2::aod::pwgem::dilepton::utils::pairutil::getPsiPair(kfp_track_posProp.GetPx(), kfp_track_posProp.GetPy(), kfp_track_posProp.GetPz(), kfp_track_eleProp.GetPx(), kfp_track_eleProp.GetPy(), kfp_track_eleProp.GetPz()); + break; + } + LOG(debug) << "Propagation to offset" << offsetR << " cm failed for " << (pPropagatedSuccess ? "negative" : "positive") << " track. Trying smaller offset."; + } + if (filltable && modeTrackPropagation == TrackPropMode::kBoth) { + registry.fill(HIST("V0/hPhivPropagationCompare"), phiv, phivFast); + registry.fill(HIST("V0/hPsiPairPropagationCompare"), psipair, psipairFast); + } + } + + if (phiv == 999.f || psipair == 999.f) { // o2-linter: disable=magic-number (nonsensical default value) + LOG(debug) << "Propagation failed for all radii (" << propV0LegsRadius << ", 30, 10 cm). Using default values for phiv and psipair (999.f)."; + } KFParticle kfp_pos_DecayVtx = kfp_pos; // Don't set Primary Vertex KFParticle kfp_ele_DecayVtx = kfp_ele; // Don't set Primary Vertex kfp_pos_DecayVtx.TransportToPoint(xyz.data()); // Don't set Primary Vertex @@ -1100,46 +1459,56 @@ struct PhotonConversionBuilder { if (!ele.hasITS() && !pos.hasITS()) { // V0s with TPConly-TPConly if (v0cuts.max_r_itsmft_ss < rxy && rxy < trackcuts.maxX + v0cuts.margin_r_tpc) { if (v0photoncandidate.getPCA() > v0cuts.max_dcav0dau_tpc_inner_fc) { + mLastFitReject = __LINE__; // rejection point return; } } else { if (v0photoncandidate.getPCA() > v0cuts.max_dcav0dau_tpconly) { + mLastFitReject = __LINE__; // rejection point return; } } } else if (opt.nonSVertexer && recovery.finderMaxPCAMixed > 0.f && pos.hasITS() != ele.hasITS()) { if (v0photoncandidate.getPCA() > recovery.finderMaxPCAMixed) { + mLastFitReject = __LINE__; // rejection point return; } } else { // V0s with ITS hits if (rxy < v0cuts.max_r_req_its) { if (v0photoncandidate.getPCA() > v0cuts.max_dcav0dau_itsibss) { + mLastFitReject = __LINE__; // rejection point return; } } else { if (v0photoncandidate.getPCA() > v0cuts.max_dcav0dau_its) { + mLastFitReject = __LINE__; // rejection point return; } } } if (isITSonlyTrack(pos) && v0photoncandidate.getPosPt() > trackcuts.maxpt_itsonly) { + mLastFitReject = __LINE__; // rejection point return; } if (isITSonlyTrack(ele) && v0photoncandidate.getElePt() > trackcuts.maxpt_itsonly) { + mLastFitReject = __LINE__; // rejection point return; } if (v0photoncandidate.getChi2NDF() > 6e+3) { // protection for uint16. // o2-linter: disable=magic-number (protection for uint16) + mLastFitReject = __LINE__; // rejection point return; } if (std::fabs(v0photoncandidate.getDcaXYToPV()) > v0cuts.max_dcatopv_xy_v0 || std::fabs(v0photoncandidate.getDcaZToPV()) > v0cuts.max_dcatopv_z_v0) { + mLastFitReject = __LINE__; // rejection point return; } if (!opt.relaxAP && !checkAP(v0photoncandidate.getAlpha(), v0photoncandidate.getQt(), v0cuts.max_alpha_ap, v0cuts.max_qt_ap)) { // store only photon conversions + mLastFitReject = __LINE__; // rejection point return; } @@ -1152,12 +1521,14 @@ struct PhotonConversionBuilder { const float score = dedupScore(v0photoncandidate.getCosPA(), v0photoncandidate.getPCA(), gap); if (opt.probe != nullptr) { *opt.probe = {.ok = true, .score = score}; + mLastFitReject = __LINE__; // rejection point (diagnostic) return; } const float purity = usePurity() ? purityScore(pos, ele, cospa_kf, v0photoncandidate.getPCA(), meeSV, gap, v0photoncandidate.getPsiPair()) : -999.f; if (opt.purityGate) { registry.fill(HIST("Finder/hScore"), purity); - if (purity <= recovery.finderMinScore) { + if (purity <= (opt.minPurity > -998.f ? opt.minPurity : recovery.finderMinScore.value)) { // o2-linter: disable=magic-number (unset marker) + mLastFitReject = __LINE__; // rejection point (diagnostic) return; } } @@ -1173,6 +1544,9 @@ struct PhotonConversionBuilder { v0Helper.svX = gammaKF_DecayVtx.GetX(); v0Helper.svY = gammaKF_DecayVtx.GetY(); v0Helper.svZ = gammaKF_DecayVtx.GetZ(); + v0Helper.pxAtSV = gammaKF_DecayVtx.GetPx(); + v0Helper.pyAtSV = gammaKF_DecayVtx.GetPy(); + v0Helper.pzAtSV = gammaKF_DecayVtx.GetPz(); v0Helper.purity = purity; v0Helper.cosPAxy = cospaXYKF; v0Helper.cosPA3D = cospa_kf; @@ -1199,6 +1573,7 @@ struct PhotonConversionBuilder { } } if (!isSelectedML) { + mLastFitReject = __LINE__; // rejection point return; } if (filltable) { @@ -1264,11 +1639,16 @@ struct PhotonConversionBuilder { cospa_kf, cospaXYKF, cospaRZKF, v0photoncandidate.getPCA(), v0photoncandidate.getAlpha(), v0photoncandidate.getQt(), v0photoncandidate.getChi2NDF()); v0photonsphivpsi(v0photoncandidate.getPhiV(), v0photoncandidate.getPsiPair()); + v0photonskfchi2(chi2NDFVtx, chi2PV); // v0photonskfcov(gammaKF_PV.GetCovariance(9), gammaKF_PV.GetCovariance(14), gammaKF_PV.GetCovariance(20), gammaKF_PV.GetCovariance(13), gammaKF_PV.GetCovariance(19), gammaKF_PV.GetCovariance(18)); fillTrackTable(pos, kfp_pos_DecayVtx, posdcaXY, posdcaZ); // positive leg first fillTrackTable(ele, kfp_ele_DecayVtx, eledcaXY, eledcaZ); // negative leg second + if (fillLegCov) { + fillLegCovTable(pTrack); + fillLegCovTable(nTrack); + } } // end of fill table } @@ -1278,6 +1658,20 @@ struct PhotonConversionBuilder { return CandKey{.v0Id = c.v0ID, .collisionId = c.colID, .posId = c.posID, .eleId = c.eleID}; } + // MC diagnostics: the trace of a leg pair of one genuine photon, nullptr for any other pair or without diagnostics + TrueLegTrace* trueLegTrace(const int64_t posId, const int64_t eleId) + { + if (mDiag == nullptr) { + return nullptr; + } + const auto ip = mLegMother.find(posId); + const auto ie = mLegMother.find(eleId); + if (ip == mLegMother.end() || ie == mLegMother.end() || ip->second != ie->second) { + return nullptr; + } + return &mDiag->trueLegTrace[{posId, eleId}]; + } + std::map candidateIndexByKey() const { std::map index; @@ -1302,7 +1696,6 @@ struct PhotonConversionBuilder { } } - // photon finder // finder leg: helix circle and the time window in which the track can belong to a collision (TPC-only legs: from the drift-time asymmetry) template bool makeFinderLeg(TTrack const& track, TCollisions const& collisions, const int64_t bcRef, FinderLeg& leg) @@ -1345,9 +1738,10 @@ struct PhotonConversionBuilder { { std::set usedLegs; std::set> builtPairs; - // legs of accepted candidates are not free; with finderFreeCollinearLegs, legs of collinear candidate pairs stay free - std::vector collinear(vecV0Dedup.size(), false); - if (recovery.finderFreeCollinearLegs > 0.f) { + // legs of accepted candidates are not free; with finderFreeCollinearLegs, legs of collinear candidate pairs stay free; with + // finderFreeAllLegs no leg is blocked (only pairs already built are skipped) + std::vector collinear(vecV0Dedup.size(), recovery.finderFreeAllLegs.value); + if (recovery.finderFreeCollinearLegs > 0.f && !recovery.finderFreeAllLegs) { std::map> candsOfColl; for (size_t i = 0; i < vecV0Dedup.size(); ++i) { candsOfColl[vecV0Dedup[i].colID].push_back(i); @@ -1368,9 +1762,13 @@ struct PhotonConversionBuilder { } } } + std::vector inCandidate(tracks.size(), 0); // CPU: builtPairs is looked up only when both legs are in a candidate for (size_t i = 0; i < vecV0Dedup.size(); ++i) { const auto& c = vecV0Dedup[i]; builtPairs.emplace(c.posID, c.eleID); + if (c.posID >= 0 && c.posID < static_cast(inCandidate.size()) && c.eleID >= 0 && c.eleID < static_cast(inCandidate.size())) { + inCandidate[c.posID] = inCandidate[c.eleID] = 1; + } if (!collinear[i]) { usedLegs.insert(c.posID); usedLegs.insert(c.eleID); @@ -1423,25 +1821,100 @@ struct PhotonConversionBuilder { const FinderCuts cuts{recovery.finderMaxDeltaTgl, recovery.finderMaxCircleGap, v0cuts.min_r_tpconly, v0cuts.max_r_req_its, recovery.finderRMargin, trackcuts.maxX + v0cuts.margin_r_tpc, recovery.finderMinCosXY, recovery.finderMaxDeltaZ, recovery.finderMaxDz, d_bz}; - const FitOptions finderFit{.nonSVertexer = true, .purityGate = true}; + const FitOptions finderFit{.nonSVertexer = !recovery.finderUniformFit.value, .purityGate = !recovery.finderUniformFit.value}; const size_t nBefore = vecV0Dedup.size(); - for (const auto& p : posLegs) { - for (auto itE = std::lower_bound(eleLegs.begin(), eleLegs.end(), p.tgl - recovery.finderMaxDeltaTgl, [](const FinderLeg& c, float v) { return c.tgl < v; }); - itE != eleLegs.end() && itE->tgl <= p.tgl + recovery.finderMaxDeltaTgl; ++itE) { - if ((!p.noITS && !itE->noITS) || builtPairs.contains({p.id, itE->id})) { - continue; + std::array stepCount{}; // filled into Finder/hSteps once per DF (CPU: one fill per pair dominated on data) + auto isBuilt = [&](int64_t a, int64_t b) { return inCandidate[a] && inCandidate[b] && builtPairs.contains({a, b}); }; + // one leg pair, in the order of the original loop (positive legs in track order, negative legs in tgl order) + auto tryPair = [&](FinderLeg const& p, FinderLeg const& e) { + TrueLegTrace* trace = nullptr; + if constexpr (isMC) { + trace = trueLegTrace(p.id, e.id); + } + if ((!p.noITS && !e.noITS) || isBuilt(p.id, e.id)) { + if (trace != nullptr) { + trace->finderStep = (!p.noITS && !e.noITS) ? -8 : -7; // o2-linter: disable=magic-number (trace codes) + } + return; + } + int64_t colId = -1; + const int step = evalFinderPair(p, e, colls, cuts, xB, yB, vDrift, colId); + if (step >= 0 && step < FinderStep::kNFindStep) { + stepCount[step] += 1.; + } + if (trace != nullptr) { + trace->finderStep = step; + } + if (step == FinderStep::kFindCollision) { + fitV0Candidate(collisions.rawIteratorAt(colId), tracks.rawIteratorAt(p.id), tracks.rawIteratorAt(e.id), -1, false, finderFit); + if (trace != nullptr) { + trace->finderLine = mLastFitReject; + } + } + }; + const auto tglLess = [](const FinderLeg& c, float v) { return c.tgl < v; }; + // time-binning of the negative legs: the time window of a positive leg is intersected with the time windows of the negative legs in the same bin. Reason for implementation: faster than checking all negative legs for each positive leg + const bool timeBinned = (!isMC || mDiag == nullptr) && !eleLegs.empty(); + std::vector> eleInBin; + double tBin0 = 0.; + const double binWidth = 5000.; // ns + if (timeBinned) { + double tLoAll = eleLegs.front().tMin, tHiAll = eleLegs.front().tMax; + for (const auto& e : eleLegs) { + tLoAll = std::min(tLoAll, e.tMin); + tHiAll = std::max(tHiAll, e.tMax); + } + tBin0 = tLoAll; + eleInBin.resize(static_cast((tHiAll - tLoAll) / binWidth) + 1); + for (uint32_t k = 0; k < eleLegs.size(); ++k) { // k increasing: every bin stays sorted by index = tgl order + const auto b0 = static_cast((eleLegs[k].tMin - tBin0) / binWidth); + const auto b1 = std::min(static_cast((eleLegs[k].tMax - tBin0) / binWidth), eleInBin.size() - 1); + for (size_t b = b0; b <= b1; ++b) { + eleInBin[b].push_back(k); } - int64_t colId = -1; - const int step = evalFinderPair(p, *itE, colls, cuts, xB, yB, vDrift, colId); - if (step >= 0) { - registry.fill(HIST("Finder/hSteps"), static_cast(step)); + } + } + std::vector tried; + for (const auto& p : posLegs) { + const auto lo = static_cast(std::lower_bound(eleLegs.begin(), eleLegs.end(), p.tgl - recovery.finderMaxDeltaTgl, tglLess) - eleLegs.begin()); + if (!timeBinned) { + for (uint32_t k = lo; k < eleLegs.size() && eleLegs[k].tgl <= p.tgl + recovery.finderMaxDeltaTgl; ++k) { + tryPair(p, eleLegs[k]); } - if (step == FinderStep::kFindCollision) { - fitV0Candidate(collisions.rawIteratorAt(colId), tracks.rawIteratorAt(p.id), tracks.rawIteratorAt(itE->id), -1, false, finderFit); + continue; + } + const auto hi = static_cast(std::upper_bound(eleLegs.begin(), eleLegs.end(), p.tgl + recovery.finderMaxDeltaTgl, [](float v, const FinderLeg& c) { return v < c.tgl; }) - eleLegs.begin()); + if (lo >= hi || p.tMax < tBin0) { + continue; + } + tried.clear(); + const auto b0 = static_cast(std::max(0., (p.tMin - tBin0) / binWidth)); + const auto b1 = std::min(static_cast((p.tMax - tBin0) / binWidth), eleInBin.size() - 1); + for (size_t b = b0; b <= b1 && b < eleInBin.size(); ++b) { + const auto& v = eleInBin[b]; + for (auto it = std::lower_bound(v.begin(), v.end(), lo); it != v.end() && *it < hi; ++it) { + tried.push_back(*it); } } + if (b1 > b0) { + std::sort(tried.begin(), tried.end()); + tried.erase(std::unique(tried.begin(), tried.end()), tried.end()); + } + for (const auto k : tried) { + tryPair(p, eleLegs[k]); + } + } + for (int st = 0; st < FinderStep::kNFindStep; ++st) { + if (stepCount[st] > 0.) { + registry.fill(HIST("Finder/hSteps"), static_cast(st), stepCount[st]); + } } registry.fill(HIST("Finder/hNAdded"), static_cast(vecV0Dedup.size() - nBefore)); + if (mDiag != nullptr) { + for (size_t i = nBefore; i < vecV0Dedup.size(); ++i) { + mDiag->finderKeys.insert(keyOf(vecV0Dedup[i])); + } + } } // two stored photons that use two pieces of one track (same tgl, compatible parameters): keep the better one @@ -1468,21 +1941,39 @@ struct PhotonConversionBuilder { const float chi2 = splitPieceChi2(trA, trB, d_bz); return chi2 >= 0.f && chi2 < dedup.splitLegMaxChi2; }; - std::vector> keptPos, keptEle; + // the keptPosOf and keptEleOf maps are keyed by collisionId (or 0 for mixed instances) and contain a map of tgl to the kept leg information + struct KeptLeg { + int64_t id; + size_t owner; + size_t order; + }; + std::unordered_map> keptPosOf, keptEleOf; + std::unordered_map tglOf; + auto tgl = [&](int64_t id) { + const auto it = tglOf.find(id); + return it != tglOf.end() ? it->second : (tglOf[id] = tracks.rawIteratorAt(id).tgl()); + }; + auto firstOwner = [&](std::multimap const& kept, int64_t id) { + const float t = tgl(id); + size_t bestOrder = std::numeric_limits::max(); + int64_t owner = -1; + for (auto it = kept.lower_bound(t - dedup.splitLegMaxDTgl); it != kept.end() && it->first <= t + dedup.splitLegMaxDTgl; ++it) { + if (it->second.order < bestOrder && samePiece(id, it->second.id)) { + bestOrder = it->second.order; + owner = static_cast(it->second.owner); + } + } + return owner; + }; std::vector removed(stored_fullv0Ids.size(), 0); int nRemoved = 0; for (const auto& [rank, k] : order) { const auto& key = stored_fullv0Ids[k]; - int64_t owner = -1; - for (size_t m = 0; m < keptPos.size() && owner < 0; ++m) { - if (samePiece(key.posId, keptPos[m].first)) { - owner = static_cast(keptPos[m].second); - } - } - for (size_t m = 0; m < keptEle.size() && owner < 0; ++m) { - if (samePiece(key.eleId, keptEle[m].first)) { - owner = static_cast(keptEle[m].second); - } + auto& keptPos = keptPosOf[kMixedInstance ? static_cast(key.collisionId) : 0]; + auto& keptEle = keptEleOf[kMixedInstance ? static_cast(key.collisionId) : 0]; + int64_t owner = firstOwner(keptPos, key.posId); + if (owner < 0) { + owner = firstOwner(keptEle, key.eleId); } if (owner >= 0) { removed[k] = 1; @@ -1492,8 +1983,8 @@ struct PhotonConversionBuilder { } continue; } - keptPos.emplace_back(key.posId, k); - keptEle.emplace_back(key.eleId, k); + keptPos.emplace(tgl(key.posId), KeptLeg{key.posId, k, keptPos.size()}); + keptEle.emplace(tgl(key.eleId), KeptLeg{key.eleId, k, keptEle.size()}); } if (nRemoved > 0) { std::vector kept; @@ -1521,14 +2012,24 @@ struct PhotonConversionBuilder { storedOfColl[key.collisionId].push_back(it->second); } } - // the AP cut is the main reason the true pairing of a crossed group was not built - auto fitPair = [&](int64_t colId, int64_t posId, int64_t eleId, PairFit* probe) { - fitV0Candidate(collisions.rawIteratorAt(colId), tracks.rawIteratorAt(posId), tracks.rawIteratorAt(eleId), -1, false, - FitOptions{.nonSVertexer = true, .relaxAP = true, .probe = probe}); + auto fitPair = [&](int64_t colId, int64_t posId, int64_t eleId, PairFit* probe, bool original = false) { + const auto pos = tracks.rawIteratorAt(posId); + SourcePV srcPv; + const SourcePV* pv = nullptr; + if constexpr (requires { pos.sourcePvX(); }) { + if (original && mixed.refitOriginalsAtSourceVertex) { + srcPv = SourcePV{pos.sourcePvX(), pos.sourcePvY(), pos.sourcePvZ(), pos.sourcePvCovXX(), pos.sourcePvCovXY(), pos.sourcePvCovXZ(), pos.sourcePvCovYY(), pos.sourcePvCovYZ(), pos.sourcePvCovZZ(), pos.sourcePvChi2(), pos.sourcePvNContrib()}; + pv = &srcPv; + } + } + const auto ele = tracks.rawIteratorAt(eleId); + const bool finderLike = mixed.uniformPairFit ? !(pos.hasITS() && ele.hasITS()) : !recovery.finderUniformFit.value; // uniformPairFit (run 2): options by the legs + fitV0Candidate(collisions.rawIteratorAt(colId), pos, ele, -1, false, + FitOptions{.nonSVertexer = finderLike, .relaxAP = finderLike, .purityGate = mixed.uniformPairFit && finderLike && recovery.runFinder.value, .probe = probe, .pv = pv}); }; - auto probeFit = [&](int64_t colId, int64_t posId, int64_t eleId) { + auto probeFit = [&](int64_t colId, int64_t posId, int64_t eleId, bool original = false) { PairFit out; - fitPair(colId, posId, eleId, &out); + fitPair(colId, posId, eleId, &out, original); return out; }; auto crossedPrefilter = [&](int64_t posId, int64_t eleId) { @@ -1546,7 +2047,7 @@ struct PhotonConversionBuilder { auto it = storedFit.find(idx); if (it == storedFit.end()) { const auto& c = vecV0Dedup[idx]; - it = storedFit.emplace(idx, probeFit(c.colID, c.posID, c.eleID)).first; + it = storedFit.emplace(idx, probeFit(c.colID, c.posID, c.eleID, true)).first; } return it->second; }; @@ -1572,12 +2073,19 @@ struct PhotonConversionBuilder { } const PairFit storedI = storedFitOf(members[a]); const PairFit storedJ = storedFitOf(members[b]); - const PairFit crossedI = crossedPrefilter(ci.posID, cj.eleID) ? probeFit(colId, ci.posID, cj.eleID) : PairFit{}; - const PairFit crossedJ = crossedPrefilter(cj.posID, ci.eleID) ? probeFit(colId, cj.posID, ci.eleID) : PairFit{}; + const bool preI = crossedPrefilter(ci.posID, cj.eleID); + const PairFit crossedI = preI ? probeFit(colId, ci.posID, cj.eleID) : PairFit{}; + const int lineI = preI && !crossedI.ok ? mLastFitReject : 0; + const bool preJ = crossedPrefilter(cj.posID, ci.eleID); + const PairFit crossedJ = preJ ? probeFit(colId, cj.posID, ci.eleID) : PairFit{}; + const int lineJ = preJ && !crossedJ.ok ? mLastFitReject : 0; Test t; t.rec = {keyOf(ci), keyOf(cj), q, storedI.ok && storedJ.ok && crossedI.ok && crossedJ.ok, false}; + t.rec.fail = (storedI.ok ? 0 : 1) | (storedJ.ok ? 0 : 2) | (preI ? 0 : 4) | (preJ ? 0 : 8) | (preI && !crossedI.ok ? 16 : 0) | (preJ && !crossedJ.ok ? 32 : 0); // o2-linter: disable=magic-number (bit mask) + t.rec.line = lineI > 0 ? lineI : lineJ; if (t.rec.complete) { // gain > 0: the crossed pairing has the lower (better) summed dedup score t.gain = (storedI.score + storedJ.score) - (crossedI.score + crossedJ.score); + t.rec.gain = t.gain; } tests.push_back(t); } @@ -1653,6 +2161,10 @@ struct PhotonConversionBuilder { Preslice perCollision = o2::aod::v0::collisionId; std::map pca_map; // (v0.globalIndex(), collision.globalIndex(), pos.globalIndex(), ele.globalIndex()) -> pca std::map cospa_map; // (v0.globalIndex(), collision.globalIndex(), pos.globalIndex(), ele.globalIndex()) -> cospa + int mLastFitReject = 0; // source line of the last rejection in fitV0Candidate, 0 = accepted (diagnostic) + DedupDiag* mDiag = nullptr; // run 1 with MC diagnostics: where the finder and fillV0Table record the genuine leg pairs + std::unordered_map mLegMother; // MC: track -> first mother of its MC electron (genuine leg pairs share it) + std::unordered_map mOccOfSourceEvent; // run 2: mixer event uid -> occupancy (from its same-event artificial event) std::vector vecV0Dedup; // vector with all V0Candidates that is used to sort them by score (see struct V0CandidateHelper for more details of content) std::vector> stored_v0Ids; // (pos.globalIndex(), ele.globalIndex()) std::vector stored_fullv0Ids; // (v0.globalIndex(), collision.globalIndex(), pos.globalIndex(), ele.globalIndex()) @@ -1744,6 +2256,11 @@ struct PhotonConversionBuilder { stored_v0Ids.clear(); stored_fullv0Ids.clear(); nv0_map.clear(); + mGroupOfKey.clear(); + mPermuteResults.clear(); + if constexpr (kMixedInstance) { + mGroupSizeOfCand.assign(nCand, 0); + } int nGreedyFallback = 0; // conflict groups: candidates connected through a shared leg @@ -1813,9 +2330,22 @@ struct PhotonConversionBuilder { std::make_pair(sol.bestCount - sol.bestWithCount[k], sol.bestWithScore[k] - sol.bestScore); } } + if constexpr (kMixedInstance) { // permutation test: the same group in reversed order must select the same candidates + const std::vector legMaskRev(legMask.rbegin(), legMask.rend()); + const std::vector scoresRev(scores.rbegin(), scores.rend()); + const auto solRev = solveConflictGroup(legMaskRev, scoresRev, dedup.groupMatchingScoreThreshold.value, false); + bool changed = false; + for (int k = 0; k < nMembers; ++k) { + changed = changed || (((solRev.bestMask & (1u << (nMembers - 1 - k))) != 0) != (selected[k] != 0)); + } + mPermuteResults.push_back({vecV0Dedup[members[0]].colID, nMembers, changed, false}); + } } else { // Too large to enumerate: greedy by score ++nGreedyFallback; + if constexpr (kMixedInstance) { + mPermuteResults.push_back({vecV0Dedup[members[0]].colID, nMembers, false, true}); + } std::vector usedTracks; usedTracks.reserve(2 * nMembers); for (int k = 0; k < nMembers; ++k) { @@ -1830,15 +2360,76 @@ struct PhotonConversionBuilder { } } - // store the winners, book the blockers of the losers - for (int k = 0; k < nMembers; ++k) { - const auto& cand = vecV0Dedup[members[k]]; - if (selected[k] != 0) { - stored_v0Ids.emplace_back(cand.posID, cand.eleID); - stored_fullv0Ids.push_back(keyOf(cand)); - nv0_map[cand.colID]++; - continue; - } + // run 1 (MC diagnostic of the swap decision): half-swap rule: if a candidate is worse than two others that share its legs, and the crossed candidate is not present, replace it + if (dedup.dedupHalfSwapRule && nMembers >= 3) { + int nReplaced = 0; + for (int t = 0; t < nMembers; ++t) { + if (selected[t] != 0) { + continue; + } + const auto& cT = vecV0Dedup[members[t]]; + int w1 = -1, w2 = -1; + for (int k = 0; k < nMembers; ++k) { + if (selected[k] == 0 || k == t) { + continue; + } + const auto& cW = vecV0Dedup[members[k]]; + if (cW.posID == cT.posID) { + w1 = k; + } + if (cW.eleID == cT.eleID) { + w2 = k; + } + } + if (w1 < 0 || w2 < 0 || w1 == w2) { + continue; + } + const auto& cW1 = vecV0Dedup[members[w1]]; + const auto& cW2 = vecV0Dedup[members[w2]]; + if (!(cT.score < std::min(cW1.score, cW2.score))) { + continue; + } + bool complementIsCandidate = false; + for (int k = 0; k < nMembers; ++k) { + const auto& cC = vecV0Dedup[members[k]]; + if (cC.posID == cW2.posID && cC.eleID == cW1.eleID) { + complementIsCandidate = true; + break; + } + } + if (complementIsCandidate) { + continue; + } + selected[w1] = 0; + selected[w2] = 0; + selected[t] = 1; + ++nReplaced; + } + registry.fill(HIST("Dedup/hHalfSwapRule"), static_cast(nReplaced)); + } + + std::array groupInfo{root, nMembers, 0, static_cast(std::count(selected.begin(), selected.end(), 1))}; + if constexpr (kMixedInstance) { + std::set legsOfGroup; + for (const int& m : members) { + legsOfGroup.insert(vecV0Dedup[m].posID); + legsOfGroup.insert(vecV0Dedup[m].eleID); + mGroupSizeOfCand[m] = nMembers; + } + groupInfo[2] = static_cast(legsOfGroup.size()); + } + // store the winners, book the blockers of the losers + for (int k = 0; k < nMembers; ++k) { + const auto& cand = vecV0Dedup[members[k]]; + if (selected[k] != 0) { + stored_v0Ids.emplace_back(cand.posID, cand.eleID); + stored_fullv0Ids.push_back(keyOf(cand)); + nv0_map[cand.colID]++; + if constexpr (kMixedInstance) { + mGroupOfKey[keyOf(cand)] = groupInfo; + } + continue; + } if (diag == nullptr) { continue; } @@ -1919,7 +2510,22 @@ struct PhotonConversionBuilder { void build(TCollisions const& collisions, TV0s const& v0s, TTracks const& tracks, TBCs const&, DedupDiag* diag = nullptr) { mRelaxedKeys.clear(); - // step 1: SVertexer V0s -> dedup candidates + mLegDcaCache.clear(); + mDiag = diag; + mLegMother.clear(); + if constexpr (isMC) { + if (diag != nullptr) { // genuine leg pairs = opposite-sign MC electrons with the same first mother (MCDedup/Inventory/hSwapOrigin) + for (const auto& track : tracks) { + if (!track.has_mcParticle()) { + continue; + } + const auto mc = track.template mcParticle_as(); + if (std::abs(mc.pdgCode()) == kElectron && mc.has_mothers()) { + mLegMother[track.globalIndex()] = mc.mothersIds()[0]; + } + } + } + } for (const auto& collision : collisions) { if constexpr (isMC) { if (!collision.has_mcCollision()) { @@ -1954,7 +2560,6 @@ struct PhotonConversionBuilder { } // end of v0 loop } // end of collision loop - // photon finder if (recovery.runFinder) { runPhotonFinder(collisions, tracks); } @@ -1962,7 +2567,6 @@ struct PhotonConversionBuilder { stored_v0Ids.reserve(pca_map.size()); // number of photon candidates per DF stored_fullv0Ids.reserve(pca_map.size()); // number of photon candidates per DF - // step 3: deduplication switch (dedup.deduplicationMode.value) { case V0DeduplicationMode::Pairwise: selectPairwise(diag); @@ -1978,7 +2582,6 @@ struct PhotonConversionBuilder { break; } - // split legs, ambiguity resolver (on the stored photons only) if (dedup.resolveSplitLegs) { resolveSplitLegConflicts(tracks, diag); } @@ -2027,7 +2630,6 @@ struct PhotonConversionBuilder { } } - // refit the stored photons and write the tables (SVertexer V0s through fillV0Table, the others directly) for (const auto& fullv0Id : stored_fullv0Ids) { auto v0Id = fullv0Id.v0Id; const auto collisionId = fullv0Id.collisionId; @@ -2058,8 +2660,11 @@ struct PhotonConversionBuilder { } } fitV0Candidate(collision, tracks.rawIteratorAt(fullv0Id.posId), tracks.rawIteratorAt(fullv0Id.eleId), v0Id, true, - FitOptions{.nonSVertexer = true, .relaxAP = mRelaxedKeys.contains(fullv0Id)}); + FitOptions{.nonSVertexer = !recovery.finderUniformFit.value, .relaxAP = mRelaxedKeys.contains(fullv0Id) && !recovery.finderUniformFit.value}); } // end of fullv0Id loop + if (fillFreeLegs || fillFreeLegIds) { + writeFreeLegs(collisions, tracks, skimRejected); + } // for (const auto& collision : collisions) { // if constexpr (isMC) { @@ -2080,6 +2685,1235 @@ struct PhotonConversionBuilder { vecV0Dedup.clear(); } // end of build + // MC photon pair information for the deduplication diagnostics (run 1) and the mixer (run 2) + struct MixPairPhoton { + float px = 0.f, py = 0.f, pz = 0.f; + std::pair posKey{-1, -1}, eleKey{-1, -1}; + bool posTrue = false, eleTrue = false; + int64_t posRow = -1, eleRow = -1; // PCMMixLegs rows + bool original = true; // leg pairing of a run-1 photon (false: crossed pairing built in run 2) + std::array group{-1, 0, 0, 0}; // run-2 conflict group (id, candidates, legs, stored photons), id -1: unknown + float vx = 0.f, vy = 0.f, vz = 0.f; // conversion point (run 1 before, run 2 after) + struct LegMC { // MC of one leg for the fake-pair classes (ids within the DF of the source event) + int64_t evt = -1; + int pid = -1, mid = -1, gmid = -1, pdg = 0, mpdg = 0, trk = -1, anc = -1; + int ggid = -1, root = -1, rootPdg = 0; // mother of the grandmother, root of the decay chain of the photon + bool its = false; + float phi = 0.f, tgl = 0.f, sharedFrac = 0.f; // track direction and TPC shared-cluster fraction (data-computable) + float vx = 0.f, vy = 0.f, vz = 0.f; // run-1 conversion point of the photon the leg belonged to + }; + LegMC posMC, eleMC; + }; + template + static void setMixPairLegs(MixPairPhoton& p, TLeg const& pos, TLeg const& ele) + { + p.posKey = {pos.sourceEventUid(), pos.mcMotherId()}; + p.eleKey = {ele.sourceEventUid(), ele.mcMotherId()}; + p.posTrue = pos.mcMotherId() >= 0 && pos.mcMotherPdg() == kGamma; + p.eleTrue = ele.mcMotherId() >= 0 && ele.mcMotherPdg() == kGamma; + using LegMC = typename MixPairPhoton::LegMC; + auto legMC = [](TLeg const& l) { + return LegMC{l.sourceEventUid(), l.mcParticleId(), l.mcMotherId(), l.mcGrandMotherId(), l.mcPdg(), l.mcMotherPdg(), l.origTrackId(), l.mcAncestorId(), + l.mcGGMotherId(), l.mcRootId(), l.mcRootPdg(), static_cast(l.hasITS()), l.phi(), l.tgl(), l.tpcFractionSharedCls(), l.sourceVx(), l.sourceVy(), l.sourceVz()}; + }; + p.posMC = legMC(pos); + p.eleMC = legMC(ele); + } + + static int mixCommonOrigin(MixPairPhoton::LegMC const& x, MixPairPhoton::LegMC const& y) + { + if (x.mid < 0 || y.mid < 0 || (x.evt == y.evt && x.mid == y.mid)) { + return -1; + } + if (x.evt != y.evt) { + return 0; + } + if (x.gmid >= 0 && x.gmid == y.gmid) { + return 3; + } + if (x.ggid >= 0 && x.ggid == y.ggid) { + return 2; + } + return (x.root >= 0 && x.root == y.root) ? 1 : 0; + } + // common origin of the two MC photons whose legs form a photon pair (true pair, full or half swap): first leg vs the first leg of + // another MC photon among the remaining three + static int mixPairOrigin(MixPairPhoton const& a, MixPairPhoton const& b) + { + const auto& x = a.posMC; + for (const auto* y : {&a.eleMC, &b.posMC, &b.eleMC}) { + if (y->mid >= 0 && x.mid >= 0 && !(x.evt == y->evt && x.mid == y->mid)) { + return mixCommonOrigin(x, *y); + } + } + return -1; + } + + static bool mixRelated(MixPairPhoton const& a, MixPairPhoton const& b) + { + using L = MixPairPhoton::LegMC; + auto radiatedBy = [](L const& x, L const& y) { return x.evt == y.evt && x.gmid >= 0 && x.gmid == y.pid; }; + auto siblings = [](L const& x, L const& y) { return x.evt == y.evt && x.gmid >= 0 && x.gmid == y.gmid && x.mid != y.mid; }; + for (const auto* x : {&a.posMC, &a.eleMC}) { + for (const auto* y : {&b.posMC, &b.eleMC}) { + if (radiatedBy(*x, *y) || radiatedBy(*y, *x) || siblings(*x, *y)) { + return true; + } + } + } + return false; + } + + static int mixFakeClass(MixPairPhoton const& a, MixPairPhoton const& b) + { + using L = MixPairPhoton::LegMC; + const std::array la{&a.posMC, &a.eleMC}, lb{&b.posMC, &b.eleMC}; + auto samePart = [](L const& x, L const& y) { return x.evt == y.evt && x.pid >= 0 && x.pid == y.pid; }; + auto sameMother = [](L const& x, L const& y) { return x.evt == y.evt && x.mid >= 0 && x.mid == y.mid; }; + auto daughterOf = [](L const& x, L const& y) { return x.evt == y.evt && x.mid >= 0 && x.mid == y.pid; }; + auto truePhoton = [&](MixPairPhoton const& p) { return p.posMC.mpdg == kGamma && p.eleMC.mpdg == kGamma && sameMother(p.posMC, p.eleMC); }; + for (const auto* x : la) { + for (const auto* y : lb) { + if (x->evt == y->evt && x->trk >= 0 && x->trk == y->trk) { + return 12; + } + } + } + if (truePhoton(a) && truePhoton(b) && sameMother(a.posMC, b.posMC)) { + return 1; + } + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 2; ++j) { + if (samePart(*la[i], *lb[j])) { + return (la[1 - i]->mpdg == kGamma && lb[1 - j]->mpdg == kGamma) ? 2 : 3; + } + } + } + for (const auto* x : la) { + for (const auto* y : lb) { + if (x->mpdg == kGamma && sameMother(*x, *y)) { + return 4; + } + } + } + for (const auto* x : la) { + for (const auto* y : lb) { + if (daughterOf(*x, *y) || daughterOf(*y, *x)) { + return 5; + } + } + } + if (mixRelated(a, b)) { + return 5; + } + for (const auto* x : la) { + for (const auto* y : lb) { + if (sameMother(*x, *y)) { + return 6; + } + } + } + const bool aFake = !truePhoton(a), bFake = !truePhoton(b); + if (aFake && bFake) { + return 7; + } + const auto& f = aFake ? a : b; + if (sameMother(f.posMC, f.eleMC)) { + return 8; + } + if (std::abs(f.posMC.pdg) != kElectron || std::abs(f.eleMC.pdg) != kElectron) { + return 9; + } + if (f.posMC.mpdg == kGamma && f.eleMC.mpdg == kGamma) { + return 10; + } + return 11; + } + // two photons come from one shower: a leg of each has the same physical-primary ancestor (photons of one primary photon/e+-) + static bool mixSameAncestor(MixPairPhoton const& a, MixPairPhoton const& b) + { + for (const auto* x : {&a.posMC, &a.eleMC}) { + for (const auto* y : {&b.posMC, &b.eleMC}) { + if (x->evt == y->evt && x->anc >= 0 && x->anc == y->anc) { + return true; + } + } + } + return false; + } + + static std::array mixPairGeometry(MixPairPhoton const& a, MixPairPhoton const& b) + { + using L = MixPairPhoton::LegMC; + auto mid = [](L const& p, L const& e) { return std::array{0.5f * (p.vx + e.vx), 0.5f * (p.vy + e.vy), 0.5f * (p.vz + e.vz)}; }; + const auto ca = mid(a.posMC, a.eleMC), cb = mid(b.posMC, b.eleMC); + auto legDist = [](L const& x, L const& y) { return std::hypot(RecoDecay::constrainAngle(x.phi - y.phi, -o2::constants::math::PI), x.tgl - y.tgl); }; + return {std::hypot(ca[0] - cb[0], ca[1] - cb[1], ca[2] - cb[2]), std::min(legDist(a.posMC, b.posMC), legDist(a.eleMC, b.eleMC)), + std::max({a.posMC.sharedFrac, a.eleMC.sharedFrac, b.posMC.sharedFrac, b.eleMC.sharedFrac})}; + } + // MC photon pair relationship: 0 unrelated, 1 radiated by the other, 2 siblings (same grandmother), 3 daughter of the other + static int mixRelKind(MixPairPhoton const& a, MixPairPhoton const& b) + { + using L = MixPairPhoton::LegMC; + auto radiatedBy = [](L const& x, L const& y) { return x.evt == y.evt && x.gmid >= 0 && x.gmid == y.pid; }; + auto siblings = [](L const& x, L const& y) { return x.evt == y.evt && x.gmid >= 0 && x.gmid == y.gmid && x.mid != y.mid; }; + auto daughterOf = [](L const& x, L const& y) { return x.evt == y.evt && x.mid >= 0 && x.mid == y.pid; }; + int kind = 0; + for (const auto* x : {&a.posMC, &a.eleMC}) { + for (const auto* y : {&b.posMC, &b.eleMC}) { + if (radiatedBy(*x, *y) || radiatedBy(*y, *x)) { + return 1; + } + if (daughterOf(*x, *y) || daughterOf(*y, *x)) { + kind = 3; + } else if (kind == 0 && siblings(*x, *y)) { + kind = 2; + } + } + } + return kind; + } + struct BremsPointing { + float dMin = 999.f, angle = 3.15f, rEmit = -1.f, outward = 0.f, ptB = 0.f; + }; + template + BremsPointing mixBremsPointing(MixPairPhoton const& a, MixPairPhoton const& b, TLegs const& legs) const + { + constexpr float StepCm = 0.25f, MaxArcCm = 100.f, MarginCm = 2.f; + BremsPointing best; + for (int order = 0; order < 2; ++order) { + const auto& emitter = order == 0 ? a : b; + const auto& brems = order == 0 ? b : a; + const float pB = std::hypot(brems.px, brems.py, brems.pz); + if (pB <= 0.f) { + continue; + } + const std::array u{brems.px / pB, brems.py / pB, brems.pz / pB}; + const float rA = std::hypot(emitter.vx, emitter.vy), rB = std::hypot(brems.vx, brems.vy); + for (const int64_t row : {emitter.posRow, emitter.eleRow}) { + const auto leg = legs.rawIteratorAt(row); + const o2::track::TrackPar par(leg.x(), leg.alpha(), std::array{leg.y(), leg.z(), leg.snp(), leg.tgl(), leg.signed1Pt()}); + o2::math_utils::CircleXYf_t circle; + float sna = 0.f, csa = 0.f; + par.getCircleParams(d_bz, circle, sna, csa); + std::array x0{}, p0{}; + par.getXYZGlo(x0); + par.getPxPyPzGlo(p0); + if (circle.rC <= 0.f) { + continue; + } + const float psi0 = std::atan2(x0[1] - circle.yC, x0[0] - circle.xC); + const float sense = ((x0[0] - circle.xC) * p0[1] - (x0[1] - circle.yC) * p0[0]) > 0.f ? 1.f : -1.f; // direction of motion on the circle + const float tgl = par.getTgl(), norm = std::sqrt(1.f + tgl * tgl); + constexpr int NSteps = static_cast((MaxArcCm + MaxArcCm) / StepCm); + for (int iStep = 0; iStep <= NSteps; ++iStep) { + const float s = -MaxArcCm + static_cast(iStep) * StepCm; + const float psi = psi0 + sense * s / circle.rC; + const std::array h{circle.xC + circle.rC * std::cos(psi), circle.yC + circle.rC * std::sin(psi), x0[2] + tgl * s}; + const float rH = std::hypot(h[0], h[1]); + if (rH < rA - MarginCm || rH > rB + MarginCm) { + continue; // emission between the two conversions + } + const std::array d{h[0] - brems.vx, h[1] - brems.vy, h[2] - brems.vz}; + const float t = d[0] * u[0] + d[1] * u[1] + d[2] * u[2]; + if (t > 0.f) { + continue; // in front of B's conversion point + } + const float dist = std::hypot(d[0] - t * u[0], d[1] - t * u[1], d[2] - t * u[2]); + if (dist < best.dMin) { + const std::array tan{-sense * std::sin(psi) / norm, sense * std::cos(psi) / norm, tgl / norm}; + best.dMin = dist; + best.angle = std::acos(std::clamp(tan[0] * u[0] + tan[1] * u[1] + tan[2] * u[2], -1.f, 1.f)); + best.rEmit = rH; + best.outward = rB > rA ? 1.f : 0.f; + best.ptB = std::hypot(brems.px, brems.py); + } + } + } + } + return best; + } + // skip pairs of photons that come from the same source event (MC truth) when mixing, if requested + bool skipSameSourcePair(const float type, MixPairPhoton const& a, MixPairPhoton const& b) + { + if (type < 0.5f) { + return false; + } + const int64_t e = a.posMC.evt; + const bool same = e >= 0 && a.eleMC.evt == e && b.posMC.evt == e && b.eleMC.evt == e; + return same && mixed.skipSameSourcePairs.value; + } + void fillSameSourcePairs(const float type, std::vector const& photons) + { + if (type < 0.5f) { + return; + } + for (size_t i = 0; i < photons.size(); ++i) { + for (size_t j = i + 1; j < photons.size(); ++j) { + const auto& a = photons[i]; + const auto& b = photons[j]; + const int64_t e = a.posMC.evt; + if (e >= 0 && a.eleMC.evt == e && b.posMC.evt == e && b.eleMC.evt == e) { + registry.fill(HIST("Mixed/Pairs/hSameSource"), qInv(a.px, a.py, a.pz, b.px, b.py, b.pz), mixPairClass(a, b)); + } + } + } + } + void fillMixFakeClasses(const float type, const float stage, const int64_t colId, std::vector const& photons, + std::set> const& candLegs) + { + for (size_t i = 0; i < photons.size(); ++i) { + for (size_t j = i + 1; j < photons.size(); ++j) { + const auto& a = photons[i]; + const auto& b = photons[j]; + if (stage > 0.5f && skipSameSourcePair(type, a, b)) { + continue; + } + const float q = qInv(a.px, a.py, a.pz, b.px, b.py, b.pz); + if (q < 0.5f && mixPairGeometry(a, b)[2] < 0.4f) { // o2-linter: disable=magic-number (normalisation range, analysis leg cut) + registry.fill(HIST("Mixed/Pairs/hQClean"), type, stage, q); + } + if (q >= 0.2f) { // o2-linter: disable=magic-number (diagnostic range) + continue; + } + const auto nIts = static_cast(a.posMC.its + a.eleMC.its + b.posMC.its + b.eleMC.its); + const float amb = (candLegs.contains({colId, a.posRow, b.eleRow}) || candLegs.contains({colId, b.posRow, a.eleRow})) ? 1.f : 0.f; + const float cls = mixPairClass(a, b); + registry.fill(HIST("Mixed/Pairs/hFakeClass"), type, stage, 0.f, q, nIts, amb); + registry.fill(HIST("Mixed/Pairs/hFakeClass"), type, stage, cls, q, nIts, amb); + { + const float rA = std::hypot(a.vx, a.vy), rB = std::hypot(b.vx, b.vy); + const bool aSofter = std::hypot(a.px, a.py, a.pz) < std::hypot(b.px, b.py, b.pz); + const float rSoft = aSofter ? rA : rB, rHard = aSofter ? rB : rA; + registry.fill(HIST("Mixed/Pairs/hFakeClassOrder"), type, stage, cls, q, rSoft < rHard ? 0.f : 1.f, std::fabs(rA - rB)); + } + if (q < 0.1f) { // o2-linter: disable=magic-number (diagnostic range) + const auto g = mixPairGeometry(a, b); + registry.fill(HIST("Mixed/Pairs/hFakeClassGeo"), type, stage, cls, q, amb, mixSameAncestor(a, b) ? 1.f : 0.f, g[0], + std::log10(std::max(g[1], 1e-6f)), g[2]); // o2-linter: disable=magic-number (log floor) + if (stage > 0.5f) { + registry.fill(HIST("Mixed/Pairs/hPairOrigin"), type, cls, q, static_cast(mixPairOrigin(a, b))); + } + if (stage > 0.5f && a.group[0] >= 0 && b.group[0] >= 0) { + // same group: its context; different groups: the larger one + const bool sameGroup = a.group[0] == b.group[0]; + const auto& gr = (sameGroup || a.group[1] >= b.group[1]) ? a.group : b.group; + registry.fill(HIST("Mixed/Pairs/hGroupCtx"), type, cls, q, amb, sameGroup ? 1.f : 0.f, static_cast(std::min(gr[1], 20)), + static_cast(std::min(gr[2], 33)), static_cast(std::min(gr[3], 10))); // o2-linter: disable=magic-number (last bins) + } + } + } + } + } + // MC class of a photon pair (see fillMixFakeClasses) + static float mixPairClass(MixPairPhoton const& a, MixPairPhoton const& b) + { + const int truth = mixPairTruth(a, b); + if (truth == 3) { + return static_cast(mixFakeClass(a, b)); + } + if (truth == 1 || truth == 2) { + return (truth == 1 ? 13.f : 14.f) + (mixRelated(a, b) ? 3.f : 0.f); + } + return 15.f; + } + + template + void fillMixSwapBrems(const float type, const float stage, const int64_t colId, std::vector const& photons, + std::map, float> const& candScore, TLegs const& legs) + { + constexpr float MaxQ = 0.1f, MaxShared = 0.4f; + auto score = [&](int64_t pos, int64_t ele) { + const auto it = candScore.find({colId, pos, ele}); + return it == candScore.end() ? -1.f : it->second; + }; + for (size_t i = 0; i < photons.size(); ++i) { + for (size_t j = i + 1; j < photons.size(); ++j) { + const auto& a = photons[i]; + const auto& b = photons[j]; + if (stage > 0.5f && skipSameSourcePair(type, a, b)) { + continue; + } + const float q = qInv(a.px, a.py, a.pz, b.px, b.py, b.pz); + if (q >= MaxQ) { + continue; + } + const float cls = mixPairClass(a, b); + const float clean = mixPairGeometry(a, b)[2] < MaxShared ? 1.f : 0.f; + const auto bp = mixBremsPointing(a, b, legs); + registry.fill(HIST("Mixed/Pairs/hBrems"), type, stage, cls, static_cast(mixRelKind(a, b)), q, + std::log10(std::max(bp.dMin, 1e-3f)), std::log10(std::max(bp.angle, 1e-4f)), bp.rEmit, bp.outward, bp.ptB, clean); // o2-linter: disable=magic-number (log floors) + if (stage < 0.5f || (cls != 14.f && cls != 17.f)) { // o2-linter: disable=magic-number (swap classes) + continue; + } + const float sa = score(a.posRow, a.eleRow), sb = score(b.posRow, b.eleRow); + const float t1 = score(a.posRow, b.eleRow), t2 = score(b.posRow, a.eleRow); + float decision = 2.f; + if (t1 >= 0.f && t2 >= 0.f) { + decision = (sa + sb < t1 + t2) ? 1.f : 0.f; + } + const float origin = (a.original && b.original) ? 1.f : ((!a.original && !b.original) ? 0.f : 2.f); + registry.fill(HIST("Mixed/Pairs/hSwapDecision"), type, origin, decision, q, clean); + } + } + } + // truth of a photon pair: 1 two true photons, 2 the legs of two true photons crossed (full swap), 3 other with a fake, 4 no label + static int mixPairTruth(MixPairPhoton const& a, MixPairPhoton const& b) + { + if (a.posKey.second < 0 || a.eleKey.second < 0 || b.posKey.second < 0 || b.eleKey.second < 0) { + return 4; + } + const bool allTrue = a.posTrue && a.eleTrue && b.posTrue && b.eleTrue; + if (allTrue && a.posKey == a.eleKey && b.posKey == b.eleKey && a.posKey != b.posKey) { + return 1; + } + if (allTrue && a.posKey == b.eleKey && a.eleKey == b.posKey && a.posKey != a.eleKey) { + return 2; + } + return 3; + } + template + void fillMixPairs(const float type, std::vector const& photons) + { + for (size_t i = 0; i < photons.size(); ++i) { + for (size_t j = i + 1; j < photons.size(); ++j) { + const auto& a = photons[i]; + const auto& b = photons[j]; + if (after && skipSameSourcePair(type, a, b)) { + continue; + } + const float q = qInv(a.px, a.py, a.pz, b.px, b.py, b.pz); + const auto truth = static_cast(mixPairTruth(a, b)); + if constexpr (after) { + registry.fill(HIST("Mixed/Pairs/hQAfter"), type, 0.f, q); + registry.fill(HIST("Mixed/Pairs/hQAfter"), type, truth, q); + } else { + registry.fill(HIST("Mixed/Pairs/hQBefore"), type, 0.f, q); + registry.fill(HIST("Mixed/Pairs/hQBefore"), type, truth, q); + } + } + } + } + + // fill the occupancy of the source events and the deduplication diagnostics for the mixed-event photon pairs + template + void fillMatchedConfigs(TMixCollisions const& collisions, TMixLegs const& legs) + { + for (const auto& collision : collisions) { // same-event artificial events carry the occupancy of their source event + if (!collision.isMixed()) { + mOccOfSourceEvent[collision.sourceEventUid()] = collision.occupancy(); + } + } + using Key3 = std::tuple; + std::set cands, stored; + for (const auto& c : vecV0Dedup) { + cands.insert(Key3{c.colID, c.posID, c.eleID}); + } + std::unordered_map storedPartner; + for (const auto& k : stored_fullv0Ids) { + stored.insert(Key3{k.collisionId, k.posId, k.eleId}); + storedPartner.try_emplace(k.posId, k.eleId); + storedPartner.try_emplace(k.eleId, k.posId); + } + for (const auto& collision : collisions) { + const int64_t colId = collision.globalIndex(); + const float type = collision.isMixed() ? 1.f : 0.f; + // MC photons with both legs present: (source event, mother) -> (pos row, ele row) + std::map, std::array> photonLegs; + for (int i = collision.firstLeg(); i < collision.firstLeg() + collision.nLegs(); ++i) { + const auto& l = mLegIt[i]; + if (l.mcMotherId() < 0 || l.mcMotherPdg() != kGamma) { + continue; + } + auto [it, inserted] = photonLegs.try_emplace(std::make_pair(static_cast(l.sourceEventUid()), l.mcMotherId()), std::array{-1, -1}); + it->second[l.sign() > 0 ? 0 : 1] = i; + } + std::vector> photons; + for (const auto& [k, rows] : photonLegs) { + if (rows[0] >= 0 && rows[1] >= 0) { + photons.push_back(rows); + } + } + for (size_t i = 0; i < photons.size(); ++i) { + for (size_t j = i + 1; j < photons.size(); ++j) { + const auto& ap = mLegIt[photons[i][0]]; + const auto& am = mLegIt[photons[i][1]]; + const auto& bp = mLegIt[photons[j][0]]; + const auto& bm = mLegIt[photons[j][1]]; + // related: same mother (siblings) or one photon radiated by a leg particle of the other (bremsstrahlung), as mixRelated + const bool sameEvt = ap.sourceEventUid() == bp.sourceEventUid(); + if (type > 0.5f && sameEvt && mixed.skipSameSourceCrossed) { + continue; // injection: a configuration of two current-event photons is a same-event one + } + const float qTrue = qInv(ap.mcMotherPx(), ap.mcMotherPy(), ap.mcMotherPz(), bp.mcMotherPx(), bp.mcMotherPy(), bp.mcMotherPz()); + if (qTrue >= 0.05f) { // o2-linter: disable=magic-number (diagnostic range) + continue; + } + const int gA = ap.mcGrandMotherId(), gB = bp.mcGrandMotherId(); + const bool related = sameEvt && ((gA >= 0 && gA == gB) || (gA >= 0 && (gA == bp.mcParticleId() || gA == bm.mcParticleId())) || + (gB >= 0 && (gB == ap.mcParticleId() || gB == am.mcParticleId()))); + const int64_t A = photons[i][0], a = photons[i][1], B = photons[j][0], b = photons[j][1]; // pos / ele rows + float input = 2.f; + if (ap.sourcePhotonUid() == am.sourcePhotonUid() && bp.sourcePhotonUid() == bm.sourcePhotonUid() && ap.sourcePhotonUid() != bp.sourcePhotonUid()) { + input = 0.f; + } else if (ap.sourcePhotonUid() == bm.sourcePhotonUid() && bp.sourcePhotonUid() == am.sourcePhotonUid() && ap.sourcePhotonUid() != bp.sourcePhotonUid()) { + input = 1.f; + } + const int nGen = static_cast(cands.contains(Key3{colId, A, a})) + static_cast(cands.contains(Key3{colId, B, b})); + const int nCross = static_cast(cands.contains(Key3{colId, A, b})) + static_cast(cands.contains(Key3{colId, B, a})); + const bool sGen = stored.contains(Key3{colId, A, a}) && stored.contains(Key3{colId, B, b}); + const bool sCross = stored.contains(Key3{colId, A, b}) && stored.contains(Key3{colId, B, a}); + float dOcc = 0.f; // ME: occupancy of the photon's source event - current event, the photon with the larger |difference| + if (type > 0.5f) { + float best = 0.f; + for (const int64_t uid : {static_cast(ap.sourceEventUid()), static_cast(bp.sourceEventUid())}) { + const auto it = mOccOfSourceEvent.find(uid); + const float d = it == mOccOfSourceEvent.end() ? -19999.f : it->second - collision.occupancy(); + if (std::fabs(d) > std::fabs(best)) { + best = d; + } + } + dOcc = best; + } + const int nTpcOnly = static_cast(!ap.hasITS()) + static_cast(!am.hasITS()) + static_cast(!bp.hasITS()) + static_cast(!bm.hasITS()); + registry.fill(HIST("Mixed/Decision/hMatched"), type, qTrue, input, static_cast(nGen), static_cast(nCross), sGen ? 0.f : (sCross ? 1.f : 2.f), related ? 1.f : 0.f, static_cast(collision.numContrib()), static_cast(collision.nLegs()) / 2.f, collision.occupancy(), std::clamp(dOcc, -19999.f, 19999.f), static_cast(nTpcOnly)); + // central electrode: true conversion point (ME partners shifted by the alignment, < 2 cm) and the leg's tan lambda (unchanged by it) + constexpr float kTpcRin = 85.f, kTpcRout = 247.f; // o2-linter: disable=magic-number (TPC drift volume, diagnostic) + auto crossesCE = [&](auto const& l) { + const float rConv = std::hypot(l.mcVx(), l.mcVy()); + if (rConv > kTpcRout) { + return 0; + } + const float z1 = l.mcVz() + l.tgl() * (std::max(rConv, kTpcRin) - rConv), z2 = l.mcVz() + l.tgl() * (kTpcRout - rConv); + return z1 * z2 <= 0.f ? 1 : 0; + }; + auto absEta = [](float px, float py, float pz) { return std::fabs(std::asinh(pz / std::max(std::hypot(px, py), 1.e-6f))); }; + const float minEta = std::min(absEta(ap.mcMotherPx(), ap.mcMotherPy(), ap.mcMotherPz()), absEta(bp.mcMotherPx(), bp.mcMotherPy(), bp.mcMotherPz())); + const float minZ = std::min(std::fabs(ap.mcVz()), std::fabs(bp.mcVz())); + const int nCE = crossesCE(ap) + crossesCE(am) + crossesCE(bp) + crossesCE(bm); + registry.fill(HIST("Mixed/Decision/hMatchedCE"), type, qTrue, input, sGen ? 0.f : (sCross ? 1.f : 2.f), related ? 1.f : 0.f, std::min(minEta, 1.99f), std::min(minZ, 299.f), static_cast(nCE), static_cast(nTpcOnly)); + // leg fates: 0 not stored, 1 own genuine partner, 2 crossed partner, 3 leg of a third complete photon, 4 leg of an incomplete MC photon, 5 no photon mother + std::array nFate{}; + const std::array rows{A, a, B, b}; + const std::array own{a, A, b, B}, crossed{b, B, a, A}; + for (int k = 0; k < 4; ++k) { + const auto it = storedPartner.find(rows[k]); + if (it == storedPartner.end()) { + ++nFate[0]; + } else if (it->second == own[k]) { + ++nFate[1]; + } else if (it->second == crossed[k]) { + ++nFate[2]; + } else { + const auto p = legs.rawIteratorAt(it->second); + if (p.mcMotherId() < 0 || p.mcMotherPdg() != kGamma) { + ++nFate[5]; + } else { + const auto pIt = photonLegs.find(std::make_pair(static_cast(p.sourceEventUid()), p.mcMotherId())); + const bool complete = pIt != photonLegs.end() && pIt->second[0] >= 0 && pIt->second[1] >= 0; + ++nFate[complete ? 3 : 4]; + } + } + } + registry.fill(HIST("Mixed/Decision/hMatchedLegs"), type, qTrue, input, sGen ? 0.f : (sCross ? 1.f : 2.f), related ? 1.f : 0.f, + static_cast(nFate[0]), static_cast(nFate[1]), static_cast(nFate[2]), static_cast(nFate[3]), + static_cast(nFate[4]), static_cast(nFate[5])); + } + } + } + } + + // truth of a photon pair: 0 both genuine, 1 crossed, 2 other (at least one leg not from a true photon), for all candidates of a collision + template + void fillFourLegAll(TMixCollisions const& collisions, TMixLegs const& legs, const size_t nFit) + { + using Key3 = std::tuple; + std::map firstCand; + std::map> originalsOfColl; + for (size_t i = 0; i < vecV0Dedup.size(); ++i) { + const auto& c = vecV0Dedup[i]; + if (firstCand.emplace(Key3{c.colID, c.posID, c.eleID}, static_cast(i)).second && c.v0ID >= 0 && i < nFit) { + originalsOfColl[c.colID].push_back(static_cast(i)); + } + } + std::set storedSet; + for (const auto& k : stored_fullv0Ids) { + storedSet.insert(Key3{k.collisionId, k.posId, k.eleId}); + } + auto truePhoton = [&](int64_t posRow, int64_t eleRow) { + const auto p = legs.rawIteratorAt(posRow); + const auto e = legs.rawIteratorAt(eleRow); + return p.mcMotherId() >= 0 && p.sourceEventUid() == e.sourceEventUid() && p.mcMotherId() == e.mcMotherId() && p.mcMotherPdg() == kGamma; + }; + for (const auto& [colId, orig] : originalsOfColl) { + const float type = collisions.rawIteratorAt(colId).isMixed() ? 1.f : 0.f; + for (size_t i = 0; i < orig.size(); ++i) { + for (size_t j = i + 1; j < orig.size(); ++j) { + const auto& a = vecV0Dedup[orig[i]]; + const auto& b = vecV0Dedup[orig[j]]; + const auto itX1 = firstCand.find(Key3{colId, a.posID, b.eleID}); + const auto itX2 = firstCand.find(Key3{colId, b.posID, a.eleID}); + if (itX1 == firstCand.end() || itX2 == firstCand.end()) { + continue; + } + const auto& x1 = vecV0Dedup[itX1->second]; + const auto& x2 = vecV0Dedup[itX2->second]; + const auto ap = legs.rawIteratorAt(a.posID); + const auto bp = legs.rawIteratorAt(b.posID); + const bool distinctMothers = ap.sourceEventUid() != bp.sourceEventUid() || ap.mcMotherId() != bp.mcMotherId(); + const bool physO = truePhoton(a.posID, a.eleID) && truePhoton(b.posID, b.eleID) && distinctMothers; + const bool physX = truePhoton(a.posID, b.eleID) && truePhoton(b.posID, a.eleID) && distinctMothers; + const float truth = physO ? 0.f : (physX ? 1.f : 2.f); + const float qGen = physX ? qInv(x1.pxAtPV, x1.pyAtPV, x1.pzAtPV, x2.pxAtPV, x2.pyAtPV, x2.pzAtPV) : qInv(a.pxAtPV, a.pyAtPV, a.pzAtPV, b.pxAtPV, b.pyAtPV, b.pzAtPV); + if (qGen >= 0.05f) { // o2-linter: disable=magic-number (diagnostic range) + continue; + } + const bool storedO = storedSet.contains(Key3{colId, a.posID, a.eleID}) && storedSet.contains(Key3{colId, b.posID, b.eleID}); + const bool storedX = storedSet.contains(Key3{colId, a.posID, b.eleID}) && storedSet.contains(Key3{colId, b.posID, a.eleID}); + const float actual = storedO ? 0.f : (storedX ? 1.f : 2.f); + const int crossedOrigin = (static_cast(itX1->second) >= nFit ? 1 : 0) + (static_cast(itX2->second) >= nFit ? 1 : 0); + const int origOrigin = (ap.sourceV0Id() < 0 ? 1 : 0) + (bp.sourceV0Id() < 0 ? 1 : 0); + registry.fill(HIST("Mixed/Decision/hFourLegAll"), type, truth, actual, qGen, static_cast(crossedOrigin), static_cast(origOrigin)); + } + } + } + } + + template + void fillFourLegTruthAligned(TMixCollisions const& collisions, TMixLegs const& legs, const int64_t colId, const float type, const float truth, + const float isolated, const float actual, V0CandidateHelper const& a, V0CandidateHelper const& b, + V0CandidateHelper const& x1, V0CandidateHelper const& x2, const int groupSize) + { + const bool origTrue = truth < 0.5f; + const auto& t1 = origTrue ? a : x1; + const auto& t2 = origTrue ? b : x2; + const auto& w1 = origTrue ? x1 : a; + const auto& w2 = origTrue ? x2 : b; + // decision in truth terms: 0 genuine assignment, 1 wrong assignment, 2 other + auto inTruth = [&](float decision) { return decision > 1.5f ? 2.f : ((decision < 0.5f) == origTrue ? 0.f : 1.f); }; + const float choice = inTruth(isolated); + MixPairPhoton p1, p2; + setMixPairLegs(p1, legs.rawIteratorAt(t1.posID), legs.rawIteratorAt(t1.eleID)); + setMixPairLegs(p2, legs.rawIteratorAt(t2.posID), legs.rawIteratorAt(t2.eleID)); + const int relKind = mixRelKind(p1, p2); + const auto rel = static_cast(relKind); + const float rel2 = relKind > 0 ? 1.f : 0.f; + const float s0 = dedup.groupMatchingScoreThreshold.value; + const float qTrue = qInv(t1.pxAtPV, t1.pyAtPV, t1.pzAtPV, t2.pxAtPV, t2.pyAtPV, t2.pzAtPV); + const int nClipT = (t1.score >= s0 ? 1 : 0) + (t2.score >= s0 ? 1 : 0); + const int nClipW = (w1.score >= s0 ? 1 : 0) + (w2.score >= s0 ? 1 : 0); + auto angle = [](float cpa) { return std::acos(std::clamp(cpa, -1.f, 1.f)); }; + const float dTheta = 1e3f * ((angle(w1.cosPA) + angle(w2.cosPA)) - (angle(t1.cosPA) + angle(t2.cosPA))); // mrad + const float dPCA = (w1.pca + w2.pca) - (t1.pca + t2.pca); + const std::array rows{t1.posID, t1.eleID, t2.posID, t2.eleID}; + float fshMax = 0.f; + int nITS = 0; + for (const auto& r : rows) { + const auto l = legs.rawIteratorAt(r); + fshMax = std::max(fshMax, static_cast(l.tpcFractionSharedCls())); + nITS += l.hasITS() ? 1 : 0; + } + registry.fill(HIST("Mixed/Decision/hFourLegTruth"), type, truth, choice, rel, qTrue, static_cast(3 * nClipT + nClipW), dTheta, dPCA, fshMax); + { + const int origin = mixCommonOrigin(p1.posMC, p2.posMC); + auto rootClass = [](int pdg) { + switch (std::abs(pdg)) { + case 111: + return 1.f; // o2-linter: disable=pdg/explicit-code (diagnostic classes) + case 221: + return 2.f; // o2-linter: disable=pdg/explicit-code + case 223: + return 3.f; // o2-linter: disable=pdg/explicit-code + case 331: + return 4.f; // o2-linter: disable=pdg/explicit-code + case 113: + case 213: + case 313: + case 323: + return 5.f; // o2-linter: disable=pdg/explicit-code + case 310: + case 130: + case 311: + return 6.f; // o2-linter: disable=pdg/explicit-code + default: + return std::abs(pdg) > 1000 ? 7.f : 0.f; // o2-linter: disable=magic-number (baryons) + } + }; + registry.fill(HIST("Mixed/Decision/hFourLegOrigin"), type, truth, choice, rel, qTrue, static_cast(origin), origin > 0 ? rootClass(p1.posMC.rootPdg) : 0.f); + } + + // TPC leg proximity: the minimum distance of the two legs of one photon to the two legs of the other photon, at 17 radii in the TPC + constexpr int NRadii = 17; + auto positions = [&](int64_t row) { + std::array, NRadii> xyz{}; + std::array ok{}; + const auto tp0 = getTrackPar(legs.rawIteratorAt(row)); + for (int k = 0; k < NRadii; ++k) { + auto tp = tp0; + float x = 0.f; + ok[k] = tp.getXatLabR(85.f + 10.f * k, x, d_bz) && tp.propagateParamTo(x, d_bz); // o2-linter: disable=magic-number (TPC radii) + if (ok[k]) { + tp.getXYZGlo(xyz[k]); + } + } + return std::make_pair(xyz, ok); + }; + const auto pT1p = positions(t1.posID), pT1e = positions(t1.eleID), pT2p = positions(t2.posID), pT2e = positions(t2.eleID); + auto minDist = [&](auto const& u, auto const& v) { + float d = 999.f; + for (int k = 0; k < NRadii; ++k) { + if (u.second[k] && v.second[k]) { + d = std::min(d, std::hypot(u.first[k][0] - v.first[k][0], u.first[k][1] - v.first[k][1], u.first[k][2] - v.first[k][2])); + } + } + return d; + }; + const float dOS = std::min(minDist(pT1p, pT2e), minDist(pT2p, pT1e)); // legs that the wrong assignment pairs + const float dSS = std::min(minDist(pT1p, pT2p), minDist(pT1e, pT2e)); // control: same-sign legs + const float occ = collisions.rawIteratorAt(colId).occupancy(); + registry.fill(HIST("Mixed/Decision/hFourLegTrack"), type, truth, choice, rel2, std::log10(std::max(dOS, 1e-3f)), std::log10(std::max(dSS, 1e-3f)), + static_cast(nITS), occ); // o2-linter: disable=magic-number (log floor) + + auto mother = [&](int64_t row) { + const auto l = legs.rawIteratorAt(row); + return std::array{l.mcMotherPx(), l.mcMotherPy(), l.mcMotherPz()}; + }; + const auto m1 = mother(t1.posID), m2 = mother(t2.posID); + auto phi = [](float px, float py) { return std::atan2(py, px); }; + auto eta = [](float px, float py, float pz) { return std::asinh(pz / std::max(std::hypot(px, py), 1e-6f)); }; + const float uPhi = RecoDecay::constrainAngle(phi(m2[0], m2[1]) - phi(m1[0], m1[1]), -o2::constants::math::PI) > 0.f ? 1.f : -1.f; + const float uEta = eta(m2[0], m2[1], m2[2]) - eta(m1[0], m1[1], m1[2]) > 0.f ? 1.f : -1.f; + const float rPhi1 = uPhi * RecoDecay::constrainAngle(phi(t1.pxAtPV, t1.pyAtPV) - phi(m1[0], m1[1]), -o2::constants::math::PI); + const float rPhi2 = -uPhi * RecoDecay::constrainAngle(phi(t2.pxAtPV, t2.pyAtPV) - phi(m2[0], m2[1]), -o2::constants::math::PI); + const float rEta1 = uEta * (eta(t1.pxAtPV, t1.pyAtPV, t1.pzAtPV) - eta(m1[0], m1[1], m1[2])); + const float rEta2 = -uEta * (eta(t2.pxAtPV, t2.pyAtPV, t2.pzAtPV) - eta(m2[0], m2[1], m2[2])); + registry.fill(HIST("Mixed/Decision/hFourLegResid"), type, truth, choice, rel2, rPhi1, rPhi2, rEta1, rEta2); + + const std::vector legMask{0b0011, 0b1100, 0b1001, 0b0110}; + auto decide = [&](std::vector const& sc, float thr) { + const auto sol = solveConflictGroup(legMask, sc, thr, false); + return inTruth(sol.bestMask == 0b0011 ? 0.f : (sol.bestMask == 0b1100 ? 1.f : 2.f)); // o2-linter: disable=magic-number (subset masks) + }; + auto scores = [&](float w) { + return std::vector{getScoreV0(a.cosPA, a.pca, w), getScoreV0(b.cosPA, b.pca, w), getScoreV0(x1.cosPA, x1.pca, w), getScoreV0(x2.cosPA, x2.pca, w)}; + }; + const std::vector baseScores{a.score, b.score, x1.score, x2.score}; + registry.fill(HIST("Mixed/Decision/hFourLegAlt"), type, truth, rel2, choice, decide(scores(1.f), s0), decide(scores(0.f), s0), decide(baseScores, -1.f), + decide(scores(dedup.deduplicationScoreWeight.value), s0)); + + const auto col = collisions.rawIteratorAt(colId); + auto pointing = [&](V0CandidateHelper const& c) { + const float dx = c.svX - col.posX(), dy = c.svY - col.posY(), dz = c.svZ - col.posZ(); + const float dPhi = RecoDecay::constrainAngle(std::atan2(c.pyAtSV, c.pxAtSV) - std::atan2(dy, dx), -o2::constants::math::PI); + const float dLam = std::atan2(c.pzAtSV, std::hypot(c.pxAtSV, c.pyAtSV)) - std::atan2(dz, std::hypot(dx, dy)); + return std::make_pair(1e3f * dPhi, 1e3f * dLam); // mrad + }; + const auto pt1 = pointing(t1), pt2 = pointing(t2), pw1 = pointing(w1), pw2 = pointing(w2); + const float dPhiSum = (std::fabs(pw1.first) + std::fabs(pw2.first)) - (std::fabs(pt1.first) + std::fabs(pt2.first)); + const float dLamSum = (std::fabs(pw1.second) + std::fabs(pw2.second)) - (std::fabs(pt1.second) + std::fabs(pt2.second)); + registry.fill(HIST("Mixed/Decision/hFourLegPoint"), type, truth, choice, rel2, qTrue, dPhiSum, dLamSum, pt1.second, pt2.second, pt1.first, pt2.first); + + // log dump of the few cases where the isolated decision is wrong, but the actual decision is correct and genuine + if (type < 0.5f && origTrue && isolated > 0.5f && isolated < 1.5f && actual < 0.5f && mNAnomalyDump < 200) { // o2-linter: disable=magic-number (cap) + ++mNAnomalyDump; + LOGF(info, "[4leg-anom] col %ld group %d scores A %.4f B %.4f X1 %.4f X2 %.4f s0 %.2f q %.4f rel %d", colId, groupSize, a.score, b.score, x1.score, x2.score, s0, qTrue, relKind); + } + } + + template + void fillFourLegDecisions(TMixCollisions const& collisions, TMixLegs const& legs, const size_t nFit) + { + fillFourLegAll(collisions, legs, nFit); + using Key3 = std::tuple; + std::map firstCand; // first fit of a leg pair = its dedup candidate + std::map> originalsOfColl; + for (size_t i = 0; i < nFit; ++i) { + const auto& c = vecV0Dedup[i]; + if (firstCand.emplace(Key3{c.colID, c.posID, c.eleID}, static_cast(i)).second && c.v0ID >= 0) { + originalsOfColl[c.colID].push_back(static_cast(i)); + } + } + std::set storedSet; + for (const auto& k : stored_fullv0Ids) { + storedSet.insert(Key3{k.collisionId, k.posId, k.eleId}); + } + auto truePhoton = [&](int64_t posRow, int64_t eleRow) { + const auto p = legs.rawIteratorAt(posRow); + const auto e = legs.rawIteratorAt(eleRow); + return p.mcMotherId() >= 0 && p.sourceEventUid() == e.sourceEventUid() && p.mcMotherId() == e.mcMotherId() && p.mcMotherPdg() == kGamma; + }; + const float s0 = dedup.groupMatchingScoreThreshold.value; + for (const auto& [colId, orig] : originalsOfColl) { + const float type = collisions.rawIteratorAt(colId).isMixed() ? 1.f : 0.f; + for (size_t i = 0; i < orig.size(); ++i) { + for (size_t j = i + 1; j < orig.size(); ++j) { + const auto& a = vecV0Dedup[orig[i]]; + const auto& b = vecV0Dedup[orig[j]]; + const auto itX1 = firstCand.find(Key3{colId, a.posID, b.eleID}); + const auto itX2 = firstCand.find(Key3{colId, b.posID, a.eleID}); + if (itX1 == firstCand.end() || itX2 == firstCand.end()) { + continue; // not both assignments available + } + const auto& x1 = vecV0Dedup[itX1->second]; + const auto& x2 = vecV0Dedup[itX2->second]; + const float qO = qInv(a.pxAtPV, a.pyAtPV, a.pzAtPV, b.pxAtPV, b.pyAtPV, b.pzAtPV); + const float qX = qInv(x1.pxAtPV, x1.pyAtPV, x1.pzAtPV, x2.pxAtPV, x2.pyAtPV, x2.pzAtPV); + if (std::min(qO, qX) >= 0.1f) { // o2-linter: disable=magic-number (diagnostic range) + continue; + } + // the isolated decision is the dedup objective on only these four candidates, the actual decision is the stored set after the dedup and the resolvers + const std::vector legMask{0b0011, 0b1100, 0b1001, 0b0110}; + const std::vector scores{a.score, b.score, x1.score, x2.score}; + const auto sol = solveConflictGroup(legMask, scores, s0, false); + const float isolated = sol.bestMask == 0b0011 ? 0.f : (sol.bestMask == 0b1100 ? 1.f : 2.f); // o2-linter: disable=magic-number (subset masks) + const bool storedO = storedSet.contains(Key3{colId, a.posID, a.eleID}) && storedSet.contains(Key3{colId, b.posID, b.eleID}); + const bool storedX = storedSet.contains(Key3{colId, a.posID, b.eleID}) && storedSet.contains(Key3{colId, b.posID, a.eleID}); + const float actual = storedO ? 0.f : (storedX ? 1.f : 2.f); + const auto ap = legs.rawIteratorAt(a.posID); + const auto bp = legs.rawIteratorAt(b.posID); + const bool distinctMothers = ap.sourceEventUid() != bp.sourceEventUid() || ap.mcMotherId() != bp.mcMotherId(); + const bool physO = truePhoton(a.posID, a.eleID) && truePhoton(b.posID, b.eleID) && distinctMothers; + const bool physX = truePhoton(a.posID, b.eleID) && truePhoton(b.posID, a.eleID) && distinctMothers; + const float truth = physO ? 0.f : (physX ? 1.f : 2.f); + const int groupSize = std::max({mGroupSizeOfCand[orig[i]], mGroupSizeOfCand[orig[j]], mGroupSizeOfCand[itX1->second], mGroupSizeOfCand[itX2->second]}); + // originals without an SVertexer V0 are fitted without the AP cut, the crossed pairings with it (asymmetric options) + const bool relaxedOriginal = ap.sourceV0Id() < 0 || bp.sourceV0Id() < 0; + registry.fill(HIST("Mixed/Decision/hFourLeg"), type, truth, isolated, actual, qO, qX, static_cast(std::min(groupSize, 15)), // o2-linter: disable=magic-number (last bin) + (x1.score + x2.score) - (a.score + b.score), relaxedOriginal ? 1.f : 0.f); + if (std::min(qO, qX) < 0.05f) { // o2-linter: disable=magic-number (diagnostic range) + // what tips the 4-leg decision: score components, geometry and kinematics of the two conversions, MC relation + auto angle = [](float cpa) { return std::acos(std::clamp(cpa, -1.f, 1.f)); }; + const float dPCA = (x1.pca + x2.pca) - (a.pca + b.pca); + const float dAngle = (angle(x1.cosPA) + angle(x2.cosPA)) - (angle(a.cosPA) + angle(b.cosPA)); + const float dR = std::fabs(std::hypot(a.svX, a.svY) - std::hypot(b.svX, b.svY)); + const float dZ = std::fabs(a.svZ - b.svZ); + auto pLeg = [&](int64_t row) { + const auto l = legs.rawIteratorAt(row); + return std::fabs(1.f / l.signed1Pt()) * std::sqrt(1.f + l.tgl() * l.tgl()); + }; + auto asym = [](float p1, float p2) { return std::fabs(p1 - p2) / std::max(p1 + p2, 1e-6f); }; + const float sameSignSim = std::min(asym(pLeg(a.posID), pLeg(b.posID)), asym(pLeg(a.eleID), pLeg(b.eleID))); + MixPairPhoton pa, pb; + setMixPairLegs(pa, legs.rawIteratorAt(a.posID), legs.rawIteratorAt(a.eleID)); + setMixPairLegs(pb, legs.rawIteratorAt(b.posID), legs.rawIteratorAt(b.eleID)); + registry.fill(HIST("Mixed/Decision/hFourLegKin"), type, truth, isolated, dPCA, dAngle, dR, dZ, sameSignSim, mixRelated(pa, pb) ? 1.f : 0.f); + } + if (truth < 1.5f && std::min(qO, qX) < 0.05f) { // o2-linter: disable=magic-number (genuine assignment exists; diagnostic range) + fillFourLegTruthAligned(collisions, legs, colId, type, truth, isolated, actual, a, b, x1, x2, groupSize); + } + } + } + } + for (const auto& r : mPermuteResults) { + registry.fill(HIST("Mixed/Decision/hPermute"), collisions.rawIteratorAt(r.colID).isMixed() ? 1.f : 0.f, static_cast(std::min(r.nMembers, 30)), // o2-linter: disable=magic-number (last bin) + r.changed ? 1.f : 0.f, r.greedy ? 1.f : 0.f); + } + } + + // run 2: build the mixed event, fill the crossed pairings, and fill the diagnostics of the run-1 photons (pooled free legs) before the dedup and resolvers + template + void buildMixed(TMixCollisions const& collisions, TMixLegs const& legs) + { + const auto tDF = std::chrono::steady_clock::now(); + mRelaxedKeys.clear(); + mLegDcaCache.clear(); + // build the leg iterator cache for the current event + mLegIt.clear(); + mLegIt.reserve(legs.size()); + for (int64_t i = 0; i < static_cast(legs.size()); ++i) { + mLegIt.push_back(legs.rawIteratorAt(i)); + } + std::unordered_map> inputOfColl; // artificial event -> input photons (pair diagnostics) + auto crossedPrefilter = [&](auto const& pos, auto const& ele) { + if (std::fabs(pos.tgl() - ele.tgl()) > recovery.finderMaxDeltaTgl) { + return false; + } + const o2::track::TrackAuxPar cp(getTrackPar(pos), d_bz); + const o2::track::TrackAuxPar ce(getTrackPar(ele), d_bz); + return std::fabs(circleGap(cp, ce)) < recovery.finderMaxCircleGap; + }; + for (const auto& collision : collisions) { + const float type = collision.isMixed() ? 1.f : 0.f; + const auto t0 = std::chrono::steady_clock::now(); + registry.fill(HIST("Mixed/hEvents"), type); + std::vector posLegs, eleLegs; + for (int i = collision.firstLeg(); i < collision.firstLeg() + collision.nLegs(); ++i) { + (mLegIt[i].sign() > 0 ? posLegs : eleLegs).push_back(i); + } + std::unordered_map circleOf; + std::unordered_map eleOfPhoton; + std::unordered_map nEleOfSource; + std::vector> eleByTgl; + eleByTgl.reserve(eleLegs.size()); + for (const auto& ie : eleLegs) { + eleOfPhoton.emplace(mLegIt[ie].sourcePhotonUid(), ie); + nEleOfSource[mLegIt[ie].sourceEventUid()]++; + eleByTgl.emplace_back(mLegIt[ie].tgl(), ie); + } + std::sort(eleByTgl.begin(), eleByTgl.end()); + const bool tglWindow = mixed.prefilterCrossed && !mixed.uniformPairFit; + if (mixed.prefilterCrossed) { + for (const auto& i : posLegs) { + circleOf.emplace(i, o2::track::TrackAuxPar(getTrackPar(mLegIt[i]), d_bz)); + } + for (const auto& i : eleLegs) { + circleOf.emplace(i, o2::track::TrackAuxPar(getTrackPar(mLegIt[i]), d_bz)); + } + } + auto crossedPrefilterCached = [&](int64_t ip, int64_t ie) { + return std::fabs(mLegIt[ip].tgl() - mLegIt[ie].tgl()) <= recovery.finderMaxDeltaTgl && std::fabs(circleGap(circleOf.at(ip), circleOf.at(ie))) < recovery.finderMaxCircleGap; + }; + std::vector before; // crossed pairings of this artificial event, before the dedup and resolvers + for (const auto& ip : posLegs) { + const auto& pos = mLegIt[ip]; + if (const auto itE = eleOfPhoton.find(pos.sourcePhotonUid()); itE != eleOfPhoton.end()) { + const auto ie = itE->second; + const auto& ele = mLegIt[ie]; + { + MixPairPhoton p; + p.px = pos.sourcePx(); + p.py = pos.sourcePy(); + p.pz = pos.sourcePz(); + setMixPairLegs(p, pos, ele); + p.posRow = ip; + p.eleRow = ie; + p.vx = pos.sourceVx(); + p.vy = pos.sourceVy(); + p.vz = pos.sourceVz(); + before.push_back(p); + } + } + } + registry.fill(HIST("Mixed/hNPhotonsIn"), type, static_cast(before.size())); // run-1 photons (pooled free legs are single legs) + fillMixPairs(type, before); + inputOfColl[collision.globalIndex()] = before; + std::vector visit; // negative legs paired with this positive leg, in the original (row) order + for (const auto& ip : posLegs) { + const auto& pos = mLegIt[ip]; + visit.clear(); + if (tglWindow) { // crossed pairings are prefiltered by tgl, then by circle gap + const float tglP = pos.tgl(); + for (auto it = std::lower_bound(eleByTgl.begin(), eleByTgl.end(), std::make_pair(tglP - recovery.finderMaxDeltaTgl - 1e-4f, int64_t{-1})); it != eleByTgl.end() && it->first <= tglP + recovery.finderMaxDeltaTgl + 1e-4f; ++it) { + visit.push_back(it->second); + } + if (const auto itE = eleOfPhoton.find(pos.sourcePhotonUid()); itE != eleOfPhoton.end()) { + visit.push_back(itE->second); + } + std::sort(visit.begin(), visit.end()); + visit.erase(std::unique(visit.begin(), visit.end()), visit.end()); + // crossed pairings outside the window: counted in the cut flow as prefilter rejections (not split by MC in hCrossedFit) + const bool hasPartner = eleOfPhoton.contains(pos.sourcePhotonUid()); + int nOutside = mixed.skipSameSourceCrossed ? static_cast(eleLegs.size()) - nEleOfSource[pos.sourceEventUid()] : static_cast(eleLegs.size()) - (hasPartner ? 1 : 0); + for (const auto& ie : visit) { + const auto& ele = mLegIt[ie]; + if (ele.sourcePhotonUid() != pos.sourcePhotonUid() && !(mixed.skipSameSourceCrossed && pos.sourceEventUid() == ele.sourceEventUid())) { + nOutside--; + } + } + if (nOutside > 0) { + registry.fill(HIST("Mixed/hCandidates"), type, 2.f, static_cast(nOutside)); + } + } else { + visit.assign(eleLegs.begin(), eleLegs.end()); + } + for (const auto& ie : visit) { + const auto& ele = mLegIt[ie]; + const size_t nBefore = vecV0Dedup.size(); + const bool originalPairing = pos.sourcePhotonUid() == ele.sourcePhotonUid(); + if (!originalPairing && mixed.skipSameSourceCrossed && pos.sourceEventUid() == ele.sourceEventUid()) { + continue; // injection: same-source crossed pairings were decided by run 1 + } + if (!originalPairing && mixed.skipFreeFreeCrossed && pos.sourceV0Id() == -2 && ele.sourceV0Id() == -2) { // o2-linter: disable=magic-number (pooled free leg) + continue; // overlay: two free legs, rejected by run 1 + } + if (!originalPairing && mixed.freeFreeFinderOnly && pos.sourceV0Id() == -2 && ele.sourceV0Id() == -2 && pos.hasITS() && ele.hasITS()) { // o2-linter: disable=magic-number (pooled free leg) + continue; // overlay: two free legs with ITS, only an SVertexer V0 in run 1 + } + if (mixed.uniformPairFit) { + // the crossed pairings are fitted with the same options as the originals, but without the AP cut if they were not found by an SVertexer V0 + const bool finderLike = !(pos.hasITS() && ele.hasITS()); + registry.fill(HIST("Mixed/hCandidates"), type, originalPairing ? 0.f : 2.f); + if (mixed.prefilterCrossed && !crossedPrefilter(pos, ele)) { + continue; + } + if (!originalPairing) { + registry.fill(HIST("Mixed/hCandidates"), type, 3.f); + } + const SourcePV srcPv{pos.sourcePvX(), pos.sourcePvY(), pos.sourcePvZ(), pos.sourcePvCovXX(), pos.sourcePvCovXY(), pos.sourcePvCovXZ(), + pos.sourcePvCovYY(), pos.sourcePvCovYZ(), pos.sourcePvCovZZ(), pos.sourcePvChi2(), pos.sourcePvNContrib()}; + fitV0Candidate(collision, pos, ele, originalPairing ? ip : -1, false, + FitOptions{.nonSVertexer = finderLike, .relaxAP = finderLike, .purityGate = finderLike && recovery.runFinder.value, .pv = (originalPairing && mixed.refitOriginalsAtSourceVertex) ? &srcPv : nullptr}); + if (vecV0Dedup.size() > nBefore) { + registry.fill(HIST("Mixed/hCandidates"), type, originalPairing ? 1.f : 4.f); + } + continue; + } + if (originalPairing) { // the original pairings are fitted with the same options as in run 1, but without the AP cut if they were not found by an SVertexer V0 + registry.fill(HIST("Mixed/hCandidates"), type, 0.f); + const SourcePV srcPv{pos.sourcePvX(), pos.sourcePvY(), pos.sourcePvZ(), pos.sourcePvCovXX(), pos.sourcePvCovXY(), pos.sourcePvCovXZ(), + pos.sourcePvCovYY(), pos.sourcePvCovYZ(), pos.sourcePvCovZZ(), pos.sourcePvChi2(), pos.sourcePvNContrib()}; + fitV0Candidate(collision, pos, ele, ip, false, + FitOptions{.nonSVertexer = pos.sourceV0Id() < 0 && !recovery.finderUniformFit.value, .relaxAP = pos.sourceV0Id() < 0 && !recovery.finderUniformFit.value, .pv = mixed.refitOriginalsAtSourceVertex ? &srcPv : nullptr}); + if (vecV0Dedup.size() > nBefore) { + registry.fill(HIST("Mixed/hCandidates"), type, 1.f); + } + continue; + } + registry.fill(HIST("Mixed/hCandidates"), type, 2.f); + // crossed pairings: the same prefilter and the same leg-based fit options for every pairing (original pairings keep v0Id = row of the positive leg) + const bool twoPhotons = pos.mcMotherPdg() == kGamma && ele.mcMotherPdg() == kGamma && pos.mcMotherId() >= 0 && ele.mcMotherId() >= 0 && + (pos.sourceEventUid() != ele.sourceEventUid() || pos.mcMotherId() != ele.mcMotherId()); + const float qTwo = twoPhotons ? qInv(pos.mcMotherPx(), pos.mcMotherPy(), pos.mcMotherPz(), ele.mcMotherPx(), ele.mcMotherPy(), ele.mcMotherPz()) : 1.f; + const float genuineClose = (twoPhotons && qTwo < 0.05f) ? 1.f : 0.f; // o2-linter: disable=magic-number (diagnostic range) + const auto nITSx = static_cast((pos.hasITS() ? 1 : 0) + (ele.hasITS() ? 1 : 0)); + if (mixed.prefilterCrossed && !crossedPrefilterCached(ip, ie)) { + if (mixed.mixedDiagnostics) { + registry.fill(HIST("Mixed/Decision/hCrossedFit"), type, genuineClose, 1.f, nITSx, std::min(qTwo, 0.0499f)); + } + continue; + } + registry.fill(HIST("Mixed/hCandidates"), type, 3.f); + const bool anyFree = pos.sourceV0Id() == -2 || ele.sourceV0Id() == -2; // o2-linter: disable=magic-number (pooled free leg) + const bool freeLegPair = mixed.freeLegRun1Rules && anyFree; + const bool finderLikeFree = freeLegPair && !(pos.hasITS() && ele.hasITS()); + if (mixed.diagFreeLegFits && anyFree) { // trial fits with both option sets, candidates removed again + const bool finderLike = !(pos.hasITS() && ele.hasITS()); + auto trial = [&](FitOptions const& o) { + const size_t n0 = vecV0Dedup.size(); + fitV0Candidate(collision, pos, ele, -1, false, o); + // cppcheck-suppress knownConditionTrueFalse + if (vecV0Dedup.size() > n0) { + const auto k = keyOf(vecV0Dedup.back()); + pca_map.erase(k); + cospa_map.erase(k); + vecV0Dedup.pop_back(); + return 0; + } + return std::max(mLastFitReject, 2); + }; + const int lenient = trial(FitOptions{.relaxAP = mixed.crossedRelaxAP.value}); + const int run1 = trial(FitOptions{.nonSVertexer = finderLike, .purityGate = finderLike && recovery.runFinder.value}); + const auto nFree = static_cast((pos.sourceV0Id() == -2 ? 1 : 0) + (ele.sourceV0Id() == -2 ? 1 : 0)); // o2-linter: disable=magic-number (pooled free leg) + registry.fill(HIST("Mixed/Decision/hFreeLegFit"), type, genuineClose > 0.5f ? 0.f : 1.f, static_cast(std::min(lenient, 1999)), static_cast(std::min(run1, 1999)), nITSx, nFree); + } + const int mask = mixed.freeLegRuleMask.value; + const FitOptions freeOpt{.nonSVertexer = finderLikeFree && (mask & 4) != 0, .relaxAP = (mask & 1) == 0 && mixed.crossedRelaxAP.value, // o2-linter: disable=magic-number (bit mask) + .purityGate = finderLikeFree && (mask & 2) != 0 && recovery.runFinder.value, + .minPurity = mixed.freeLegMinPurity.value}; // o2-linter: disable=magic-number (bit mask) + fitV0Candidate(collision, pos, ele, -1, false, freeLegPair ? freeOpt : FitOptions{.relaxAP = mixed.crossedRelaxAP.value}); + if (vecV0Dedup.size() > nBefore) { + registry.fill(HIST("Mixed/hCandidates"), type, 4.f); + } + if (mixed.mixedDiagnostics) { + registry.fill(HIST("Mixed/Decision/hCrossedFit"), type, genuineClose, vecV0Dedup.size() > nBefore ? 0.f : static_cast(mLastFitReject), nITSx, std::min(qTwo, 0.0499f)); + } + } + } + registry.fill(HIST("Mixed/hTimeCandidates"), type, std::chrono::duration(std::chrono::steady_clock::now() - t0).count()); + } + + const size_t nFit = vecV0Dedup.size(); // candidates of the run-2 fit (the resolvers may append more) + switch (dedup.deduplicationMode.value) { + case V0DeduplicationMode::Pairwise: + selectPairwise(nullptr); + break; + case V0DeduplicationMode::KeepAll: + keepAll(); + break; + case V0DeduplicationMode::GroupMatching: + selectByConflictGroup(nullptr); + break; + default: + selectGreedy(legs.size(), nullptr); + break; + } + if (dedup.resolveSplitLegs) { + resolveSplitLegConflicts(legs, nullptr); + } + if (recovery.runSwapResolver) { + runSwapResolver(collisions, legs, nullptr); + } + if (mixed.mixedDiagnostics && dedup.deduplicationMode.value == static_cast(V0DeduplicationMode::GroupMatching)) { + fillFourLegDecisions(collisions, legs, nFit); + } + if (mixed.mixedDiagnostics) { + fillMatchedConfigs(collisions, legs); + } + + std::unordered_map> nStored; // artificial event -> (originals, crossed) + std::unordered_map> after; // artificial event -> stored photons + for (const auto& key : stored_fullv0Ids) { + const auto collision = collisions.rawIteratorAt(key.collisionId); + const auto pos = legs.rawIteratorAt(key.posId); + const auto ele = legs.rawIteratorAt(key.eleId); + const bool original = key.v0Id >= 0; + const float type = collision.isMixed() ? 1.f : 0.f; + const auto nWritten = v0photonskf.lastIndex(); + const SourcePV srcPv{pos.sourcePvX(), pos.sourcePvY(), pos.sourcePvZ(), pos.sourcePvCovXX(), pos.sourcePvCovXY(), pos.sourcePvCovXZ(), + pos.sourcePvCovYY(), pos.sourcePvCovYZ(), pos.sourcePvCovZZ(), pos.sourcePvChi2(), pos.sourcePvNContrib()}; + const bool finderLike = mixed.uniformPairFit && !(pos.hasITS() && ele.hasITS()); // uniformPairFit: options by the legs, as in the candidate fit + fitV0Candidate(collision, pos, ele, key.v0Id, true, + FitOptions{.nonSVertexer = mixed.uniformPairFit ? finderLike : (original && pos.sourceV0Id() < 0 && !recovery.finderUniformFit.value), + .relaxAP = mRelaxedKeys.contains(key) || (mixed.uniformPairFit ? finderLike : (original ? (pos.sourceV0Id() < 0 && !recovery.finderUniformFit.value) : mixed.crossedRelaxAP.value)), + .purityGate = finderLike && recovery.runFinder.value, + .pv = (original && mixed.refitOriginalsAtSourceVertex) ? &srcPv : nullptr}); + if (v0photonskf.lastIndex() == nWritten) { + continue; // rejected by the ML cut (not a dedup candidate then either) + } + const auto& c = vecV0Dedup.back(); + nStored[key.collisionId][original ? 0 : 1]++; + registry.fill(HIST("Mixed/hStored"), type, original ? 0.f : 1.f); + if (original) { + const float pt1 = std::hypot(pos.sourcePx(), pos.sourcePy()); + const float pt2 = std::hypot(c.pxAtPV, c.pyAtPV); + registry.fill(HIST("Mixed/hOrigRelDPt"), type, pt2 / pt1 - 1.f); + registry.fill(HIST("Mixed/hOrigDPhi"), type, RecoDecay::constrainAngle(RecoDecay::phi(c.pxAtPV, c.pyAtPV) - RecoDecay::phi(pos.sourcePx(), pos.sourcePy()), -o2::constants::math::PI)); + registry.fill(HIST("Mixed/hOrigDEta"), type, RecoDecay::eta(std::array{c.pxAtPV, c.pyAtPV, c.pzAtPV}) - RecoDecay::eta(std::array{pos.sourcePx(), pos.sourcePy(), pos.sourcePz()})); + registry.fill(HIST("Mixed/hOrigExact"), type, (c.pxAtPV == pos.sourcePx() && c.pyAtPV == pos.sourcePy() && c.pzAtPV == pos.sourcePz()) ? 1.f : 0.f); + } else if (pos.sourceV0Id() != -2 && ele.sourceV0Id() != -2) { // -2: pooled free leg, no run-1 photon + registry.fill(HIST("Mixed/hCrossedSourceQ"), type, qInv(pos.sourcePx(), pos.sourcePy(), pos.sourcePz(), ele.sourcePx(), ele.sourcePy(), ele.sourcePz())); + registry.fill(HIST("Mixed/hCrossedSourceDist"), type, std::hypot(pos.sourceVx() - ele.sourceVx(), pos.sourceVy() - ele.sourceVy(), pos.sourceVz() - ele.sourceVz())); + registry.fill(HIST("Mixed/hCrossedSourceEvents"), type, pos.sourceEventUid() == ele.sourceEventUid() ? 0.f : 1.f); + } + float truth = 4.f; // no label + if (pos.mcMotherId() >= 0 && ele.mcMotherId() >= 0) { + const bool truePhoton = pos.sourceEventUid() == ele.sourceEventUid() && pos.mcMotherId() == ele.mcMotherId() && pos.mcMotherPdg() == kGamma; + truth = (original ? 0.f : 2.f) + (truePhoton ? 0.f : 1.f); + } + registry.fill(HIST("Mixed/hStoredTruth"), type, truth); + MixPairPhoton stored; + stored.px = c.pxAtPV; + stored.py = c.pyAtPV; + stored.pz = c.pzAtPV; + setMixPairLegs(stored, pos, ele); + stored.posRow = key.posId; + stored.eleRow = key.eleId; + stored.original = original; + if (const auto itGroup = mGroupOfKey.find(key); itGroup != mGroupOfKey.end()) { + stored.group = itGroup->second; + } + stored.vx = c.svX; + stored.vy = c.svY; + stored.vz = c.svZ; + after[key.collisionId].push_back(stored); + } + for (const auto& collision : collisions) { + const float type = collision.isMixed() ? 1.f : 0.f; + const auto it = nStored.find(collision.globalIndex()); + const std::array n = it == nStored.end() ? std::array{0, 0} : it->second; + const auto itIn = inputOfColl.find(collision.globalIndex()); + const int nIn = itIn == inputOfColl.end() ? 0 : static_cast(itIn->second.size()); // run-1 photons (not the pooled free legs) + registry.fill(HIST("Mixed/hNStoredVsIn"), type, nIn, n[0] + n[1]); + registry.fill(HIST("Mixed/hNOriginalLost"), type, nIn - n[0]); + registry.fill(HIST("Mixed/hNCrossedStored"), type, n[1]); + if (const auto itA = after.find(collision.globalIndex()); mixed.mixedDiagnostics && itA != after.end()) { + fillMixPairs(type, itA->second); + fillSameSourcePairs(type, itA->second); + } + } + + if (mixed.mixedDiagnostics) { // MC diagnostics of the pairings (cost on data) + // alternative pairings of the input pairs: passed the run-2 fit (candidate) / stored + std::set> candLegs, storedLegs; + for (const auto& c : vecV0Dedup) { + candLegs.emplace(c.colID, c.posID, c.eleID); + } + for (const auto& k : stored_fullv0Ids) { + storedLegs.emplace(k.collisionId, k.posId, k.eleId); + } + for (const auto& [colId, input] : inputOfColl) { + fillMixFakeClasses(collisions.rawIteratorAt(colId).isMixed() ? 1.f : 0.f, 0.f, colId, input, candLegs); + } + for (const auto& [colId, out] : after) { + fillMixFakeClasses(collisions.rawIteratorAt(colId).isMixed() ? 1.f : 0.f, 1.f, colId, out, candLegs); + } + // swap-symmetric counting (data-computable): the decision between the two pairings of an ambiguous 4-leg configuration no longer + // enters the pair count, in the same and the mixed event alike + { + std::map, std::array> candMom; // first fit of a leg pair = its dedup candidate + for (const auto& c : vecV0Dedup) { + candMom.emplace(std::make_tuple(static_cast(c.colID), static_cast(c.posID), static_cast(c.eleID)), std::array{c.pxAtPV, c.pyAtPV, c.pzAtPV}); + } + for (const auto& [colId, out] : after) { + const float type = collisions.rawIteratorAt(colId).isMixed() ? 1.f : 0.f; + for (size_t i = 0; i < out.size(); ++i) { + for (size_t j = i + 1; j < out.size(); ++j) { + const auto& a = out[i]; + const auto& b = out[j]; + if (skipSameSourcePair(type, a, b)) { + continue; + } + const float q = qInv(a.px, a.py, a.pz, b.px, b.py, b.pz); + const auto itX1 = candMom.find(std::make_tuple(static_cast(colId), a.posRow, b.eleRow)); + const auto itX2 = candMom.find(std::make_tuple(static_cast(colId), b.posRow, a.eleRow)); + const bool ambiguous = itX1 != candMom.end() && itX2 != candMom.end(); + const float qX = ambiguous ? qInv(itX1->second[0], itX1->second[1], itX1->second[2], itX2->second[0], itX2->second[1], itX2->second[2]) : 999.f; + if (q >= 0.5f && (!ambiguous || qX >= 0.5f)) { // o2-linter: disable=magic-number (histogram range) + continue; + } + const float cls = mixPairClass(a, b); + if (ambiguous) { + registry.fill(HIST("Mixed/Pairs/hQSym"), type, cls, q, 0.5f); + registry.fill(HIST("Mixed/Pairs/hQSym"), type, cls, qX, 0.5f); + } else { + registry.fill(HIST("Mixed/Pairs/hQSym"), type, cls, q); + } + } + } + } + } + std::map, float> candScore; // first fit of a leg pair = its dedup candidate + for (const auto& c : vecV0Dedup) { + candScore.emplace(std::make_tuple(static_cast(c.colID), static_cast(c.posID), static_cast(c.eleID)), c.score); + } + for (const auto& [colId, input] : inputOfColl) { + fillMixSwapBrems(collisions.rawIteratorAt(colId).isMixed() ? 1.f : 0.f, 0.f, colId, input, candScore, legs); + } + for (const auto& [colId, out] : after) { + fillMixSwapBrems(collisions.rawIteratorAt(colId).isMixed() ? 1.f : 0.f, 1.f, colId, out, candScore, legs); + } + for (const auto& [colId, input] : inputOfColl) { + const float type = collisions.rawIteratorAt(colId).isMixed() ? 1.f : 0.f; + for (size_t i = 0; i < input.size(); ++i) { + for (size_t j = i + 1; j < input.size(); ++j) { + const auto& a = input[i]; + const auto& b = input[j]; + const int truth = mixPairTruth(a, b); + if (truth != 1 && truth != 2) { + continue; + } + const float q = qInv(a.px, a.py, a.pz, b.px, b.py, b.pz); + if (q >= 0.1f) { // o2-linter: disable=magic-number (diagnostic range) + continue; + } + const auto alt1 = std::make_tuple(colId, a.posRow, b.eleRow); + const auto alt2 = std::make_tuple(colId, b.posRow, a.eleRow); + const int nAlt = static_cast(candLegs.contains(alt1)) + static_cast(candLegs.contains(alt2)); + int outcome = 2; + if (storedLegs.contains({colId, a.posRow, a.eleRow}) && storedLegs.contains({colId, b.posRow, b.eleRow})) { + outcome = 0; + } else if (storedLegs.contains(alt1) && storedLegs.contains(alt2)) { + outcome = 1; + } + if (truth == 1) { + registry.fill(HIST("Mixed/Pairs/hAltTrueTrue"), type, static_cast(3 * nAlt + outcome), q); + } else { + registry.fill(HIST("Mixed/Pairs/hAltSwap"), type, static_cast(3 * nAlt + outcome), q); + } + } + } + } + } + + pca_map.clear(); + cospa_map.clear(); + nv0_map.clear(); + stored_v0Ids.clear(); + stored_fullv0Ids.clear(); + vecV0Dedup.clear(); + registry.fill(HIST("Mixed/hTimeDF"), std::chrono::duration(std::chrono::steady_clock::now() - tDF).count()); + } + o2::framework::expressions::Filter v0Filter = o2::aod::v0::v0Type > static_cast(0); using FilteredV0s = o2::soa::Filtered; @@ -2221,6 +4055,80 @@ struct PhotonConversionBuilder { } } + template + void fillRun1FourLeg(TMCParticles const& mcparticles, DedupDiag const& diag, std::vector const& cls, std::vector const& motherPos, + std::set const& storedSet, std::map& nCandsOfTrack) + { + using Key3 = std::tuple; + const int nCand = static_cast(diag.snapshot.size()); + std::map firstCand; // first fit of a leg pair = its candidate + std::map, int> trueOfPhoton; // (collision, MC photon) -> first true candidate + for (int i = 0; i < nCand; ++i) { + const auto& c = diag.snapshot[i]; + firstCand.emplace(Key3{c.colID, c.posID, c.eleID}, i); + if (cls[i] == kV0True) { + trueOfPhoton.emplace(std::make_pair(static_cast(c.colID), motherPos[i]), i); + } + } + std::map>> photonsOfColl; + for (const auto& [k, i] : trueOfPhoton) { + photonsOfColl[k.first].emplace_back(k.second, i); + } + auto stored = [&](int i) { return storedSet.contains(keyOf(diag.snapshot[i])); }; + const std::vector legMask{0b0011, 0b1100, 0b1001, 0b0110}; + for (const auto& [colId, phs] : photonsOfColl) { + for (size_t i = 0; i < phs.size(); ++i) { + for (size_t j = i + 1; j < phs.size(); ++j) { + const auto& a = diag.snapshot[phs[i].second]; + const auto& b = diag.snapshot[phs[j].second]; + if (a.posID == b.posID || a.eleID == b.eleID) { + continue; // two MC photons sharing a track (should not happen for true candidates) + } + const auto itX1 = firstCand.find(Key3{colId, a.posID, b.eleID}); + const auto itX2 = firstCand.find(Key3{colId, b.posID, a.eleID}); + if (itX1 == firstCand.end() || itX2 == firstCand.end()) { + continue; + } + const auto mA = mcparticles.rawIteratorAt(phs[i].first); + const auto mB = mcparticles.rawIteratorAt(phs[j].first); + const float qTrue = qInv(mA.px(), mA.py(), mA.pz(), mB.px(), mB.py(), mB.pz()); + if (qTrue >= 0.05f) { // o2-linter: disable=magic-number (diagnostic range) + continue; + } + const int iA = phs[i].second, iB = phs[j].second, iX1 = itX1->second, iX2 = itX2->second; + const int nT = (stored(iA) ? 1 : 0) + (stored(iB) ? 1 : 0), nX = (stored(iX1) ? 1 : 0) + (stored(iX2) ? 1 : 0); + float outcome = 5.f; + if (nT == 2 && nX == 0) { + outcome = 0.f; + } else if (nT == 0 && nX == 2) { + outcome = 1.f; + } else if (nT == 1 && nX == 0) { + outcome = 2.f; + } else if (nT == 0 && nX == 1) { + outcome = 3.f; + } else if (nT == 0 && nX == 0) { + outcome = 4.f; + } + const std::vector scores{a.score, b.score, diag.snapshot[iX1].score, diag.snapshot[iX2].score}; + const auto sol = solveConflictGroup(legMask, scores, dedup.groupMatchingScoreThreshold.value, false); + const float isolated = sol.bestMask == 0b0011 ? 0.f : (sol.bestMask == 0b1100 ? 1.f : 2.f); // o2-linter: disable=magic-number (subset masks) + const int nThird = nCandsOfTrack[a.posID] + nCandsOfTrack[a.eleID] + nCandsOfTrack[b.posID] + nCandsOfTrack[b.eleID] - 8; // o2-linter: disable=magic-number (each leg is in 2 of the 4) + bool related = false; + if (mA.has_mothers() && mB.has_mothers()) { + const auto gA = mcparticles.rawIteratorAt(mA.mothersIds()[0]); + const auto gB = mcparticles.rawIteratorAt(mB.mothersIds()[0]); + related = gA.globalIndex() == gB.globalIndex() || std::abs(gA.pdgCode()) == kElectron || std::abs(gB.pdgCode()) == kElectron; + } + const bool finder = a.v0ID < 0 || b.v0ID < 0; + registry.fill(HIST("MCDedup/FourLeg/hRun1"), qTrue, outcome, isolated, static_cast(std::clamp(nThird, 0, 5)), related ? 1.f : 0.f, finder ? 1.f : 0.f); // o2-linter: disable=magic-number (last bin) + const int crossedOrigin = (diag.snapshot[iX1].v0ID < 0 ? 1 : 0) + (diag.snapshot[iX2].v0ID < 0 ? 1 : 0); // finder off: non-SVertexer = swap resolver + const int trueOrigin = (a.v0ID < 0 ? 1 : 0) + (b.v0ID < 0 ? 1 : 0); + registry.fill(HIST("MCDedup/FourLeg/hRun1Origin"), qTrue, outcome, static_cast(crossedOrigin), static_cast(trueOrigin), nThird > 0 ? 1.f : 0.f, related ? 1.f : 0.f); + } + } + } + } + template void fillDedupTruthDiagnostics(TCollisions const& collisions, TTracks const& tracks, TMCParticles const& mcparticles, DedupDiag const& diag) { @@ -2262,10 +4170,25 @@ struct PhotonConversionBuilder { } } + std::map nCandsOfTrack; + for (int i = 0; i < nCand; ++i) { + nCandsOfTrack[diag.snapshot[i].posID]++; + nCandsOfTrack[diag.snapshot[i].eleID]++; + } + auto fillFateProps = [&](int ci, float fateBin) { + const auto& c = diag.snapshot[ci]; + const int nITS = (tracks.rawIteratorAt(c.posID).hasITS() ? 1 : 0) + (tracks.rawIteratorAt(c.eleID).hasITS() ? 1 : 0); + const int nOther = nCandsOfTrack[c.posID] + nCandsOfTrack[c.eleID] - 2; + registry.fill(HIST("MCDedup/Photons/hFateProps"), fateBin, std::hypot(c.pxAtPV, c.pyAtPV), std::hypot(c.svX, c.svY), static_cast(nITS), + std::acos(std::clamp(c.cosPA, -1.f, 1.f)), c.pca, static_cast(std::min(nOther, 10))); // o2-linter: disable=magic-number (last bin) + }; + std::map> blockersOfPhoton; for (const auto& [mid, f] : fate) { if (f.alive) { registry.fill(HIST("MCDedup/Photons/hFate"), 0.f); + const auto itStored = std::find_if(f.cands.begin(), f.cands.end(), [&](int ci) { return storedSet.contains(key[ci]); }); + fillFateProps(itStored != f.cands.end() ? *itStored : f.best, 0.f); continue; } auto& blockers = blockersOfPhoton[mid]; @@ -2304,6 +4227,10 @@ struct PhotonConversionBuilder { } // hVictimBlockerClass below counts one entry per blocker and a victim can have two. registry.fill(HIST("MCDedup/Photons/hFate"), blockers.empty() ? 2.f : 1.f); + const bool lostToCross = std::any_of(blockers.begin(), blockers.end(), [&](int w) { return cls[w] == kV0CrossLegFake; }); + if (f.best >= 0) { + fillFateProps(f.best, blockers.empty() ? 3.f : (lostToCross ? 1.f : 2.f)); + } if (blockers.empty()) { registry.fill(HIST("MCDedup/Photons/hBlockerClass"), 3.f); // o2-linter: disable=magic-number (bin 3 = no blocker) @@ -2321,6 +4248,49 @@ struct PhotonConversionBuilder { } } + fillRun1FourLeg(mcparticles, diag, cls, motherPos, storedSet, nCandsOfTrack); + + { + std::map, int> storedOfTrack; // (collision, track) -> stored candidate + for (int i = 0; i < nCand; ++i) { + if (storedSet.contains(key[i])) { + const auto& c = diag.snapshot[i]; + storedOfTrack[{c.colID, c.posID}] = i; + storedOfTrack[{c.colID, c.eleID}] = i; + } + } + const float s0 = dedup.groupMatchingScoreThreshold.value; + auto contrib = [&](float sc) { return std::max(s0 - sc, 1e-3f); }; // o2-linter: disable=magic-number (objective floor) + for (const auto& [mid, f] : fate) { + if (f.best < 0) { + continue; + } + const auto& t = diag.snapshot[f.best]; + const float pt = std::hypot(t.pxAtPV, t.pyAtPV); + if (f.alive) { + registry.fill(HIST("MCDedup/HalfSwap/hSource"), 0.f, t.score, 0.f, 0.f, pt); + continue; + } + const auto ip = storedOfTrack.find({t.colID, t.posID}); + const auto ie = storedOfTrack.find({t.colID, t.eleID}); + const bool hp = ip != storedOfTrack.end(), he = ie != storedOfTrack.end(); + float cat = 3.f; + if (hp && he) { + cat = ip->second != ie->second ? 1.f : 4.f; + } else if (hp || he) { + cat = 2.f; + } + float dObj = 0.f, dRaw = 0.f; + if (cat == 1.f) { + const auto& w1 = diag.snapshot[ip->second]; + const auto& w2 = diag.snapshot[ie->second]; + dObj = contrib(w1.score) + contrib(w2.score) - contrib(t.score); + dRaw = 0.5f * (w1.score + w2.score) - t.score; + } + registry.fill(HIST("MCDedup/HalfSwap/hSource"), cat, t.score, std::clamp(dObj, -0.499f, 0.499f), std::clamp(dRaw, -0.999f, 0.999f), pt); + } + } + if (dedup.cfgExtendedDiag) { fillCopyDiagnostics(collisions, mcparticles, diag, fate); } @@ -2434,9 +4404,199 @@ struct PhotonConversionBuilder { } } } + + { + struct Elig { + int64_t pos = -1, ele = -1; + int nIts = 0; + float vx = 0.f, vy = 0.f; + }; + std::map elig; + for (const auto& track : tracks) { + if (track.pt() < trackcuts.min_pt_trackiu || !checkV0leg(track)) { + continue; + } + const int64_t mid = getPhotonMotherId(track, mcparticles); + if (mid < 0) { + continue; + } + auto& e = elig[mid]; + if (track.sign() > 0) { + e.pos = track.globalIndex(); + } else { + e.ele = track.globalIndex(); + } + if (track.hasITS()) { + e.nIts++; + } + const auto mc = track.template mcParticle_as(); + e.vx = mc.vx(); + e.vy = mc.vy(); + } + std::map crossedStoredOfPhoton; + std::map, std::pair> crossedOfPair; // (offered, stored) + std::map, std::vector> crossedOriginOfPair; // stored crossed: 0 SVertexer, 1 finder, 2 swap resolver + std::map, std::vector> crossedIndexOfPair; // stored crossed: candidate index + std::map, const SwapRecord*> swapRecordOf; + for (const auto& r : diag.swaps) { + swapRecordOf[std::minmax(r.keyI, r.keyJ)] = &r; + } + for (int i = 0; i < nCand; ++i) { + if (cls[i] != kV0CrossLegFake) { + continue; + } + auto& cp = crossedOfPair[std::minmax(motherPos[i], motherEle[i])]; + cp.first++; + if (storedSet.contains(key[i])) { + cp.second++; + crossedOriginOfPair[std::minmax(motherPos[i], motherEle[i])].push_back(key[i].v0Id >= 0 ? 0 : (diag.finderKeys.contains(key[i]) ? 1 : 2)); + crossedIndexOfPair[std::minmax(motherPos[i], motherEle[i])].push_back(i); + crossedStoredOfPhoton[motherPos[i]]++; + crossedStoredOfPhoton[motherEle[i]]++; + } + } + std::map> eligByMcColl; + for (const auto& [mid, e] : elig) { + if (e.pos >= 0 && e.ele >= 0) { + eligByMcColl[mcparticles.iteratorAt(mid).mcCollisionId()].push_back(mid); + } + } + auto history = [&](int64_t mid, int& detail) { // run-1 history of one true photon, see MCDedup/Inventory/hSwapOrigin + detail = -10; + if (fate.contains(mid)) { + return 0; + } + const auto& e = elig.at(mid); + const auto it = diag.trueLegTrace.find({e.pos, e.ele}); + const TrueLegTrace t = it == diag.trueLegTrace.end() ? TrueLegTrace{} : it->second; + if (t.svLine > 0) { + detail = t.svLine; + return 1; + } + if (t.svLine == 0) { + return 6; // o2-linter: disable=magic-number (history codes) + } + detail = t.finderStep; + if (e.nIts == 2 || t.finderStep == -8) { // o2-linter: disable=magic-number (both legs with ITS) + detail = -8; // o2-linter: disable=magic-number (trace code) + return 2; + } + if (t.finderStep == FinderStep::kFindCollision) { + detail = t.finderLine; + return t.finderLine > 0 ? 5 : 6; // o2-linter: disable=magic-number (history codes) + } + return t.finderStep >= -1 ? 4 : 3; // o2-linter: disable=magic-number (history codes) + }; + auto photonFate = [&](int64_t mid) { // 0 no true candidate, 1 candidate lost, 2 stored, 3 lost but a leg in a stored crossed candidate + const auto it = fate.find(mid); + const bool inCrossed = crossedStoredOfPhoton.contains(mid); + if (it == fate.end()) { + return inCrossed ? 3 : 0; + } + if (it->second.alive) { + return 2; + } + return inCrossed ? 3 : 1; + }; + for (const auto& [mcCol, mids] : eligByMcColl) { + std::vector closest(mids.size(), 0.0999f); + for (size_t a = 0; a < mids.size(); ++a) { + const auto ga = mcparticles.iteratorAt(mids[a]); + for (size_t b = a + 1; b < mids.size(); ++b) { + const auto gb = mcparticles.iteratorAt(mids[b]); + const float qTrue = qInv(ga.px(), ga.py(), ga.pz(), gb.px(), gb.py(), gb.pz()); + closest[a] = std::min(closest[a], qTrue); + closest[b] = std::min(closest[b], qTrue); + if (qTrue >= 0.1f) { + continue; + } + const float e1 = ga.p(), e2 = gb.p(); + const float asym = (e1 + e2) > 0.f ? std::fabs(e1 - e2) / (e1 + e2) : 0.f; + const float kt = 0.5f * std::hypot(ga.px() + gb.px(), ga.py() + gb.py()); + const int fa = photonFate(mids[a]), fb = photonFate(mids[b]); + const auto itx = crossedOfPair.find(std::minmax(mids[a], mids[b])); + const int nCrossOff = itx == crossedOfPair.end() ? 0 : std::min(itx->second.first, 2); + const int nCrossSt = itx == crossedOfPair.end() ? 0 : itx->second.second; + const int nStoredTrue = (fa == 2 ? 1 : 0) + (fb == 2 ? 1 : 0); + int pairFate = 5; + if (nStoredTrue == 2) { + pairFate = 0; + } else if (nStoredTrue == 1 && nCrossSt == 0) { + pairFate = 1; + } else if (nStoredTrue == 0 && nCrossSt == 0) { + pairFate = 2; + } else if (nStoredTrue == 0 && nCrossSt == 2) { + pairFate = 3; + } else if (nCrossSt == 1) { + pairFate = 4; + } + const int nTrueCand = (fate.contains(mids[a]) ? 1 : 0) + (fate.contains(mids[b]) ? 1 : 0); + registry.fill(HIST("MCDedup/Inventory/hPair"), qTrue, std::min(asym, 0.999f), std::min(kt, 1.499f), static_cast(pairFate), static_cast(nTrueCand), static_cast(nCrossOff)); + int detA = -10, detB = -10; + int hA = history(mids[a], detA), hB = history(mids[b], detB); + if (qTrue < 0.02f) { // o2-linter: disable=magic-number (close pairs) + if (hA > 0) { + registry.fill(HIST("MCDedup/Inventory/hNeverDetail"), static_cast(pairFate), static_cast(hA), static_cast(detA)); + } + if (hB > 0) { + registry.fill(HIST("MCDedup/Inventory/hNeverDetail"), static_cast(pairFate), static_cast(hB), static_cast(detB)); + } + } + if (pairFate == 3 || pairFate == 4) { // o2-linter: disable=magic-number (swap, one crossed stored) + auto o = crossedOriginOfPair.at(std::minmax(mids[a], mids[b])); + const int o1 = std::min(o[0], o.size() > 1 ? o[1] : o[0]), o2 = std::max(o[0], o.size() > 1 ? o[1] : o[0]); + static constexpr std::array, 3> OriginCode{{{0, 1, 3}, {1, 2, 4}, {3, 4, 5}}}; + if (hA < hB) { + std::swap(hA, hB); + } + registry.fill(HIST("MCDedup/Inventory/hSwapOrigin"), qTrue, pairFate == 3 ? 0.f : 1.f, static_cast(nTrueCand), static_cast(OriginCode[o1][o2]), static_cast(hA), static_cast(hB)); + } + if (pairFate == 3) { // o2-linter: disable=magic-number (full swap): what the swap resolver did with the two stored crossed photons + const auto& ix = crossedIndexOfPair.at(std::minmax(mids[a], mids[b])); + const auto& x1 = diag.snapshot[ix[0]]; + const auto& x2 = diag.snapshot[ix.size() > 1 ? ix[1] : ix[0]]; + int outcome = 8; // o2-linter: disable=magic-number (outcome codes) + float gain = -1.999f; + int line = 0; + const float qRec = qInv(x1.pxAtPV, x1.pyAtPV, x1.pzAtPV, x2.pxAtPV, x2.pyAtPV, x2.pzAtPV); + if (x1.colID != x2.colID) { + outcome = 0; + } else if (!recovery.runSwapResolver.value) { + outcome = 8; // o2-linter: disable=magic-number (outcome codes) + } else if (qRec > recovery.swapMaxQ) { + outcome = 1; + } else if (std::hypot(x1.svX - x2.svX, x1.svY - x2.svY, x1.svZ - x2.svZ) > recovery.swapMaxConvDist) { + outcome = 2; + } else if (const auto it = swapRecordOf.find(std::minmax(keyOf(x1), keyOf(x2))); it != swapRecordOf.end()) { + const auto& r = *it->second; + if (r.complete) { + gain = std::clamp(-r.gain, -1.999f, 1.999f); // crossed (stored) minus true (alternative) + outcome = r.gain > 0.f ? 7 : 6; // o2-linter: disable=magic-number (outcome codes) + } else if ((r.fail & 3) != 0) { // o2-linter: disable=magic-number (bit mask) + outcome = 5; // o2-linter: disable=magic-number (outcome codes) + } else if ((r.fail & 48) != 0) { // o2-linter: disable=magic-number (bit mask) + outcome = 4; // o2-linter: disable=magic-number (outcome codes) + line = r.line; + } else { + outcome = 3; + } + } + registry.fill(HIST("MCDedup/Inventory/hSwapLoss"), qTrue, static_cast(nTrueCand), static_cast(outcome), gain); + if (outcome == 4 && qTrue < 0.02f) { // o2-linter: disable=magic-number (close pairs) + registry.fill(HIST("MCDedup/Inventory/hSwapLossCut"), static_cast(nTrueCand), static_cast(line)); + } + } + } + } + for (size_t a = 0; a < mids.size(); ++a) { + const auto ga = mcparticles.iteratorAt(mids[a]); + const auto& e = elig.at(mids[a]); + registry.fill(HIST("MCDedup/Inventory/hPhoton"), std::min(ga.p(), 1.999f), std::min(std::hypot(e.vx, e.vy), 89.9f), static_cast(e.nIts), static_cast(photonFate(mids[a])), closest[a]); + } + } + } } - // MC, cfgExtendedDiag: class of the stored photons by source, stored photon pairs at low q, outcome of the resolver tests template void fillExtendedDiagnostics(TTracks const& tracks, TMCParticles const& mcparticles, DedupDiag const& diag) { @@ -2448,8 +4608,10 @@ struct PhotonConversionBuilder { std::array p{}; }; std::map candOfKey; + std::set> candLegs; // (collision, pos, ele) of all dedup candidates for (const auto& c : diag.snapshot) { candOfKey[keyOf(c)] = &c; + candLegs.emplace(c.colID, c.posID, c.eleID); } std::map> storedOfColl; for (const auto& key : diag.stored) { @@ -2478,6 +4640,8 @@ struct PhotonConversionBuilder { cat = 0; } else if (a.cls == kV0CrossLegFake && b.cls == kV0CrossLegFake && a.mPos == b.mEle && a.mEle == b.mPos) { cat = 1; + const int nTrue = static_cast(candLegs.contains({col, a.key.posId, b.key.eleId})) + static_cast(candLegs.contains({col, b.key.posId, a.key.eleId})); + registry.fill(HIST("MCDedup/Stored/hSwapTrueCands"), static_cast(nTrue), q); } registry.fill(HIST("MCDedup/Stored/hPairClass"), static_cast(cat), q); } @@ -2491,9 +4655,17 @@ struct PhotonConversionBuilder { isTrue[k] = classifyV0Truth(tracks.rawIteratorAt(legs[k].first), tracks.rawIteratorAt(legs[k].second), mcparticles, mPos, mEle) == kV0True ? 1 : 0; } registry.fill(HIST("MCDedup/Swap/hCategory"), static_cast(3 * (isTrue[0] + isTrue[1]) + isTrue[2] + isTrue[3]), s.swapped ? 1.f : 0.f, s.q); + int outcome = 3; // o2-linter: disable=magic-number (outcome codes) + if (s.swapped) { + outcome = 4; // o2-linter: disable=magic-number (outcome codes) + } else if (!s.complete) { + outcome = (s.fail & 3) != 0 ? 2 : ((s.fail & 48) != 0 ? 1 : 0); // o2-linter: disable=magic-number (bit mask) + } + registry.fill(HIST("MCDedup/Swap/hResolverTruth"), static_cast(isTrue[0] + isTrue[1]), static_cast(isTrue[2] + isTrue[3]), static_cast(outcome), s.complete ? std::clamp(s.gain, -1.999f, 1.999f) : -1.999f, std::min(s.q, 0.0999f)); } } + // ===== process functions ===== void processRec(MyCollisions const& collisions, FilteredV0s const& v0s, MyTracksIU const& tracks, MyBCs const& bcs) { build(collisions, v0s, tracks, bcs); @@ -2524,6 +4696,45 @@ struct PhotonConversionBuilder { } PROCESS_SWITCH(PhotonConversionBuilder, processRec_OnlyIfDielectron, "process reconstructed info for data", false); + using MixCollisionsIn = std::conditional_t; + using MixLegsIn = std::conditional_t; + std::vector().rawIteratorAt(0))> mLegIt; // run 2: one iterator per PCMMixLegs row of the DF (buildMixed) + void processMixed(MixCollisionsIn const& collisions, MixLegsIn const& legs, MyBCs const& bcs) + { + if constexpr (kMixedInstance) { + if (collisions.size() == 0 || bcs.size() == 0) { + return; + } + const auto bc = bcs.begin(); // B field / material of this DF's run + initCCDB(bc); + updateCCDB(bc); + buildMixed(collisions, legs); + } else { + LOG(fatal) << "processMixed only in photon-conversion-builder-mixed (o2-analysis-em-photon-conversion-builder-mixed)"; + } + } + PROCESS_SWITCH(PhotonConversionBuilder, processMixed, "builder run 2 of the co-reconstructed mixed event (input from photon-mixed-event-producer), own instance only", false); + + void processMixedDerived(MixCollisionsIn const& collisions, MixLegsIn const& legs) + { + if constexpr (kMixedInstance) { + if (collisions.size() == 0) { + return; + } + const auto event = collisions.begin(); // B field / material of this DF's run + const auto* grp = ccdb->template getForTimeStamp("GLO/Config/GRPMagField", event.timestamp()); + if (grp == nullptr) { + LOG(fatal) << "processMixedDerived: no GRPMagField for timestamp " << event.timestamp(); + } + initCCDBFor(event.runNumber(), event.timestamp(), *grp); + updateCCDBAt(event.timestamp()); + buildMixed(collisions, legs); + } else { + LOG(fatal) << "processMixedDerived only in photon-conversion-builder-mixed (o2-analysis-em-photon-conversion-builder-mixed)"; + } + } + PROCESS_SWITCH(PhotonConversionBuilder, processMixedDerived, "builder run 2 on derived data (input from photon-mixed-event-producer processDerived), own instance only", false); + // void processRec_SWT_OnlyIfDielectron(soa::Join const& collisions, FilteredV0s const& v0s, MyTracksIU const& tracks, MyBCs const& bcs) // { // build(collisions, v0s, tracks, bcs); diff --git a/PWGEM/PhotonMeson/TableProducer/photonconversionbuilderMixed.cxx b/PWGEM/PhotonMeson/TableProducer/photonconversionbuilderMixed.cxx new file mode 100644 index 00000000000..5514fb86c31 --- /dev/null +++ b/PWGEM/PhotonMeson/TableProducer/photonconversionbuilderMixed.cxx @@ -0,0 +1,28 @@ +// Copyright 2019-2025 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. +// + +/// \file photonconversionbuilderMixed.cxx +/// \brief builder run 2 of the mixed event: rebuilds the photons of the artificial events of photon-mixed-event-producer +/// from their legs (set processRec = false, processMixed = true) +/// \author Stefanie Mrozinski + +#include "PWGEM/PhotonMeson/TableProducer/photonconversionbuilder.h" + +#include +#include +#include + +o2::framework::WorkflowSpec defineDataProcessing(o2::framework::ConfigContext const& context) +{ + return o2::framework::WorkflowSpec{ + adaptAnalysisTask>(context, o2::framework::TaskName{"photon-conversion-builder-mixed"})}; +} diff --git a/PWGEM/PhotonMeson/TableProducer/photonconversionbuilderTmpTable.cxx b/PWGEM/PhotonMeson/TableProducer/photonconversionbuilderTmpTable.cxx index 0346617358c..1c873c197ed 100644 --- a/PWGEM/PhotonMeson/TableProducer/photonconversionbuilderTmpTable.cxx +++ b/PWGEM/PhotonMeson/TableProducer/photonconversionbuilderTmpTable.cxx @@ -23,5 +23,5 @@ o2::framework::WorkflowSpec defineDataProcessing(o2::framework::ConfigContext const& context) { return o2::framework::WorkflowSpec{ - adaptAnalysisTask>(context, o2::framework::TaskName{"photon-conversion-builder-tmptable"})}; + adaptAnalysisTask>(context, o2::framework::TaskName{"photon-conversion-builder-tmptable"})}; } diff --git a/PWGEM/PhotonMeson/Tasks/photonhbt.cxx b/PWGEM/PhotonMeson/Tasks/photonhbt.cxx index fa490de325d..9c89b99bd94 100644 --- a/PWGEM/PhotonMeson/Tasks/photonhbt.cxx +++ b/PWGEM/PhotonMeson/Tasks/photonhbt.cxx @@ -21,6 +21,7 @@ #include "PWGEM/PhotonMeson/DataModel/gammaTables.h" #include "PWGEM/PhotonMeson/Utils/PCMUtilities.h" #include "PWGEM/PhotonMeson/Utils/PairUtilities.h" +#include "PWGEM/PhotonMeson/Utils/V0DedupUtilities.h" #include "Common/Core/RecoDecay.h" #include "Common/Core/trackUtilities.h" @@ -68,6 +69,7 @@ #include #include #include +#include #include #include #include @@ -106,6 +108,9 @@ using MyV0Photon = MyV0Photons::iterator; using MyMCV0Legs = soa::Join; using MyMCV0Leg = MyMCV0Legs::iterator; +// output of builder run 2 (co-reconstructed mixed event, processCoReco): photons of the artificial events of PCMMixCollisions +using MyV0PhotonsMix = soa::Join; + static constexpr float kMinMagnitude = 1e-12f; static constexpr float kMinCosine = 1e-12f; static constexpr float kMinSigma = 1e-9f; @@ -272,6 +277,16 @@ struct Photonhbt { // m(e+e-) of the two leg-swapped combinations std::array mee{999.f, 999.f}; float meeOverQ{999.f}; + // geometry of the two leg-swapped combinations (a's e+ with b's e-, b's e+ with a's e-): would the swapped legs make a photon? + // helix circles from the conversion point and the leg momentum; gap = |c+ - c-| - (r+ + r-) (0 for a conversion), |tgl+ - tgl-|, + // z+ - z- of the two legs at the touching point of the circles (vertex-subtracted z, so mixed pairs are treated like same pairs) + std::array gap{999.f, 999.f}; + std::array dTgl{999.f, 999.f}; + std::array dz{999.f, 999.f}; + int bestCombo{-1}; // the more photon-like combination (smaller |gap| / gapMax + |dz| / dzMax) + float gapBest{999.f}, dTglBest{999.f}, dzBest{999.f}; + bool geomPhotonLike{false}; // a combination inside the windows crosspair_group.cfgCrossMax{Gap,DTgl,Dz} + float qSwap{-1.f}; // q_inv of the two leg-swapped "photons" (a's e+ with b's e-, b's e+ with a's e-): the q this pair would have if the legs had been swapped }; struct TruthGamma { @@ -360,6 +375,12 @@ struct Photonhbt { Configurable cfgFillLegSimilaritySparse{"cfgFillLegSimilaritySparse", true, "book/fill the like-sign leg similarity sparse (duplicate diagnostic: DeltaEta, DeltaPhi, pT ratio, dNsigma_e vs qinv)"}; Configurable cfgFillLegSimilarityMixSparse{"cfgFillLegSimilarityMixSparse", false, "book/fill the MIXED-EVENT leg similarity sparse (kinematic baseline for the SE-ME duplicate excess; needs cfgFillLegSimilaritySparse)"}; Configurable cfgFillR1R2Sparse{"cfgFillR1R2Sparse", true, "book/fill the FullRange (R1, R2, q_inv) sparse: fix one conversion radius, see where the partner sits, per q_inv"}; + // pair-class QA on the co-reconstructed mixed event (processPairClassMix) + Configurable cfgPairClassMaxQ{"cfgPairClassMaxQ", 0.5f, "pair-class QA: max q_inv of the pairs (GeV/c)"}; + Configurable cfgPairClassMixDepth{"cfgPairClassMixDepth", 10, "pair-class QA, standard mixed event: same-event artificial events kept per pool"}; + Configurable cfgPairClassMixZBinWidth{"cfgPairClassMixZBinWidth", 2.f, "pair-class QA, standard mixed event: pool z-vertex bin width (cm)"}; + Configurable cfgPairClassMixCentBinWidth{"cfgPairClassMixCentBinWidth", 10.f, "pair-class QA, standard mixed event: pool centrality (FT0C) bin width (%)"}; + Configurable cfgPairClassMaxSharedFrac{"cfgPairClassMaxSharedFrac", 0.4f, "pair-class QA, clean flag: all four legs with TPC shared-cluster fraction below this"}; } qaflags; // ─── HBT analysis mode ─────────────────────────────────────────────────────────── @@ -419,6 +440,10 @@ struct Photonhbt { ConfigurableAxis confCentBins{"confCentBins", {VARIABLE_WIDTH, 0.f, 5.f, 10.f, 20.f, 30.f, 40.f, 50.f, 60.f, 70.f, 80.f, 90.f, 100.f, 999.f}, "Mixing bins - centrality"}; ConfigurableAxis confEPBinsBins{"confEPBinsBins", {16, -o2::constants::math::PIHalf, +o2::constants::math::PIHalf}, "Mixing bins - EP angle"}; ConfigurableAxis confOccupancyBins{"confOccupancyBins", {VARIABLE_WIDTH, -1, 1e+10}, "Mixing bins - occupancy"}; + // processCoReco (output of builder run 2): which mixed event fills Pair/mix + Configurable cfgCoRecoMixMode{"cfgCoRecoMixMode", 0, "processCoReco: 0 co-reconstructed mixed event (pairs inside the mixed artificial events), 1 standard mixed event built here from the same-event artificial events (pools as for processAnalysis, no BC gap), for old vs new on the same photons"}; + Configurable cfgCoRecoSkipOneSource{"cfgCoRecoSkipOneSource", true, "processCoReco: skip pairs of a mixed artificial event whose four legs all come from one source event (same-event pairs by construction)"}; + Configurable cfgCoRecoMC{"cfgCoRecoMC", false, "processCoReco: MC diagnostics from the leg labels carried in PCMMixLegs (truth-type CFs as processMC, pair-class decomposition)"}; } mixing; // ─── Centrality slection ───────────────────────────────────────────────── @@ -500,6 +525,10 @@ struct Photonhbt { Configurable cfgDoCrossPairCut{"cfgDoCrossPairCut", false, "reject pairs with a photon-like crossed combination; applied to SE AND ME"}; Configurable cfgCrossMaxMeeRatio{"cfgCrossMaxMeeRatio", 0.2f, "veto: min(m_ee^cross)/q_inv below this; genuine pairs sit near 0.5, leg swaps near 0"}; Configurable cfgCrossMaxQinvQA{"cfgCrossMaxQinvQA", 0.3f, "fill crossed QA sparses only below this qinv"}; + Configurable cfgDoCrossGeomCut{"cfgDoCrossGeomCut", false, "reject pairs whose leg-swapped combination is geometrically a photon: helix circles of the swapped legs touching (|gap| < cfgCrossMaxGap), |tgl+ - tgl-| < cfgCrossMaxDTgl and the legs' z at the touching point within cfgCrossMaxDz (vertex-subtracted); applied to SE AND ME. A stored leg-swapped pair (two crossed fakes of two collinear photons) always has such a combination (the true photons), a genuine pair only when the two photons are collinear and convert on one line"}; + Configurable cfgCrossMaxGap{"cfgCrossMaxGap", 1.5f, "geometric crossed veto: max |circle gap| of the swapped legs (cm)"}; + Configurable cfgCrossMaxDTgl{"cfgCrossMaxDTgl", 0.1f, "geometric crossed veto: max |tgl+ - tgl-| of the swapped legs"}; + Configurable cfgCrossMaxDz{"cfgCrossMaxDz", 2.f, "geometric crossed veto: max |z+ - z-| of the swapped legs at the touching point (cm)"}; } crosspair; struct : ConfigurableGroup { @@ -542,6 +571,7 @@ struct Photonhbt { Configurable cfgMaxPCA{"cfgMaxPCA", 3.0, "max distance between 2 legs"}; Configurable cfgMaxChi2KF{"cfgMaxChi2KF", 1e+10, "max chi2/ndf with KF"}; Configurable cfgRejectV0OnITSIB{"cfgRejectV0OnITSIB", true, "reject V0s on ITSib"}; + Configurable cfgMinNCrossedRowsOverFindable{"cfgMinNCrossedRowsOverFindable", 0.8f, "min. TPC crossed rows / findable clusters of the legs (was hard-coded 0.8)"}; Configurable cfgDisableITSOnlyTrack{"cfgDisableITSOnlyTrack", false, "disable ITS-only tracks"}; Configurable cfgDisableTPCOnlyTrack{"cfgDisableTPCOnlyTrack", false, "disable TPC-only tracks"}; Configurable cfgMinNClusterTPC{"cfgMinNClusterTPC", 70, "min ncluster TPC"}; @@ -674,9 +704,19 @@ struct Photonhbt { void init(InitContext& context) { isMC = context.mOptions.get("processMC"); + const bool doCoReco = context.mOptions.get("processCoReco"); + if (doCoReco && (isMC || context.mOptions.get("processAnalysis"))) { + LOGF(fatal, "processCoReco fills the same Pair/same and Pair/mix histograms as processAnalysis / processMC: run it in its own photonhbt instance"); + } + if (doCoReco) { + isMC = mixing.cfgCoRecoMC.value; + } if (context.mOptions.get("processMCAOD")) { addMCAODHistograms(); } + if (context.mOptions.get("processPairClassMix")) { + addPairClassMixHistograms(); + } if (pairsep.cfgCloseThrCm.value.size() != 4) { // o2-linter: disable=magic-number (four thresholds) LOGF(fatal, "cfgCloseThrCm must contain exactly four thresholds, got %zu", pairsep.cfgCloseThrCm.value.size()); @@ -698,7 +738,7 @@ struct Photonhbt { (ggpaircuts.cfgDoEllipseCut.value || ggpaircuts.cfgDoRCut.value || ggpaircuts.cfgDoZCut.value || ggpaircuts.cfgMinDRCosOA.value > 0.f || ggpaircuts.cfgMaxAsymmetry.value >= 0.f || pairsep.cfgDoPairMergeCut.value || - crosspair.cfgDoCrossPairCut.value || cfgDoPhotonClassPairCut.value); + crosspair.cfgDoCrossPairCut.value || crosspair.cfgDoCrossGeomCut.value || cfgDoPhotonClassPairCut.value); LOGF(info, "photonhbt QA level %d -> pairQA %d, singlePhotonQA %d, legPairQA %d, pairSep %d, dRdZ sparse %d, R1R2 sparse %d, pairSep afterCut %d", qaLevel, mQa.pairQa, mQa.singlePhotonQa, mQa.legPairQa, mQa.pairSepQa, mQa.fillDRDZSparse, mQa.fillR1R2Sparse, mQa.pairSepAfterCut); mRunNumber = 0; @@ -713,6 +753,9 @@ struct Photonhbt { mPhotonClassSelA = pairutil::buildV0PhotonClassSelection(photonclassA); mPhotonClassSelB = pairutil::buildV0PhotonClassSelection(photonclassB); addhistograms(); + if (doCoReco) { + addCoRecoHistograms(); + } std::random_device seedGen; engine = std::mt19937(seedGen()); dist01 = std::uniform_int_distribution(0, 1); @@ -750,9 +793,10 @@ struct Photonhbt { LOGF(info, "photonhbt pair merging cut: %s, D_pair > %.2f, N_close < %d (0 = off); control window %.2f < q_inv < %.2f", pairsep.cfgDoPairMergeCut.value ? "ON" : "off", pairsep.cfgDPairCut.value, pairsep.cfgNCloseCut.value, pairsep.cfgCondQinvMin.value, pairsep.cfgCondQinvMax.value); - LOGF(info, "photonhbt: crossed-pair QA %s, cut %s (min(m_ee)/q_inv > %.2f)", + LOGF(info, "photonhbt: crossed-pair QA %s, cut %s (min(m_ee)/q_inv > %.2f), geometric cut %s (|gap| < %.2f cm, |dtgl| < %.3f, |dz| < %.2f cm)", crosspair.cfgDoCrossPairQA.value ? "on" : "off", - crosspair.cfgDoCrossPairCut.value ? "ON" : "off", crosspair.cfgCrossMaxMeeRatio.value); + crosspair.cfgDoCrossPairCut.value ? "ON" : "off", crosspair.cfgCrossMaxMeeRatio.value, + crosspair.cfgDoCrossGeomCut.value ? "ON" : "off", crosspair.cfgCrossMaxGap.value, crosspair.cfgCrossMaxDTgl.value, crosspair.cfgCrossMaxDz.value); LOGF(info, "photonhbt: q_inv gates -- QA < %.3f, FullRange < %.3f, PairSep < %.3f", qaflags.cfgMaxQinvForQA.value, qaflags.cfgMaxQinvForFullRange.value, pairsep.cfgPairSepMaxQinv.value); @@ -825,7 +869,7 @@ struct Photonhbt { fV0PhotonCut.RejectITSib(pcmcuts.cfgRejectV0OnITSIB); fV0PhotonCut.SetMinNClustersTPC(pcmcuts.cfgMinNClusterTPC); fV0PhotonCut.SetMinNCrossedRowsTPC(pcmcuts.cfgMinNCrossedRows); - fV0PhotonCut.SetMinNCrossedRowsOverFindableClustersTPC(0.8); + fV0PhotonCut.SetMinNCrossedRowsOverFindableClustersTPC(pcmcuts.cfgMinNCrossedRowsOverFindable); fV0PhotonCut.SetMaxFracSharedClustersTPC(pcmcuts.cfgMaxFracSharedClustersTPC); fV0PhotonCut.SetChi2PerClusterTPC(0.0, pcmcuts.cfgMaxChi2TPC); fV0PhotonCut.SetTPCNsigmaElRange(pcmcuts.cfgMinTPCNsigmaEl, pcmcuts.cfgMaxTPCNsigmaEl); @@ -1113,13 +1157,17 @@ struct Photonhbt { void addCrossPairHistograms() { - if (!(crosspair.cfgDoCrossPairQA.value || crosspair.cfgDoCrossPairCut.value)) { + if (!(crosspair.cfgDoCrossPairQA.value || crosspair.cfgDoCrossPairCut.value || crosspair.cfgDoCrossGeomCut.value)) { return; } for (const auto& sm : {std::string("Pair/same/CrossPair/"), std::string("Pair/mix/CrossPair/")}) { fRegistryCF.add((sm + "hSparse_MeeRatio_dR_Qinv").c_str(), "crossed-hypothesis QA;min(m_{ee}^{cross})/q_{inv};|R_{1}-R_{2}| (cm);q_{inv} (GeV/c)", kTHnSparseF, {{100, 0.f, 2.f}, {80, 0.f, 80.f}, {60, 0.f, 0.3f}}, true); + fRegistryCF.add((sm + "hSparse_CrossGeom_Qinv").c_str(), + "geometry of the more photon-like leg-swapped combination;circle gap (cm);|z_{+} - z_{-}| at the touching point (cm);|tgl_{+} - tgl_{-}|;q_{inv} (GeV/c)", + kTHnSparseF, {{100, -5.f, 5.f}, {60, 0.f, 6.f}, {50, 0.f, 0.5f}, {30, 0.f, 0.15f}}, true); + fRegistryCF.add((sm + "hSwapTemplate").c_str(), "swap template: q_inv the pair would have with swapped legs;q_{inv} of the pair (GeV/c);q_{inv} of the leg-swapped pairing (GeV/c);0 swapped combination not photon-like, 1 photon-like (in the geometric window)", kTH3F, {{60, 0.f, 0.3f}, {60, 0.f, 0.3f}, {2, -0.5f, 1.5f}}, true); auto h = fRegistryCF.add((sm + "hVetoCounter").c_str(), "crossed-pair veto;;pairs", kTH1D, {{2, -0.5f, 1.5f}}, true); h->GetXaxis()->SetBinLabel(1, "evaluated"); h->GetXaxis()->SetBinLabel(2, "vetoed"); @@ -1244,6 +1292,8 @@ struct Photonhbt { kTHnSparseF, {axisPsiPair, axisPhiV, axisQinv, axisTruthType, axisIsTruePhoton}, true); const AxisSpec axisMeeRatio{100, 0.f, 2.f, "min(m_{ee}^{cross})/q_{inv}"}; + fRegistryPairMC.add("Pair/same/MC/hSparse_SwapTemplate_Type", "swap template per truth type;q_{inv} of the pair (GeV/c);q_{inv} of the leg-swapped pairing (GeV/c);swapped combination photon-like;truth type", kTHnSparseF, {{60, 0.f, 0.3f}, {60, 0.f, 0.3f}, {2, -0.5f, 1.5f}, {7, -0.5f, 6.5f}}, true); + fRegistryPairMC.add("Pair/same/MC/hSparse_CrossGeom_Qinv_Type", "geometry of the more photon-like leg-swapped combination per truth type;circle gap (cm);|z_{+} - z_{-}| at the touching point (cm);q_{inv} (GeV/c);truth type", kTHnSparseF, {{100, -5.f, 5.f}, {60, 0.f, 6.f}, {30, 0.f, 0.15f}, {7, -0.5f, 6.5f}}, true); fRegistryPairMC.add("Pair/same/MC/hSparse_MeeRatio_Qinv_Type", "crossed-pair hypothesis per truth type;" "min(m_{ee}^{cross})/q_{inv};q_{inv} (GeV/c);truth type", @@ -1281,6 +1331,8 @@ struct Photonhbt { const AxisSpec axisFakeSubtype{3, -0.5f, 2.5f, "0=ordinary, 1=swap doublet, 2=partial swap"}; fRegistryPairMC.add("Pair/same/MC/hFakeSubtypeVsQinv", "fake pair subtype vs q_{inv};q_{inv} (GeV/c);subtype", kTH2D, {axisQinv, axisFakeSubtype}, true); + fRegistryPairMC.add("Pair/same/MC/hQrecVsQtrueVsKt", "distinct true photons;q_{inv}^{true} (GeV/c);q_{inv}^{rec} (GeV/c);k_{T}^{rec} (GeV/c)", kTH3F, + {{300, 0.f, 0.3f}, {300, 0.f, 0.3f}, {std::vector{0.1, 0.2, 0.3, 0.4, 0.6, 1.0, 2.0}}}, false); fRegistryPairMC.add("Pair/same/MC/hDEtaDPhi_truePairs", "true reco pairs (TrueTrueDistinct+SamePhoton+Pi0);" "#Delta#eta_{#gamma#gamma};#Delta#phi_{#gamma#gamma} (rad)", @@ -1376,6 +1428,13 @@ struct Photonhbt { fRegistryTruthMC.add("MC/TruthAO2D/hMinRconv_vs_kT_truthConverted", "denominator: min(R_{conv}) vs k_{T};k_{T} (GeV/c);min(R_{conv}^{true}) (cm)", kTH2D, {axisKt, axRconv}, true); fRegistryTruthMC.add("MC/TruthAO2D/hMinRconv_vs_kT_bothPhotonsSelected", "numerator: min(R_{conv}) vs k_{T};k_{T} (GeV/c);min(R_{conv}^{true}) (cm)", kTH2D, {axisKt, axRconv}, true); + const AxisSpec axSpPt{std::vector{0.1, 0.125, 0.15, 0.175, 0.2, 0.25, 0.3, 0.35, 0.4, 0.5, 0.6, 0.7, 0.8, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0}, "p_{T}^{true} (GeV/c)"}; + const AxisSpec axSpR{90, 0.f, 180.f, "R_{conv}^{true} (cm)"}; + const AxisSpec axSpEta{16, -0.8f, 0.8f, "#eta^{true}"}; + for (const auto& stage : {"converted", "built", "selected"}) { + fRegistryTruthMC.add((std::string("MC/SinglePhotonEff/hPtREta_") + stage).c_str(), (std::string("true primary converted photons, ") + stage + ";p_{T}^{true} (GeV/c);R_{conv}^{true} (cm);#eta^{true}").c_str(), kTH3F, {axSpPt, axSpR, axSpEta}, true); + } + // ─── Stage summary fRegistryTruthMC.add("MC/TruthAO2D/hStage_vs_kT", "efficiency waterfall;k_{T} (GeV/c);stage (0=converted,1=all4legs,2=bothBuilt,3=bothSel)", kTH2D, {axisKt, AxisSpec{4, -0.5f, 3.5f, "stage"}}, true); fRegistryTruthMC.add("MC/TruthAO2D/hStageConsistency", "stage consistency (expect all at 0);N(V0 built but legs not found);counts", kTH1D, {AxisSpec{20, -0.5f, 19.5f, "N_{bad}"}}, true); @@ -1899,15 +1958,70 @@ struct Photonhbt { if (qinv > 0.f) { c.meeOverQ = std::min(c.mee[0], c.mee[1]) / qinv; } + { // swap template: q_inv of the two leg-swapped combinations (the leg-level combinatorics of a crossed pair) + const auto f1 = legVec(a, 0) + legVec(b, 1); + const auto f2 = legVec(b, 0) + legVec(a, 1); + const auto dq = f1 - f2; + c.qSwap = static_cast(std::sqrt(std::max(0., -dq.M2()))); + } + // geometry: helix circle of leg i (cm) from the conversion point and the leg momentum, as legHelixAt + auto circle = [&](PhotonWithLegs const& p, int i, float& xc, float& yc, float& r) { + r = p.fLegPt[i] / (0.3f * std::fabs(mBzT)) * kCmPerM; // o2-linter: disable=magic-number (0.3 GeV/c per T m) + const float sgn = (mBzT >= 0.f ? 1.f : -1.f) * (i == 0 ? 1.f : -1.f); + xc = p.fVx + sgn * r * std::sin(p.fLegPhi[i]); + yc = p.fVy - sgn * r * std::cos(p.fLegPhi[i]); + }; + // z of leg i at the point of its circle closest to (xT, yT): signed arc along the motion from the conversion point, local z + auto zAt = [&](PhotonWithLegs const& p, int i, float xc, float yc, float r, float xT, float yT) { + const float phiStart = std::atan2(p.fVy - yc, p.fVx - xc); + const float phiT = std::atan2(yT - yc, xT - xc); + const bool ccw = std::cos(p.fLegPhi[i]) * (-(p.fVy - yc)) + std::sin(p.fLegPhi[i]) * (p.fVx - xc) > 0.f; // momentum along the counterclockwise tangent + const float dphi = RecoDecay::constrainAngle(phiT - phiStart, -o2::constants::math::PI); + return (p.fVz - p.fVtxZ) + r * (ccw ? dphi : -dphi) * std::sinh(p.fLegEta[i]); + }; + const float gapMax = std::max(crosspair.cfgCrossMaxGap.value, kMinSigma), dzMax = std::max(crosspair.cfgCrossMaxDz.value, kMinSigma); + float bestScore = 999.f; + if (std::fabs(mBzT) > kMinSigma) { + for (int combo = 0; combo < 2; ++combo) { // o2-linter: disable=magic-number (the two leg-swap combinations) + PhotonWithLegs const& pPos = (combo == 0) ? a : b; + PhotonWithLegs const& pEle = (combo == 0) ? b : a; + float xP = 0.f, yP = 0.f, rP = 0.f, xE = 0.f, yE = 0.f, rE = 0.f; + circle(pPos, 0, xP, yP, rP); + circle(pEle, 1, xE, yE, rE); + const float dC = std::hypot(xE - xP, yE - yP); + if (!(dC > kMinSigma) || !(rP + rE > kMinSigma)) { + continue; + } + c.gap[combo] = dC - (rP + rE); + c.dTgl[combo] = std::fabs(std::sinh(pPos.fLegEta[0]) - std::sinh(pEle.fLegEta[1])); + const float w = rP / (rP + rE); + const float xT = xP + w * (xE - xP), yT = yP + w * (yE - yP); // touching point of the circles + c.dz[combo] = zAt(pPos, 0, xP, yP, rP, xT, yT) - zAt(pEle, 1, xE, yE, rE, xT, yT); + const float score = std::fabs(c.gap[combo]) / gapMax + std::fabs(c.dz[combo]) / dzMax; + if (score < bestScore) { + bestScore = score; + c.bestCombo = combo; + c.gapBest = c.gap[combo]; + c.dTglBest = c.dTgl[combo]; + c.dzBest = c.dz[combo]; + } + if (std::fabs(c.gap[combo]) < crosspair.cfgCrossMaxGap.value && c.dTgl[combo] < crosspair.cfgCrossMaxDTgl.value && std::fabs(c.dz[combo]) < crosspair.cfgCrossMaxDz.value) { + c.geomPhotonLike = true; + } + } + } return c; } [[nodiscard]] inline bool passCrossPairVeto(CrossObs const& c) const { - if (!crosspair.cfgDoCrossPairCut.value) { - return true; + if (crosspair.cfgDoCrossPairCut.value && c.meeOverQ <= crosspair.cfgCrossMaxMeeRatio.value) { + return false; + } + if (crosspair.cfgDoCrossGeomCut.value && c.geomPhotonLike) { + return false; } - return c.meeOverQ > crosspair.cfgCrossMaxMeeRatio.value; + return true; } template @@ -1915,10 +2029,16 @@ struct Photonhbt { { constexpr auto dir = (ev_id == 0) ? "Pair/same/CrossPair/" : "Pair/mix/CrossPair/"; fRegistryCF.fill(HIST(dir) + HIST("hVetoCounter"), vetoed ? 1.0 : 0.0); + if (c.qSwap >= 0.f) { + fRegistryCF.fill(HIST(dir) + HIST("hSwapTemplate"), qinv, c.qSwap, c.geomPhotonLike ? 1.f : 0.f); + } if (qinv > crosspair.cfgCrossMaxQinvQA.value) { return; } fRegistryCF.fill(HIST(dir) + HIST("hSparse_MeeRatio_dR_Qinv"), c.meeOverQ, deltaR, qinv); + if (c.bestCombo >= 0) { + fRegistryCF.fill(HIST(dir) + HIST("hSparse_CrossGeom_Qinv"), c.gapBest, std::fabs(c.dzBest), c.dTglBest, qinv); + } } /*************************************************/ @@ -2737,7 +2857,7 @@ struct Photonhbt { if (isInsideEllipse(obs.deta, obs.dphi)) { continue; } - if (crosspair.cfgDoCrossPairQA.value || crosspair.cfgDoCrossPairCut.value) { + if (crosspair.cfgDoCrossPairQA.value || crosspair.cfgDoCrossPairCut.value || crosspair.cfgDoCrossGeomCut.value) { const auto cross = computeCrossObs(pwl1, pwl2, obs.qinv); const bool vetoed = !passCrossPairVeto(cross); fillCrossPair<0>(cross, obs.qinv, obs.deltaR, vetoed); @@ -2837,7 +2957,7 @@ struct Photonhbt { if (isInsideEllipse(obs.deta, obs.dphi)) { continue; } - if (crosspair.cfgDoCrossPairQA.value || crosspair.cfgDoCrossPairCut.value) { + if (crosspair.cfgDoCrossPairQA.value || crosspair.cfgDoCrossPairCut.value || crosspair.cfgDoCrossGeomCut.value) { const auto cross = computeCrossObs(g1, g2, obs.qinv); const bool vetoed = !passCrossPairVeto(cross); fillCrossPair<1>(cross, obs.qinv, obs.deltaR, vetoed); @@ -2988,7 +3108,7 @@ struct Photonhbt { if (isInsideEllipse(obs.deta, obs.dphi)) { continue; } - if (crosspair.cfgDoCrossPairQA.value || crosspair.cfgDoCrossPairCut.value) { + if (crosspair.cfgDoCrossPairQA.value || crosspair.cfgDoCrossPairCut.value || crosspair.cfgDoCrossGeomCut.value) { const auto cross = computeCrossObs(pwl1, pwl2, obs.qinv); const bool vetoed = !passCrossPairVeto(cross); fillCrossPair<0>(cross, obs.qinv, obs.deltaR, vetoed); @@ -3044,6 +3164,13 @@ struct Photonhbt { } } + if (mc1.sameMother && mc1.isTruePhoton && mc2.sameMother && mc2.isTruePhoton && mc1.motherId != mc2.motherId) { + const auto& t1 = mcParticles.iteratorAt(mc1.motherId); + const auto& t2 = mcParticles.iteratorAt(mc2.motherId); + const ROOT::Math::PtEtaPhiMVector vt1(t1.pt(), t1.eta(), t1.phi(), 0.), vt2(t2.pt(), t2.eta(), t2.phi(), 0.); + const float qTrue = static_cast(std::sqrt(std::max(0., -(vt1 - vt2).M2()))); + fRegistryPairMC.fill(HIST("Pair/same/MC/hQrecVsQtrueVsKt"), qTrue, obs.qinv, obs.kt); + } fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_PsiPair_PhiV_Qinv_Type"), std::fabs(g1.psipair()), g1.phiv(), obs.qinv, static_cast(static_cast(truthType)), (mc1.sameMother && mc1.isTruePhoton) ? 1.f : 0.f); fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_PsiPair_PhiV_Qinv_Type"), std::fabs(g2.psipair()), g2.phiv(), obs.qinv, static_cast(static_cast(truthType)), (mc2.sameMother && mc2.isTruePhoton) ? 1.f : 0.f); if (truthType == PairTruthType::FakeFake || truthType == PairTruthType::TrueFake) { @@ -3055,6 +3182,12 @@ struct Photonhbt { if (crossMC.meeOverQ < 900.f) { // o2-linter: disable=magic-number (qinv was zero, skip) fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_MeeRatio_Qinv_Type"), crossMC.meeOverQ, obs.qinv, static_cast(static_cast(truthType))); } + if (crossMC.bestCombo >= 0) { + fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_CrossGeom_Qinv_Type"), crossMC.gapBest, std::fabs(crossMC.dzBest), obs.qinv, static_cast(static_cast(truthType))); + } + if (crossMC.qSwap >= 0.f) { + fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_SwapTemplate_Type"), obs.qinv, crossMC.qSwap, crossMC.geomPhotonLike ? 1.f : 0.f, static_cast(static_cast(truthType))); + } switch (truthType) { case PairTruthType::TrueTrueDistinct: @@ -3153,7 +3286,7 @@ struct Photonhbt { if (isInsideEllipse(obs.deta, obs.dphi)) { continue; } - if (crosspair.cfgDoCrossPairQA.value || crosspair.cfgDoCrossPairCut.value) { + if (crosspair.cfgDoCrossPairQA.value || crosspair.cfgDoCrossPairCut.value || crosspair.cfgDoCrossGeomCut.value) { const auto cross = computeCrossObs(g1, g2, obs.qinv); const bool vetoed = !passCrossPairVeto(cross); fillCrossPair<1>(cross, obs.qinv, obs.deltaR, vetoed); @@ -3400,6 +3533,18 @@ struct Photonhbt { fRegistryTruthMC.fill(HIST("MC/TruthAO2D/hStageConsistency"), static_cast(nBad)); } + // single-photon efficiency: every true converted primary photon of this collision, built (V0 of its own legs) / selected (HBT cut) + for (const auto& tg : trueGammas) { + fRegistryTruthMC.fill(HIST("MC/SinglePhotonEff/hPtREta_converted"), tg.pt, tg.rTrue, tg.eta); + const auto it = gammaRecoMap.find(tg.id); + if (it != gammaRecoMap.end() && it->second.hasV0) { + fRegistryTruthMC.fill(HIST("MC/SinglePhotonEff/hPtREta_built"), tg.pt, tg.rTrue, tg.eta); + if (it->second.passesCut) { + fRegistryTruthMC.fill(HIST("MC/SinglePhotonEff/hPtREta_selected"), tg.pt, tg.rTrue, tg.eta); + } + } + } + for (const auto& tg : trueGammas) { const bool posFound = legIdsThisCollision.contains(tg.posId) > 0; const bool negFound = legIdsThisCollision.contains(tg.negId) > 0; @@ -4204,6 +4349,1110 @@ struct Photonhbt { } PROCESS_SWITCH(Photonhbt, processMCAOD, "truth/SVertexer pair QA directly on AO2Ds", false); + + static constexpr int kPcGamma = 22, kPcElectron = 11; + struct PcLeg { // MC of one leg (ids within the DF of the source event), as the builder's MixPairPhoton::LegMC + int64_t evt = -1; + int pid = -1, mid = -1, gmid = -1, pdg = 0, mpdg = 0, gpdg = 0, trk = -1, anc = -1, ggid = -1, root = -1, ggpdg = 0, rootPdg = 0; + float sharedFrac = 0.f; + float vx = 0.f, vy = 0.f, vz = 0.f; // MC production vertex = true conversion point (valid for pid >= 0) + float px = 0.f, py = 0.f, pz = 0.f; // reconstructed leg momentum at the fitted vertex, before the mass constraint (V0LegsMix) + float mpx = 0.f, mpy = 0.f, mpz = 0.f; // MC momentum of the mother (the true photon for a conversion leg) + bool its = false; // leg has ITS hits + bool free = false; // pooled free leg (PCMMixLegs sourceV0Id -2, overlay mixed event) + int64_t srcPhoton = -1; // run-1 photon the leg came from (PCMMixLegs sourcePhotonUid; own uid for a free leg) + }; + struct PcPhoton { + float px = 0.f, py = 0.f, pz = 0.f; + float vx = 0.f, vy = 0.f, vz = 0.f; // reconstructed conversion point + bool original = true; // run-1 pairing (false: crossed pairing built in run 2) + bool svertexer = true; // run-1 photon was an SVertexer V0 (false: built by the finder) + float chi2 = 0.f, dcaxy = 0.f, dcaz = 0.f, cospa = 1.f; // KF chi2/ndf, DCA to the PV (cm), cos of the pointing angle to the PV + float chi2Vtx = -1.f, chi2PV = -1.f, psipair = 0.f; // KF chi2/ndf of the vertex fit alone, chi2 to the PV, psi_pair (processPairClassMix) + int nFree = 0; // legs that were pooled free legs (PCMMixLegs sourceV0Id -2, overlay): 0, 1, 2 + PcLeg pos, ele; + }; + static inline std::map>> mPcPools; // standard mixed event: (z bin, centrality bin) -> last cfgMixDepth same-event artificial events + + void addPairClassMixHistograms() + { + const int nq = std::max(1, static_cast(std::lround(qaflags.cfgPairClassMaxQ.value * 1000.f))); + const AxisSpec axSample{4, -0.5f, 3.5f, "0 same event, 1 standard mixed event, 2 co-reconstructed mixed event, 3 co-reco ME, one source event"}; + const AxisSpec axClass{21, -0.5f, 20.5f, "pair class: 13 true distinct, 14 full swap, 4 half swap, 1-3 duplicates, 16 bremsstrahlung (both true), 5 bremsstrahlung with a fake, 18 photons of the same mother (pi0/eta, both true), 19 swap of two such photons, 20 same mother with a fake, 17 related swap, 15 no label, 6-12 other fakes"}; + const AxisSpec axQ{nq, 0.f, qaflags.cfgPairClassMaxQ.value, "#it{q}_{inv} (GeV/#it{c})"}; + const AxisSpec axOrigin{4, -0.5f, 3.5f, "stored photons: 0 both SVertexer originals, 1 one finder original, 2 both finder, 3 a crossed pairing"}; + const AxisSpec axClean{2, -0.5f, 1.5f, "all four legs below the TPC shared-cluster cut"}; + fRegistryPairMC.add("PairClass/Pairs/hPairClass", "photon pairs by MC class", kTHnSparseF, {axSample, axClass, axQ, axOrigin, axClean}); + fRegistryPairMC.add("PairClass/Pairs/hDEQinv", "photon pairs: |#Delta E| vs q_{inv} by MC class", kTHnSparseF, {axSample, axClass, {100, 0.f, 0.5f, "#it{q}_{inv} (GeV/#it{c})"}, {100, 0.f, 0.5f, "|#it{E}_{1} #minus #it{E}_{2}| (GeV)"}, {std::vector{0., 0.2, 0.4, 0.6, 0.9, 1.5, 3.}, "#it{k}_{T} (GeV/#it{c})"}}); + fRegistryPairMC.add("PairClass/Pairs/hFreeLegPair", "photon pairs by MC class: photons built with a pooled free leg", kTHnSparseF, {axSample, axClass, axQ, {3, -0.5f, 2.5f, "photons of the pair with a free leg: 0, 1, 2"}, {3, -0.5f, 2.5f, "crossed photons (run-2 pairings) in the pair: 0, 1, 2"}}); + fRegistryPairMC.add("PairClass/Pairs/hHalfSwapLegs", "half swaps: run-1 origin of the two legs of the shared true photon", kTHnSparseF, {axSample, axQ, {4, -0.5f, 3.5f, "legs of the shared photon: 0 both free legs, 1 one stored photon (taken apart), 2 two different stored photons, 3 one free one stored"}, {3, -0.5f, 2.5f, "photons of the pair with a free leg: 0, 1, 2"}}); + fRegistryPairMC.add("PairClass/Pairs/hKtQ", "photon pairs: k_{T} vs q_{inv} by MC class", kTHnSparseF, {axSample, axClass, axQ, {100, 0.f, 2.f, "#it{k}_{T} (GeV/#it{c})"}}); + fRegistryPairMC.add("PairClass/Pairs/hQinvQout", "photon pairs: q_{inv} vs |q_{out}| (LCMS) by MC class", kTHnSparseF, {axSample, axClass, axQ, {60, 0.f, 0.3f, "|#it{q}_{out}| (LCMS) (GeV/#it{c})"}, {std::vector{0., 0.2, 0.4, 0.6, 0.9, 1.5, 3.}, "#it{k}_{T} (GeV/#it{c})"}}); + fRegistryPairMC.add("PairClass/Pairs/hPairOpening", "photon pairs with q_{inv} < 50 MeV: orientation of the opening by MC class", kTHnSparseF, {axSample, axClass, {10, 0.f, 0.05f, "#it{q}_{inv} (GeV/#it{c})"}, {30, 0.f, 0.15f, "|#it{q}_{side}| (LCMS) (GeV/#it{c})"}, {30, 0.f, 0.15f, "|#it{q}_{long}| (LCMS) (GeV/#it{c})"}, {18, 0.f, 1.5708f, "#psi = atan(|#Delta#eta| / |#Delta#varphi|) (rad)"}}); + fRegistryPairMC.add("PairClass/Pairs/hSoftHard", "photon pairs with q_{inv} < 100 MeV: soft vs hard photon (reconstructed) by MC class", kTHnSparseF, {axSample, axClass, {20, 0.f, 0.1f, "#it{q}_{inv} (GeV/#it{c})"}, {20, 0.f, 1.f, "#it{E}_{soft} / #it{E}_{hard}"}, {60, -60.f, 60.f, "#it{R}_{xy}(soft) #minus #it{R}_{xy}(hard) (cm)"}, {18, 0.f, 90.f, "#it{R}_{xy}(hard) (cm)"}, {18, 0.f, 1.5708f, "#psi (rad)"}}); + fRegistryPairMC.add("PairClass/Pairs/hPairRes", "pairs of two true photons, q_{inv} < 100 MeV: resolution by MC class", kTHnSparseF, {axSample, axClass, {20, 0.f, 0.1f, "#it{q}_{inv} (GeV/#it{c})"}, {40, -0.02f, 0.02f, "#it{q}_{inv}^{rec} #minus #it{q}_{inv}^{true} (GeV/#it{c})"}, {40, -0.1f, 0.1f, "#Delta#it{E}^{rec} #minus #Delta#it{E}^{true} (GeV)"}}); + fRegistryPairMC.add("PairClass/Pairs/hPairDist", "photon pairs: conversion-point distances by MC class", kTHnSparseF, {axSample, axClass, {50, 0.f, 0.5f, "#it{q}_{inv} (GeV/#it{c})"}, {100, 0.f, 50.f, "reconstructed: distance of the two conversion points (cm)"}, {104, -2.f, 50.f, "true: largest distance of the legs' production vertices (cm)"}}); + fRegistryPairMC.add("PairClass/Pairs/hPairQ", "photon pairs: fitted q, four-leg invariant mass, true q by MC class", kTHnSparseF, {axSample, axClass, {100, 0.f, 0.2f, "#it{q}_{inv} of the fitted photons (GeV/#it{c})"}, {100, 0.f, 0.2f, "#it{M} of the four legs (GeV/#it{c}^{2})"}, {100, 0.f, 0.2f, "#it{q}_{inv} of the two true photons (GeV/#it{c})"}, {6, 0.f, 1.5f, "#it{k}_{T} (GeV/#it{c})"}}); + fRegistryPairMC.add("PairClass/Pairs/hPairAlpha", "photon pairs: signed energy sharing of the two photons by MC class", kTHnSparseF, {axSample, axClass, {50, 0.f, 0.1f, "#it{q}_{inv} (GeV/#it{c})"}, {20, -1.f, 1.f, "#alpha_{1}"}, {20, -1.f, 1.f, "#alpha_{2}"}, {10, 0.f, 1.f, "|#it{E}_{1} #minus #it{E}_{2}| / (#it{E}_{1} + #it{E}_{2}) of the mother photons"}}); + fRegistryPairMC.add("PairClass/Pairs/hPairLegs", "photon pairs: leg diagnostics by MC class", kTHnSparseF, {axSample, axClass, {20, 0.f, 0.1f, "#it{q}_{inv} (GeV/#it{c})"}, {15, 0.f, 0.3f, "max over the two photons of |tan#lambda_{+} #minus tan#lambda_{#minus}|"}, {10, 0.f, 1.f, "softest leg #it{p} (GeV/#it{c})"}, {5, -0.5f, 4.5f, "legs with ITS (of 4)"}, axOrigin}); + fRegistryPairMC.add("PairClass/Pairs/hPairCE", "photon pairs: central-electrode variables by MC class", kTHnSparseF, {axSample, axClass, {20, 0.f, 0.1f, "#it{q}_{inv} (GeV/#it{c})"}, {std::vector{0., 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.6, 0.9, 2.}, "min |#eta| of the two photons"}, {std::vector{0., 1., 2., 5., 10., 20., 50., 300.}, "min |z_{conv}^{true}| (cm)"}, {5, -0.5f, 4.5f, "legs crossing the central electrode in the TPC (of 4)"}, {5, -0.5f, 4.5f, "legs with ITS (of 4)"}}); + fRegistryPairMC.add("PairClass/Pairs/hPairR", "photon pairs: conversion radii by MC class", kTHnSparseF, {axSample, axClass, {20, 0.f, 0.1f, "#it{q}_{inv} (GeV/#it{c})"}, {90, 0.f, 90.f, "#it{R}_{xy} of the inner photon (cm)"}, {90, 0.f, 90.f, "#it{R}_{xy} of the outer photon (cm)"}, {91, -1.f, 90.f, "true #it{R}_{xy} of the inner photon (cm), -1 without MC"}}); + fRegistryPairMC.add("PairClass/Pairs/hBrems", "bremsstrahlung pairs (classes 16 and 5, MC)", kTHnSparseF, {axSample, {2, -0.5f, 1.5f, "0 class 16 (both true), 1 class 5 (with a fake)"}, {20, 0.f, 0.1f, "#it{q}_{inv} (GeV/#it{c})"}, {5, -0.5f, 4.5f, "radiating electron from: 0 photon conversion, 1 #pi^{0} Dalitz, 2 #eta Dalitz, 3 other, 4 not found"}, {6, -0.5f, 5.5f, "mother of the parent photon: 0 #pi^{0}, 1 #eta, 2 #omega, 3 e^{#pm} (bremsstrahlung), 4 other, 5 none / not a photon"}, {50, 0.f, 0.25f, "#theta(#gamma_{brems}, radiating e^{#pm} rec.) (rad)"}, {50, 0.f, 0.25f, "#theta(#gamma_{brems}, parent #gamma) (rad), MC"}, {60, -40.f, 80.f, "true #it{R}_{xy}(#gamma_{brems}) #minus #it{R}_{xy}(parent) (cm)"}, {20, 0.f, 1.f, "#it{E}(#gamma_{brems}) / #it{E}(parent), MC"}}); + fRegistryPairMC.add("PairClass/Pairs/hPhotonQuality", "photons of the pairs by MC class (each photon once per pair)", kTHnSparseF, {axSample, axClass, {20, 0.f, 0.1f, "#it{q}_{inv} (GeV/#it{c})"}, {2, -0.5f, 1.5f, "0 photon radiated by an e^{#pm} (MC mother e^{#pm}), 1 other"}, {50, 0.f, 50.f, "KF #chi^{2}/ndf"}, {60, 0.f, 0.3f, "pointing angle to the PV (rad)"}, {50, 0.f, 5.f, "|DCA_{xy}| to the PV (cm)"}, {50, 0.f, 5.f, "|DCA_{z}| to the PV (cm)"}}); + { + const char* anc = "0 #pi^{0}, 1 #eta, 2 #omega, 3 #eta', 4 #rho/K*, 5 K^{0}, 6 baryon, 7 none (primary), 8 other"; + fRegistryPairMC.add("PairClass/Pairs/hBremsAncestry", "bremsstrahlung pairs (classes 16 and 5, MC): ancestry of the parent photon (the photon whose electron radiated)", kTHnSparseF, {axSample, {2, -0.5f, 1.5f, "0 class 16 (both true), 1 class 5 (with a fake)"}, {20, 0.f, 0.1f, "#it{q}_{inv} (GeV/#it{c})"}, {9, -0.5f, 8.5f, Form("mother of the parent photon: %s", anc)}, {9, -0.5f, 8.5f, Form("mother of that particle: %s", anc)}, {9, -0.5f, 8.5f, Form("root of the decay chain: %s", anc)}}); + } + fRegistryPairMC.add("PairClass/Pairs/hPhotonKF", "photons of the pairs by MC class (each photon once per pair)", kTHnSparseF, {axSample, axClass, {20, 0.f, 0.1f, "#it{q}_{inv} (GeV/#it{c})"}, {2, -0.5f, 1.5f, "0 photon radiated by an e^{#pm} (MC mother e^{#pm}), 1 other"}, {50, 0.f, 25.f, "KF #chi^{2}/ndf of the conversion-vertex fit"}, {60, 0.f, 300.f, "KF #chi^{2} to the primary vertex"}, {40, -0.2f, 0.2f, "#psi_{pair} (rad)"}}); + fRegistryPairMC.add("PairClass/Pairs/hHiddenBrems", "true distinct pairs (class 13) with a photon radiated by an e^{#pm} (MC)", kTHnSparseF, {axSample, {20, 0.f, 0.1f, "#it{q}_{inv} (GeV/#it{c})"}, {3, -0.5f, 2.5f, "photons of the pair radiated by an e^{#pm}"}, {4, -0.5f, 3.5f, "partner vs the radiating e^{#pm}: 0 unrelated, 1 same mother (e.g. Dalitz #gamma), 2 same root ancestor, 3 partner is itself radiated"}, {6, -0.5f, 5.5f, "radiating e^{#pm} from: 0 photon conversion, 1 #pi^{0} Dalitz, 2 #eta Dalitz, 3 heavy flavour (D, B), 4 other hadron, 5 none / unknown"}, {3, -0.5f, 2.5f, "partner vs the radiating e^{#pm}: 0 different first physical primary, 1 same first physical primary, 2 n/a"}}); + fRegistryPairMC.add("PairClass/Photons/hLegTrueDist", "stored photons: distance of the true production vertices of the two legs;0 same event, 1 mixed event;0 SVertexer original, 1 finder original, 2 crossed pairing;distance (cm)", kTH3F, {{2, -0.5f, 1.5f}, {3, -0.5f, 2.5f}, {104, -2.f, 50.f}}); + auto hEv = fRegistryPairMC.add("PairClass/Events/hArtificialEvents", "artificial events;;events", kTH1F, {{4, -0.5f, 3.5f}}); + hEv->GetXaxis()->SetBinLabel(1, "same event"); + hEv->GetXaxis()->SetBinLabel(2, "mixed event"); + hEv->GetXaxis()->SetBinLabel(3, "standard mixing: partner events used"); + hEv->GetXaxis()->SetBinLabel(4, "standard mixing: events without a partner"); + fRegistryPairMC.add("PairClass/Events/hNPhotons", "photons per artificial event;0 same event, 1 mixed event;photons", kTH2F, {{2, -0.5f, 1.5f}, {100, -0.5f, 99.5f}}); + fRegistryPairMC.add("PairClass/Photons/hOrigin", "stored photons;0 same event, 1 mixed event;0 SVertexer original, 1 finder original, 2 crossed pairing", kTH2F, {{2, -0.5f, 1.5f}, {3, -0.5f, 2.5f}}); + fRegistryPairMC.add("PairClass/Legs/hNSigma", "legs of stored photons (same-event artificial events)", kTHnSparseF, {{3, -0.5f, 2.5f, "MC: 0 electron from a photon, 1 electron other, 2 not an electron"}, {60, 0.f, 3.f, "#it{p}_{TPC in} (GeV/#it{c})"}, {100, -5.f, 5.f, "n#sigma_{e}^{TPC}"}, {120, -6.f, 6.f, "n#sigma_{#pi}^{TPC}"}, {2, -0.5f, 1.5f, "has ITS"}}); + } + + static bool pcSamePart(PcLeg const& x, PcLeg const& y) { return x.evt == y.evt && x.pid >= 0 && x.pid == y.pid; } + static bool pcSameMother(PcLeg const& x, PcLeg const& y) { return x.evt == y.evt && x.mid >= 0 && x.mid == y.mid; } + static bool pcDaughterOf(PcLeg const& x, PcLeg const& y) { return x.evt == y.evt && x.mid >= 0 && x.mid == y.pid; } + static bool pcRadiatedBy(PcLeg const& x, PcLeg const& y) { return x.evt == y.evt && x.gmid >= 0 && x.gmid == y.pid; } + static bool pcSiblings(PcLeg const& x, PcLeg const& y) { return x.evt == y.evt && x.gmid >= 0 && x.gmid == y.gmid && x.mid != y.mid; } + static bool pcTruePhoton(PcPhoton const& p) { return p.pos.mpdg == kPcGamma && p.ele.mpdg == kPcGamma && pcSameMother(p.pos, p.ele); } + // bremsstrahlung: a leg of one photon radiated the other photon (the photon's grandmother is that electron) + static bool pcRelated(PcPhoton const& a, PcPhoton const& b) + { + for (const auto* x : {&a.pos, &a.ele}) { + for (const auto* y : {&b.pos, &b.ele}) { + if (pcRadiatedBy(*x, *y) || pcRadiatedBy(*y, *x)) { + return true; + } + } + } + return false; + } + static bool pcSiblingPair(PcPhoton const& a, PcPhoton const& b) + { + for (const auto* x : {&a.pos, &a.ele}) { + for (const auto* y : {&b.pos, &b.ele}) { + if (pcSiblings(*x, *y)) { + return true; + } + } + } + return false; + } + // 1 two true photons, 2 the legs of two true photons crossed (full swap), 3 other with a fake, 4 no label + static int pcPairTruth(PcPhoton const& a, PcPhoton const& b) + { + if (a.pos.mid < 0 || a.ele.mid < 0 || b.pos.mid < 0 || b.ele.mid < 0) { + return 4; + } + auto key = [](PcLeg const& l) { return std::make_pair(l.evt, l.mid); }; + auto isTrue = [](PcLeg const& l) { return l.mid >= 0 && l.mpdg == kPcGamma; }; + const bool allTrue = isTrue(a.pos) && isTrue(a.ele) && isTrue(b.pos) && isTrue(b.ele); + if (allTrue && key(a.pos) == key(a.ele) && key(b.pos) == key(b.ele) && key(a.pos) != key(b.pos)) { + return 1; + } + if (allTrue && key(a.pos) == key(b.ele) && key(a.ele) == key(b.pos) && key(a.pos) != key(a.ele)) { + return 2; + } + return 3; + } + + static int pcFakeClass(PcPhoton const& a, PcPhoton const& b) + { + const std::array la{&a.pos, &a.ele}, lb{&b.pos, &b.ele}; + for (const auto* x : la) { + for (const auto* y : lb) { + if (x->evt == y->evt && x->trk >= 0 && x->trk == y->trk) { + return 12; + } + } + } + if (pcTruePhoton(a) && pcTruePhoton(b) && pcSameMother(a.pos, b.pos)) { + return 1; + } + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 2; ++j) { + if (pcSamePart(*la[i], *lb[j])) { + return (la[1 - i]->mpdg == kPcGamma && lb[1 - j]->mpdg == kPcGamma) ? 2 : 3; + } + } + } + for (const auto* x : la) { + for (const auto* y : lb) { + if (x->mpdg == kPcGamma && pcSameMother(*x, *y)) { + return 4; + } + } + } + for (const auto* x : la) { + for (const auto* y : lb) { + if (pcDaughterOf(*x, *y) || pcDaughterOf(*y, *x)) { + return 5; + } + } + } + if (pcRelated(a, b)) { + return 5; + } + if (pcSiblingPair(a, b)) { + return 20; + } + for (const auto* x : la) { + for (const auto* y : lb) { + if (pcSameMother(*x, *y)) { + return 6; + } + } + } + const bool aFake = !pcTruePhoton(a), bFake = !pcTruePhoton(b); + if (aFake && bFake) { + return 7; + } + const auto& f = aFake ? a : b; + if (pcSameMother(f.pos, f.ele)) { + return 8; + } + if (std::abs(f.pos.pdg) != kPcElectron || std::abs(f.ele.pdg) != kPcElectron) { + return 9; + } + if (f.pos.mpdg == kPcGamma && f.ele.mpdg == kPcGamma) { + return 10; + } + return 11; + } + static int pcPairClass(PcPhoton const& a, PcPhoton const& b) + { + const int truth = pcPairTruth(a, b); + if (truth == 3) { + return pcFakeClass(a, b); + } + if (truth == 1 || truth == 2) { + if (pcRelated(a, b)) { + return truth == 1 ? 16 : 17; // bremsstrahlung + } + if (pcSiblingPair(a, b)) { + return truth == 1 ? 18 : 19; // photons of the same mother + } + return truth == 1 ? 13 : 14; + } + return 15; + } + // --- end of the copied classification --- + static int pcPairOrigin(PcPhoton const& a, PcPhoton const& b) + { + if (!a.original || !b.original) { + return 3; + } + return (a.svertexer ? 0 : 1) + (b.svertexer ? 0 : 1); + } + void fillPcPair(const float sample, PcPhoton const& a, PcPhoton const& b) + { + const float q = o2::pwgem::photonmeson::qInv(a.px, a.py, a.pz, b.px, b.py, b.pz); + if (q >= qaflags.cfgPairClassMaxQ.value) { + return; + } + const float clean = std::max({a.pos.sharedFrac, a.ele.sharedFrac, b.pos.sharedFrac, b.ele.sharedFrac}) < qaflags.cfgPairClassMaxSharedFrac.value ? 1.f : 0.f; + const auto cls = static_cast(pcPairClass(a, b)); + fRegistryPairMC.fill(HIST("PairClass/Pairs/hPairClass"), sample, cls, q, static_cast(pcPairOrigin(a, b)), clean); + fRegistryPairMC.fill(HIST("PairClass/Pairs/hKtQ"), sample, cls, q, 0.5f * std::hypot(a.px + b.px, a.py + b.py)); + if (cls == 4.f) { // o2-linter: disable=magic-number (half swap) + for (const auto* x : {&a.pos, &a.ele}) { + for (const auto* y : {&b.pos, &b.ele}) { + if (x->mpdg == kPcGamma && pcSameMother(*x, *y)) { + const float src = (x->free && y->free) ? 0.f : (x->free || y->free) ? 3.f + : (x->srcPhoton == y->srcPhoton ? 1.f : 2.f); + fRegistryPairMC.fill(HIST("PairClass/Pairs/hHalfSwapLegs"), sample, q, src, static_cast((a.nFree > 0 ? 1 : 0) + (b.nFree > 0 ? 1 : 0))); + } + } + } + } + { + const float eA = std::sqrt(a.px * a.px + a.py * a.py + a.pz * a.pz), eB = std::sqrt(b.px * b.px + b.py * b.py + b.pz * b.pz); + fRegistryPairMC.fill(HIST("PairClass/Pairs/hDEQinv"), sample, cls, q, std::fabs(eA - eB), 0.5f * std::hypot(a.px + b.px, a.py + b.py)); + } + fRegistryPairMC.fill(HIST("PairClass/Pairs/hFreeLegPair"), sample, cls, q, static_cast((a.nFree > 0 ? 1 : 0) + (b.nFree > 0 ? 1 : 0)), static_cast((a.original ? 0 : 1) + (b.original ? 0 : 1))); + { + const ROOT::Math::PxPyPzMVector v1(a.px, a.py, a.pz, 0.f), v2(b.px, b.py, b.pz, 0.f); + const auto sum = v1 + v2; + const ROOT::Math::Boost bstZ(0., 0., -sum.Pz() / sum.E()); + const auto q3 = bstZ(ROOT::Math::PxPyPzEVector(v1 - v2)).Vect(); + const float qout = std::fabs(q3.Dot(ROOT::Math::XYZVector(sum.Px(), sum.Py(), 0.).Unit())); + const float kt = 0.5f * std::hypot(a.px + b.px, a.py + b.py); + fRegistryPairMC.fill(HIST("PairClass/Pairs/hQinvQout"), sample, cls, q, qout, std::min(kt, 2.99f)); + const auto ktUnit = ROOT::Math::XYZVector(sum.Px(), sum.Py(), 0.).Unit(); + const float qside = std::fabs(q3.Dot(ROOT::Math::XYZVector(-ktUnit.Y(), ktUnit.X(), 0.))); + const float qlong = std::fabs(q3.Z()); + auto etaOf = [](PcPhoton const& p) { return std::asinh(p.pz / std::max(std::hypot(p.px, p.py), 1.e-6f)); }; + const float dEta = std::fabs(etaOf(a) - etaOf(b)); + const float dPhi = std::fabs(RecoDecay::constrainAngle(std::atan2(a.py, a.px) - std::atan2(b.py, b.px), -o2::constants::math::PI)); + const float psi = std::atan2(dEta, dPhi); + if (q < 0.05f) { // o2-linter: disable=magic-number (diagnostic range) + fRegistryPairMC.fill(HIST("PairClass/Pairs/hPairOpening"), sample, cls, q, std::min(qside, 0.149f), std::min(qlong, 0.149f), std::min(psi, 1.57f)); + } + if (q < 0.1f) { // o2-linter: disable=magic-number (diagnostic range) + const float eA = std::hypot(a.px, a.py, a.pz), eB = std::hypot(b.px, b.py, b.pz); + PcPhoton const& soft = eA <= eB ? a : b; + PcPhoton const& hard = eA <= eB ? b : a; + const float rSoft = std::hypot(soft.vx, soft.vy), rHard = std::hypot(hard.vx, hard.vy); + fRegistryPairMC.fill(HIST("PairClass/Pairs/hSoftHard"), sample, cls, q, std::min(std::min(eA, eB) / std::max(std::max(eA, eB), 1.e-6f), 0.999f), std::clamp(rSoft - rHard, -59.9f, 59.9f), std::min(rHard, 89.9f), std::min(psi, 1.57f)); + if (pcTruePhoton(a) && pcTruePhoton(b)) { + const float tA = std::hypot(a.pos.mpx, a.pos.mpy, a.pos.mpz), tB = std::hypot(b.pos.mpx, b.pos.mpy, b.pos.mpz); + const float qT = o2::pwgem::photonmeson::qInv(a.pos.mpx, a.pos.mpy, a.pos.mpz, b.pos.mpx, b.pos.mpy, b.pos.mpz); + const float dERec = eA >= eB ? eA - eB : eB - eA; + const float dETrue = eA >= eB ? tA - tB : tB - tA; // same photon order as the reconstructed one + fRegistryPairMC.fill(HIST("PairClass/Pairs/hPairRes"), sample, cls, q, std::clamp(q - qT, -0.0199f, 0.0199f), std::clamp(dERec - dETrue, -0.0999f, 0.0999f)); + } + } + } + fRegistryPairMC.fill(HIST("PairClass/Pairs/hPairDist"), sample, cls, q, std::hypot(a.vx - b.vx, a.vy - b.vy, a.vz - b.vz), pcTrueDist({&a.pos, &a.ele, &b.pos, &b.ele})); + ROOT::Math::PxPyPzMVector sum4; + const std::array legs4{&a.pos, &a.ele, &b.pos, &b.ele}; + for (const auto* l : legs4) { + sum4 += ROOT::Math::PxPyPzMVector(l->px, l->py, l->pz, o2::constants::physics::MassElectron); + } + float qTrue = -1.f; + { + std::vector mothers; + for (const auto* l : legs4) { + if (l->mid < 0 || l->mpdg != kPcGamma) { + continue; + } + bool seen = false; + for (const auto* m : mothers) { + seen = seen || pcSameMother(*l, *m); + } + if (!seen) { + mothers.push_back(l); + } + } + if (mothers.size() == 2) { + qTrue = o2::pwgem::photonmeson::qInv(mothers[0]->mpx, mothers[0]->mpy, mothers[0]->mpz, mothers[1]->mpx, mothers[1]->mpy, mothers[1]->mpz); + } + } + fRegistryPairMC.fill(HIST("PairClass/Pairs/hPairQ"), sample, cls, q, static_cast(sum4.M()), qTrue, 0.5f * std::hypot(a.px + b.px, a.py + b.py)); + if (q < 0.1f) { + auto dtgl = [](PcPhoton const& p) { const float ptp = std::hypot(p.pos.px, p.pos.py), pte = std::hypot(p.ele.px, p.ele.py); return (ptp > 0.f && pte > 0.f) ? std::fabs(p.pos.pz / ptp - p.ele.pz / pte) : 0.3f; }; + const float pmin = std::min({std::hypot(a.pos.px, a.pos.py, a.pos.pz), std::hypot(a.ele.px, a.ele.py, a.ele.pz), std::hypot(b.pos.px, b.pos.py, b.pos.pz), std::hypot(b.ele.px, b.ele.py, b.ele.pz)}); + const auto nIts = static_cast((a.pos.its ? 1 : 0) + (a.ele.its ? 1 : 0) + (b.pos.its ? 1 : 0) + (b.ele.its ? 1 : 0)); + fRegistryPairMC.fill(HIST("PairClass/Pairs/hPairLegs"), sample, cls, q, std::min(std::max(dtgl(a), dtgl(b)), 0.299f), std::min(pmin, 0.999f), nIts, static_cast(pcPairOrigin(a, b))); + constexpr float kTpcRin = 85.f, kTpcRout = 247.f; // o2-linter: disable=magic-number (TPC drift volume, diagnostic) + auto crossesCE = [&](PcLeg const& l) { + const float pt = std::hypot(l.px, l.py), rConv = std::hypot(l.vx, l.vy); + if (l.pid < 0 || pt <= 0.f || rConv > kTpcRout) { + return 0; + } + const float tgl = l.pz / pt, z1 = l.vz + tgl * (std::max(rConv, kTpcRin) - rConv), z2 = l.vz + tgl * (kTpcRout - rConv); + return z1 * z2 <= 0.f ? 1 : 0; + }; + auto absEta = [](PcPhoton const& p) { return std::fabs(std::asinh(p.pz / std::max(std::hypot(p.px, p.py), 1.e-6f))); }; + auto absZ = [](PcPhoton const& p) { return p.pos.pid >= 0 ? std::fabs(p.pos.vz) : (p.ele.pid >= 0 ? std::fabs(p.ele.vz) : 299.f); }; + const int nCE = crossesCE(a.pos) + crossesCE(a.ele) + crossesCE(b.pos) + crossesCE(b.ele); + fRegistryPairMC.fill(HIST("PairClass/Pairs/hPairCE"), sample, cls, q, std::min(std::min(absEta(a), absEta(b)), 1.99f), std::min(std::min(absZ(a), absZ(b)), 299.f), static_cast(nCE), nIts); + { + const float rA = std::hypot(a.vx, a.vy), rB = std::hypot(b.vx, b.vy); + PcPhoton const& inner = rA <= rB ? a : b; + const float rTrue = inner.pos.pid >= 0 ? std::hypot(inner.pos.vx, inner.pos.vy) : (inner.ele.pid >= 0 ? std::hypot(inner.ele.vx, inner.ele.vy) : -0.5f); + fRegistryPairMC.fill(HIST("PairClass/Pairs/hPairR"), sample, cls, q, std::min(std::min(rA, rB), 89.9f), std::min(std::max(rA, rB), 89.9f), std::min(rTrue, 89.9f)); + } + { + auto radiated = [](PcPhoton const& p) { return p.pos.mpdg == kPcGamma && p.pos.mid >= 0 && std::abs(p.pos.gpdg) == kPcElectron; }; + for (const auto* p : {&a, &b}) { + const float ang = std::acos(std::clamp(p->cospa, -1.f, 1.f)); + fRegistryPairMC.fill(HIST("PairClass/Pairs/hPhotonQuality"), sample, cls, q, radiated(*p) ? 0.f : 1.f, std::min(p->chi2, 49.9f), std::min(ang, 0.299f), std::min(std::fabs(p->dcaxy), 4.99f), std::min(std::fabs(p->dcaz), 4.99f)); + fRegistryPairMC.fill(HIST("PairClass/Pairs/hPhotonKF"), sample, cls, q, radiated(*p) ? 0.f : 1.f, std::clamp(p->chi2Vtx, 0.f, 24.99f), std::clamp(p->chi2PV, 0.f, 299.f), std::clamp(p->psipair, -0.199f, 0.199f)); + } + if (cls == 13.f && (radiated(a) || radiated(b))) { // o2-linter: disable=magic-number (true distinct) + const PcPhoton& r = radiated(a) ? a : b; + const PcPhoton& o = radiated(a) ? b : a; + float rel = 0.f; + if (radiated(o)) { + rel = 3.f; + } else if (o.pos.evt == r.pos.evt && o.pos.gmid >= 0 && o.pos.gmid == r.pos.ggid) { + rel = 1.f; // partner photon and the radiating electron have the same mother + } else if (o.pos.evt == r.pos.evt && o.pos.root >= 0 && o.pos.root == r.pos.root) { + rel = 2.f; + } + const int em = std::abs(r.pos.ggpdg); + float radFrom = 5.f; // o2-linter: disable=magic-number (category codes) + if (em == kPcGamma) { + radFrom = 0.f; + } else if (em == 111) { // o2-linter: disable=pdg/explicit-code (pi0) + radFrom = 1.f; + } else if (em == 221) { // o2-linter: disable=pdg/explicit-code (eta) + radFrom = 2.f; + } else if ((em >= 400 && em < 600) || (em >= 4000 && em < 6000)) { // o2-linter: disable=magic-number (charm and beauty hadrons) + radFrom = 3.f; + } else if (em > 100) { // o2-linter: disable=magic-number (hadrons) + radFrom = 4.f; + } + // first physical primary of the brems photon + const float sameAnc = (r.pos.anc < 0 || o.pos.anc < 0 || r.pos.evt != o.pos.evt) ? 2.f : (r.pos.anc == o.pos.anc ? 1.f : 0.f); + fRegistryPairMC.fill(HIST("PairClass/Pairs/hHiddenBrems"), sample, q, static_cast((radiated(a) ? 1 : 0) + (radiated(b) ? 1 : 0)), rel, radFrom, sameAnc); + } + } + if (cls == 16.f || cls == 5.f) { // o2-linter: disable=magic-number (bremsstrahlung classes) + // x = leg of the bremsstrahlung photon, y = the radiating electron (a leg of the partner photon): x's grandmother is y + const PcLeg *x = nullptr, *y = nullptr; + const std::array, 2> orders{{{&a, &b}, {&b, &a}}}; + for (const auto& [p1, p2] : orders) { + for (const auto* l1 : {&p1->pos, &p1->ele}) { + for (const auto* l2 : {&p2->pos, &p2->ele}) { + if (!x && pcRadiatedBy(*l1, *l2)) { + x = l1; + y = l2; + } + } + } + } + auto pdgClass = [](int pdg) { pdg = std::abs(pdg); return pdg == 111 ? 0 : (pdg == 221 ? 1 : (pdg == 223 ? 2 : (pdg == 11 ? 3 : (pdg == 0 ? 5 : 4)))); }; // o2-linter: disable=magic-number (PDG codes) + auto angle = [](float ax, float ay, float az, float bx, float by, float bz) { + const float n = std::sqrt((ax * ax + ay * ay + az * az) * (bx * bx + by * by + bz * bz)); + return n > 0.f ? std::acos(std::clamp((ax * bx + ay * by + az * bz) / n, -1.f, 1.f)) : 0.249f; + }; + float origin = 4.f, parentMother = 5.f, thE = 0.249f, thG = 0.249f, dR = -39.9f, eFrac = 0.999f; + if (x && y) { + const int mp = std::abs(y->mpdg); + origin = mp == kPcGamma ? 0.f : (mp == 111 ? 1.f : (mp == 221 ? 2.f : 3.f)); // o2-linter: disable=magic-number (PDG codes) + parentMother = mp == kPcGamma ? static_cast(pdgClass(y->gpdg)) : 5.f; + thE = std::min(angle(x->mpx, x->mpy, x->mpz, y->px, y->py, y->pz), 0.249f); + if (mp == kPcGamma) { + thG = std::min(angle(x->mpx, x->mpy, x->mpz, y->mpx, y->mpy, y->mpz), 0.249f); + const float eB = std::hypot(x->mpx, x->mpy, x->mpz), eA = std::hypot(y->mpx, y->mpy, y->mpz); + eFrac = eA > 0.f ? std::min(eB / eA, 0.999f) : 0.999f; + } + dR = std::clamp(std::hypot(x->vx, x->vy) - std::hypot(y->vx, y->vy), -39.9f, 79.9f); + } + fRegistryPairMC.fill(HIST("PairClass/Pairs/hBrems"), sample, cls == 16.f ? 0.f : 1.f, q, origin, parentMother, thE, thG, dR, eFrac); // o2-linter: disable=magic-number (class) + if (x && y && std::abs(y->mpdg) == kPcGamma) { + auto ancClass = [](int pdg) { // o2-linter: disable=pdg/explicit-code (diagnostic classes) + pdg = std::abs(pdg); + switch (pdg) { + case 111: + return 0.f; + case 221: + return 1.f; + case 223: + return 2.f; + case 331: + return 3.f; + case 113: + case 213: + case 313: + case 323: + return 4.f; + case 310: + case 130: + case 311: + return 5.f; + default: + break; + } + if (pdg == 11 || pdg == 13 || pdg == 22) { + return 8.f; // o2-linter: disable=magic-number (leptons, photon: other) + } + if (pdg > 1000) { + return 6.f; // o2-linter: disable=magic-number (baryons) + } + if (pdg < 100) { + return 7.f; // o2-linter: disable=magic-number (no hadron mother: quark, gluon, string, cluster, none) + } + return 8.f; + }; + fRegistryPairMC.fill(HIST("PairClass/Pairs/hBremsAncestry"), sample, cls == 16.f ? 0.f : 1.f, q, ancClass(y->gpdg), ancClass(y->ggpdg), ancClass(y->rootPdg)); // o2-linter: disable=magic-number (class) + } + } + auto alphaOf = [](PcPhoton const& p) { + const float lp = p.pos.px * p.px + p.pos.py * p.py + p.pos.pz * p.pz, le = p.ele.px * p.px + p.ele.py * p.py + p.ele.pz * p.pz; + return (lp + le) != 0.f ? std::clamp((lp - le) / (lp + le), -0.999f, 0.999f) : 0.f; + }; + float asym = -1.f; + { + std::vector mothers; + for (const auto* l : legs4) { + if (l->mid < 0 || l->mpdg != kPcGamma) { + continue; + } + bool seen = false; + for (const auto* m : mothers) { + seen = seen || pcSameMother(*l, *m); + } + if (!seen) { + mothers.push_back(l); + } + } + if (mothers.size() == 2) { + const float e1 = std::hypot(mothers[0]->mpx, mothers[0]->mpy, mothers[0]->mpz), e2 = std::hypot(mothers[1]->mpx, mothers[1]->mpy, mothers[1]->mpz); + asym = (e1 + e2) > 0.f ? std::min(std::fabs(e1 - e2) / (e1 + e2), 0.999f) : -1.f; + } + } + fRegistryPairMC.fill(HIST("PairClass/Pairs/hPairAlpha"), sample, cls, q, alphaOf(a), alphaOf(b), asym); + } + } + static float pcTrueDist(std::initializer_list legs) + { + float d = 0.f; + for (auto x = legs.begin(); x != legs.end(); ++x) { + if ((*x)->pid < 0) { + return -1.f; + } + for (auto y = std::next(x); y != legs.end(); ++y) { + d = std::max(d, std::hypot((*x)->vx - (*y)->vx, (*x)->vy - (*y)->vy, (*x)->vz - (*y)->vz)); + } + } + return d; + } + + void processPairClassMix(aod::PCMMixCollisions const& collisions, aod::V0PhotonsKFMix const& photons, aod::V0LegsMix const& legs, aod::PCMMixLegs const& mixLegs, + aod::V0PhotonsKFChi2Mix const& kfChi2, aod::V0PhotonsPhiVPsiMix const& phiVPsi) + { + std::map> perCollision; + std::map collisionIsMixed; + for (const auto& collision : collisions) { + collisionIsMixed[collision.globalIndex()] = collision.isMixed(); + } + auto legOf = [&](int legRow, bool sameEvent) { + const auto l = legs.rawIteratorAt(legRow); + const auto m = mixLegs.rawIteratorAt(l.trackId()); + const PcLeg out = pcLegFrom(l, m); + if (sameEvent) { + const float ident = std::abs(out.pdg) == kPcElectron ? (out.mpdg == kPcGamma ? 0.f : 1.f) : 2.f; + fRegistryPairMC.fill(HIST("PairClass/Legs/hNSigma"), ident, m.tpcInnerParam(), m.tpcNSigmaEl(), m.tpcNSigmaPi(), m.hasITS() ? 1.f : 0.f); + } + return std::make_pair(out, m.sourceV0Id()); + }; + for (const auto& photon : photons) { + PcPhoton p; + p.px = photon.px(); + p.py = photon.py(); + p.pz = photon.pz(); + p.vx = photon.vx(); + p.vy = photon.vy(); + p.vz = photon.vz(); + p.original = photon.v0Id() >= 0; + const bool sameEvent = !collisionIsMixed[photon.collisionId()]; + const auto [pos, posV0] = legOf(photon.posTrackId(), sameEvent); + const auto [ele, eleV0] = legOf(photon.negTrackId(), sameEvent); + p.pos = pos; + p.ele = ele; + p.svertexer = p.original && posV0 >= 0; + p.nFree = (posV0 == -2 ? 1 : 0) + (eleV0 == -2 ? 1 : 0); // o2-linter: disable=magic-number (free-leg stub v0Id of the mixer) + p.chi2 = photon.chiSquareNDF(); + p.dcaxy = photon.dcaXYtopv(); + p.dcaz = photon.dcaZtopv(); + p.cospa = photon.cospa(); + if (photon.globalIndex() < kfChi2.size()) { + const auto kc = kfChi2.rawIteratorAt(photon.globalIndex()); + p.chi2Vtx = kc.chi2NDFVtx(); + p.chi2PV = kc.chi2PV(); + } + if (photon.globalIndex() < phiVPsi.size()) { + p.psipair = phiVPsi.rawIteratorAt(photon.globalIndex()).psipair(); + } + perCollision[photon.collisionId()].push_back(p); + } + for (const auto& collision : collisions) { + const auto it = perCollision.find(collision.globalIndex()); + const std::vector empty; + const auto& list = it == perCollision.end() ? empty : it->second; + const float type = collision.isMixed() ? 1.f : 0.f; + fRegistryPairMC.fill(HIST("PairClass/Events/hArtificialEvents"), type); + fRegistryPairMC.fill(HIST("PairClass/Events/hNPhotons"), type, static_cast(list.size())); + for (const auto& p : list) { + const float origin = p.original ? (p.svertexer ? 0.f : 1.f) : 2.f; + fRegistryPairMC.fill(HIST("PairClass/Photons/hOrigin"), type, origin); + fRegistryPairMC.fill(HIST("PairClass/Photons/hLegTrueDist"), type, origin, pcTrueDist({&p.pos, &p.ele})); + } + for (size_t i = 0; i < list.size(); ++i) { + for (size_t j = i + 1; j < list.size(); ++j) { + const auto& a = list[i]; + const auto& b = list[j]; + float sample = 0.f; + if (collision.isMixed()) { + const bool oneSource = a.pos.evt == a.ele.evt && a.pos.evt == b.pos.evt && a.pos.evt == b.ele.evt; + sample = oneSource ? 3.f : 2.f; + } + fillPcPair(sample, a, b); + } + } + if (collision.isMixed()) { + continue; + } + // standard mixed event: this same-event artificial event against the pooled ones of the same z and centrality bin + const int zBin = static_cast(std::floor((collision.posZ() + 10.f) / qaflags.cfgPairClassMixZBinWidth.value)); + const int cBin = static_cast(std::floor(collision.centFT0C() / qaflags.cfgPairClassMixCentBinWidth.value)); + auto& pool = mPcPools[zBin * 1000 + cBin]; + if (pool.empty()) { + fRegistryPairMC.fill(HIST("PairClass/Events/hArtificialEvents"), 3.f); + } + for (const auto& partner : pool) { + fRegistryPairMC.fill(HIST("PairClass/Events/hArtificialEvents"), 2.f); + for (const auto& a : list) { + for (const auto& b : partner) { + fillPcPair(1.f, a, b); + } + } + } + pool.push_back(list); + if (static_cast(pool.size()) > qaflags.cfgPairClassMixDepth.value) { + pool.pop_front(); + } + } + } + PROCESS_SWITCH(Photonhbt, processPairClassMix, "pair classes vs q_inv on the output of builder run 2 (artificial events): same event, standard mixed event, co-reconstructed mixed event", false); + + template + static PcLeg pcLegFrom(TLeg const& l, TMixLeg const& m) + { + PcLeg out; + out.evt = m.sourceEventUid(); + out.pid = m.mcParticleId(); + out.mid = m.mcMotherId(); + out.gmid = m.mcGrandMotherId(); + out.pdg = m.mcPdg(); + out.mpdg = m.mcMotherPdg(); + out.gpdg = m.mcGrandMotherPdg(); + out.trk = m.origTrackId(); + out.anc = m.mcAncestorId(); + out.ggid = m.mcGGMotherId(); + out.root = m.mcRootId(); + out.ggpdg = m.mcGGMotherPdg(); + out.rootPdg = m.mcRootPdg(); + out.sharedFrac = l.tpcFractionSharedCls(); + out.vx = m.mcVx(); + out.vy = m.mcVy(); + out.vz = m.mcVz(); + out.px = l.px(); + out.py = l.py(); + out.pz = l.pz(); + out.mpx = m.mcMotherPx(); + out.mpy = m.mcMotherPy(); + out.mpz = m.mcMotherPz(); + out.its = m.hasITS(); + out.free = m.sourceV0Id() == -2; // o2-linter: disable=magic-number (free-leg stub v0Id of the mixer) + out.srcPhoton = m.sourcePhotonUid(); + return out; + } + + template + static PcPhoton pcPhotonFrom(TPhoton const& photon, TLegs const& legs, TMixLegs const& mixLegs) + { + PcPhoton p; + p.px = photon.px(); + p.py = photon.py(); + p.pz = photon.pz(); + p.vx = photon.vx(); + p.vy = photon.vy(); + p.vz = photon.vz(); + p.original = photon.v0Id() >= 0; + const auto lp = legs.rawIteratorAt(photon.posTrackId()); + const auto ln = legs.rawIteratorAt(photon.negTrackId()); + const auto mp = mixLegs.rawIteratorAt(lp.trackId()); + p.pos = pcLegFrom(lp, mp); + p.ele = pcLegFrom(ln, mixLegs.rawIteratorAt(ln.trackId())); + p.svertexer = p.original && mp.sourceV0Id() >= 0; + p.nFree = (mp.sourceV0Id() == -2) + (mixLegs.rawIteratorAt(ln.trackId()).sourceV0Id() == -2); // o2-linter: disable=magic-number (free-leg stub v0Id of the mixer) + p.chi2 = photon.chiSquareNDF(); + p.dcaxy = photon.dcaXYtopv(); + p.dcaz = photon.dcaZtopv(); + p.cospa = photon.cospa(); + return p; + } + + // the two photons share an AO2D track of the same source event (the trackId check of processAnalysis) + static bool pcSharedTrack(PcPhoton const& a, PcPhoton const& b) + { + for (const auto* x : {&a.pos, &a.ele}) { + for (const auto* y : {&b.pos, &b.ele}) { + if (x->evt == y->evt && x->trk >= 0 && x->trk == y->trk) { + return true; + } + } + } + return false; + } + static bool pcHasLabels(PcPhoton const& p) { return p.pos.pid >= 0 && p.ele.pid >= 0; } + // PhotonMCInfo of processMC from the labels in PCMMixLegs (same-event artificial events: all MC ids are of one event) + static PhotonMCInfo pcMCInfo(PcPhoton const& p) + { + PhotonMCInfo info{}; + if (!pcHasLabels(p)) { + return info; + } + info.hasMC = true; + info.mcPosId = p.pos.pid; + info.mcNegId = p.ele.pid; + if (p.pos.mid < 0 || p.ele.mid < 0) { + return info; + } + info.posMotherId = p.pos.mid; + info.negMotherId = p.ele.mid; + info.posMotherIsPhoton = p.pos.mpdg == kPcGamma; + info.negMotherIsPhoton = p.ele.mpdg == kPcGamma; + if (!pcSameMother(p.pos, p.ele)) { + return info; + } + info.sameMother = true; + info.motherId = p.pos.mid; + info.motherPdg = p.pos.mpdg; + info.isTruePhoton = info.motherPdg == kPcGamma; + return info; + } + // isPi0DaughterPair of processMC: two true photons of the same pi0 + static bool pcPi0Daughters(PcPhoton const& a, PcPhoton const& b) + { + return pcTruePhoton(a) && pcTruePhoton(b) && a.pos.evt == b.pos.evt && a.pos.mid != b.pos.mid && a.pos.gmid >= 0 && + a.pos.gmid == b.pos.gmid && std::abs(a.pos.gpdg) == kPi0; + } + // q_inv of the two MC photons behind the four legs, -1 unless the four legs come from exactly two photons + static float pcTrueQ(PcPhoton const& a, PcPhoton const& b) + { + std::vector mothers; + for (const auto* l : {&a.pos, &a.ele, &b.pos, &b.ele}) { + if (l->mid < 0 || l->mpdg != kPcGamma) { + continue; + } + bool seen = false; + for (const auto* m : mothers) { + seen = seen || pcSameMother(*l, *m); + } + if (!seen) { + mothers.push_back(l); + } + } + if (mothers.size() != 2) { + return -1.f; + } + return o2::pwgem::photonmeson::qInv(mothers[0]->mpx, mothers[0]->mpy, mothers[0]->mpz, mothers[1]->mpx, mothers[1]->mpy, mothers[1]->mpz); + } + // signed energy sharing: which charge carries more of the photon momentum (data-computable swap tag, status 13ac) + static float pcAlpha(PcPhoton const& p) + { + const float lp = p.pos.px * p.px + p.pos.py * p.py + p.pos.pz * p.pz, le = p.ele.px * p.px + p.ele.py * p.py + p.ele.pz * p.pz; + return (lp + le) != 0.f ? (lp - le) / (lp + le) : 0.f; + } + // |q_out|, |q_side|, |q_long| in the LCMS, as in fillPairHistogram + static std::array lcmsQ(ROOT::Math::PtEtaPhiMVector const& v1, ROOT::Math::PtEtaPhiMVector const& v2) + { + const auto k12 = 0.5 * (v1 + v2); + const ROOT::Math::XYZVector uvOut(k12.Px() / k12.Pt(), k12.Py() / k12.Pt(), 0), uvLong(0, 0, 1); + const ROOT::Math::XYZVector uvSide = uvOut.Cross(uvLong); + const ROOT::Math::PxPyPzEVector v1c(v1), v2c(v2); + const float betaZ = (v1 + v2).Beta() * std::cos((v1 + v2).Theta()); + const ROOT::Math::Boost bstZ(0, 0, -betaZ); + const auto q3 = bstZ(v1c - v2c).Vect(); + return {static_cast(std::fabs(q3.Dot(uvOut))), static_cast(std::fabs(q3.Dot(uvSide))), static_cast(std::fabs(q3.Dot(uvLong)))}; + } + + void addCoRecoHistograms() + { + const AxisSpec axSample{2, -0.5f, 1.5f, "0 same event, 1 mixed event"}; + const AxisSpec axOrigin{4, -0.5f, 3.5f, "stored photons: 0 both SVertexer originals, 1 one finder original, 2 both finder, 3 a crossed pairing"}; + const AxisSpec axSign{2, -0.5f, 1.5f, "#alpha_{1}#alpha_{2} > 0 (same charge carries more momentum)"}; + auto hEv = fRegistry.add("CoReco/hArtificialEvents", "artificial events (PCMMixCollisions);;events", kTH1F, {{4, -0.5f, 3.5f}}); + hEv->GetXaxis()->SetBinLabel(1, "same event"); + hEv->GetXaxis()->SetBinLabel(2, "same event, centrality"); + hEv->GetXaxis()->SetBinLabel(3, "mixed event"); + hEv->GetXaxis()->SetBinLabel(4, "mixed event, centrality"); + fRegistry.add("CoReco/hNPhotons", "photons per artificial event (before the photon cut);0 same event, 1 mixed event;photons", kTH2F, {axSample, {100, -0.5f, 99.5f}}); + fRegistry.add("CoReco/hOneSourcePairs", "mixed artificial events: pairs with all four legs from one source event;q_{inv} (GeV/c);pairs", kTH1F, {axisQinv}); + fRegistry.add("CoReco/hPairOrigin", "accepted pairs by origin of the stored photons", kTHnSparseF, {axSample, axOrigin, axisQinv, axisKt, axSign}); + if (!isMC) { + return; + } + const AxisSpec axClass{21, -0.5f, 20.5f, "pair class: 13 true distinct, 14 full swap, 4 half swap, 1-3 duplicates, 16 bremsstrahlung (both true), 5 bremsstrahlung with a fake, 18 photons of the same mother (pi0/eta, both true), 19 swap of two such photons, 20 same mother with a fake, 17 related swap, 15 no label, 6-12 other fakes"}; + const AxisSpec axQFine{200, 0.f, 0.2f, "#it{q}_{inv} (GeV/#it{c})"}; + const AxisSpec axQTrue{201, -0.001f, 0.2f, "#it{q}_{inv} of the two MC photons (GeV/#it{c}), -1 if not two photons"}; + const AxisSpec axKtCoarse{std::vector{0., 0.2, 0.4, 0.6, 1.0, 2.0}, "#it{k}_{T} (GeV/#it{c})"}; + fRegistryPairMC.add("CoReco/MC/hClass3D", "accepted pairs by MC class: LCMS q components (CF binning)", kTHnSparseF, {axSample, axClass, axisQout, axisQside, axisQlong, axisKt}); + fRegistryPairMC.add("CoReco/MC/hClassQ", "accepted pairs by MC class: reconstructed vs true q", kTHnSparseF, {axSample, axClass, axQFine, axQTrue, axKtCoarse}); + fRegistryPairMC.add("CoReco/MC/hClassTag", "accepted pairs by MC class: photon origin and energy-sharing sign", kTHnSparseF, {axSample, axClass, axQFine, axOrigin, axSign}); + } + + // processMC's mixed-event truth fill (both photons labelled): both true / one fake / both fake + template + void fillMixTruth(TCollision const& collision, const int true1, const int true2, PairQAObservables const& obs) + { + if (true1 < 0 || true2 < 0) { + return; + } + const int nTrue = true1 + true2; + if (nTrue == 2) { // o2-linter: disable=magic-number (both photons true) + fRegistryPairMC.fill(HIST("Pair/mix/MC/hQinv_TrueTrue"), obs.qinv); + fillPairHistogramMC<1, PairTruthType::TrueTrueDistinct>(collision, obs.v1, obs.v2); + } else if (nTrue == 1) { + fRegistryPairMC.fill(HIST("Pair/mix/MC/hQinv_TrueFake"), obs.qinv); + fillPairHistogramMC<1, PairTruthType::TrueFake>(collision, obs.v1, obs.v2); + } else { + fRegistryPairMC.fill(HIST("Pair/mix/MC/hQinv_FakeFake"), obs.qinv); + fillPairHistogramMC<1, PairTruthType::FakeFake>(collision, obs.v1, obs.v2); + } + } + + // processMC's same-event truth block for an accepted pair, with the MC information from PCMMixLegs + template + void fillCoRecoSameTruth(TCollision const& collision, TPhoton const& g1, TPhoton const& g2, PcPhoton const& a, PcPhoton const& b, + PairQAObservables const& obs, LegPairObservables const& legObs, PhotonWithLegs const& pwl1, PhotonWithLegs const& pwl2, + const bool doQA, const bool doFR) + { + if (!pcHasLabels(a) || !pcHasLabels(b)) { + fillPairHistogramNoLabel(collision, obs.v1, obs.v2); + fRegistryPairMC.fill(HIST("Pair/same/MC/NoLabel/hDEtaDPhi"), obs.deta, obs.dphi); + fRegistryPairMC.fill(HIST("Pair/same/MC/NoLabel/hKt"), obs.kt); + fRegistryPairMC.fill(HIST("Pair/same/MC/NoLabel/hQinv"), obs.qinv); + return; + } + const auto mc1 = pcMCInfo(a), mc2 = pcMCInfo(b); + auto truthType = classifyPairTruth(mc1, mc2); + if (truthType == PairTruthType::TrueTrueDistinct && pcPi0Daughters(a, b)) { + truthType = PairTruthType::Pi0Daughters; + } + const bool doMCQA = passQinvMCQAGate(obs.qinv); + fillMCPairQA(truthType, obs, doQA, doMCQA); + if (mQa.legPairQa) { + fillLegPairMC(truthType, legObs, obs.kt); + } + if (doFR) { + fillMCPairQAFullRange(truthType, obs); + } + const bool isTruePair = (truthType == PairTruthType::TrueTrueDistinct || truthType == PairTruthType::TrueTrueSamePhoton || + truthType == PairTruthType::Pi0Daughters); + if (isTruePair) { + fRegistryPairMC.fill(HIST("Pair/same/MC/hDEtaDPhi_truePairs"), obs.deta, obs.dphi); + if (doMCQA) { + fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_DEtaDPhi_kT_truePairs"), obs.deta, obs.dphi, obs.kt); + fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_DEtaDPhi_qinv_truePairs"), obs.deta, obs.dphi, obs.qinv); + } + } else { + fRegistryPairMC.fill(HIST("Pair/same/MC/hDEtaDPhi_fakePairs"), obs.deta, obs.dphi); + if (doMCQA) { + fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_DEtaDPhi_kT_fakePairs"), obs.deta, obs.dphi, obs.kt); + fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_DEtaDPhi_qinv_fakePairs"), obs.deta, obs.dphi, obs.qinv); + } + } + if (mc1.sameMother && mc1.isTruePhoton && mc2.sameMother && mc2.isTruePhoton && mc1.motherId != mc2.motherId) { + const ROOT::Math::PxPyPzMVector t1(a.pos.mpx, a.pos.mpy, a.pos.mpz, 0.), t2(b.pos.mpx, b.pos.mpy, b.pos.mpz, 0.); + const float qTrue = static_cast(std::sqrt(std::max(0., -(t1 - t2).M2()))); + fRegistryPairMC.fill(HIST("Pair/same/MC/hQrecVsQtrueVsKt"), qTrue, obs.qinv, obs.kt); + } + fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_PsiPair_PhiV_Qinv_Type"), std::fabs(g1.psipair()), g1.phiv(), obs.qinv, static_cast(static_cast(truthType)), (mc1.sameMother && mc1.isTruePhoton) ? 1.f : 0.f); + fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_PsiPair_PhiV_Qinv_Type"), std::fabs(g2.psipair()), g2.phiv(), obs.qinv, static_cast(static_cast(truthType)), (mc2.sameMother && mc2.isTruePhoton) ? 1.f : 0.f); + if (truthType == PairTruthType::FakeFake || truthType == PairTruthType::TrueFake) { + fRegistryPairMC.fill(HIST("Pair/same/MC/hFakeSubtypeVsQinv"), obs.qinv, static_cast(classifyFakeSubtype(mc1, mc2))); + } + const auto crossMC = computeCrossObs(pwl1, pwl2, obs.qinv); + if (crossMC.meeOverQ < 900.f) { // o2-linter: disable=magic-number (qinv was zero, skip) + fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_MeeRatio_Qinv_Type"), crossMC.meeOverQ, obs.qinv, static_cast(static_cast(truthType))); + } + if (crossMC.bestCombo >= 0) { + fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_CrossGeom_Qinv_Type"), crossMC.gapBest, std::fabs(crossMC.dzBest), obs.qinv, static_cast(static_cast(truthType))); + } + if (crossMC.qSwap >= 0.f) { + fRegistryPairMC.fill(HIST("Pair/same/MC/hSparse_SwapTemplate_Type"), obs.qinv, crossMC.qSwap, crossMC.geomPhotonLike ? 1.f : 0.f, static_cast(static_cast(truthType))); + } + switch (truthType) { + case PairTruthType::TrueTrueDistinct: + fillPairHistogramMC<0, PairTruthType::TrueTrueDistinct>(collision, obs.v1, obs.v2); + break; + case PairTruthType::TrueTrueSamePhoton: + fillPairHistogramMC<0, PairTruthType::TrueTrueSamePhoton>(collision, obs.v1, obs.v2); + break; + case PairTruthType::SharedMcLeg: + fillPairHistogramMC<0, PairTruthType::SharedMcLeg>(collision, obs.v1, obs.v2); + break; + case PairTruthType::TrueFake: + fillPairHistogramMC<0, PairTruthType::TrueFake>(collision, obs.v1, obs.v2); + break; + case PairTruthType::FakeFake: + fillPairHistogramMC<0, PairTruthType::FakeFake>(collision, obs.v1, obs.v2); + break; + case PairTruthType::Pi0Daughters: + fillPairHistogramMC<0, PairTruthType::Pi0Daughters>(collision, obs.v1, obs.v2); + break; + default: + break; + } + } + + template + int pairCoRecoEvent(TCollision const& collision, std::vector const& ids, MyV0PhotonsMix const& photons, + std::vector const& pc, const float centForQA, const float occupancy, const bool pool) + { + constexpr bool kSame = ev_id == 0; + constexpr float kSample = kSame ? 0.f : 1.f; + std::vector selected(ids.size(), false); + for (size_t i = 0; i < ids.size(); ++i) { + const auto g = photons.rawIteratorAt(ids[i]); + selected[i] = fV0PhotonCut.IsSelected(g); + if (kSame && selected[i] && mQa.singlePhotonQa) { + fillSinglePhotonQAStep<0>(g); + } + } + const auto keyDFCollision = std::make_pair(ndf, static_cast(collision.globalIndex())); + std::unordered_set idsAfterPairCuts; + int ndiphoton = 0; + for (size_t i = 0; i < ids.size(); ++i) { + if (!selected[i]) { + continue; + } + for (size_t j = i + 1; j < ids.size(); ++j) { + if (!selected[j]) { + continue; + } + const auto g1 = photons.rawIteratorAt(ids[i]); + const auto g2 = photons.rawIteratorAt(ids[j]); + const auto& a = pc[ids[i]]; + const auto& b = pc[ids[j]]; + if (pcSharedTrack(a, b)) { + continue; + } + if constexpr (!kSame) { + if (a.pos.evt == a.ele.evt && a.pos.evt == b.pos.evt && a.pos.evt == b.ele.evt) { + fRegistry.fill(HIST("CoReco/hOneSourcePairs"), o2::pwgem::photonmeson::qInv(a.px, a.py, a.pz, b.px, b.py, b.pz)); + if (mixing.cfgCoRecoSkipOneSource.value) { + continue; + } + } + } + if (!passAsymmetryCut(g1.pt(), g2.pt())) { + continue; + } + const auto pos1 = g1.template posTrack_as(), ele1 = g1.template negTrack_as(); + const auto pos2 = g2.template posTrack_as(), ele2 = g2.template negTrack_as(); + auto obs = buildPairQAObservables(g1, g2); + if (!obs.valid) { + continue; + } + const bool doQA = passQinvQAGate(obs.qinv), doFR = passQinvFullRangeGate(obs.qinv); + const auto legObs = buildLegPairObservables(g1, g2, pos1, ele1, pos2, ele2); + auto pwl1 = makePhotonWithLegs(g1, pos1, ele1, collision.posZ()); + auto pwl2 = makePhotonWithLegs(g2, pos2, ele2, collision.posZ()); + if (isMC) { + pwl1.fIsTruePhoton = pcHasLabels(a) ? (pcTruePhoton(a) ? 1 : 0) : -1; + pwl2.fIsTruePhoton = pcHasLabels(b) ? (pcTruePhoton(b) ? 1 : 0) : -1; + } + const auto sep = computePairSep(pwl1, pwl2); + + // ─── before pair cuts ───────────────────────────────────── + fillPairSep(sep, obs); + if (doQA) { + fillPairQAStep(obs, centForQA, occupancy); + if constexpr (kSame) { + fillLegPairQAStep<0>(legObs, obs.kt); + } + } + if (doFR) { + fillFullRangeDeltaRCosOA(obs.qinv, obs.drOverCosOA); + } + if constexpr (kSame) { + fillLegSimilarity(pos1, pos2, obs.qinv); + fillLegSimilarity(ele1, ele2, obs.qinv); + } else { + fillLegSimilarityMix(pwl1, pwl2, obs.qinv); + } + + // ─── pair cuts ──────────────────────────────────────────── + if (!passPhotonClassPairCut(pwl1.legCounts(), pwl2.legCounts())) { + continue; + } + if (obs.drOverCosOA < ggpaircuts.cfgMinDRCosOA) { + continue; + } + if (!passPairMergeCut(sep)) { + continue; + } + if (!passRZCut(obs.deltaR, obs.deltaZ)) { + continue; + } + if (isInsideEllipse(obs.deta, obs.dphi)) { + continue; + } + if (crosspair.cfgDoCrossPairQA.value || crosspair.cfgDoCrossPairCut.value || crosspair.cfgDoCrossGeomCut.value) { + const auto cross = computeCrossObs(pwl1, pwl2, obs.qinv); + const bool vetoed = !passCrossPairVeto(cross); + fillCrossPair(cross, obs.qinv, obs.deltaR, vetoed); + if (vetoed) { + continue; + } + } + + // ─── after pair cuts ────────────────────────────────────── + fillPairSep(sep, obs); + idsAfterPairCuts.insert(ids[i]); + idsAfterPairCuts.insert(ids[j]); + if (doQA) { + fillPairQAStep(obs, centForQA, occupancy); + if constexpr (kSame) { + fillLegPairQAStep<1>(legObs, obs.kt); + } + } + if (doFR) { + fillFullRangeQA(obs, centForQA, occupancy); + } + fillPairHistogram(collision, obs.v1, obs.v2, 1.f); + if constexpr (kSame) { + fRegistryCF.fill(HIST("Pair/same/hPhi_lowerPtV0"), (g1.pt() < g2.pt()) ? g1.phi() : g2.phi()); + } else { + fRegistryCF.fill(HIST("Pair/mix/hPhi_lowerPtV0"), (g1.pt() < g2.pt()) ? g1.phi() : g2.phi()); + } + ndiphoton++; + const auto origin = static_cast(pcPairOrigin(a, b)); + const float sameSign = pcAlpha(a) * pcAlpha(b) > 0.f ? 1.f : 0.f; + fRegistry.fill(HIST("CoReco/hPairOrigin"), kSample, origin, obs.qinv, obs.kt, sameSign); + + if (isMC) { + if constexpr (kSame) { + fillCoRecoSameTruth(collision, g1, g2, a, b, obs, legObs, pwl1, pwl2, doQA, doFR); + } else { + fillMixTruth(collision, pwl1.fIsTruePhoton, pwl2.fIsTruePhoton, obs); + } + const auto cls = static_cast(pcPairClass(a, b)); + const auto q3 = lcmsQ(obs.v1, obs.v2); + fRegistryPairMC.fill(HIST("CoReco/MC/hClass3D"), kSample, cls, q3[0], q3[1], q3[2], obs.kt); + fRegistryPairMC.fill(HIST("CoReco/MC/hClassQ"), kSample, cls, obs.qinv, pcTrueQ(a, b), obs.kt); + fRegistryPairMC.fill(HIST("CoReco/MC/hClassTag"), kSample, cls, obs.qinv, origin, sameSign); + } + if (pool) { + if (usedPhotonIdsPerCol.insert(static_cast(ids[i])).second) { + emh1->AddTrackToEventPool(keyDFCollision, pwl1); + } + if (usedPhotonIdsPerCol.insert(static_cast(ids[j])).second) { + emh1->AddTrackToEventPool(keyDFCollision, pwl2); + } + } + } + } + if (kSame && mQa.singlePhotonQa) { + for (size_t i = 0; i < ids.size(); ++i) { + if (selected[i] && idsAfterPairCuts.contains(ids[i])) { + fillSinglePhotonQAStep<1>(photons.rawIteratorAt(ids[i])); + } + } + } + usedPhotonIdsPerCol.clear(); + return ndiphoton; + } + + template + void mixStandardCoReco(TCollision const& collision, std::tuple const& keyBin, const float centForQA, const float occupancy) + { + const auto keyDFCollision = std::make_pair(ndf, static_cast(collision.globalIndex())); + const auto& selectedPhotons = emh1->GetTracksPerCollision(keyDFCollision); + const auto& poolIDs = emh1->GetCollisionIdsFromEventPool(keyBin); + for (const auto& mixID : poolIDs) { + if (mixID == keyDFCollision) { + continue; + } + const auto& poolPhotons = emh1->GetTracksPerCollision(mixID); + for (const auto& g1 : selectedPhotons) { + for (const auto& g2 : poolPhotons) { + if (!passAsymmetryCut(g1.pt(), g2.pt())) { + continue; + } + if (!passFastQinvGate(g1, g2)) { + continue; + } + auto obs = buildPairQAObservables(g1, g2); + if (!obs.valid) { + continue; + } + const bool doQA = passQinvQAGate(obs.qinv), doFR = passQinvFullRangeGate(obs.qinv); + const auto sep = computePairSep(g1, g2); + fillPairSep<1>(sep, obs); + if (doQA) { + fillPairQAStep<1, 0>(obs, centForQA, occupancy); + } + if (doFR) { + fillFullRangeDeltaRCosOA<1>(obs.qinv, obs.drOverCosOA); + } + fillLegSimilarityMix(g1, g2, obs.qinv); + if (!passPhotonClassPairCut(g1.legCounts(), g2.legCounts())) { + continue; + } + if (obs.drOverCosOA < ggpaircuts.cfgMinDRCosOA) { + continue; + } + if (!passPairMergeCut(sep)) { + continue; + } + if (!passRZCut(obs.deltaR, obs.deltaZ)) { + continue; + } + if (isInsideEllipse(obs.deta, obs.dphi)) { + continue; + } + if (crosspair.cfgDoCrossPairQA.value || crosspair.cfgDoCrossPairCut.value || crosspair.cfgDoCrossGeomCut.value) { + const auto cross = computeCrossObs(g1, g2, obs.qinv); + const bool vetoed = !passCrossPairVeto(cross); + fillCrossPair<1>(cross, obs.qinv, obs.deltaR, vetoed); + if (vetoed) { + continue; + } + } + fillPairSep<1, true>(sep, obs); + if (doQA) { + fillPairQAStep<1, 1>(obs, centForQA, occupancy); + } + if (doFR) { + fillFullRangeQA<1>(obs, centForQA, occupancy); + } + fillPairHistogram<1>(collision, obs.v1, obs.v2, 1.f); + if (isMC) { + fillMixTruth(collision, g1.fIsTruePhoton, g2.fIsTruePhoton, obs); + } + fRegistryCF.fill(HIST("Pair/mix/hPhi_lowerPtV0"), (g1.pt() < g2.pt()) ? g1.phi() : g2.phi()); + } + } + } + emh1->AddCollisionIdAtLast(keyBin, keyDFCollision); + } + + void processCoReco(aod::PCMMixCollisions const& collisions, MyV0PhotonsMix const& photons, aod::V0LegsMix const& legs, aod::PCMMixLegs const& mixLegs) + { + // run-2 photons per artificial event, and the leg view of every photon (source event, AO2D track, MC labels, run-1 origin) + std::vector> photonsOf(collisions.size()); + std::vector pc(photons.size()); + for (const auto& photon : photons) { + if (photon.collisionId() >= 0 && photon.collisionId() < static_cast(collisions.size())) { + photonsOf[photon.collisionId()].push_back(photon.globalIndex()); + } + pc[photon.globalIndex()] = pcPhotonFrom(photon, legs, mixLegs); + } + const bool standardMix = mixing.cfgCoRecoMixMode.value == 1; + for (const auto& collision : collisions) { + const bool mixedEvent = collision.isMixed(); + const auto& ids = photonsOf[collision.globalIndex()]; + fRegistry.fill(HIST("CoReco/hArtificialEvents"), mixedEvent ? 2.f : 0.f); + fRegistry.fill(HIST("CoReco/hNPhotons"), mixedEvent ? 1.f : 0.f, static_cast(ids.size())); + const std::array cent = {collision.centFT0M(), collision.centFT0A(), collision.centFT0C()}; + const float centForQA = cent[mixing.cfgCentEstimator]; + if (centForQA < centralitySelection.cfgCentMin || centralitySelection.cfgCentMax < centForQA) { + continue; + } + fRegistry.fill(HIST("CoReco/hArtificialEvents"), mixedEvent ? 3.f : 1.f); + if (mixedEvent && standardMix) { + continue; + } + initCCDB(collision); + const float occupancy = collision.occupancy(); + if (mixedEvent) { + pairCoRecoEvent<1>(collision, ids, photons, pc, centForQA, occupancy, false); + continue; + } + const int ndiphoton = pairCoRecoEvent<0>(collision, ids, photons, pc, centForQA, occupancy, standardMix && mixing.cfgDoMix.value); + if (standardMix && mixing.cfgDoMix.value && ndiphoton > 0) { + const auto keyBin = std::make_tuple(binOf(ztxBinEdges, collision.posZ()), binOf(centBinEdges, centForQA), 0, binOf(occBinEdges, occupancy)); + mixStandardCoReco(collision, keyBin, centForQA, occupancy); + } + } + ndf++; + } + PROCESS_SWITCH(Photonhbt, processCoReco, "analysis (CF, QA, MC truth types) on the output of builder run 2: same event and co-reconstructed mixed event from the artificial events", false); }; WorkflowSpec defineDataProcessing(ConfigContext const& context)