@@ -302,7 +302,25 @@ struct HStrangeCorrelation {
302302
303303 struct : ConfigurableGroup {
304304 std::string prefix = " pairLossK0Configurations" ;
305- Configurable<bool > compactComparisonMode{" compactComparisonMode" , false , " write only the Rec/Truth/Gen/Final comparison and cumulative Rec control variants; suppress the original PairLoss diagnostic suite" };
305+ // processPairLossK0MC is split into three independent parts. Each has its own
306+ // switch, they write into disjoint folders, and any combination of them may
307+ // run in the same job -- including all three at once.
308+ // doStageDiagnostics PairLossK0/{Stage,State,Geometry,Matching,Response,
309+ // TrackQA,V0QA}: the truth-pair reconstruction ladder
310+ // and its close-pair diagnostics
311+ // doRecComparison PairLossK0/Comparison: the cumulative
312+ // Rec/Truth/Gen/Final variant ladder. Runs the exact
313+ // reconstructed correlation path internally, so
314+ // processSameEventHV0s must be off when it is on.
315+ // doGenLevelStudy PairLossK0/GenStudy: generator-level only, see the
316+ // comment on runGenLevelStudy in processPairLossK0MC
317+ Configurable<bool > doStageDiagnostics{" doStageDiagnostics" , true , " part 1: fill the PairLossK0 truth-pair reconstruction ladder and its diagnostics" };
318+ Configurable<bool > doRecComparison{" doRecComparison" , false , " part 2: fill the PairLossK0/Comparison cumulative Rec/Truth/Gen/Final ladder (runs the exact reconstructed path internally)" };
319+ Configurable<bool > doGenLevelStudy{" doGenLevelStudy" , false , " part 3: fill the PairLossK0/GenStudy generated-vs-reconstructed split, using generator-level event selection only" };
320+ // Generated charged multiplicity of the MC collision, counted in |eta| < 0.8
321+ // by mCounter. Plain ConfigurableAxis: unlike the correlation axes it is NOT
322+ // trimmed by skipUnderOverflowInTHn, so what you configure is what you get.
323+ 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" };
306324 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" };
307325 Configurable<bool > applyRecoEventSelection{" applyRecoEventSelection" , true , " apply the standard reconstructed-event selection in the K0 pair-loss diagnostic" };
308326 Configurable<float > daughterPtMin{" daughterPtMin" , 0 .05f , " minimum generated daughter pT for the findable K0 category" };
@@ -621,7 +639,7 @@ struct HStrangeCorrelation {
621639 };
622640
623641 // Per-MC-collision context used only while the ordinary Rec implementation
624- // is running inside compact PairLoss comparison mode . It is thread-local
642+ // is running inside the PairLoss Rec comparison part . It is thread-local
625643 // static because adding another task data member exceeds the number of
626644 // elements supported by the O2 task-reflection machinery.
627645 struct PairLossComparisonContext {
@@ -1440,7 +1458,7 @@ struct HStrangeCorrelation {
14401458 }
14411459 fillCorrelationHistogram (histos.get <THn>(HIST (" sameEvent/Signal/" ) + HIST (V0names[Index])), binFillThn, etaWeight, efficiency * efficiencyTrigg, totalEffUncert, purityTrigg, purityTriggErr);
14421460
1443- // Compact PairLoss control study. Stage 0 is filled from this
1461+ // PairLoss Rec control study. Stage 0 is filled from this
14441462 // exact Rec signal branch, so it is not a hand-written
14451463 // approximation of the reconstructed pair selection. Each next
14461464 // stage adds exactly one cumulative condition.
@@ -2477,7 +2495,7 @@ struct HStrangeCorrelation {
24772495 const AxisSpec axisMultNDim{edgesMult, " mult percentile" };
24782496 const AxisSpec axisMultiplicityNDim{edgesMultiplicity, " Multiplicity" };
24792497
2480- if (doprocessPairLossK0MC && ! pairLossK0Configurations.compactComparisonMode ) {
2498+ if (doprocessPairLossK0MC && pairLossK0Configurations.doStageDiagnostics ) {
24812499 const AxisSpec axisPairLossEventStage{6 , -0.5 , 5.5 , " Event-selection stage" };
24822500 const AxisSpec axisPairLossNRecoCollisions{11 , -0.5 , 10.5 , " #it{N}_{reco collisions} per MC collision" };
24832501 const AxisSpec axisPairLossStage{PairLossK0NStages, -0.5 , static_cast <double >(PairLossK0NStages) - 0.5 , " Reconstruction stage" };
@@ -2629,9 +2647,9 @@ struct HStrangeCorrelation {
26292647 }
26302648 }
26312649
2632- if (doprocessPairLossK0MC && pairLossK0Configurations.compactComparisonMode ) {
2650+ if (doprocessPairLossK0MC && pairLossK0Configurations.doRecComparison ) {
26332651 if (doprocessSameEventHV0s) {
2634- LOGF (fatal, " compact PairLoss comparison mode already runs the exact Rec path internally; set processSameEventHV0s=false to avoid double filling" );
2652+ LOGF (fatal, " pairLossK0Configurations.doRecComparison already runs the exact Rec path internally; set processSameEventHV0s=false to avoid double filling" );
26352653 }
26362654 constexpr int PairLossComparisonNVariants = 16 ;
26372655 const AxisSpec axisPairLossComparisonVariant{PairLossComparisonNVariants, -0.5 , PairLossComparisonNVariants - 0.5 , " cumulative Rec control variant" };
@@ -2699,6 +2717,45 @@ struct HStrangeCorrelation {
26992717 }
27002718 }
27012719
2720+ if (doprocessPairLossK0MC && pairLossK0Configurations.doGenLevelStudy ) {
2721+ const AxisSpec axisGenStudyNch{pairLossK0Configurations.axisGenStudyNch , " #it{N}_{ch}^{gen} (|#eta| < 0.8)" };
2722+ const AxisSpec axisGenStudyEventStage{4 , -0.5 , 3.5 , " Generated-event selection stage" };
2723+ // Findability of the K0, in exactly the sense the stage ladder uses: it
2724+ // decayed to pi+ pi- and both charged daughters are inside the tracking
2725+ // acceptance set by daughterPtMin / daughterEtaMax. Kept as an axis rather
2726+ // than as a separate folder so that the inclusive and the findable-only
2727+ // answer come out of one and the same object.
2728+ const AxisSpec axisGenStudyFindable{2 , -0.5 , 1.5 , " K^{0}_{S} findable" };
2729+
2730+ histos.add (" PairLossK0/GenStudy/hEventCounter" , " generator-level event selection" , kTH1F , {axisGenStudyEventStage});
2731+ histos.add (" PairLossK0/GenStudy/hNch" , " generated charged multiplicity of selected MC collisions" , kTH1F , {axisGenStudyNch});
2732+ histos.add (" PairLossK0/GenStudy/hNRecoCollisions" , " reconstructed collisions per selected MC collision" , kTH1F , {{11 , -0.5 , 10.5 }});
2733+
2734+ // Gen/ holds every generated object that passes the generator-level
2735+ // selection; Reconstructed/ and NotReconstructed/ split that same set by
2736+ // whether the object has a reconstructed counterpart. All three are filled
2737+ // with generated coordinates, so Gen == Reconstructed + NotReconstructed
2738+ // bin by bin and NotReconstructed/Gen reads directly as the loss.
2739+ histos.add (" PairLossK0/GenStudy/Gen/hTrigger" , " generated triggers;#it{p}_{T}^{gen} (GeV/#it{c});#eta^{gen};#varphi^{gen};#it{N}_{ch}^{gen}" , kTHnF , {axesConfigurations.axisPtQA , axesConfigurations.axisEta , axesConfigurations.axisPhi , axisGenStudyNch});
2740+ histos.add (" PairLossK0/GenStudy/Gen/hK0Short" , " generated K0s;#it{p}_{T}^{gen} (GeV/#it{c});#eta^{gen};#varphi^{gen};#it{N}_{ch}^{gen};findable" , kTHnF , {axesConfigurations.axisPtQA , axesConfigurations.axisEta , axesConfigurations.axisPhi , axisGenStudyNch, axisGenStudyFindable});
2741+ histos.addClone (" PairLossK0/GenStudy/Gen/" , " PairLossK0/GenStudy/Reconstructed/" );
2742+ histos.addClone (" PairLossK0/GenStudy/Gen/" , " PairLossK0/GenStudy/NotReconstructed/" );
2743+
2744+ for (auto const & histogram : {histos.get <THn>(HIST (" PairLossK0/GenStudy/Gen/hK0Short" )),
2745+ histos.get <THn>(HIST (" PairLossK0/GenStudy/Reconstructed/hK0Short" )),
2746+ histos.get <THn>(HIST (" PairLossK0/GenStudy/NotReconstructed/hK0Short" ))}) {
2747+ histogram->GetAxis (4 )->SetBinLabel (1 , " not findable" );
2748+ histogram->GetAxis (4 )->SetBinLabel (2 , " findable" );
2749+ }
2750+
2751+ auto genStudyEventCounter = histos.get <TH1 >(HIST (" PairLossK0/GenStudy/hEventCounter" ));
2752+ const std::array<std::string_view, 4 > genStudyEventLabels = {" MC collisions" , " INEL>0 (generated)" , " |vtx z| < cut (generated)" , " has >= 1 rec collision" };
2753+ for (size_t i = 0 ; i < genStudyEventLabels.size (); ++i) {
2754+ genStudyEventCounter->GetXaxis ()->SetBinLabel (i + 1 , genStudyEventLabels[i].data ());
2755+ }
2756+ genStudyEventCounter->GetYaxis ()->SetTitle (" MC collisions" );
2757+ }
2758+
27022759 if (doprocessMixedEventHV0sInBuffer || doprocessMixedEventHCascadesInBuffer) {
27032760 validCollisions.resize (histos.get <TH1 >(HIST (" axes/hMultAxis" ))->GetNbinsX () * histos.get <TH1 >(HIST (" axes/hVertexZAxis" ))->GetNbinsX ());
27042761 for (size_t i = 0 ; i < validCollisions.size (); ++i) {
@@ -4185,7 +4242,152 @@ struct HStrangeCorrelation {
41854242 V0DatasWithoutTrackX const & v0Candidates,
41864243 TracksComplete const & tracks)
41874244 {
4188- if (pairLossK0Configurations.compactComparisonMode ) {
4245+ // Part 3: a self-contained generator-level study. It deliberately touches no
4246+ // reconstructed quantity in its event selection or in any of its axes: the
4247+ // event is selected on generated INEL>0 and the generated vertex only, the
4248+ // multiplicity is counted from generated particles, and every object is
4249+ // filled with generated coordinates.
4250+ //
4251+ // Reconstruction enters in exactly one place -- whether a generated object
4252+ // has a reconstructed counterpart at all -- and that splits the very same
4253+ // generated sample into Reconstructed/ and NotReconstructed/. Because all
4254+ // three folders share generated coordinates, Gen == Reconstructed +
4255+ // NotReconstructed bin by bin, so NotReconstructed/Gen reads directly as
4256+ // "in which pT, eta, phi and multiplicity region do generated objects fail
4257+ // to be reconstructed".
4258+ //
4259+ // "Reconstructed" is the loosest possible statement, with no quality
4260+ // selection of any kind: for a trigger, some track in some reconstructed
4261+ // collision of this MC collision carries its MC label; for a K0, some V0
4262+ // candidate carries its MC core. Objects belonging to an MC collision that
4263+ // produced no reconstructed collision at all therefore land in
4264+ // NotReconstructed/ too; hEventCounter and hNRecoCollisions are there so
4265+ // that contribution can be separated out afterwards.
4266+ auto runGenLevelStudy = [&]() {
4267+ histos.fill (HIST (" PairLossK0/GenStudy/hEventCounter" ), 0 .0f );
4268+
4269+ // Generated-level event selection. No reconstructed variable is used.
4270+ if (masterConfigurations.selectINELgtZERO && !o2::pwglf::isINELgt0mc (mcParticles, pdgDB)) {
4271+ return ;
4272+ }
4273+ histos.fill (HIST (" PairLossK0/GenStudy/hEventCounter" ), 1 .0f );
4274+ if (std::abs (mcCollision.posZ ()) > masterConfigurations.zVertexCut ) {
4275+ return ;
4276+ }
4277+ histos.fill (HIST (" PairLossK0/GenStudy/hEventCounter" ), 2 .0f );
4278+ if (recCollisions.size () > 0 ) {
4279+ histos.fill (HIST (" PairLossK0/GenStudy/hEventCounter" ), 3 .0f );
4280+ }
4281+ histos.fill (HIST (" PairLossK0/GenStudy/hNRecoCollisions" ), recCollisions.size ());
4282+
4283+ // Multiplicity of this MC collision: generated charged physical primaries
4284+ // within |eta| < 0.8. Primaries always, independent of every analysis
4285+ // configurable, so that the multiplicity axis keeps one fixed meaning.
4286+ //
4287+ // Deliberately a local counter rather than a member: the shared mCounter
4288+ // only gets its PDG database wired up when processPrediction runs, and its
4289+ // mSelectPrimaries follows doAssocPhysicalPrimary. Adding a second counter
4290+ // as a task member is not an option either -- the struct is already at the
4291+ // member limit that Framework/StructToTuple.h can destructure. The object
4292+ // is a bool and a pointer, so building it per MC collision costs nothing.
4293+ o2::pwglf::ParticleCounter<o2::framework::O2DatabasePDG> genStudyCounter;
4294+ genStudyCounter.mPdgDatabase = pdgDB.service ;
4295+ genStudyCounter.mSelectPrimaries = true ;
4296+ const float generatedNch = genStudyCounter.countEta08 (mcParticles);
4297+ histos.fill (HIST (" PairLossK0/GenStudy/hNch" ), generatedNch);
4298+
4299+ // Reconstructed-object bookkeeping. The framework has already grouped
4300+ // recCollisions by this MC collision, so dereferencing a reconstructed
4301+ // collision back to its MC collision needs no extra work here, and the
4302+ // generated event selection above is by construction identical for all of
4303+ // them.
4304+ std::unordered_set<int64_t > reconstructedTrackMcIds;
4305+ std::unordered_set<int64_t > reconstructedV0McIds;
4306+ for (auto const & collision : recCollisions) {
4307+ const auto trackSlice = tracks.sliceBy (pairLossTracksPerCollision, collision.globalIndex ());
4308+ for (auto const & track : trackSlice) {
4309+ const auto trackLabel = trackLabels.iteratorAt (track.globalIndex ());
4310+ if (trackLabel.has_mcParticle ()) {
4311+ reconstructedTrackMcIds.insert (trackLabel.mcParticleId ());
4312+ }
4313+ }
4314+ const auto v0Slice = v0Candidates.sliceBy (pairLossV0sPerCollision, collision.globalIndex ());
4315+ for (auto const & v0 : v0Slice) {
4316+ const auto v0MC = v0MCCores.iteratorAt (v0.globalIndex ());
4317+ if (v0MC.particleIdMC () < 0 || v0MC.pdgCode () != PDG_t::kK0Short ) {
4318+ continue ;
4319+ }
4320+ reconstructedV0McIds.insert (v0MC.particleIdMC ());
4321+ }
4322+ }
4323+
4324+ for (auto const & mcParticle : mcParticles) {
4325+ const float genPt = mcParticle.pt ();
4326+ const float genEta = mcParticle.eta ();
4327+ const float genPhi = mcParticle.phi ();
4328+ if (std::abs (genEta) > etaSel) {
4329+ continue ;
4330+ }
4331+
4332+ if (isPairLossTriggerPdg (mcParticle.pdgCode ()) &&
4333+ genPt >= axisRanges[3 ][0 ] && genPt <= axisRanges[3 ][1 ] &&
4334+ (!masterConfigurations.doTriggPhysicalPrimary || mcParticle.isPhysicalPrimary ())) {
4335+ // Same charge requirement the other two parts apply, so that the three
4336+ // trigger definitions stay comparable.
4337+ auto const * pdgParticle = pdgDB->GetParticle (mcParticle.pdgCode ());
4338+ const double charge = pdgParticle != nullptr ? pdgParticle->Charge () : 0.0 ;
4339+ const int sign = charge > 0.0 ? 1 : (charge < 0.0 ? -1 : 0 );
4340+ if (!((triggerParticleCharge > 0 && sign < 0 ) || (triggerParticleCharge < 0 && sign > 0 ) || sign == 0 )) {
4341+ histos.fill (HIST (" PairLossK0/GenStudy/Gen/hTrigger" ), genPt, genEta, genPhi, generatedNch);
4342+ if (reconstructedTrackMcIds.count (mcParticle.globalIndex ()) > 0 ) {
4343+ histos.fill (HIST (" PairLossK0/GenStudy/Reconstructed/hTrigger" ), genPt, genEta, genPhi, generatedNch);
4344+ } else {
4345+ histos.fill (HIST (" PairLossK0/GenStudy/NotReconstructed/hTrigger" ), genPt, genEta, genPhi, generatedNch);
4346+ }
4347+ }
4348+ }
4349+
4350+ if (mcParticle.pdgCode () == PDG_t::kK0Short &&
4351+ genPt >= axisRanges[2 ][0 ] && genPt <= axisRanges[2 ][1 ] &&
4352+ (!doAssocPhysicalPrimary || mcParticle.isPhysicalPrimary ())) {
4353+ // Same findability definition the stage ladder uses: decayed to
4354+ // pi+ pi- with both charged daughters inside the tracking acceptance.
4355+ // A K0 that is not findable could never have been reconstructed, so
4356+ // splitting on it is what makes NotReconstructed/ interpretable --
4357+ // without it the folder is dominated by decays whose daughters simply
4358+ // left the acceptance.
4359+ bool hasPositiveDaughter = false ;
4360+ bool hasNegativeDaughter = false ;
4361+ bool daughtersInAcceptance = true ;
4362+ for (auto const & daughter : mcParticle.daughters_as <aod::McParticles>()) {
4363+ if (daughter.pdgCode () != PDG_t::kPiPlus && daughter.pdgCode () != -PDG_t::kPiPlus ) {
4364+ continue ;
4365+ }
4366+ if (daughter.pdgCode () == PDG_t::kPiPlus ) {
4367+ hasPositiveDaughter = true ;
4368+ } else {
4369+ hasNegativeDaughter = true ;
4370+ }
4371+ if (daughter.pt () < pairLossK0Configurations.daughterPtMin ||
4372+ std::abs (daughter.eta ()) > pairLossK0Configurations.daughterEtaMax ) {
4373+ daughtersInAcceptance = false ;
4374+ }
4375+ }
4376+ const float k0Findable = (hasPositiveDaughter && hasNegativeDaughter && daughtersInAcceptance) ? 1 .0f : 0 .0f ;
4377+
4378+ histos.fill (HIST (" PairLossK0/GenStudy/Gen/hK0Short" ), genPt, genEta, genPhi, generatedNch, k0Findable);
4379+ if (reconstructedV0McIds.count (mcParticle.globalIndex ()) > 0 ) {
4380+ histos.fill (HIST (" PairLossK0/GenStudy/Reconstructed/hK0Short" ), genPt, genEta, genPhi, generatedNch, k0Findable);
4381+ } else {
4382+ histos.fill (HIST (" PairLossK0/GenStudy/NotReconstructed/hK0Short" ), genPt, genEta, genPhi, generatedNch, k0Findable);
4383+ }
4384+ }
4385+ }
4386+ };
4387+
4388+ // Part 2. Wrapped in a lambda so that its own early exits leave the other
4389+ // two parts free to run: the three parts are independent, not exclusive.
4390+ auto runRecComparison = [&]() {
41894391 if (recCollisions.size () == 0 ) {
41904392 return ;
41914393 }
@@ -4588,9 +4790,19 @@ struct HStrangeCorrelation {
45884790 }
45894791
45904792 pairLossComparison.clear ();
4793+ };
4794+
4795+ if (pairLossK0Configurations.doGenLevelStudy ) {
4796+ runGenLevelStudy ();
4797+ }
4798+ if (pairLossK0Configurations.doRecComparison ) {
4799+ runRecComparison ();
4800+ }
4801+ if (!pairLossK0Configurations.doStageDiagnostics ) {
45914802 return ;
45924803 }
45934804
4805+ // Part 1 follows.
45944806 histos.fill (HIST (" PairLossK0/Event/hCounter" ), 0 .0f );
45954807 histos.fill (HIST (" PairLossK0/Event/hNRecoCollisions" ), recCollisions.size ());
45964808
0 commit comments