@@ -330,6 +330,9 @@ struct HStrangeCorrelation {
330330 // trimmed by skipUnderOverflowInTHn, so what you configure is what you get.
331331 ConfigurableAxis axisGenStudyNch{" axisGenStudyNch" , {VARIABLE_WIDTH , 0 .0f , 2 .0f , 5 .0f , 10 .0f , 15 .0f , 20 .0f , 25 .0f , 30 .0f , 40 .0f , 60 .0f , 100 .0f }, " generated charged multiplicity in |#eta| < 0.8" };
332332 Configurable<bool > doClosureTestStages{" doClosureTestStages" , true , " create and fill the whole ClosureTest/PairLossK0 folder: the truth and any-reconstructed-object stages of the truth h-K0 pair, mirroring the first processPairLossK0MC stages" };
333+ Configurable<bool > doEventQualityStudy{" doEventQualityStudy" , false , " add eight EventQuality folders to the closure PairLossK0 study; requires doClosureTestStages" };
334+ Configurable<int > eventQualityDeltaPhiBins{" eventQualityDeltaPhiBins" , 36 , " delta phi bins in the sparse event-quality histograms" };
335+ Configurable<int > eventQualityDeltaEtaBins{" eventQualityDeltaEtaBins" , 20 , " delta eta bins in the sparse event-quality histograms" };
333336 Configurable<bool > applyRecoEventSelection{" applyRecoEventSelection" , true , " apply the standard reconstructed-event selection in the K0 pair-loss diagnostic" };
334337 Configurable<bool > fillFinalPairOnce{" fillFinalPairOnce" , true , " part 1: fill each truth h-K0 pair at most once at the final stage. Set false to loop over all reconstructed trigger x K0 combinations, as the data path does" };
335338 Configurable<bool > finalPairUseBestCollisionOnly{" finalPairUseBestCollisionOnly" , true , " part 1: build the final trigger and K0 objects only in the best collision. Set false to use every associated reconstructed collision that passes the event selection, pairing within one collision, as the data path does" };
@@ -532,6 +535,23 @@ struct HStrangeCorrelation {
532535 " V0 final selection" ,
533536 " Final reconstructed pair" };
534537
538+ // Independent event classifications, not cumulative cuts. Values are fixed
539+ // diagnostic bins (labels below), and never change the parent pair selection.
540+ static constexpr int PairLossNEventQualityGroups = 8 ;
541+ static constexpr std::array<std::string_view, PairLossNEventQualityGroups> PairLossEventQualityNames = {
542+ " NContributors" , " PVMaxPull" , " PVPurity" , " CollisionAssociation" ,
543+ " TimeResolution" , " Occupancy" , " GenMultiplicity" , " EventShape" };
544+ static constexpr std::array<std::array<std::string_view, 6 >, PairLossNEventQualityGroups> PairLossEventQualityLabels = {{
545+ {" no contributors" , " 1-2" , " 3-5" , " 6-10" , " 11-20" , " >=21" },
546+ {" invalid covariance/pull" , " max |pull| <1" , " 1<=max |pull|<3" , " 3<=max |pull|<5" , " max |pull|>=5" , " unused" },
547+ {" no PV tracks" , " missing MC labels" , " purity <0.95" , " 0.95<=purity<1" , " purity =1" , " unused" },
548+ {" no matched tracks" , " one rec collision" , " multiple: best fraction >=0.9" , " multiple: 0.5<=fraction<0.9" , " multiple: fraction <0.5" , " unused" },
549+ {" invalid time resolution" , " 0<sigma_t<25 ns" , " 25<=sigma_t<100 ns" , " 100<=sigma_t<500 ns" , " sigma_t>=500 ns" , " unused" },
550+ {" unavailable" , " 0" , " 1-99" , " 100-499" , " 500-999" , " >=1000" },
551+ {" 0" , " 1-4" , " 5-9" , " 10-19" , " 20-39" , " >=40" },
552+ {" fewer than 3 shape tracks" , " S_T<0.3" , " 0.3<=S_T<0.7" , " S_T>=0.7" , " unused" , " unused" },
553+ }};
554+
535555 struct PairLossTrackInfo {
536556 int64_t globalIndex = -1 ;
537557 float pt = 0 .0f ;
@@ -3119,6 +3139,32 @@ struct HStrangeCorrelation {
31193139 histos.add (" ClosureTest/PairLossK0/Final/sameEvent/K0Short" , " truth h-K0 pairs whose trigger and K0 both have a fully selected reconstructed counterpart in the same reconstructed collision" , kTHnF , {axisDeltaPhiNDim, axisDeltaEtaNDim, axisPtAssocNDim, axisPtTriggerNDim, axisVtxZNDim, axisMultNDim});
31203140 histos.add (" ClosureTest/PairLossK0/Final/hTrigger" , " truth triggers with a fully selected reconstructed counterpart;#it{p}_{T}^{truth} (GeV/#it{c});#eta^{truth};#varphi^{truth}" , kTH3F , {axesConfigurations.axisPtQA , axesConfigurations.axisEta , axesConfigurations.axisPhi });
31213141 histos.add (" ClosureTest/PairLossK0/Final/hK0Short" , " truth K0s with a fully selected reconstructed counterpart;#it{p}_{T}^{truth} (GeV/#it{c});#eta^{truth};#varphi^{truth}" , kTH3F , {axesConfigurations.axisPtQA , axesConfigurations.axisEta , axesConfigurations.axisPhi });
3142+ if (pairLossK0Configurations.doEventQualityStudy ) {
3143+ if (pairLossK0Configurations.eventQualityDeltaPhiBins <= 0 || pairLossK0Configurations.eventQualityDeltaEtaBins <= 0 ) {
3144+ LOGF (fatal, " Event-quality angular bin counts must be positive" );
3145+ }
3146+ // Six sparse axes; omit vertex and multiplicity dimensions. Each folder
3147+ // has exactly three histograms, including its event/trigger denominators.
3148+ const std::vector<AxisSpec> qualityAxes = {
3149+ {6 , -0.5 , 5.5 , " event class" },
3150+ {3 , -0.5 , 2.5 , " entry kind" },
3151+ {pairLossK0Configurations.eventQualityDeltaPhiBins , axisRanges[0 ][0 ], axisRanges[0 ][1 ], " #Delta#varphi^{truth}" },
3152+ {pairLossK0Configurations.eventQualityDeltaEtaBins , axisRanges[1 ][0 ], axisRanges[1 ][1 ], " #Delta#eta^{truth}" },
3153+ axisPtAssocNDim,
3154+ axisPtTriggerNDim,
3155+ };
3156+ for (int group = 0 ; group < PairLossNEventQualityGroups; ++group) {
3157+ for (auto const & stage : {" Truth" , " AnyTrackBoth" , " Final" }) {
3158+ auto histogram = histos.add <THnSparse>(fmt::format (" ClosureTest/PairLossK0/EventQuality/{}/{}" , PairLossEventQualityNames[group], stage), " unit-weight counts; select entry kind before projection" , kTHnSparseF , qualityAxes);
3159+ for (size_t bin = 0 ; bin < PairLossEventQualityLabels[group].size (); ++bin) {
3160+ histogram->GetAxis (0 )->SetBinLabel (bin + 1 , PairLossEventQualityLabels[group][bin].data ());
3161+ }
3162+ histogram->GetAxis (1 )->SetBinLabel (1 , " event (common baseline)" );
3163+ histogram->GetAxis (1 )->SetBinLabel (2 , " trigger" );
3164+ histogram->GetAxis (1 )->SetBinLabel (3 , " pair" );
3165+ }
3166+ }
3167+ }
31223168 }
31233169 for (int i = 0 ; i < AssocParticleTypes; i++) {
31243170 if (TESTBIT (doCorrelation, i)) {
@@ -5621,7 +5667,7 @@ struct HStrangeCorrelation {
56215667 }
56225668 }
56235669
5624- void processClosureTest (aod::McCollision const & /* mcCollision*/ ,
5670+ void processClosureTest (aod::McCollision const & mcCollision,
56255671 soa::SmallGroups<soa::Join<aod::McCollisionLabels, aod::Collisions, aod::EvSels, aod::CentFT0Ms, aod::CentFT0Cs, aod::PVMults>> const & recCollisions,
56265672 aod::McParticles const & mcParticles,
56275673 aod::V0MCCores const & v0MCCores,
@@ -5838,6 +5884,116 @@ struct HStrangeCorrelation {
58385884
58395885 const float pairLossBestCollisionVtxZ = collision.posZ ();
58405886 const float pairLossBestCollisionMultiplicity = masterConfigurations.doPPAnalysis ? collision.centFT0M () : collision.centFT0C ();
5887+ std::array<int , PairLossNEventQualityGroups> eventQualityClasses{};
5888+ if (pairLossK0Configurations.doEventQualityStudy ) {
5889+ // Category edges correspond to the labels declared above.
5890+ auto classFromEdges = [](auto value, auto const & edges) {
5891+ return static_cast <int >(std::upper_bound (edges.begin (), edges.end (), value) - edges.begin ());
5892+ };
5893+ constexpr double PurePVThreshold = 0.95 ;
5894+ constexpr double DominantCollisionFraction = 0.9 ;
5895+ constexpr double MajorityCollisionFraction = 0.5 ;
5896+ constexpr float GenEtaMax = 0 .8f ;
5897+ constexpr float ShapePtMin = 0 .15f ;
5898+ constexpr int MinShapeTracks = 3 ;
5899+ const auto nContributors = collision.numContrib ();
5900+ eventQualityClasses[0 ] = classFromEdges (nContributors, std::array{1u , 3u , 6u , 11u , 21u });
5901+
5902+ // Maximum component pull, not a 3D chi-square: covariance correlations
5903+ // are deliberately not used. Invalid covariance gets its own category.
5904+ const std::array<double , 3 > residuals = {collision.posX () - mcCollision.posX (), collision.posY () - mcCollision.posY (), collision.posZ () - mcCollision.posZ ()};
5905+ const std::array<double , 3 > variances = {collision.covXX (), collision.covYY (), collision.covZZ ()};
5906+ double maxPull = 0 .;
5907+ bool validPull = true ;
5908+ for (size_t component = 0 ; component < variances.size (); ++component) {
5909+ if (!std::isfinite (variances[component]) || variances[component] <= 0 . || !std::isfinite (residuals[component])) {
5910+ validPull = false ;
5911+ break ;
5912+ }
5913+ maxPull = std::max (maxPull, std::abs (residuals[component]) / std::sqrt (variances[component]));
5914+ }
5915+ eventQualityClasses[1 ] = !validPull || !std::isfinite (maxPull) ? 0 : 1 + classFromEdges (maxPull, std::array{1 ., 3 ., 5 .});
5916+
5917+ std::unordered_set<int64_t > currentMcIds;
5918+ int nAnyMatched = 0 ;
5919+ int nGenCharged = 0 ;
5920+ int nShapeTracks = 0 ;
5921+ double sumPt = 0 ., q2x = 0 ., q2y = 0 .;
5922+ for (auto const & particle : mcParticles) {
5923+ currentMcIds.insert (particle.globalIndex ());
5924+ nAnyMatched += pairLossAnyTrackMcParticleIds.count (particle.globalIndex ()) != 0 ;
5925+ if (!particle.isPhysicalPrimary () || !std::isfinite (particle.eta ()) || std::abs (particle.eta ()) >= GenEtaMax) {
5926+ continue ;
5927+ }
5928+ auto const * pdgParticle = pdgDB->GetParticle (particle.pdgCode ());
5929+ if (pdgParticle == nullptr || pdgParticle->Charge () == 0 .) {
5930+ continue ;
5931+ }
5932+ ++nGenCharged;
5933+ // Linearized transverse sphericity, using truth primary charged
5934+ // particles with |eta|<0.8 and pT>=0.15 GeV/c (not spherocity).
5935+ if (std::isfinite (particle.pt ()) && particle.pt () >= ShapePtMin && std::isfinite (particle.phi ())) {
5936+ ++nShapeTracks;
5937+ sumPt += particle.pt ();
5938+ q2x += particle.pt () * std::cos (2 . * particle.phi ());
5939+ q2y += particle.pt () * std::sin (2 . * particle.phi ());
5940+ }
5941+ }
5942+ int nPVTracks = 0 , nLabeledPVTracks = 0 , nSameMcPVTracks = 0 ;
5943+ std::unordered_set<int64_t > bestMatchedMcIds;
5944+ const auto bestTracks = tracks.sliceBy (pairLossTracksPerCollision, collision.globalIndex ());
5945+ for (auto const & track : bestTracks) {
5946+ const bool sameMc = track.has_mcParticle () && currentMcIds.count (track.mcParticleId ()) != 0 ;
5947+ if (sameMc) {
5948+ bestMatchedMcIds.insert (track.mcParticleId ());
5949+ }
5950+ if (track.isPVContributor ()) {
5951+ ++nPVTracks;
5952+ nLabeledPVTracks += track.has_mcParticle ();
5953+ nSameMcPVTracks += sameMc;
5954+ }
5955+ }
5956+ // Missing labels take precedence; purity otherwise uses all PV tracks.
5957+ const double purity = nPVTracks > 0 ? static_cast <double >(nSameMcPVTracks) / nPVTracks : 0 .;
5958+ eventQualityClasses[2 ] = nPVTracks == 0 ? 0 : nLabeledPVTracks < nPVTracks ? 1
5959+ : purity < PurePVThreshold ? 2
5960+ : nSameMcPVTracks < nPVTracks ? 3
5961+ : 4 ;
5962+ const double bestFraction = nAnyMatched > 0 ? static_cast <double >(bestMatchedMcIds.size ()) / nAnyMatched : 0 .;
5963+ eventQualityClasses[3 ] = nAnyMatched == 0 ? 0 : recCollisions.size () == 1 ? 1
5964+ : bestFraction >= DominantCollisionFraction ? 2
5965+ : bestFraction >= MajorityCollisionFraction ? 3
5966+ : 4 ;
5967+ const double timeResolution = collision.collisionTimeRes (); // ns
5968+ eventQualityClasses[4 ] = !std::isfinite (timeResolution) || timeResolution <= 0 . ? 0 : 1 + classFromEdges (timeResolution, std::array{25 ., 100 ., 500 .});
5969+ const auto occupancy = collision.trackOccupancyInTimeRange ();
5970+ eventQualityClasses[5 ] = occupancy < 0 ? 0 : 1 + classFromEdges (occupancy, std::array{1 , 100 , 500 , 1000 });
5971+ eventQualityClasses[6 ] = classFromEdges (nGenCharged, std::array{1 , 5 , 10 , 20 , 40 });
5972+ if (nShapeTracks >= MinShapeTracks && sumPt > 0 .) {
5973+ const double sphericity = std::clamp (1 . - std::hypot (q2x, q2y) / sumPt, 0 ., 1 .);
5974+ eventQualityClasses[7 ] = 1 + classFromEdges (sphericity, std::array{0.3 , 0.7 });
5975+ }
5976+ }
5977+ auto fillEventQuality = [&](int entryKind, float deltaPhi, float deltaEta, float assocPt, float triggerPt, bool hasAny, bool hasFinal) {
5978+ if (!pairLossK0Configurations.doEventQualityStudy ) {
5979+ return ;
5980+ }
5981+ static_for<0 , PairLossNEventQualityGroups - 1 >([&](auto i) {
5982+ constexpr int Index = i.value ;
5983+ histos.fill (HIST (" ClosureTest/PairLossK0/EventQuality/" ) + HIST (PairLossEventQualityNames[Index]) + HIST (" /Truth" ), eventQualityClasses[Index], entryKind, deltaPhi, deltaEta, assocPt, triggerPt);
5984+ if (hasAny) {
5985+ histos.fill (HIST (" ClosureTest/PairLossK0/EventQuality/" ) + HIST (PairLossEventQualityNames[Index]) + HIST (" /AnyTrackBoth" ), eventQualityClasses[Index], entryKind, deltaPhi, deltaEta, assocPt, triggerPt);
5986+ }
5987+ if (hasFinal) {
5988+ histos.fill (HIST (" ClosureTest/PairLossK0/EventQuality/" ) + HIST (PairLossEventQualityNames[Index]) + HIST (" /Final" ), eventQualityClasses[Index], entryKind, deltaPhi, deltaEta, assocPt, triggerPt);
5989+ }
5990+ });
5991+ };
5992+ // Unused coordinates lie inside regular bins. Select entry kind before
5993+ // projecting; reset pair-coordinate ranges for event/trigger counts.
5994+ const float countDeltaPhi = (axisRanges[0 ][0 ] + axisRanges[0 ][1 ]) * 0 .5f ;
5995+ const float countDeltaEta = (axisRanges[1 ][0 ] + axisRanges[1 ][1 ]) * 0 .5f ;
5996+ fillEventQuality (0 , countDeltaPhi, countDeltaEta, axisRanges[2 ][0 ], axisRanges[3 ][0 ], true , true );
58415997 std::vector<PairLossTruthTrackInfo> pairLossTruthTriggers;
58425998 std::vector<PairLossTruthK0Info> pairLossTruthK0s;
58435999
@@ -5902,6 +6058,9 @@ struct HStrangeCorrelation {
59026058 // at truth level in Truth/ and AnyTrack/, at any level in AnyTrackK0/ and
59036059 // AnyTrackBoth/, at fully-selected level in Final/.
59046060 for (auto const & truthTrigger : pairLossTruthTriggers) {
6061+ if (pairLossK0Configurations.doEventQualityStudy ) {
6062+ fillEventQuality (1 , countDeltaPhi, countDeltaEta, axisRanges[2 ][0 ], truthTrigger.pt , pairLossAnyTrackMcParticleIds.count (truthTrigger.globalIndex ) != 0 , pairLossHasFinalTrigger (truthTrigger.globalIndex ));
6063+ }
59056064 histos.fill (HIST (" ClosureTest/PairLossK0/Truth/hTrigger" ), truthTrigger.pt , truthTrigger.eta , truthTrigger.phi );
59066065 histos.fill (HIST (" ClosureTest/PairLossK0/AnyTrackK0/hTrigger" ), truthTrigger.pt , truthTrigger.eta , truthTrigger.phi );
59076066 if (pairLossAnyTrackMcParticleIds.find (truthTrigger.globalIndex ) != pairLossAnyTrackMcParticleIds.end ()) {
@@ -5952,7 +6111,9 @@ struct HStrangeCorrelation {
59526111 if (triggerHasAnyTrack && k0HasAnyV0) {
59536112 histos.fill (HIST (" ClosureTest/PairLossK0/AnyTrackBoth/sameEvent/K0Short" ), truthDeltaPhi, truthDeltaEta, truthK0.pt , truthTrigger.pt , pairLossBestCollisionVtxZ, pairLossBestCollisionMultiplicity);
59546113 }
5955- if (pairLossHasFinalPair (truthTrigger.globalIndex , truthK0.globalIndex )) {
6114+ const bool hasFinalPair = pairLossHasFinalPair (truthTrigger.globalIndex , truthK0.globalIndex );
6115+ fillEventQuality (2 , truthDeltaPhi, truthDeltaEta, truthK0.pt , truthTrigger.pt , triggerHasAnyTrack && k0HasAnyV0, hasFinalPair);
6116+ if (hasFinalPair) {
59566117 histos.fill (HIST (" ClosureTest/PairLossK0/Final/sameEvent/K0Short" ), truthDeltaPhi, truthDeltaEta, truthK0.pt , truthTrigger.pt , pairLossBestCollisionVtxZ, pairLossBestCollisionMultiplicity);
59576118 }
59586119 }
0 commit comments