From c4d1e2002865192228da767c90b8a290b5396024 Mon Sep 17 00:00:00 2001 From: Anton Riedel Date: Thu, 9 Jul 2026 08:55:45 +0200 Subject: [PATCH 1/8] Feat: add multiplicity estimation for mc only case --- PWGCF/Femto/Core/mcBuilder.h | 26 +++++++++++++++++++++ PWGCF/Femto/TableProducer/femtoProducer.cxx | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/PWGCF/Femto/Core/mcBuilder.h b/PWGCF/Femto/Core/mcBuilder.h index 425f391e4c5..d37f09c36ab 100644 --- a/PWGCF/Femto/Core/mcBuilder.h +++ b/PWGCF/Femto/Core/mcBuilder.h @@ -43,6 +43,7 @@ struct ConfMc : o2::framework::ConfigurableGroup { std::string prefix = std::string("MonteCarlo"); o2::framework::Configurable passThrough{"passThrough", false, "Passthrough all MC collisions and particles"}; o2::framework::Configurable findLastPartonicMother{"findLastPartonicMother", true, "If true, the partonic mother will be the first parton directly after the initial collision. If false, the partonic mother will be the last parton before hadronization"}; + o2::framework::Configurable etaAcceptanceMcOnly{"etaAcceptanceMcOnly", 0.8, "For MC ONLY processing. |eta| acceptance for estimating primary track multiplicity"}; }; struct McBuilderProducts : o2::framework::ProducesGroup { @@ -152,6 +153,7 @@ class McBuilder return; } mPassThrough = config.passThrough.value; + mEtaAcceptanceMcOnly = config.etaAcceptanceMcOnly.value; mFindLastPartonicMother = config.findLastPartonicMother.value; LOG(info) << "Initialization done..."; } @@ -201,6 +203,28 @@ class McBuilder mCollisionMap.emplace(mcCol.globalIndex(), mcProducts.producedMcCollisions.lastIndex()); } + // for mc only + template + void fillMcCollision(T1 const& mcCol, T2 const& mcParticles, T3& mcProducts) + { + float centrality = 0; // no centrality estimator for mc only, so set to 0 + float multiplicity = 0; // no multiplicity estimator for mc only + + // define multiplicity ourselves by counting primary particles for |eta|,0.8 + // this is similar to how define it in data + for (auto const& mcParticle : mcParticles) { + if (mcParticle.isPhysicalPrimary() && (std::fabs(mcParticle.eta()) < mEtaAcceptanceMcOnly)) { + multiplicity += 1; + } + } + + mcProducts.producedMcCollisions( + mcCol.posZ(), + multiplicity, + centrality); + mCollisionMap.emplace(mcCol.globalIndex(), mcProducts.producedMcCollisions.lastIndex()); + } + template void fillMcParticle(T1 const& mcParticle, T2 const& mcParticles, T3 const& mcCol, T4& mcProducts) { @@ -591,6 +615,8 @@ class McBuilder bool mProduceOmegaLabels = false; bool mProduceMcMotherLabels = false; + float mEtaAcceptanceMcOnly = 0.8; + std::unordered_map mCollisionMap; std::unordered_map mMcParticleMap; diff --git a/PWGCF/Femto/TableProducer/femtoProducer.cxx b/PWGCF/Femto/TableProducer/femtoProducer.cxx index 7758651b289..ae91ecdf286 100644 --- a/PWGCF/Femto/TableProducer/femtoProducer.cxx +++ b/PWGCF/Femto/TableProducer/femtoProducer.cxx @@ -546,7 +546,7 @@ struct FemtoProducer { { mcBuilder.reset(mcParticles); for (const auto& mcCol : mcCols) { - mcBuilder.fillMcCollision(mcCol, mcProducts); + mcBuilder.fillMcCollision(mcCol, mcParticles, mcProducts); auto particlesThisCollision = mcParticles.sliceBy(perMcCollision, mcCol.globalIndex()); for (const auto& mcParticle : particlesThisCollision) { mcBuilder.fillMcParticle(mcParticle, mcParticles, mcCol, mcProducts); From da2c8761883160e0d6d8cecb240498a977415a5d Mon Sep 17 00:00:00 2001 From: Anton Riedel Date: Fri, 10 Jul 2026 15:18:28 +0200 Subject: [PATCH 2/8] Feat: add histograms for filters --- PWGCF/Femto/Core/baseSelection.h | 146 ++++++++-- PWGCF/Femto/Core/cascadeBuilder.h | 221 ++++++++++---- PWGCF/Femto/Core/collisionBuilder.h | 305 ++++++++++++-------- PWGCF/Femto/Core/kinkBuilder.h | 148 ++++++++-- PWGCF/Femto/Core/selectionContainer.h | 35 ++- PWGCF/Femto/Core/trackBuilder.h | 235 ++++++++------- PWGCF/Femto/Core/v0Builder.h | 205 +++++++++---- PWGCF/Femto/TableProducer/femtoProducer.cxx | 18 +- 8 files changed, 896 insertions(+), 417 deletions(-) diff --git a/PWGCF/Femto/Core/baseSelection.h b/PWGCF/Femto/Core/baseSelection.h index 469c1f6c521..65d068bdce7 100644 --- a/PWGCF/Femto/Core/baseSelection.h +++ b/PWGCF/Femto/Core/baseSelection.h @@ -30,9 +30,11 @@ #include #include #include +#include +#include #include -namespace o2::analysis::femto +namespace o2::analysis::femto::baseselection { /// \class BaseSelection @@ -54,6 +56,11 @@ class BaseSelection /// \brief Destructor virtual ~BaseSelection() = default; + void init(bool passThrough) + { + mPassThrough = passThrough; + } + /// \brief Add a static-value based selection for a specific observable. /// \param observableIndex Index of the observable. /// \param selectionName Name of the selection. @@ -75,9 +82,14 @@ class BaseSelection LOG(fatal) << "Observable is not valid. Observable (index) has to be smaller than " << NumObservables; } // init selection container for selection at given index - mSelectionContainers.at(observableIndex) = SelectionContainer(selectionName, selectionValues, limitType, skipMostPermissiveBit, isMinimalCut, isOptionalCut); - - init(observableIndex); + mSelectionContainers.at(observableIndex) = selectioncontainer::SelectionContainer(selectionName, + selectionValues, + limitType, + mPassThrough ? false : skipMostPermissiveBit, + mPassThrough ? false : isMinimalCut, + mPassThrough ? false : isOptionalCut); + + addSelection(observableIndex); } /// \brief Add a function-based selection for a specific observable. @@ -103,9 +115,15 @@ class BaseSelection if (static_cast(observableIndex) >= NumObservables) { LOG(fatal) << "Observable is not valid. Observable (index) has to be smaller than " << NumObservables; } - mSelectionContainers.at(observableIndex) = SelectionContainer(selectionName, lowerLimit, upperLimit, functions, limitType, skipMostPermissiveBit, isMinimalCut, isOptionalCut); - - init(observableIndex); + mSelectionContainers.at(observableIndex) = selectioncontainer::SelectionContainer(selectionName, + lowerLimit, + upperLimit, + functions, + limitType, + mPassThrough ? false : skipMostPermissiveBit, + mPassThrough ? false : isMinimalCut, + mPassThrough ? false : isOptionalCut); + addSelection(observableIndex); } /// \brief Add a static-value based selection for a specific observable. @@ -128,9 +146,13 @@ class BaseSelection LOG(fatal) << "Observable is not valid. Observable (index) has to be smaller than " << NumObservables; } // init selection container for selection at given index - mSelectionContainers.at(observableIndex) = SelectionContainer(selectionName, selectionRanges, skipMostPermissiveBit, isMinimalCut, isOptionalCut); + mSelectionContainers.at(observableIndex) = selectioncontainer::SelectionContainer(selectionName, + selectionRanges, + mPassThrough ? false : skipMostPermissiveBit, + mPassThrough ? false : isMinimalCut, + mPassThrough ? false : isOptionalCut); - init(observableIndex); + addSelection(observableIndex); } /// \brief Add a boolean-based selection for a specific observable. @@ -144,23 +166,28 @@ class BaseSelection std::string const& selectionName, int mode) { + if (mPassThrough) { + mSelectionContainers.at(observableIndex) = selectioncontainer::SelectionContainer(selectionName, std::vector{1}, limits::LimitType::kEqual, false, false, false); + return; + } + switch (mode) { case -1: // cut is optional and we store a bit for it - mSelectionContainers.at(observableIndex) = SelectionContainer(selectionName, std::vector{1}, limits::LimitType::kEqual, false, false, true); + mSelectionContainers.at(observableIndex) = selectioncontainer::SelectionContainer(selectionName, std::vector{1}, limits::LimitType::kEqual, false, false, true); break; case 0: // cut is disabled; initialize with empty vector so evaluation bails out early - mSelectionContainers.at(observableIndex) = SelectionContainer(selectionName, std::vector{}, limits::LimitType::kEqual, false, false, false); + mSelectionContainers.at(observableIndex) = selectioncontainer::SelectionContainer(selectionName, std::vector{}, limits::LimitType::kEqual, false, false, false); break; case 1: // mandatory cut; only one threshold so the most permissive bit is skipped and no extra bit is stored - mSelectionContainers.at(observableIndex) = SelectionContainer(selectionName, std::vector{1}, limits::LimitType::kEqual, true, true, false); + mSelectionContainers.at(observableIndex) = selectioncontainer::SelectionContainer(selectionName, std::vector{1}, limits::LimitType::kEqual, true, true, false); break; case 2: // pass through mode; cut is neither minimal nor optional and we store all bits - mSelectionContainers.at(observableIndex) = SelectionContainer(selectionName, std::vector{1}, limits::LimitType::kEqual, false, false, false); + mSelectionContainers.at(observableIndex) = selectioncontainer::SelectionContainer(selectionName, std::vector{1}, limits::LimitType::kEqual, false, false, false); break; default: LOG(fatal) << "Invalid switch for boolean selection"; } - init(observableIndex); + addSelection(observableIndex); } /// \brief Update the limits of a function-based selection for a specific observable. @@ -255,10 +282,15 @@ class BaseSelection /// \param comments Vector of comment strings, one per selection threshold. void addComments(int observableIndex, std::vector const& comments) { mSelectionContainers.at(observableIndex).addComments(comments); } + [[nodiscard]] bool isPassThrough() const { return mPassThrough; } + /// \brief Check whether all required and optional cuts are passed. /// \return True if all minimal cuts pass and, if optional cuts are present, at least one of them passes. [[nodiscard]] bool passesAllRequiredSelections() const { + if (mPassThrough) { + return true; + } if (mHasMinimalSelection && !mHasOptionalSelection) { return mPassesMinimalSelections; } @@ -363,7 +395,6 @@ class BaseSelection LOG(info) << " Selections:"; for (std::size_t j = 0; j < container.getNSelections(); ++j) { - std::stringstream line; std::string sel = container.getValueAsString(j); std::string comment = container.getComment(j); @@ -392,9 +423,12 @@ class BaseSelection /// \tparam HistName Name of the histogram to create in the registry. /// \param registry Pointer to the histogram registry. template - void setupContainers(o2::framework::HistogramRegistry* registry) + void setupSelectionHistogram(o2::framework::HistogramRegistry* registry) { - mHistRegistry = registry; + if (!mHistRegistry) { + mHistRegistry = registry; + } + // create histogram with one bin per selection, plus two summary bins (all analyzed, all passed) int nBins = mNSelection + 2; mHistRegistry->add(HistName, "; Selection Bits; Entries", o2::framework::HistType::kTH1F, {{nBins, -0.5, nBins - 0.5}}); @@ -418,8 +452,64 @@ class BaseSelection mHistRegistry->get(HIST(HistName))->GetXaxis()->SetBinLabel(mNSelection + 2, "All passed"); } + /// \brief Setup a histogram tracking how many candidates pass each individual filter bound. + /// \tparam FilterHistName Name of the histogram (must be unique in registry). + /// \param registry Pointer to the histogram registry. + /// \param filters Ordered list of (name, value) pairs, one per filter bound. The position in + /// this vector must match the caller's filter enum values (e.g. filters[kPtMin] + /// describes the kPtMin bound), since that's what fillFilter() indexes into. + template + void setupFilterHistogram(o2::framework::HistogramRegistry* registry, + std::vector> const& filters) + { + if (!mHistRegistry) { + mHistRegistry = registry; + } + + mNFilters = filters.size(); + // two extra bins: "All analyzed" and "All passed", mirroring setupContainers() + registry->add(FilterHistName, "; Filter; Entries", o2::framework::HistType::kTH1F, + {{static_cast(mNFilters) + 2, -0.5, static_cast(mNFilters) + 1.5}}); + + auto* axis = registry->get(HIST(FilterHistName))->GetXaxis(); + for (std::size_t i = 0; i < mNFilters; ++i) { + std::ostringstream label; + label << "FilterName" << selectioncontainer::ValueDelimiter << filters[i].first << selectioncontainer::SectionDelimiter + << "FilterValue" << selectioncontainer::ValueDelimiter << filters[i].second; + axis->SetBinLabel(static_cast(i) + 1, label.str().c_str()); + } + axis->SetBinLabel(static_cast(mNFilters) + 1, "All analyzed"); + axis->SetBinLabel(static_cast(mNFilters) + 2, "All passed"); + } + + /// \brief Fill one bin of the filter histogram if the corresponding filter bound was passed. + /// \tparam FilterHistName Name of the histogram (same one passed to setupFilterHistogram). + /// \param filterIndex Index of the filter bound (an enum value from the caller's filter enum). + /// \param passed Whether the candidate passed this specific filter bound. + template + void fillFilter(int filterIndex, bool passed) const + { + if (passed) { + mHistRegistry->fill(HIST(FilterHistName), filterIndex); + } + } + + /// \brief Fill the two summary bins of the filter histogram: called once per candidate, + /// after all individual filter bounds have been checked. + /// \tparam FilterHistName Name of the histogram (same one passed to setupFilterHistogram). + /// \param passedAll Whether the candidate passed every configured filter bound. + template + void fillFilterSummary(bool passedAll) const + + { + mHistRegistry->fill(HIST(FilterHistName), static_cast(mNFilters)); + if (passedAll || mPassThrough) { + mHistRegistry->fill(HIST(FilterHistName), static_cast(mNFilters) + 1); + } + } + protected: - void init(int observableIndex) + void addSelection(int observableIndex) { // check if any selections are configured if (mSelectionContainers.at(observableIndex).isEmpty()) { @@ -445,15 +535,17 @@ class BaseSelection } o2::framework::HistogramRegistry* mHistRegistry = nullptr; - std::array, NumObservables> mSelectionContainers = {}; ///< Array of selection containers, one per observable - std::bitset mFinalBitmask = {}; ///< Assembled bitmask combining all observable selections - std::size_t mNSelectionBits = 0; ///< Number of bits occupied in the bitmask (excludes skipped most-permissive bits) - std::size_t mNSelection = 0; ///< Total number of configured selection thresholds across all observables - bool mHasMinimalSelection = false; ///< True if at least one observable has a mandatory (minimal) cut configured - bool mPassesMinimalSelections = true; ///< True if all mandatory (minimal) cuts have been passed so far - bool mHasOptionalSelection = false; ///< True if at least one observable has an optional cut configured - bool mPassesOptionalSelections = false; ///< True if at least one optional cut has been passed + std::array, NumObservables> mSelectionContainers = {}; ///< Array of selection containers, one per observable + std::bitset mFinalBitmask = {}; ///< Assembled bitmask combining all observable selections + std::size_t mNSelectionBits = 0; ///< Number of bits occupied in the bitmask (excludes skipped most-permissive bits) + std::size_t mNSelection = 0; ///< Total number of configured selection thresholds across all observables + std::size_t mNFilters = 0; ///< Total number of configured (pre)filter + bool mHasMinimalSelection = false; ///< True if at least one observable has a mandatory (minimal) cut configured + bool mPassesMinimalSelections = true; ///< True if all mandatory (minimal) cuts have been passed so far + bool mHasOptionalSelection = false; ///< True if at least one observable has an optional cut configured + bool mPassesOptionalSelections = false; ///< True if at least one optional cut has been passed + bool mPassThrough = false; }; -} // namespace o2::analysis::femto +} // namespace o2::analysis::femto::baseselection #endif // PWGCF_FEMTO_CORE_BASESELECTION_H_ diff --git a/PWGCF/Femto/Core/cascadeBuilder.h b/PWGCF/Femto/Core/cascadeBuilder.h index cd92a1afb9f..4d82c2cdaa2 100644 --- a/PWGCF/Femto/Core/cascadeBuilder.h +++ b/PWGCF/Femto/Core/cascadeBuilder.h @@ -172,10 +172,48 @@ const std::unordered_map cascadeSelectionNames = { {kCascadeSelsMax, "Cascade Selections Max"}}; +/// enum for all cascade pre-filters (evaluated in checkFilters, before the selection bitmask) +enum CascadeFilters { + kPtMin, + kPtMax, + kEtaMin, + kEtaMax, + kPhiMin, + kPhiMax, + kLambdaMassMin, + kLambdaMassMax, + kXiMassMin, + kXiMassMax, + kRejectionOmegaMass, + kOmegaMassMin, + kOmegaMassMax, + kRejectionXiMass, + kCascadeFiltersMax +}; + +constexpr char XiFilterHistName[] = "hXiFilters"; +constexpr char OmegaFilterHistName[] = "hOmegaFilters"; +const std::unordered_map cascadeFilterNames = { + {kPtMin, "ptMin"}, + {kPtMax, "ptMax"}, + {kEtaMin, "etaMin"}, + {kEtaMax, "etaMax"}, + {kPhiMin, "phiMin"}, + {kPhiMax, "phiMax"}, + {kLambdaMassMin, "lambdaMassMin"}, + {kLambdaMassMax, "lambdaMassMax"}, + {kXiMassMin, "xiMassMin"}, + {kXiMassMax, "xiMassMax"}, + {kRejectionOmegaMass, "rejectOmega"}, + {kOmegaMassMin, "omegaMassMin"}, + {kOmegaMassMax, "omegaMassMax"}, + {kRejectionXiMass, "rejectXi"}, +}; + /// \class FemtoDreamTrackCuts /// \brief Cut class to contain and execute all cuts applied to tracks -template -class CascadeSelection : public BaseSelection +template +class CascadeSelection : public baseselection::BaseSelection { public: CascadeSelection() = default; @@ -184,13 +222,7 @@ class CascadeSelection : public BaseSelection void configure(o2::framework::HistogramRegistry* registry, T1 const& config, T2 const& filter) { - // check for pass through mode - mPassThrough = config.passThrough.value; - - // if pass through mode is activated, each cut is neutral (i.e. neither minimal nor optional and we do - // store all bits, so the most permissive bit is not skipped for minimal selections) - const bool isMinimalCut = !mPassThrough; - const bool skipMostPermissiveBit = !mPassThrough; + this->init(config.passThrough.value); if constexpr (modes::isEqual(cascadeType, modes::Cascade::kXi)) { mXiMassLowerLimit = filter.massXiMin.value; @@ -198,7 +230,7 @@ class CascadeSelection : public BaseSelectionaddSelection(kBachelorTpcPion, cascadeSelectionNames.at(kBachelorTpcPion), config.bachelorTpcPion.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); + this->addSelection(kBachelorTpcPion, cascadeSelectionNames.at(kBachelorTpcPion), config.bachelorTpcPion.value, limits::kAbsUpperLimit, true, true, false); } if constexpr (modes::isEqual(cascadeType, modes::Cascade::kOmega)) { mOmegaMassLowerLimit = filter.massOmegaMin.value; @@ -206,7 +238,7 @@ class CascadeSelection : public BaseSelectionaddSelection(kBachelorTpcKaon, cascadeSelectionNames.at(kBachelorTpcKaon), config.bachelorTpcKaon.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); + this->addSelection(kBachelorTpcKaon, cascadeSelectionNames.at(kBachelorTpcKaon), config.bachelorTpcKaon.value, limits::kAbsUpperLimit, true, true, false); } mPtMin = filter.ptMin.value; @@ -218,21 +250,39 @@ class CascadeSelection : public BaseSelectionaddSelection(kPosDauTpc, cascadeSelectionNames.at(kPosDauTpc), config.posDauTpc.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kNegDauTpc, cascadeSelectionNames.at(kNegDauTpc), config.negDauTpc.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - - this->addSelection(kCascadeCpaMin, cascadeSelectionNames.at(kCascadeCpaMin), config.cascadeCpaMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kCascadeTransRadMin, cascadeSelectionNames.at(kCascadeTransRadMin), config.cascadeTransRadMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kCascadeDcaDaughMax, cascadeSelectionNames.at(kCascadeDcaDaughMax), config.cascadeDcaDauMax.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kLambdaCpaMin, cascadeSelectionNames.at(kLambdaCpaMin), config.lambdaCpaMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kLambdaTransRadMin, cascadeSelectionNames.at(kLambdaTransRadMin), config.lambdaTransRadMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kLambdaDcaDauMax, cascadeSelectionNames.at(kLambdaDcaDauMax), config.lambdaDcaDauMax.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kLambdaDcaToPvMin, cascadeSelectionNames.at(kLambdaDcaToPvMin), config.lambdaDcaToPvMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kDauAbsEtaMax, cascadeSelectionNames.at(kDauAbsEtaMax), config.dauAbsEtaMax.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kDauDcaMin, cascadeSelectionNames.at(kDauDcaMin), config.dauDcaMin.value, limits::kAbsLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kDauTpcClsMin, cascadeSelectionNames.at(kDauTpcClsMin), config.dauTpcClustersMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - - this->setupContainers(registry); + this->addSelection(kPosDauTpc, cascadeSelectionNames.at(kPosDauTpc), config.posDauTpc.value, limits::kAbsUpperLimit, true, true, false); + this->addSelection(kNegDauTpc, cascadeSelectionNames.at(kNegDauTpc), config.negDauTpc.value, limits::kAbsUpperLimit, true, true, false); + + this->addSelection(kCascadeCpaMin, cascadeSelectionNames.at(kCascadeCpaMin), config.cascadeCpaMin.value, limits::kLowerLimit, true, true, false); + this->addSelection(kCascadeTransRadMin, cascadeSelectionNames.at(kCascadeTransRadMin), config.cascadeTransRadMin.value, limits::kLowerLimit, true, true, false); + this->addSelection(kCascadeDcaDaughMax, cascadeSelectionNames.at(kCascadeDcaDaughMax), config.cascadeDcaDauMax.value, limits::kAbsUpperLimit, true, true, false); + this->addSelection(kLambdaCpaMin, cascadeSelectionNames.at(kLambdaCpaMin), config.lambdaCpaMin.value, limits::kLowerLimit, true, true, false); + this->addSelection(kLambdaTransRadMin, cascadeSelectionNames.at(kLambdaTransRadMin), config.lambdaTransRadMin.value, limits::kLowerLimit, true, true, false); + this->addSelection(kLambdaDcaDauMax, cascadeSelectionNames.at(kLambdaDcaDauMax), config.lambdaDcaDauMax.value, limits::kAbsUpperLimit, true, true, false); + this->addSelection(kLambdaDcaToPvMin, cascadeSelectionNames.at(kLambdaDcaToPvMin), config.lambdaDcaToPvMin.value, limits::kLowerLimit, true, true, false); + this->addSelection(kDauAbsEtaMax, cascadeSelectionNames.at(kDauAbsEtaMax), config.dauAbsEtaMax.value, limits::kAbsUpperLimit, true, true, false); + this->addSelection(kDauDcaMin, cascadeSelectionNames.at(kDauDcaMin), config.dauDcaMin.value, limits::kAbsLowerLimit, true, true, false); + this->addSelection(kDauTpcClsMin, cascadeSelectionNames.at(kDauTpcClsMin), config.dauTpcClustersMin.value, limits::kLowerLimit, true, true, false); + + this->setupSelectionHistogram(registry); + this->template setupFilterHistogram( + registry, + { + {cascadeFilterNames.at(kPtMin), mPtMin}, + {cascadeFilterNames.at(kPtMax), mPtMax}, + {cascadeFilterNames.at(kEtaMin), mEtaMin}, + {cascadeFilterNames.at(kEtaMax), mEtaMax}, + {cascadeFilterNames.at(kPhiMin), mPhiMin}, + {cascadeFilterNames.at(kPhiMax), mPhiMax}, + {cascadeFilterNames.at(kLambdaMassMin), mLambdaMassMin}, + {cascadeFilterNames.at(kLambdaMassMax), mLambdaMassMax}, + {cascadeFilterNames.at(kXiMassMin), mXiMassLowerLimit}, + {cascadeFilterNames.at(kXiMassMax), mXiMassUpperLimit}, + {cascadeFilterNames.at(kRejectionOmegaMass), mRejectOmegaHypothesis ? 1.f : 0.f}, + {cascadeFilterNames.at(kOmegaMassMin), mOmegaMassLowerLimit}, + {cascadeFilterNames.at(kOmegaMassMax), mOmegaMassUpperLimit}, + {cascadeFilterNames.at(kRejectionXiMass), mRejectXiHypothesis ? 1.f : 0.f}, + }); }; template @@ -282,47 +332,94 @@ class CascadeSelection : public BaseSelectionassembleBitmask(); + this->assembleBitmask(); }; template bool checkFilters(const T& cascade) const { - // check kinematics - const bool kinematicsOK = - (cascade.pt() > mPtMin && cascade.pt() < mPtMax) && - (cascade.eta() > mEtaMin && cascade.eta() < mEtaMax) && - (cascade.phi() > mPhiMin && cascade.phi() < mPhiMax); - - if (!kinematicsOK) { - return false; - } - - // check mass of daughter lambda - const bool lambdaOK = - (cascade.mLambda() > mLambdaMassMin && cascade.mLambda() < mLambdaMassMax); - - if (!lambdaOK) { - return false; - } - - // check mass hypothesis + bool pass = true; + bool p = false; + + // kinematics + p = cascade.pt() > mPtMin; + this->template fillFilter(kPtMin, p); + pass &= p; + + p = cascade.pt() < mPtMax; + this->template fillFilter(kPtMax, p); + pass &= p; + + p = cascade.eta() > mEtaMin; + this->template fillFilter(kEtaMin, p); + pass &= p; + + p = cascade.eta() < mEtaMax; + this->template fillFilter(kEtaMax, p); + pass &= p; + + p = cascade.phi() > mPhiMin; + this->template fillFilter(kPhiMin, p); + pass &= p; + + p = cascade.phi() < mPhiMax; + this->template fillFilter(kPhiMax, p); + pass &= p; + + // mass of daughter lambda (gating AND-cut) + p = cascade.mLambda() > mLambdaMassMin; + this->template fillFilter(kLambdaMassMin, p); + pass &= p; + + p = cascade.mLambda() < mLambdaMassMax; + this->template fillFilter(kLambdaMassMax, p); + pass &= p; + + // mass hypothesis: signal window is a gating AND-cut, competing hypothesis is a + // rejection OR-cut (pass if outside its window, or if rejection is disabled); + // the competing window's own min/max bins are diagnostic only. if constexpr (modes::isEqual(cascadeType, modes::Cascade::kXi)) { - // Xi candidate must be inside Xi window and outside Omega - return (cascade.mXi() > mXiMassLowerLimit && cascade.mXi() < mXiMassUpperLimit) && - (!mRejectOmegaHypothesis || (cascade.mOmega() < mOmegaMassLowerLimit || cascade.mOmega() > mOmegaMassUpperLimit)); + p = cascade.mXi() > mXiMassLowerLimit; + this->template fillFilter(kXiMassMin, p); + pass &= p; + + p = cascade.mXi() < mXiMassUpperLimit; + this->template fillFilter(kXiMassMax, p); + pass &= p; + + bool const belowOmega = cascade.mOmega() < mOmegaMassLowerLimit; + this->template fillFilter(kOmegaMassMin, belowOmega); + bool const aboveOmega = cascade.mOmega() > mOmegaMassUpperLimit; + this->template fillFilter(kOmegaMassMax, aboveOmega); + + p = !mRejectOmegaHypothesis || belowOmega || aboveOmega; + this->template fillFilter(kRejectionOmegaMass, p); + pass &= p; } if constexpr (modes::isEqual(cascadeType, modes::Cascade::kOmega)) { - // Omega candidate must be inside Omega window and outside Xi - return (cascade.mOmega() > mOmegaMassLowerLimit && cascade.mOmega() < mOmegaMassUpperLimit) && - (!mRejectXiHypothesis || (cascade.mXi() < mXiMassLowerLimit || cascade.mXi() > mXiMassUpperLimit)); + p = cascade.mOmega() > mOmegaMassLowerLimit; + this->template fillFilter(kOmegaMassMin, p); + pass &= p; + + p = cascade.mOmega() < mOmegaMassUpperLimit; + this->template fillFilter(kOmegaMassMax, p); + pass &= p; + + bool const belowXi = cascade.mXi() < mXiMassLowerLimit; + this->template fillFilter(kXiMassMin, belowXi); + bool const aboveXi = cascade.mXi() > mXiMassUpperLimit; + this->template fillFilter(kXiMassMax, aboveXi); + + p = !mRejectXiHypothesis || belowXi || aboveXi; + this->template fillFilter(kRejectionXiMass, p); + pass &= p; } - return false; // should never happen - } + this->template fillFilterSummary(pass); - [[nodiscard]] bool passThroughAllCascades() const { return mPassThrough; } + return this->isPassThrough() || pass; + } protected: bool mRejectOmegaHypothesis = false; @@ -333,8 +430,6 @@ class CascadeSelection : public BaseSelection produceOmegaExtras{"produceOmegaExtras", -1, "Produce OmegaExtras (-1: auto; 0 off; 1 on)"}; }; -template +template class CascadeBuilder { public: @@ -411,11 +506,11 @@ class CascadeBuilder int64_t posDaughterIndex = 0; int64_t negDaughterIndex = 0; for (const auto& cascade : cascades) { - if (!mCascadeSelection.passThroughAllCascades() && !mCascadeSelection.checkFilters(cascade)) { + if (!mCascadeSelection.checkFilters(cascade)) { continue; } mCascadeSelection.applySelections(cascade, tracks, col); - if (!mCascadeSelection.passThroughAllCascades() && !mCascadeSelection.passesAllRequiredSelections()) { + if (!mCascadeSelection.passesAllRequiredSelections()) { continue; } @@ -445,11 +540,11 @@ class CascadeBuilder int64_t posDaughterIndex = 0; int64_t negDaughterIndex = 0; for (const auto& cascade : cascades) { - if (!mCascadeSelection.passThroughAllCascades() && !mCascadeSelection.checkFilters(cascade)) { + if (!mCascadeSelection.checkFilters(cascade)) { continue; } mCascadeSelection.applySelections(cascade, tracks, col); - if (!mCascadeSelection.passThroughAllCascades() && !mCascadeSelection.passesAllRequiredSelections()) { + if (!mCascadeSelection.passesAllRequiredSelections()) { continue; } @@ -534,7 +629,7 @@ class CascadeBuilder bool fillAnyTable() { return mFillAnyTable; } private: - CascadeSelection mCascadeSelection; + CascadeSelection mCascadeSelection; bool mFillAnyTable = false; bool mProduceXis = false; bool mProduceXiMasks = false; diff --git a/PWGCF/Femto/Core/collisionBuilder.h b/PWGCF/Femto/Core/collisionBuilder.h index 8bce654f441..7080ed19286 100644 --- a/PWGCF/Femto/Core/collisionBuilder.h +++ b/PWGCF/Femto/Core/collisionBuilder.h @@ -74,7 +74,7 @@ struct ConfCollisionBits : o2::framework::ConfigurableGroup { o2::framework::Configurable noHighMultCollInPrevRof{"noHighMultCollInPrevRof", 0, "veto an event if FT0C amplitude in previous ITS ROF is above threshold (-1: stored in bitmaks; 0 off; 1 on)"}; o2::framework::Configurable isGoodItsLayer3{"isGoodItsLayer3", 0, "number of inactive chips on ITS layer 3 is below maximum allowed value (-1: stored in bitmaks; 0 off; 1 on)"}; o2::framework::Configurable isGoodItsLayer0123{"isGoodItsLayer0123", 0, "numbers of inactive chips on ITS layers 0-3 are below maximum allowed values (-1: stored in bitmaks; 0 off; 1 on)"}; - o2::framework::Configurable isGoodItsLayersAll{"isGoodItsLayersAll", 0, "numbers of inactive chips on all ITS layers are below maximum allowed values (-1: stored in bitmaks; 0 off; 1 on)"}; + o2::framework::Configurable isGoodItsLayerAll{"isGoodItsLayerAll", 0, "numbers of inactive chips on all ITS layers are below maximum allowed values (-1: stored in bitmaks; 0 off; 1 on)"}; o2::framework::Configurable> occupancyMin{"occupancyMin", {}, "Minimum occpancy"}; o2::framework::Configurable> occupancyMax{"occupancyMax", {}, "Maximum occpancy"}; o2::framework::Configurable> sphericityMin{"sphericityMin", {}, "Minimum sphericity"}; @@ -127,7 +127,7 @@ enum CollisionSels { kNoHighMultCollInPrevRof, ///< veto an event if FT0C amplitude in previous ITS ROF is above threshold kIsGoodItsLayer3, ///< number of inactive chips on ITS layer 3 is below maximum allowed value kIsGoodItsLayer0123, ///< numbers of inactive chips on ITS layers 0-3 are below maximum allowed values - kIsGoodItsLayersAll, ///< numbers of inactive chips on all ITS layers are below maximum allowed values + kIsGoodItsLayerAll, ///< numbers of inactive chips on all ITS layers are below maximum allowed values kOccupancyMin, ///< Min. occupancy kOccupancyMax, ///< Max. occupancy kSphericityMin, ///< Min. sphericity @@ -153,26 +153,62 @@ const std::unordered_map collisionSelectionNames = { {kNoHighMultCollInPrevRof, "No high mult collsions in previous ROF"}, {kIsGoodItsLayer3, "Is good ITS layer 3"}, {kIsGoodItsLayer0123, "Is good ITS layer 0-3"}, - {kIsGoodItsLayersAll, "Is good ITS layer all"}, + {kIsGoodItsLayerAll, "Is good ITS layer all"}, {kOccupancyMin, "Minimum Occupancy"}, {kOccupancyMax, "Maximum Occupancy"}, {kSphericityMin, "Minimum Sphericity"}, {kSphericityMax, "Maximum Sphericity"}, - - {kTriggers, "Triggers"} - + {kTriggers, "Triggers"}}; + +/// enum for all collision pre-filters (evaluated in checkFilters, before the selection bitmask) +enum CollisionFilters { + kFilterVtxZMin, + kFilterVtxZMax, + kFilterMultMin, + kFilterMultMax, + kFilterCentMin, + kFilterCentMax, + kFilterMagFieldMin, + kFilterMagFieldMax, + kFilterSphericityMin, + kFilterSphericityMax, + kFilterRctFlags, + kFilterCollisionFiltersMax }; -template -class CollisionSelection : public BaseSelection +constexpr char CollisionFilterHistName[] = "hCollisionFilters"; +const std::unordered_map collisionFilterNames = { + {kFilterVtxZMin, "vtxZMin"}, + {kFilterVtxZMax, "vtxZMax"}, + {kFilterMultMin, "multMin"}, + {kFilterMultMax, "multMax"}, + {kFilterCentMin, "centMin"}, + {kFilterCentMax, "centMax"}, + {kFilterMagFieldMin, "magFieldMin"}, + {kFilterMagFieldMax, "magFieldMax"}, + {kFilterSphericityMin, "sphericityMin"}, + {kFilterSphericityMax, "sphericityMax"}, + {kFilterRctFlags, "rctFlagsOkFilter"}}; + +template +class CollisionSelection : public baseselection::BaseSelection { public: CollisionSelection() = default; ~CollisionSelection() override = default; - template - void configure(o2::framework::HistogramRegistry* registry, T1 const& filter, T2 const& config) + /// \brief Configure the collision selection: kinematic/quality filters, RCT flag checker, + /// trigger (Zorro) setup, and the selection bitmask. + /// \param registry Histogram registry. + /// \param filter ConfCollisionFilters (kinematic/quality pre-filter bounds). + /// \param config ConfCollisionBits (selection bits + trigger list). + /// \param confRct ConfCollisionRctFlags (RCT flag checker configuration). + /// \param confCcdb ConfCcdb (needed for the trigger CCDB path). + template + void configure(o2::framework::HistogramRegistry* registry, T1 const& filter, T2 const& config, T3 const& confRct, T4 const& confCcdb) { + this->init(config.passThrough.value); + // filters mVtxZMin = filter.vtxZMin.value; mVtxZMax = filter.vtxZMax.value; @@ -185,53 +221,87 @@ class CollisionSelection : public BaseSelectionaddSelection(kSel8, collisionSelectionNames.at(kSel8), 2); - this->addSelection(kNoSameBunchPileUp, collisionSelectionNames.at(kNoSameBunchPileUp), 2); - this->addSelection(kIsGoodZvtxFt0VsPv, collisionSelectionNames.at(kIsGoodZvtxFt0VsPv), 2); - this->addSelection(kNoCollInTimeRangeNarrow, collisionSelectionNames.at(kNoCollInTimeRangeNarrow), 2); - this->addSelection(kNoCollInTimeRangeStrict, collisionSelectionNames.at(kNoCollInTimeRangeStrict), 2); - this->addSelection(kNoCollInTimeRangeStandard, collisionSelectionNames.at(kNoCollInTimeRangeStandard), 2); - this->addSelection(kNoCollInRofStrict, collisionSelectionNames.at(kNoCollInRofStrict), 2); - this->addSelection(kNoCollInRofStandard, collisionSelectionNames.at(kNoCollInRofStandard), 2); - this->addSelection(kNoHighMultCollInPrevRof, collisionSelectionNames.at(kNoHighMultCollInPrevRof), 2); - this->addSelection(kIsGoodItsLayer3, collisionSelectionNames.at(kIsGoodItsLayer3), 2); - this->addSelection(kIsGoodItsLayer0123, collisionSelectionNames.at(kIsGoodItsLayer0123), 2); - this->addSelection(kIsGoodItsLayersAll, collisionSelectionNames.at(kIsGoodItsLayersAll), 2); - } else { - this->addSelection(kSel8, collisionSelectionNames.at(kSel8), config.sel8.value); - this->addSelection(kNoSameBunchPileUp, collisionSelectionNames.at(kNoSameBunchPileUp), config.noSameBunchPileup.value); - this->addSelection(kIsGoodZvtxFt0VsPv, collisionSelectionNames.at(kIsGoodZvtxFt0VsPv), config.isGoodZvtxFt0VsPv.value); - this->addSelection(kNoCollInTimeRangeNarrow, collisionSelectionNames.at(kNoCollInTimeRangeNarrow), config.noCollInTimeRangeNarrow.value); - this->addSelection(kNoCollInTimeRangeStrict, collisionSelectionNames.at(kNoCollInTimeRangeStrict), config.noCollInTimeRangeStrict.value); - this->addSelection(kNoCollInTimeRangeStandard, collisionSelectionNames.at(kNoCollInTimeRangeStandard), config.noCollInTimeRangeStandard.value); - this->addSelection(kNoCollInRofStrict, collisionSelectionNames.at(kNoCollInRofStrict), config.noCollInRofStrict.value); - this->addSelection(kNoCollInRofStandard, collisionSelectionNames.at(kNoCollInRofStandard), config.noCollInRofStandard.value); - this->addSelection(kNoHighMultCollInPrevRof, collisionSelectionNames.at(kNoHighMultCollInPrevRof), config.noHighMultCollInPrevRof.value); - this->addSelection(kIsGoodItsLayer3, collisionSelectionNames.at(kIsGoodItsLayer3), config.isGoodItsLayer3.value); - this->addSelection(kIsGoodItsLayer0123, collisionSelectionNames.at(kIsGoodItsLayer0123), config.isGoodItsLayer0123.value); - this->addSelection(kIsGoodItsLayersAll, collisionSelectionNames.at(kIsGoodItsLayersAll), config.isGoodItsLayersAll.value); + // RCT flag checker + mUseRctFlags = confRct.useRctFlags.value; + if (mUseRctFlags) { + LOG(info) << "Init RCT flag checker with label: " << confRct.label.value << "; use ZDC: " << confRct.useZdc.value << "; Limited acceptance is bad: " << confRct.treatLimitedAcceptanceAsBad.value; + mRctFlagsChecker.init(confRct.label.value, confRct.useZdc.value, confRct.treatLimitedAcceptanceAsBad.value); } - const bool isMinimalCut = !mPassThrough; - const bool isOptionalCut = !mPassThrough; - const bool skipMostPermissiveBit = !mPassThrough; + // trigger (Zorro) setup + if (!config.triggers.value.empty()) { + mUseTrigger = true; + for (size_t i = 0; i < config.triggers.value.size(); ++i) { + mTriggerNames += config.triggers.value[i]; + if (i != config.triggers.value.size() - 1) { + mTriggerNames += ","; + } + } + mZorro.setBaseCCDBPath(confCcdb.triggerPath.value); + } - this->addSelection(kOccupancyMin, collisionSelectionNames.at(kOccupancyMin), config.occupancyMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kOccupancyMax, collisionSelectionNames.at(kOccupancyMax), config.occupancyMax.value, limits::kUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kSphericityMin, collisionSelectionNames.at(kSphericityMin), config.sphericityMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kSphericityMax, collisionSelectionNames.at(kSphericityMax), config.sphericityMax.value, limits::kUpperLimit, skipMostPermissiveBit, isMinimalCut, false); + this->addSelection(kSel8, collisionSelectionNames.at(kSel8), config.sel8.value); + this->addSelection(kNoSameBunchPileUp, collisionSelectionNames.at(kNoSameBunchPileUp), config.noSameBunchPileup.value); + this->addSelection(kIsVertexItsTpc, collisionSelectionNames.at(kIsVertexItsTpc), config.isVertexItsTpc.value); + this->addSelection(kIsGoodZvtxFt0VsPv, collisionSelectionNames.at(kIsGoodZvtxFt0VsPv), config.isGoodZvtxFt0VsPv.value); + this->addSelection(kNoCollInTimeRangeNarrow, collisionSelectionNames.at(kNoCollInTimeRangeNarrow), config.noCollInTimeRangeNarrow.value); + this->addSelection(kNoCollInTimeRangeStrict, collisionSelectionNames.at(kNoCollInTimeRangeStrict), config.noCollInTimeRangeStrict.value); + this->addSelection(kNoCollInTimeRangeStandard, collisionSelectionNames.at(kNoCollInTimeRangeStandard), config.noCollInTimeRangeStandard.value); + this->addSelection(kNoCollInRofStrict, collisionSelectionNames.at(kNoCollInRofStrict), config.noCollInRofStrict.value); + this->addSelection(kNoCollInRofStandard, collisionSelectionNames.at(kNoCollInRofStandard), config.noCollInRofStandard.value); + this->addSelection(kNoHighMultCollInPrevRof, collisionSelectionNames.at(kNoHighMultCollInPrevRof), config.noHighMultCollInPrevRof.value); + this->addSelection(kIsGoodItsLayer3, collisionSelectionNames.at(kIsGoodItsLayer3), config.isGoodItsLayer3.value); + this->addSelection(kIsGoodItsLayer0123, collisionSelectionNames.at(kIsGoodItsLayer0123), config.isGoodItsLayer0123.value); + this->addSelection(kIsGoodItsLayerAll, collisionSelectionNames.at(kIsGoodItsLayerAll), config.isGoodItsLayerAll.value); + this->addSelection(kOccupancyMin, collisionSelectionNames.at(kOccupancyMin), config.occupancyMin.value, limits::kLowerLimit, true, true, false); + this->addSelection(kOccupancyMax, collisionSelectionNames.at(kOccupancyMax), config.occupancyMax.value, limits::kUpperLimit, true, true, false); + this->addSelection(kSphericityMin, collisionSelectionNames.at(kSphericityMin), config.sphericityMin.value, limits::kLowerLimit, true, true, false); + this->addSelection(kSphericityMax, collisionSelectionNames.at(kSphericityMax), config.sphericityMax.value, limits::kUpperLimit, true, true, false); std::vector triggerValues(config.triggers.value.size(), 1.f); - this->addSelection(kTriggers, collisionSelectionNames.at(kTriggers), triggerValues, limits::kEqualArray, false, false, isOptionalCut); + this->addSelection(kTriggers, collisionSelectionNames.at(kTriggers), triggerValues, limits::kEqualArray, false, false, true); this->addComments(kTriggers, config.triggers.value); - this->setupContainers(registry); + this->setupSelectionHistogram(registry); + this->template setupFilterHistogram( + registry, + { + {collisionFilterNames.at(kFilterVtxZMin), mVtxZMin}, + {collisionFilterNames.at(kFilterVtxZMax), mVtxZMax}, + {collisionFilterNames.at(kFilterMultMin), mMultMin}, + {collisionFilterNames.at(kFilterMultMax), mMultMax}, + {collisionFilterNames.at(kFilterCentMin), mCentMin}, + {collisionFilterNames.at(kFilterCentMax), mCentMax}, + {collisionFilterNames.at(kFilterMagFieldMin), mMagFieldMin}, + {collisionFilterNames.at(kFilterMagFieldMax), mMagFieldMax}, + {collisionFilterNames.at(kFilterSphericityMin), mSphericityMin}, + {collisionFilterNames.at(kFilterSphericityMax), mSphericityMax}, + {collisionFilterNames.at(kFilterRctFlags), mUseRctFlags ? 1.f : 0.f}, + }); }; + /// \brief Initialize the Zorro trigger machinery for a new run. No-op if no triggers configured. + template + void initTrigger(int runNumber, uint64_t timestamp, T1& ccdb, T2& histRegistry) + { + if (!mUseTrigger) { + return; + } + LOG(info) << "Init Zorro with Run Number: " << runNumber << "; timestamp: " << timestamp << "; Trigger Names: " << mTriggerNames; + mZorro.initCCDB(ccdb.service, runNumber, timestamp, mTriggerNames); + mZorro.populateHistRegistry(histRegistry, runNumber); + } + + /// \brief Fetch trigger-of-interest decisions for the given global BC. Empty if no triggers configured. + template + std::vector getTriggerDecisions(T globalBC) + { + if (!mUseTrigger) { + return {}; + } + return mZorro.getTriggerOfInterestResults(globalBC); + } + void setMagneticField(int MagField) { mMagField = MagField; @@ -275,25 +345,61 @@ class CollisionSelection : public BaseSelection bool checkFilters(T const& col) const { - if (col.posZ() < mVtxZMin || col.posZ() > mVtxZMax) { - return false; - } - if (mMultiplicity < mMultMin || mMultiplicity > mMultMax) { - return false; - } - if (mCentrality < mCentMin || mCentrality > mCentMax) { - return false; - } - if (mMagField < mMagFieldMin || mMagField > mMagFieldMax) { - return false; - } - if (mSphericity < mSphericityMin || mSphericity > mSphericityMax) { - return false; - } - return true; + bool pass = true; + bool p = false; + + p = col.posZ() >= mVtxZMin; + this->template fillFilter(kFilterVtxZMin, p); + pass &= p; + + p = col.posZ() <= mVtxZMax; + this->template fillFilter(kFilterVtxZMax, p); + pass &= p; + + p = mMultiplicity >= mMultMin; + this->template fillFilter(kFilterMultMin, p); + pass &= p; + + p = mMultiplicity <= mMultMax; + this->template fillFilter(kFilterMultMax, p); + pass &= p; + + p = mCentrality >= mCentMin; + this->template fillFilter(kFilterCentMin, p); + pass &= p; + + p = mCentrality <= mCentMax; + this->template fillFilter(kFilterCentMax, p); + pass &= p; + + p = mMagField >= mMagFieldMin; + this->template fillFilter(kFilterMagFieldMin, p); + pass &= p; + + p = mMagField <= mMagFieldMax; + this->template fillFilter(kFilterMagFieldMax, p); + pass &= p; + + p = mSphericity >= mSphericityMin; + this->template fillFilter(kFilterSphericityMin, p); + pass &= p; + + p = mSphericity <= mSphericityMax; + this->template fillFilter(kFilterSphericityMax, p); + pass &= p; + + p = !mUseRctFlags || mRctFlagsChecker(col); + this->template fillFilter(kFilterRctFlags, p); + pass &= p; + + this->template fillFilterSummary(pass); + + return this->isPassThrough() || pass; } template @@ -314,7 +420,7 @@ class CollisionSelection : public BaseSelectionevaluateObservable(kNoHighMultCollInPrevRof, static_cast(col.selection_bit(o2::aod::evsel::kNoHighMultCollInPrevRof))); this->evaluateObservable(kIsGoodItsLayer3, static_cast(col.selection_bit(o2::aod::evsel::kIsGoodITSLayer3))); this->evaluateObservable(kIsGoodItsLayer0123, static_cast(col.selection_bit(o2::aod::evsel::kIsGoodITSLayer0123))); - this->evaluateObservable(kIsGoodItsLayersAll, static_cast(col.selection_bit(o2::aod::evsel::kIsGoodITSLayersAll))); + this->evaluateObservable(kIsGoodItsLayerAll, static_cast(col.selection_bit(o2::aod::evsel::kIsGoodITSLayersAll))); this->evaluateObservable(kOccupancyMin, col.trackOccupancyInTimeRange()); this->evaluateObservable(kOccupancyMax, col.trackOccupancyInTimeRange()); @@ -328,11 +434,9 @@ class CollisionSelection : public BaseSelectionevaluateObservable(kTriggers, trigger); } - this->assembleBitmask(); + this->assembleBitmask(); }; - [[nodiscard]] bool passThroughAllCollisions() const { return mPassThrough; } - protected: template float computeSphericity(T const& tracks) @@ -384,7 +488,14 @@ class CollisionSelection : public BaseSelection produceQns{"produceQns", -1, "Produce Qn (-1: auto; 0 off; 1 on)"}; }; -template +template class CollisionBuilder { public: @@ -424,22 +535,6 @@ class CollisionBuilder mGrpPath = confCcdb.grpPath.value; mSubGeneratorId = confFilter.subGeneratorId.value; - if (!confBits.triggers.value.empty()) { - mUseTrigger = true; - for (size_t i = 0; i < confBits.triggers.value.size(); ++i) { - mTriggerNames += confBits.triggers.value[i]; - if (i != confBits.triggers.value.size() - 1) { - mTriggerNames += ","; - } - } - mZorro.setBaseCCDBPath(confCcdb.triggerPath.value); - } - if (confRct.useRctFlags.value) { - mUseRctFlags = true; - LOG(info) << "Init RCT flag checker with label: " << confRct.label.value << "; use ZDC: " << confRct.useZdc.value << "; Limimted acceptance is bad: " << confRct.treatLimitedAcceptanceAsBad.value; - mRctFlagsChecker.init(confRct.label.value, confRct.useZdc.value, confRct.treatLimitedAcceptanceAsBad.value); - } - mProducedCollisions = utils::enableTable("FCols_001", confTable.produceCollisions.value, initContext); mProducedCollisionMasks = utils::enableTable("FColMasks_001", confTable.produceCollisionMasks.value, initContext); mProducedPositions = utils::enableTable("FColPos_001", confTable.producePositions.value, initContext); @@ -455,7 +550,7 @@ class CollisionBuilder return; } - mCollisionSelection.configure(registry, confFilter, confBits); + mCollisionSelection.configure(registry, confFilter, confBits, confRct, confCcdb); mCollisionSelection.printSelections(colSelsName); LOG(info) << "Initialization done..."; } @@ -478,11 +573,7 @@ class CollisionBuilder mMagField = mMagFieldForced; } - if (mUseTrigger) { - LOG(info) << "Init Zorro with Run Number: " << mRunNumber << "; timestamp: " << bc.timestamp() << "; Trigger Names: " << mTriggerNames; - mZorro.initCCDB(ccdb.service, mRunNumber, bc.timestamp(), mTriggerNames); - mZorro.populateHistRegistry(histRegistry, mRunNumber); - } + mCollisionSelection.initTrigger(mRunNumber, bc.timestamp(), ccdb, histRegistry); } mCollisionSelection.setMagneticField(mMagField); @@ -490,10 +581,7 @@ class CollisionBuilder mCollisionSelection.template setMultiplicity(col); mCollisionSelection.template setCentrality(col); - std::vector triggerDecisions = {}; - if (mUseTrigger) { - triggerDecisions = mZorro.getTriggerOfInterestResults(bc.globalBC()); - } + std::vector triggerDecisions = mCollisionSelection.getTriggerDecisions(bc.globalBC()); mCollisionSelection.applySelections(col, triggerDecisions); } @@ -501,18 +589,6 @@ class CollisionBuilder template bool checkCollision(T1 const& col) { - - if (mCollisionSelection.passThroughAllCollisions()) { - return true; - } - - // check RCT flags first - if (mUseRctFlags) { - if (!mRctFlagsChecker(col)) { - return false; - } - } - // make other checks return mCollisionSelection.checkFilters(col) && mCollisionSelection.passesAllRequiredSelections(); } @@ -530,11 +606,6 @@ class CollisionBuilder } } - // check RCT flags first - if (mUseRctFlags && !mRctFlagsChecker(col)) { - return false; - } - // make other checks return mCollisionSelection.checkFilters(col) && mCollisionSelection.passesAllRequiredSelections(); } @@ -605,17 +676,12 @@ class CollisionBuilder void reset() { mCollisionAlreadyFilled = false; } private: - CollisionSelection mCollisionSelection; + CollisionSelection mCollisionSelection; bool mCollisionAlreadyFilled = false; - Zorro mZorro; - bool mUseTrigger = false; int mRunNumber = -1; std::string mGrpPath = std::string(""); int mMagFieldForced = 0; int mMagField = 0; - aod::rctsel::RCTFlagsChecker mRctFlagsChecker; - bool mUseRctFlags = false; - std::string mTriggerNames = std::string(""); int mSubGeneratorId = -1; bool mFillAnyTable = false; bool mProducedCollisions = false; @@ -650,5 +716,4 @@ class CollisionBuilderDerivedToDerived }; } // namespace o2::analysis::femto::collisionbuilder -; #endif // PWGCF_FEMTO_CORE_COLLISIONBUILDER_H_ diff --git a/PWGCF/Femto/Core/kinkBuilder.h b/PWGCF/Femto/Core/kinkBuilder.h index f8bc0e310f1..3584726eabf 100644 --- a/PWGCF/Femto/Core/kinkBuilder.h +++ b/PWGCF/Femto/Core/kinkBuilder.h @@ -60,17 +60,18 @@ struct ConfKinkFilters : o2::framework::ConfigurableGroup { // selections bits for all kinks // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -#define KINK_DEFAULT_BITS \ - o2::framework::Configurable> kinkTopoDcaMax{"kinkTopoDcaMax", {2.0f}, "Maximum kink topological DCA"}; \ - o2::framework::Configurable> transRadMin{"transRadMin", {20.f}, "Minimum transverse radius (cm)"}; \ - o2::framework::Configurable> transRadMax{"transRadMax", {100.f}, "Maximum transverse radius (cm)"}; \ - o2::framework::Configurable> dauAbsEtaMax{"dauAbsEtaMax", {1.0f}, "Maximum absolute pseudorapidity for daughter track"}; \ - o2::framework::Configurable> dauDcaPvMin{"dauDcaPvMin", {0.1f}, "Minimum DCA of daughter from primary vertex (cm)"}; \ - o2::framework::Configurable> mothDcaPvMax{"mothDcaPvMax", {1.0f}, "Maximum DCA of mother from primary vertex (cm)"}; \ - o2::framework::Configurable> alphaAPMin{"alphaAPMin", {-1.0f}, "Minimum Alpha_AP for Sigma candidates"}; \ - o2::framework::Configurable> alphaAPMax{"alphaAPMax", {0.0f}, "Maximum Alpha_AP for Sigma candidates"}; \ - o2::framework::Configurable> qtAPMin{"qtAPMin", {0.15f}, "Minimum qT_AP for Sigma candidates"}; \ - o2::framework::Configurable> qtAPMax{"qtAPMax", {0.2f}, "Maximum qT_AP for Sigma candidates"}; \ +#define KINK_DEFAULT_BITS \ + o2::framework::Configurable passThrough{"passThrough", false, "If true, all Kinks are passed through. Bits for all selections are stored."}; \ + o2::framework::Configurable> kinkTopoDcaMax{"kinkTopoDcaMax", {2.0f}, "Maximum kink topological DCA"}; \ + o2::framework::Configurable> transRadMin{"transRadMin", {20.f}, "Minimum transverse radius (cm)"}; \ + o2::framework::Configurable> transRadMax{"transRadMax", {100.f}, "Maximum transverse radius (cm)"}; \ + o2::framework::Configurable> dauAbsEtaMax{"dauAbsEtaMax", {1.0f}, "Maximum absolute pseudorapidity for daughter track"}; \ + o2::framework::Configurable> dauDcaPvMin{"dauDcaPvMin", {0.1f}, "Minimum DCA of daughter from primary vertex (cm)"}; \ + o2::framework::Configurable> mothDcaPvMax{"mothDcaPvMax", {1.0f}, "Maximum DCA of mother from primary vertex (cm)"}; \ + o2::framework::Configurable> alphaAPMin{"alphaAPMin", {-1.0f}, "Minimum Alpha_AP for Sigma candidates"}; \ + o2::framework::Configurable> alphaAPMax{"alphaAPMax", {0.0f}, "Maximum Alpha_AP for Sigma candidates"}; \ + o2::framework::Configurable> qtAPMin{"qtAPMin", {0.15f}, "Minimum qT_AP for Sigma candidates"}; \ + o2::framework::Configurable> qtAPMax{"qtAPMax", {0.2f}, "Maximum qT_AP for Sigma candidates"}; \ o2::framework::Configurable> cosPointingAngleMin{"cosPointingAngleMin", {0.0f}, "Minimum cosine of pointing angle"}; // derived selection bits for sigma @@ -174,10 +175,35 @@ const std::unordered_map kinkSelectionNames = { {kQtAPMax, "qtAPMax"}, {kCosPointingAngleMin, "cosPointingAngleMin"}}; +/// enum for all kink pre-filters (evaluated in checkFilters, before the selection bitmask) +enum KinkFilters { + kPtMin, + kPtMax, + kEtaMin, + kEtaMax, + kPhiMin, + kPhiMax, + kMassMin, + kMassMax, + kKinkFiltersMax +}; + +constexpr char SigmaFilterHistName[] = "hSigmaFilters"; +constexpr char SigmaPlusFilterHistName[] = "hSigmaPlusFilters"; +const std::unordered_map kinkFilterNames = { + {kPtMin, "ptMin"}, + {kPtMax, "ptMax"}, + {kEtaMin, "etaMin"}, + {kEtaMax, "etaMax"}, + {kPhiMin, "phiMin"}, + {kPhiMax, "phiMax"}, + {kMassMin, "massMin"}, + {kMassMax, "massMax"}}; + /// \class KinkCuts /// \brief Cut class to contain and execute all cuts applied to kinks -template -class KinkSelection : public BaseSelection +template +class KinkSelection : public baseselection::BaseSelection { public: KinkSelection() = default; @@ -186,6 +212,8 @@ class KinkSelection : public BaseSelection void configure(o2::framework::HistogramRegistry* registry, T1& config, T2& filter) { + this->init(config.passThrough.value); + mPtMin = filter.ptMin.value; mPtMax = filter.ptMax.value; mEtaMin = filter.etaMin.value; @@ -220,7 +248,36 @@ class KinkSelection : public BaseSelectionaddSelection(kQtAPMax, kinkSelectionNames.at(kQtAPMax), config.qtAPMax.value, limits::kUpperLimit, true, true, false); this->addSelection(kCosPointingAngleMin, kinkSelectionNames.at(kCosPointingAngleMin), config.cosPointingAngleMin.value, limits::kLowerLimit, true, true, false); - this->setupContainers(registry); + this->setupSelectionHistogram(registry); + + if constexpr (modes::isEqual(kinkType, modes::Kink::kSigma)) { + this->template setupFilterHistogram( + registry, + { + {kinkFilterNames.at(kPtMin), mPtMin}, + {kinkFilterNames.at(kPtMax), mPtMax}, + {kinkFilterNames.at(kEtaMin), mEtaMin}, + {kinkFilterNames.at(kEtaMax), mEtaMax}, + {kinkFilterNames.at(kPhiMin), mPhiMin}, + {kinkFilterNames.at(kPhiMax), mPhiMax}, + {kinkFilterNames.at(kMassMin), mMassSigmaLowerLimit}, + {kinkFilterNames.at(kMassMax), mMassSigmaUpperLimit}, + }); + } + if constexpr (modes::isEqual(kinkType, modes::Kink::kSigmaPlus)) { + this->template setupFilterHistogram( + registry, + { + {kinkFilterNames.at(kPtMin), mPtMin}, + {kinkFilterNames.at(kPtMax), mPtMax}, + {kinkFilterNames.at(kEtaMin), mEtaMin}, + {kinkFilterNames.at(kEtaMax), mEtaMax}, + {kinkFilterNames.at(kPhiMin), mPhiMin}, + {kinkFilterNames.at(kPhiMax), mPhiMax}, + {kinkFilterNames.at(kMassMin), mMassSigmaPlusLowerLimit}, + {kinkFilterNames.at(kMassMax), mMassSigmaPlusUpperLimit}, + }); + } }; template @@ -241,7 +298,7 @@ class KinkSelection : public BaseSelection 0.f) ? std::sqrt(std::max(0.f, p2A - dp * dp / p2V0)) : 0.f; std::array vMother = {kinkCand.xDecVtx() - col.posX(), kinkCand.yDecVtx() - col.posY(), kinkCand.zDecVtx() - col.posZ()}; float vMotherNorm = std::sqrt(std::inner_product(vMother.begin(), vMother.end(), vMother.begin(), 0.f)); @@ -299,7 +356,7 @@ class KinkSelection : public BaseSelectionassembleBitmask(); + this->assembleBitmask(); }; template @@ -319,23 +376,60 @@ class KinkSelection : public BaseSelection bool checkFilters(const T& kinkCand) const { - const bool kinematicOk = ((mKinkMotherPt > mPtMin && mKinkMotherPt < mPtMax) && - (mKinkMotherEta > mEtaMin && mKinkMotherEta < mEtaMax) && - (mKinkMotherPhi > mPhiMin && mKinkMotherPhi < mPhiMax)); - if (!kinematicOk) { - return false; - } + bool pass = true; + bool p = false; + + p = mKinkMotherPt > mPtMin; + this->template fillFilter(kPtMin, p); + pass &= p; + + p = mKinkMotherPt < mPtMax; + this->template fillFilter(kPtMax, p); + pass &= p; + + p = mKinkMotherEta > mEtaMin; + this->template fillFilter(kEtaMin, p); + pass &= p; + + p = mKinkMotherEta < mEtaMax; + this->template fillFilter(kEtaMax, p); + pass &= p; + + p = mKinkMotherPhi > mPhiMin; + this->template fillFilter(kPhiMin, p); + pass &= p; + + p = mKinkMotherPhi < mPhiMax; + this->template fillFilter(kPhiMax, p); + pass &= p; if constexpr (modes::isEqual(kinkType, modes::Kink::kSigma)) { float sigmaMass = kinkCand.mSigmaMinus(); - return (sigmaMass > mMassSigmaLowerLimit && sigmaMass < mMassSigmaUpperLimit); + + p = sigmaMass > mMassSigmaLowerLimit; + this->template fillFilter(kMassMin, p); + pass &= p; + + p = sigmaMass < mMassSigmaUpperLimit; + this->template fillFilter(kMassMax, p); + pass &= p; } if constexpr (modes::isEqual(kinkType, modes::Kink::kSigmaPlus)) { float sigmaMass = kinkCand.mSigmaPlus(); - return (sigmaMass > mMassSigmaPlusLowerLimit && sigmaMass < mMassSigmaPlusUpperLimit); + + p = sigmaMass > mMassSigmaPlusLowerLimit; + this->template fillFilter(kMassMin, p); + pass &= p; + + p = sigmaMass < mMassSigmaPlusUpperLimit; + this->template fillFilter(kMassMax, p); + pass &= p; } - return false; + + this->template fillFilterSummary(pass); + + return this->isPassThrough() || pass; } [[nodiscard]] float getKinkMotherPt() const { return mKinkMotherPt; } @@ -393,7 +487,7 @@ struct ConfKinkTables : o2::framework::ConfigurableGroup { o2::framework::Configurable produceSigmaPlusExtras{"produceSigmaPlusExtras", -1, "Produce SigmaPlusExtras (-1: auto; 0 off; 1 on)"}; }; -template +template class KinkBuilder { public: @@ -561,7 +655,7 @@ class KinkBuilder bool fillAnyTable() { return mFillAnyTable; } private: - KinkSelection mKinkSelection; + KinkSelection mKinkSelection; bool mFillAnyTable = false; bool mProduceSigmas = false; bool mProduceSigmaMasks = false; diff --git a/PWGCF/Femto/Core/selectionContainer.h b/PWGCF/Femto/Core/selectionContainer.h index 4e9e1718e16..86792d6d705 100644 --- a/PWGCF/Femto/Core/selectionContainer.h +++ b/PWGCF/Femto/Core/selectionContainer.h @@ -70,6 +70,15 @@ inline const std::unordered_map limitTypeAsStrings = { }; // namespace limits +namespace selectioncontainer +{ + +// strings for selection/filter histogram +constexpr std::string_view SectionDelimiter = ":::"; +constexpr std::string_view RangeDelimiter = ";"; +constexpr std::string_view ValueDelimiter = "___"; +constexpr std::string_view NoValue = "X"; + /// \class SelectionContainer /// \brief Stores and evaluates multiple selection thresholds for a single observable. /// @@ -444,9 +453,6 @@ class SelectionContainer [[nodiscard]] std::string getBinLabel(int selectionIndex) const { std::ostringstream oss; - std::string sectionDelimiter = ":::"; - std::string valueDelimiter = "___"; - std::string noValue = "X"; // Determine value string std::string valueStr; @@ -455,7 +461,7 @@ class SelectionContainer // Print actual lower;upper interval const auto& range = mSelectionRanges.at(selectionIndex); std::ostringstream rangeStream; - rangeStream << range.first << ";" << range.second; + rangeStream << range.first << RangeDelimiter << range.second; valueStr = rangeStream.str(); } else if (mSelectionFunctions.empty()) { valueStr = std::to_string(mSelectionValues.at(selectionIndex)); @@ -463,16 +469,16 @@ class SelectionContainer valueStr = mSelectionFunctions.at(selectionIndex).GetExpFormula().Data(); } - oss << "SelectionName" << valueDelimiter << mSelectionName << sectionDelimiter - << "LimitType" << valueDelimiter << getLimitTypeAsString() << sectionDelimiter - << "MinimalCut" << valueDelimiter << (mIsMinimalCut ? "1" : "0") << sectionDelimiter - << "SkipMostPermissiveBit" << valueDelimiter << (mSkipMostPermissiveBit ? "1" : "0") << sectionDelimiter - << "OptionalCut" << valueDelimiter << (mIsOptionalCut ? "1" : "0") << sectionDelimiter - << "Shift" << valueDelimiter << getShift() << sectionDelimiter - << "Offset" << valueDelimiter << mOffset << sectionDelimiter - << "Value" << valueDelimiter << valueStr << sectionDelimiter - << "BitPosition" << valueDelimiter << (mSkipMostPermissiveBit ? (selectionIndex == 0 ? noValue : std::to_string(mOffset + selectionIndex - 1)) : std::to_string(mOffset + selectionIndex)) << sectionDelimiter - << "Comment" << valueDelimiter << (mComments.empty() ? noValue : mComments.at(selectionIndex)); + oss << "SelectionName" << ValueDelimiter << mSelectionName << SectionDelimiter + << "LimitType" << ValueDelimiter << getLimitTypeAsString() << SectionDelimiter + << "MinimalCut" << ValueDelimiter << (mIsMinimalCut ? "1" : "0") << SectionDelimiter + << "SkipMostPermissiveBit" << ValueDelimiter << (mSkipMostPermissiveBit ? "1" : "0") << SectionDelimiter + << "OptionalCut" << ValueDelimiter << (mIsOptionalCut ? "1" : "0") << SectionDelimiter + << "Shift" << ValueDelimiter << getShift() << SectionDelimiter + << "Offset" << ValueDelimiter << mOffset << SectionDelimiter + << "Value" << ValueDelimiter << valueStr << SectionDelimiter + << "BitPosition" << ValueDelimiter << (mSkipMostPermissiveBit ? (selectionIndex == 0 ? NoValue : std::to_string(mOffset + selectionIndex - 1)) : std::to_string(mOffset + selectionIndex)) << SectionDelimiter + << "Comment" << ValueDelimiter << (mComments.empty() ? NoValue : mComments.at(selectionIndex)); return oss.str(); } @@ -610,6 +616,7 @@ class SelectionContainer std::bitset mBitmask = {}; ///< Bitmask indicating which thresholds were passed during the last evaluation int mOffset = 0; ///< Bit offset of this container within the global bitmask }; +}; // namespace selectioncontainer } // namespace o2::analysis::femto diff --git a/PWGCF/Femto/Core/trackBuilder.h b/PWGCF/Femto/Core/trackBuilder.h index 7a8d4147a93..afdbe46521a 100644 --- a/PWGCF/Femto/Core/trackBuilder.h +++ b/PWGCF/Femto/Core/trackBuilder.h @@ -54,7 +54,7 @@ struct ConfTrackBits : o2::framework::ConfigurableGroup { o2::framework::Configurable passThrough{"passThrough", false, "If true, all tracks are passed through. Bits for all selections are stored."}; o2::framework::Configurable> tpcClustersMin{"tpcClustersMin", {90.f}, "Minimum number of clusters in TPC"}; o2::framework::Configurable> tpcCrossedRowsMin{"tpcCrossedRowsMin", {80.f}, "Minimum number of crossed rows in TPC"}; - o2::framework::Configurable> tpcClustersOverCrossedRows{"tpcClustersOverCrossedRows", {0.83f}, "Minimum fraction of clusters over crossed rows in TPC"}; + o2::framework::Configurable> tpcClustersOverCrossedRows{"tpcClustersOverCrossedRows", {0.0f}, "Minimum fraction of clusters over crossed rows in TPC"}; o2::framework::Configurable> tpcSharedClustersMax{"tpcSharedClustersMax", {160.f}, "Maximum number of shared clusters in TPC"}; o2::framework::Configurable> tpcSharedClusterFractionMax{"tpcSharedClusterFractionMax", {1.f}, "Maximum fraction of shared clusters in TPC"}; o2::framework::Configurable> itsClustersMin{"itsClustersMin", {5.f}, "Minimum number of clusters in ITS"}; @@ -135,7 +135,7 @@ struct ConfTrackSelection : public o2::framework::ConfigurableGroup { o2::framework::Configurable chargeAbs{"chargeAbs", 1, "Absolute value of charge (e.g. 1 for most tracks, 2 for He3). Set sign of charge to -1 for antiparticle"}; o2::framework::Configurable chargeSign{"chargeSign", 1, "Track charge sign: +1 for positive, -1 for negative, 0 for both"}; // filters for kinematics - o2::framework::Configurable ptMin{"ptMin", 0.2f, "Minimum pT (GeV/c)"}; + o2::framework::Configurable ptMin{"ptMin", 0.0f, "Minimum pT (GeV/c)"}; o2::framework::Configurable ptMax{"ptMax", 6.f, "Maximum pT (GeV/c)"}; o2::framework::Configurable etaMin{"etaMin", -0.9f, "Minimum eta"}; o2::framework::Configurable etaMax{"etaMax", 0.9f, "Maximum eta"}; @@ -144,11 +144,11 @@ struct ConfTrackSelection : public o2::framework::ConfigurableGroup { o2::framework::Configurable massMin{"massMin", 0.f, "Minimum TOF mass (only used if enabled)"}; o2::framework::Configurable massMax{"massMax", 99.f, "Maximum TOF mass (only used if enabled)"}; // track selection masks - o2::framework::Configurable maskLowMomentum{"maskLowMomentum", 1ul, "Bitmask for selections below momentum threshold"}; - o2::framework::Configurable maskHighMomentum{"maskHighMomentum", 2ul, "Bitmask for selections above momentum threshold"}; + o2::framework::Configurable maskLowMomentum{"maskLowMomentum", 1ul, "Bitmask for selections below momentum threshold"}; + o2::framework::Configurable maskHighMomentum{"maskHighMomentum", 2ul, "Bitmask for selections above momentum threshold"}; // track rejection masks - o2::framework::Configurable rejectionMaskLowMomentum{"rejectionMaskLowMomentum", 0ul, "Bitmask for rejections below momentum threshold"}; - o2::framework::Configurable rejectionMaskHighMomentum{"rejectionMaskHighMomentum", 0ul, "Bitmask for rejections above momentum threshold"}; + o2::framework::Configurable rejectionMaskLowMomentum{"rejectionMaskLowMomentum", 0ul, "Bitmask for rejections below momentum threshold"}; + o2::framework::Configurable rejectionMaskHighMomentum{"rejectionMaskHighMomentum", 0ul, "Bitmask for rejections above momentum threshold"}; // momentum threshold for PID usage o2::framework::Configurable pidThres{"pidThres", 1.2f, "Momentum threshold for using TPCTOF/TOF pid for tracks with large momentum (GeV/c)"}; }; @@ -285,10 +285,30 @@ const std::unordered_map trackSelectionNames = { {kTpctofTriton, "TPC+TOF Triton PID"}, {kTpctofHelium, "TPC+TOF He3 PID"}}; -/// \class FemtoDreamTrackCuts +// enum for all track filters (loose pre-selection, applied before quality/PID cuts) +enum TrackFilters { + kPtMin, + kPtMax, + kEtaMin, + kEtaMax, + kPhiMin, + kPhiMax, + kTrackFiltersMax +}; + +constexpr char TrackFilterHistName[] = "hTrackFilters"; +const std::unordered_map trackFilterNames = { + {kPtMin, "ptMin"}, + {kPtMax, "ptMax"}, + {kEtaMin, "etaMin"}, + {kEtaMax, "etaMax"}, + {kPhiMin, "phiMin"}, + {kPhiMax, "phiMax"}}; + +/// \class TrackSelecion /// \brief Cut class to contain and execute all cuts applied to tracks -template -class TrackSelection : public BaseSelection +template +class TrackSelection : public baseselection::BaseSelection { public: TrackSelection() = default; @@ -297,6 +317,8 @@ class TrackSelection : public BaseSelection void configure(o2::framework::HistogramRegistry* registry, T1& config, T2& filter) { + this->init(config.passThrough.value); + // filters mPtMin = filter.ptMin.value; mPtMax = filter.ptMax.value; @@ -305,90 +327,117 @@ class TrackSelection : public BaseSelectionaddSelection(kTPCnClsMin, trackSelectionNames.at(kTPCnClsMin), config.tpcClustersMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kTPCcRowsMin, trackSelectionNames.at(kTPCcRowsMin), config.tpcCrossedRowsMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kTPCnClsOvercRowsMin, trackSelectionNames.at(kTPCnClsOvercRowsMin), config.tpcClustersOverCrossedRows.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kTPCsClsMax, trackSelectionNames.at(kTPCsClsMax), config.tpcSharedClustersMax.value, limits::kUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kTPCsClsFracMax, trackSelectionNames.at(kTPCsClsFracMax), config.tpcSharedClusterFractionMax.value, limits::kUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kITSnClsMin, trackSelectionNames.at(kITSnClsMin), config.itsClustersMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kITSnClsIbMin, trackSelectionNames.at(kITSnClsIbMin), config.itsIbClustersMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kDCAxyMax, trackSelectionNames.at(kDCAxyMax), filter.ptMin.value, filter.ptMax.value, config.dcaxyMax.value, limits::kAbsUpperFunctionLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kDCAzMax, trackSelectionNames.at(kDCAzMax), filter.ptMin.value, filter.ptMax.value, config.dcazMax.value, limits::kAbsUpperFunctionLimit, skipMostPermissiveBit, isMinimalCut, false); + this->addSelection(kTPCnClsMin, trackSelectionNames.at(kTPCnClsMin), config.tpcClustersMin.value, limits::kLowerLimit, true, true, false); + this->addSelection(kTPCcRowsMin, trackSelectionNames.at(kTPCcRowsMin), config.tpcCrossedRowsMin.value, limits::kLowerLimit, true, true, false); + this->addSelection(kTPCnClsOvercRowsMin, trackSelectionNames.at(kTPCnClsOvercRowsMin), config.tpcClustersOverCrossedRows.value, limits::kLowerLimit, true, true, false); + this->addSelection(kTPCsClsMax, trackSelectionNames.at(kTPCsClsMax), config.tpcSharedClustersMax.value, limits::kUpperLimit, true, true, false); + this->addSelection(kTPCsClsFracMax, trackSelectionNames.at(kTPCsClsFracMax), config.tpcSharedClusterFractionMax.value, limits::kUpperLimit, true, true, false); + this->addSelection(kITSnClsMin, trackSelectionNames.at(kITSnClsMin), config.itsClustersMin.value, limits::kLowerLimit, true, true, false); + this->addSelection(kITSnClsIbMin, trackSelectionNames.at(kITSnClsIbMin), config.itsIbClustersMin.value, limits::kLowerLimit, true, true, false); + this->addSelection(kDCAxyMax, trackSelectionNames.at(kDCAxyMax), filter.ptMin.value, filter.ptMax.value, config.dcaxyMax.value, limits::kAbsUpperFunctionLimit, true, true, false); + this->addSelection(kDCAzMax, trackSelectionNames.at(kDCAzMax), filter.ptMin.value, filter.ptMax.value, config.dcazMax.value, limits::kAbsUpperFunctionLimit, true, true, false); // add selections for Electron pid - this->addSelection(kItsElectron, trackSelectionNames.at(kItsElectron), config.itsElectron.value, false, false, isOptionalCut && config.requirePidElectron); - this->addSelection(kTpcElectron, trackSelectionNames.at(kTpcElectron), config.tpcElectron.value, false, false, isOptionalCut && config.requirePidElectron); - this->addSelection(kTofElectron, trackSelectionNames.at(kTofElectron), config.tofElectron.value, false, false, isOptionalCut && config.requirePidElectron); - this->addSelection(kTpcitsElectron, trackSelectionNames.at(kTpcitsElectron), config.tpcitsElectron.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidElectron); - this->addSelection(kTpctofElectron, trackSelectionNames.at(kTpctofElectron), config.tpctofElectron.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidElectron); + this->addSelection(kItsElectron, trackSelectionNames.at(kItsElectron), config.itsElectron.value, false, false, config.requirePidElectron); + this->addSelection(kTpcElectron, trackSelectionNames.at(kTpcElectron), config.tpcElectron.value, false, false, config.requirePidElectron); + this->addSelection(kTofElectron, trackSelectionNames.at(kTofElectron), config.tofElectron.value, false, false, config.requirePidElectron); + this->addSelection(kTpcitsElectron, trackSelectionNames.at(kTpcitsElectron), config.tpcitsElectron.value, limits::kUpperLimit, false, false, config.requirePidElectron); + this->addSelection(kTpctofElectron, trackSelectionNames.at(kTpctofElectron), config.tpctofElectron.value, limits::kUpperLimit, false, false, config.requirePidElectron); mElectronTofThres = config.minMomTofElectron.value; // add selections for Pion pid - this->addSelection(kItsPion, trackSelectionNames.at(kItsPion), config.itsPion.value, false, false, isOptionalCut && config.requirePidPion); - this->addSelection(kTpcPion, trackSelectionNames.at(kTpcPion), config.tpcPion.value, false, false, isOptionalCut && config.requirePidPion); - this->addSelection(kTofPion, trackSelectionNames.at(kTofPion), config.tofPion.value, false, false, isOptionalCut && config.requirePidPion); - this->addSelection(kTpcitsPion, trackSelectionNames.at(kTpcitsPion), config.tpcitsPion.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidPion); - this->addSelection(kTpctofPion, trackSelectionNames.at(kTpctofPion), config.tpctofPion.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidPion); + this->addSelection(kItsPion, trackSelectionNames.at(kItsPion), config.itsPion.value, false, false, config.requirePidPion); + this->addSelection(kTpcPion, trackSelectionNames.at(kTpcPion), config.tpcPion.value, false, false, config.requirePidPion); + this->addSelection(kTofPion, trackSelectionNames.at(kTofPion), config.tofPion.value, false, false, config.requirePidPion); + this->addSelection(kTpcitsPion, trackSelectionNames.at(kTpcitsPion), config.tpcitsPion.value, limits::kUpperLimit, false, false, config.requirePidPion); + this->addSelection(kTpctofPion, trackSelectionNames.at(kTpctofPion), config.tpctofPion.value, limits::kUpperLimit, false, false, config.requirePidPion); mPionTofThres = config.minMomTofPion.value; // add selections for Kaon pid - this->addSelection(kItsKaon, trackSelectionNames.at(kItsKaon), config.itsKaon.value, false, false, isOptionalCut && config.requirePidKaon); - this->addSelection(kTpcKaon, trackSelectionNames.at(kTpcKaon), config.tpcKaon.value, false, false, isOptionalCut && config.requirePidKaon); - this->addSelection(kTofKaon, trackSelectionNames.at(kTofKaon), config.tofKaon.value, false, false, isOptionalCut && config.requirePidKaon); - this->addSelection(kTpcitsKaon, trackSelectionNames.at(kTpcitsKaon), config.tpcitsKaon.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidKaon); - this->addSelection(kTpctofKaon, trackSelectionNames.at(kTpctofKaon), config.tpctofKaon.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidKaon); + this->addSelection(kItsKaon, trackSelectionNames.at(kItsKaon), config.itsKaon.value, false, false, config.requirePidKaon); + this->addSelection(kTpcKaon, trackSelectionNames.at(kTpcKaon), config.tpcKaon.value, false, false, config.requirePidKaon); + this->addSelection(kTofKaon, trackSelectionNames.at(kTofKaon), config.tofKaon.value, false, false, config.requirePidKaon); + this->addSelection(kTpcitsKaon, trackSelectionNames.at(kTpcitsKaon), config.tpcitsKaon.value, limits::kUpperLimit, false, false, config.requirePidKaon); + this->addSelection(kTpctofKaon, trackSelectionNames.at(kTpctofKaon), config.tpctofKaon.value, limits::kUpperLimit, false, false, config.requirePidKaon); mKaonTofThres = config.minMomTofKaon.value; // add selections for Proton pid - this->addSelection(kItsProton, trackSelectionNames.at(kItsProton), config.itsProton.value, false, false, isOptionalCut && config.requirePidProton); - this->addSelection(kTpcProton, trackSelectionNames.at(kTpcProton), config.tpcProton.value, false, false, isOptionalCut && config.requirePidProton); - this->addSelection(kTofProton, trackSelectionNames.at(kTofProton), config.tofProton.value, false, false, isOptionalCut && config.requirePidProton); - this->addSelection(kTpcitsProton, trackSelectionNames.at(kTpcitsProton), config.tpcitsProton.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidProton); - this->addSelection(kTpctofProton, trackSelectionNames.at(kTpctofProton), config.tpctofProton.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidProton); + this->addSelection(kItsProton, trackSelectionNames.at(kItsProton), config.itsProton.value, false, false, config.requirePidProton); + this->addSelection(kTpcProton, trackSelectionNames.at(kTpcProton), config.tpcProton.value, false, false, config.requirePidProton); + this->addSelection(kTofProton, trackSelectionNames.at(kTofProton), config.tofProton.value, false, false, config.requirePidProton); + this->addSelection(kTpcitsProton, trackSelectionNames.at(kTpcitsProton), config.tpcitsProton.value, limits::kUpperLimit, false, false, config.requirePidProton); + this->addSelection(kTpctofProton, trackSelectionNames.at(kTpctofProton), config.tpctofProton.value, limits::kUpperLimit, false, false, config.requirePidProton); mProtonTofThres = config.minMomTofProton.value; // add selections for Deuteron pid - this->addSelection(kItsDeuteron, trackSelectionNames.at(kItsDeuteron), config.itsDeuteron.value, false, false, isOptionalCut && config.requirePidDeuteron); - this->addSelection(kTpcDeuteron, trackSelectionNames.at(kTpcDeuteron), config.tpcDeuteron.value, false, false, isOptionalCut && config.requirePidDeuteron); - this->addSelection(kTofDeuteron, trackSelectionNames.at(kTofDeuteron), config.tofDeuteron.value, false, false, isOptionalCut && config.requirePidDeuteron); - this->addSelection(kTpcitsDeuteron, trackSelectionNames.at(kTpcitsDeuteron), config.tpcitsDeuteron.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidDeuteron); - this->addSelection(kTpctofDeuteron, trackSelectionNames.at(kTpctofDeuteron), config.tpctofDeuteron.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidDeuteron); + this->addSelection(kItsDeuteron, trackSelectionNames.at(kItsDeuteron), config.itsDeuteron.value, false, false, config.requirePidDeuteron); + this->addSelection(kTpcDeuteron, trackSelectionNames.at(kTpcDeuteron), config.tpcDeuteron.value, false, false, config.requirePidDeuteron); + this->addSelection(kTofDeuteron, trackSelectionNames.at(kTofDeuteron), config.tofDeuteron.value, false, false, config.requirePidDeuteron); + this->addSelection(kTpcitsDeuteron, trackSelectionNames.at(kTpcitsDeuteron), config.tpcitsDeuteron.value, limits::kUpperLimit, false, false, config.requirePidDeuteron); + this->addSelection(kTpctofDeuteron, trackSelectionNames.at(kTpctofDeuteron), config.tpctofDeuteron.value, limits::kUpperLimit, false, false, config.requirePidDeuteron); mDeuteronTofThres = config.minMomTofDeuteron.value; // add selections for Triton pid - this->addSelection(kItsTriton, trackSelectionNames.at(kItsTriton), config.itsTriton.value, false, false, isOptionalCut && config.requirePidTriton); - this->addSelection(kTpcTriton, trackSelectionNames.at(kTpcTriton), config.tpcTriton.value, false, false, isOptionalCut && config.requirePidTriton); - this->addSelection(kTofTriton, trackSelectionNames.at(kTofTriton), config.tofTriton.value, false, false, isOptionalCut && config.requirePidTriton); - this->addSelection(kTpcitsTriton, trackSelectionNames.at(kTpcitsTriton), config.tpcitsTriton.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidTriton); - this->addSelection(kTpctofTriton, trackSelectionNames.at(kTpctofTriton), config.tpctofTriton.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidTriton); + this->addSelection(kItsTriton, trackSelectionNames.at(kItsTriton), config.itsTriton.value, false, false, config.requirePidTriton); + this->addSelection(kTpcTriton, trackSelectionNames.at(kTpcTriton), config.tpcTriton.value, false, false, config.requirePidTriton); + this->addSelection(kTofTriton, trackSelectionNames.at(kTofTriton), config.tofTriton.value, false, false, config.requirePidTriton); + this->addSelection(kTpcitsTriton, trackSelectionNames.at(kTpcitsTriton), config.tpcitsTriton.value, limits::kUpperLimit, false, false, config.requirePidTriton); + this->addSelection(kTpctofTriton, trackSelectionNames.at(kTpctofTriton), config.tpctofTriton.value, limits::kUpperLimit, false, false, config.requirePidTriton); mTritonTofThres = config.minMomTofTriton.value; // add selections for Helium pid - this->addSelection(kItsHelium, trackSelectionNames.at(kItsHelium), config.itsHelium.value, false, false, isOptionalCut && config.requirePidHelium); - this->addSelection(kTpcHelium, trackSelectionNames.at(kTpcHelium), config.tpcHelium.value, false, false, isOptionalCut && config.requirePidHelium); - this->addSelection(kTofHelium, trackSelectionNames.at(kTofHelium), config.tofHelium.value, false, false, isOptionalCut && config.requirePidHelium); - this->addSelection(kTpcitsHelium, trackSelectionNames.at(kTpcitsHelium), config.tpcitsHelium.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidHelium); - this->addSelection(kTpctofHelium, trackSelectionNames.at(kTpctofHelium), config.tpctofHelium.value, limits::kUpperLimit, false, false, isOptionalCut && config.requirePidHelium); + this->addSelection(kItsHelium, trackSelectionNames.at(kItsHelium), config.itsHelium.value, false, false, config.requirePidHelium); + this->addSelection(kTpcHelium, trackSelectionNames.at(kTpcHelium), config.tpcHelium.value, false, false, config.requirePidHelium); + this->addSelection(kTofHelium, trackSelectionNames.at(kTofHelium), config.tofHelium.value, false, false, config.requirePidHelium); + this->addSelection(kTpcitsHelium, trackSelectionNames.at(kTpcitsHelium), config.tpcitsHelium.value, limits::kUpperLimit, false, false, config.requirePidHelium); + this->addSelection(kTpctofHelium, trackSelectionNames.at(kTpctofHelium), config.tpctofHelium.value, limits::kUpperLimit, false, false, config.requirePidHelium); mHeliumTofThres = config.minMomTofHelium.value; - this->setupContainers(registry); + this->setupSelectionHistogram(registry); + this->template setupFilterHistogram( + registry, + {{trackFilterNames.at(kPtMin), mPtMin}, + {trackFilterNames.at(kPtMax), mPtMax}, + {trackFilterNames.at(kEtaMin), mEtaMin}, + {trackFilterNames.at(kEtaMax), mEtaMax}, + {trackFilterNames.at(kPhiMin), mPhiMin}, + {trackFilterNames.at(kPhiMax), mPhiMax}}); } template bool checkFilters(T const& track) const { - return ((track.pt() > mPtMin && track.pt() < mPtMax) && - (track.eta() > mEtaMin && track.eta() < mEtaMax) && - (track.phi() > mPhiMin && track.phi() < mPhiMax)); + bool pass = true; + bool p = false; + + p = track.pt() > mPtMin; + this->template fillFilter(kPtMin, p); + pass &= p; + + p = track.pt() < mPtMax; + this->template fillFilter(kPtMax, p); + pass &= p; + + p = track.eta() > mEtaMin; + this->template fillFilter(kEtaMin, p); + pass &= p; + + p = track.eta() < mEtaMax; + this->template fillFilter(kEtaMax, p); + pass &= p; + + p = track.phi() > mPhiMin; + this->template fillFilter(kPhiMin, p); + pass &= p; + + p = track.phi() < mPhiMax; + this->template fillFilter(kPhiMax, p); + pass &= p; + + this->template fillFilterSummary(pass); + + return this->isPassThrough() || pass; } template @@ -434,8 +483,20 @@ class TrackSelection : public BaseSelectionupdateLimits(kDCAzMax, Track.pt()); this->evaluateObservable(kDCAzMax, Track.dcaZ()); - if (mPassThrough) { - // if pass through is activated, we evaluate each particle hypothesis regradless of threshold + // first pass: threshold-aware PID evaluation + // determines if the track passes any optional selection and if should be stored in the first place + this->evaluatePid(Track, mElectronTofThres, Track.itsNSigmaEl(), Track.tpcNSigmaEl(), Track.tofNSigmaEl(), kItsElectron, kTpcElectron, kTofElectron, kTpcitsElectron, kTpctofElectron); + this->evaluatePid(Track, mPionTofThres, Track.itsNSigmaPi(), Track.tpcNSigmaPi(), Track.tofNSigmaPi(), kItsPion, kTpcPion, kTofPion, kTpcitsPion, kTpctofPion); + this->evaluatePid(Track, mKaonTofThres, Track.itsNSigmaKa(), Track.tpcNSigmaKa(), Track.tofNSigmaKa(), kItsKaon, kTpcKaon, kTofKaon, kTpcitsKaon, kTpctofKaon); + this->evaluatePid(Track, mProtonTofThres, Track.itsNSigmaPr(), Track.tpcNSigmaPr(), Track.tofNSigmaPr(), kItsProton, kTpcProton, kTofProton, kTpcitsProton, kTpctofProton); + this->evaluatePid(Track, mDeuteronTofThres, Track.itsNSigmaDe(), Track.tpcNSigmaDe(), Track.tofNSigmaDe(), kItsDeuteron, kTpcDeuteron, kTofDeuteron, kTpcitsDeuteron, kTpctofDeuteron); + this->evaluatePid(Track, mTritonTofThres, Track.itsNSigmaTr(), Track.tpcNSigmaTr(), Track.tofNSigmaTr(), kItsTriton, kTpcTriton, kTofTriton, kTpcitsTriton, kTpctofTriton); + this->evaluatePid(Track, mHeliumTofThres, Track.itsNSigmaHe(), Track.tpcNSigmaHe(), Track.tofNSigmaHe(), kItsHelium, kTpcHelium, kTofHelium, kTpcitsHelium, kTpctofHelium); + + // second pass: if the track passes minimal and any optional selection, + // re-evaluate all species ignoring thresholds so rejection bits are fully + // populated for all competing hypotheses + if (this->passesAllRequiredSelections()) { this->evaluatePid(Track, mElectronTofThres, Track.itsNSigmaEl(), Track.tpcNSigmaEl(), Track.tofNSigmaEl(), kItsElectron, kTpcElectron, kTofElectron, kTpcitsElectron, kTpctofElectron, true); this->evaluatePid(Track, mPionTofThres, Track.itsNSigmaPi(), Track.tpcNSigmaPi(), Track.tofNSigmaPi(), kItsPion, kTpcPion, kTofPion, kTpcitsPion, kTpctofPion, true); this->evaluatePid(Track, mKaonTofThres, Track.itsNSigmaKa(), Track.tpcNSigmaKa(), Track.tofNSigmaKa(), kItsKaon, kTpcKaon, kTofKaon, kTpcitsKaon, kTpctofKaon, true); @@ -443,36 +504,11 @@ class TrackSelection : public BaseSelectionevaluatePid(Track, mDeuteronTofThres, Track.itsNSigmaDe(), Track.tpcNSigmaDe(), Track.tofNSigmaDe(), kItsDeuteron, kTpcDeuteron, kTofDeuteron, kTpcitsDeuteron, kTpctofDeuteron, true); this->evaluatePid(Track, mTritonTofThres, Track.itsNSigmaTr(), Track.tpcNSigmaTr(), Track.tofNSigmaTr(), kItsTriton, kTpcTriton, kTofTriton, kTpcitsTriton, kTpctofTriton, true); this->evaluatePid(Track, mHeliumTofThres, Track.itsNSigmaHe(), Track.tpcNSigmaHe(), Track.tofNSigmaHe(), kItsHelium, kTpcHelium, kTofHelium, kTpcitsHelium, kTpctofHelium, true); - } else { - // first pass: threshold-aware PID evaluation - // determines if the track passes any optional selection and if should be stored in the first place - this->evaluatePid(Track, mElectronTofThres, Track.itsNSigmaEl(), Track.tpcNSigmaEl(), Track.tofNSigmaEl(), kItsElectron, kTpcElectron, kTofElectron, kTpcitsElectron, kTpctofElectron); - this->evaluatePid(Track, mPionTofThres, Track.itsNSigmaPi(), Track.tpcNSigmaPi(), Track.tofNSigmaPi(), kItsPion, kTpcPion, kTofPion, kTpcitsPion, kTpctofPion); - this->evaluatePid(Track, mKaonTofThres, Track.itsNSigmaKa(), Track.tpcNSigmaKa(), Track.tofNSigmaKa(), kItsKaon, kTpcKaon, kTofKaon, kTpcitsKaon, kTpctofKaon); - this->evaluatePid(Track, mProtonTofThres, Track.itsNSigmaPr(), Track.tpcNSigmaPr(), Track.tofNSigmaPr(), kItsProton, kTpcProton, kTofProton, kTpcitsProton, kTpctofProton); - this->evaluatePid(Track, mDeuteronTofThres, Track.itsNSigmaDe(), Track.tpcNSigmaDe(), Track.tofNSigmaDe(), kItsDeuteron, kTpcDeuteron, kTofDeuteron, kTpcitsDeuteron, kTpctofDeuteron); - this->evaluatePid(Track, mTritonTofThres, Track.itsNSigmaTr(), Track.tpcNSigmaTr(), Track.tofNSigmaTr(), kItsTriton, kTpcTriton, kTofTriton, kTpcitsTriton, kTpctofTriton); - this->evaluatePid(Track, mHeliumTofThres, Track.itsNSigmaHe(), Track.tpcNSigmaHe(), Track.tofNSigmaHe(), kItsHelium, kTpcHelium, kTofHelium, kTpcitsHelium, kTpctofHelium); - - // second pass: if the track passes minimal and any optional selection, - // re-evaluate all species ignoring thresholds so rejection bits are fully - // populated for all competing hypotheses - if (this->passesAllRequiredSelections()) { - this->evaluatePid(Track, mElectronTofThres, Track.itsNSigmaEl(), Track.tpcNSigmaEl(), Track.tofNSigmaEl(), kItsElectron, kTpcElectron, kTofElectron, kTpcitsElectron, kTpctofElectron, true); - this->evaluatePid(Track, mPionTofThres, Track.itsNSigmaPi(), Track.tpcNSigmaPi(), Track.tofNSigmaPi(), kItsPion, kTpcPion, kTofPion, kTpcitsPion, kTpctofPion, true); - this->evaluatePid(Track, mKaonTofThres, Track.itsNSigmaKa(), Track.tpcNSigmaKa(), Track.tofNSigmaKa(), kItsKaon, kTpcKaon, kTofKaon, kTpcitsKaon, kTpctofKaon, true); - this->evaluatePid(Track, mProtonTofThres, Track.itsNSigmaPr(), Track.tpcNSigmaPr(), Track.tofNSigmaPr(), kItsProton, kTpcProton, kTofProton, kTpcitsProton, kTpctofProton, true); - this->evaluatePid(Track, mDeuteronTofThres, Track.itsNSigmaDe(), Track.tpcNSigmaDe(), Track.tofNSigmaDe(), kItsDeuteron, kTpcDeuteron, kTofDeuteron, kTpcitsDeuteron, kTpctofDeuteron, true); - this->evaluatePid(Track, mTritonTofThres, Track.itsNSigmaTr(), Track.tpcNSigmaTr(), Track.tofNSigmaTr(), kItsTriton, kTpcTriton, kTofTriton, kTpcitsTriton, kTpctofTriton, true); - this->evaluatePid(Track, mHeliumTofThres, Track.itsNSigmaHe(), Track.tpcNSigmaHe(), Track.tofNSigmaHe(), kItsHelium, kTpcHelium, kTofHelium, kTpcitsHelium, kTpctofHelium, true); - } } this->assembleBitmask(); } - [[nodiscard]] bool passThroughAllTracks() const { return mPassThrough; } - protected: float mElectronTofThres = 99.f; float mPionTofThres = 99.f; @@ -488,8 +524,6 @@ class TrackSelection : public BaseSelection produceHeliumPids{"produceHeliumPids", -1, "Produce HeliumPids (-1: auto; 0 off; 1 on)"}; }; -template +template class TrackBuilder { public: @@ -567,11 +601,11 @@ class TrackBuilder return; } for (const auto& track : tracks) { - if (!mTrackSelection.passThroughAllTracks() && !mTrackSelection.checkFilters(track)) { + if (!mTrackSelection.checkFilters(track)) { continue; } mTrackSelection.applySelections(track); - if (!mTrackSelection.passThroughAllTracks() && !mTrackSelection.passesAllRequiredSelections()) { + if (!mTrackSelection.passesAllRequiredSelections()) { continue; } @@ -597,7 +631,7 @@ class TrackBuilder if constexpr (type == modes::Track::kTrack) { trackProducts.producedTrackMasks(mTrackSelection.getBitmask()); } else { - trackProducts.producedTrackMasks(static_cast(0u)); + trackProducts.producedTrackMasks(static_cast(0u)); } } if (mProduceTrackMass) { @@ -678,7 +712,6 @@ class TrackBuilder return; } for (const auto& trackWithItsPid : tracksWithItsPid) { - // NOTE: passThrough is intentionally not wired in here yet for the MC path (to be added later). if (!mTrackSelection.checkFilters(trackWithItsPid)) { continue; } @@ -696,7 +729,7 @@ class TrackBuilder template bool fillMcTrack(T1 const& col, T2& collisionProducts, T3 const& mcCols, T4 const& track, T5 const& trackWithItsPid, T6& trackProducts, T7 const& mcParticles, T8& mcBuilder, T9& mcProducts) { - // NOTE: return value added, mirroring fillTrack(), so getDaughterIndex can detect + // return value added, mirroring fillTrack(), so getDaughterIndex can detect // whether a row was actually added before trusting lastIndex(). if (!mProduceTracks) { return false; @@ -745,7 +778,7 @@ class TrackBuilder }; private: - TrackSelection mTrackSelection; + TrackSelection mTrackSelection; bool mFillAnyTable = false; bool mProduceTracks = false; bool mProduceTrackMasks = false; diff --git a/PWGCF/Femto/Core/v0Builder.h b/PWGCF/Femto/Core/v0Builder.h index 8ce1b3888bf..0f5768d7058 100644 --- a/PWGCF/Femto/Core/v0Builder.h +++ b/PWGCF/Femto/Core/v0Builder.h @@ -105,7 +105,7 @@ struct ConfK0shortBits : o2::framework::ConfigurableGroup { o2::framework::Configurable phiMax{"phiMax", 1.f * o2::constants::math::TwoPI, "Maximum phi"}; \ o2::framework::Configurable massMin{"massMin", (defaultMassMin), "Minimum invariant mass for Lambda"}; \ o2::framework::Configurable massMax{"massMax", (defaultMassMax), "Maximum invariant mass for Lambda"}; \ - o2::framework::Configurable mask{"mask", 0, "Bitmask for v0 selection"}; + o2::framework::Configurable mask{"mask", 0, "Bitmask for v0 selection"}; // base selection for analysis task for lambdas template @@ -177,10 +177,44 @@ const std::unordered_map v0SelectionNames = { {kNegDaughTpcPion, "TPC Pion PID for negative daughter"}, {kNegDaughTpcProton, "TPC Proton PID for negative daughter"}}; -/// \class FemtoDreamTrackCuts -/// \brief Cut class to contain and execute all cuts applied to tracks -template -class V0Selection : public BaseSelection +// enum for all track filters (loose pre-selection, applied before quality/PID cuts) +enum V0Filters { + kPtMin, + kPtMax, + kEtaMin, + kEtaMax, + kPhiMin, + kPhiMax, + kRejectionK0shortMass, + kK0shortMassMin, + kK0shortMassMax, + kRejectionLambdaMass, + kLambdaMassMin, + kLambdaMassMax, + kTrackFiltersMax +}; + +constexpr char LambdaFilterHistName[] = "hLambdaFilters"; +constexpr char AntiLambdaFilterHistName[] = "hAntiLambdaFilters"; +constexpr char K0shortFilterHistName[] = "hK0shortFilters"; +const std::unordered_map v0FilterNames = { + {kPtMin, "ptMin"}, + {kPtMax, "ptMax"}, + {kEtaMin, "etaMin"}, + {kEtaMax, "etaMax"}, + {kPhiMin, "phiMin"}, + {kPhiMax, "phiMax"}, + {kRejectionK0shortMass, "rejectK0short"}, + {kK0shortMassMin, "k0shortMassMin"}, + {kK0shortMassMax, "k0shortMassMin"}, + {kRejectionLambdaMass, "rejectLambda"}, + {kLambdaMassMin, "lambdaMassMin"}, + {kLambdaMassMax, "lambdaMassMin"}, +}; + +/// \brief Cut class to contain and execute all cuts applied to v0s +template +class V0Selection : public baseselection::BaseSelection { public: V0Selection() = default; @@ -189,6 +223,7 @@ class V0Selection : public BaseSelection void configure(o2::framework::HistogramRegistry* registry, T1& config, T2& filter) { + this->init(config.passThrough.value); mPtMin = filter.ptMin.value; mPtMax = filter.ptMax.value; @@ -197,13 +232,6 @@ class V0Selection : public BaseSelectionaddSelection(kPosDaughTpcProton, v0SelectionNames.at(kPosDaughTpcProton), config.posDauTpcProton.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kNegDaughTpcPion, v0SelectionNames.at(kNegDaughTpcPion), config.negDauTpcPion.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); + this->addSelection(kPosDaughTpcProton, v0SelectionNames.at(kPosDaughTpcProton), config.posDauTpcProton.value, limits::kAbsUpperLimit, true, true, false); + this->addSelection(kNegDaughTpcPion, v0SelectionNames.at(kNegDaughTpcPion), config.negDauTpcPion.value, limits::kAbsUpperLimit, true, true, false); } if constexpr (modes::isEqual(v0Type, modes::V0::kAntiLambda)) { - this->addSelection(kPosDaughTpcPion, v0SelectionNames.at(kPosDaughTpcPion), config.posDauTpcPion.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kNegDaughTpcProton, v0SelectionNames.at(kNegDaughTpcProton), config.negDauTpcProton.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); + this->addSelection(kPosDaughTpcPion, v0SelectionNames.at(kPosDaughTpcPion), config.posDauTpcPion.value, limits::kAbsUpperLimit, true, true, false); + this->addSelection(kNegDaughTpcProton, v0SelectionNames.at(kNegDaughTpcProton), config.negDauTpcProton.value, limits::kAbsUpperLimit, true, true, false); } } if constexpr (modes::isEqual(v0Type, modes::V0::kK0short)) { @@ -227,19 +255,36 @@ class V0Selection : public BaseSelectionaddSelection(kPosDaughTpcPion, v0SelectionNames.at(kPosDaughTpcPion), config.posDauTpcPion.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kNegDaughTpcPion, v0SelectionNames.at(kNegDaughTpcPion), config.negDauTpcPion.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - } - this->addSelection(kDcaDaughMax, v0SelectionNames.at(kDcaDaughMax), config.dcaDauMax.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kCpaMin, v0SelectionNames.at(kCpaMin), config.cpaMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kTransRadMin, v0SelectionNames.at(kTransRadMin), config.transRadMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kTransRadMax, v0SelectionNames.at(kTransRadMax), config.transRadMax.value, limits::kUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kDauAbsEtaMax, v0SelectionNames.at(kDauAbsEtaMax), config.dauAbsEtaMax.value, limits::kAbsUpperLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kDauDcaMin, v0SelectionNames.at(kDauDcaMin), config.dauDcaMin.value, limits::kAbsLowerLimit, skipMostPermissiveBit, isMinimalCut, false); - this->addSelection(kDauTpcClsMin, v0SelectionNames.at(kDauTpcClsMin), config.dauTpcClustersMin.value, limits::kLowerLimit, skipMostPermissiveBit, isMinimalCut, false); + this->addSelection(kPosDaughTpcPion, v0SelectionNames.at(kPosDaughTpcPion), config.posDauTpcPion.value, limits::kAbsUpperLimit, true, true, false); + this->addSelection(kNegDaughTpcPion, v0SelectionNames.at(kNegDaughTpcPion), config.negDauTpcPion.value, limits::kAbsUpperLimit, true, true, false); + } - this->setupContainers(registry); + this->addSelection(kDcaDaughMax, v0SelectionNames.at(kDcaDaughMax), config.dcaDauMax.value, limits::kAbsUpperLimit, true, true, false); + this->addSelection(kCpaMin, v0SelectionNames.at(kCpaMin), config.cpaMin.value, limits::kLowerLimit, true, true, false); + this->addSelection(kTransRadMin, v0SelectionNames.at(kTransRadMin), config.transRadMin.value, limits::kLowerLimit, true, true, false); + this->addSelection(kTransRadMax, v0SelectionNames.at(kTransRadMax), config.transRadMax.value, limits::kUpperLimit, true, true, false); + this->addSelection(kDauAbsEtaMax, v0SelectionNames.at(kDauAbsEtaMax), config.dauAbsEtaMax.value, limits::kAbsUpperLimit, true, true, false); + this->addSelection(kDauDcaMin, v0SelectionNames.at(kDauDcaMin), config.dauDcaMin.value, limits::kAbsLowerLimit, true, true, false); + this->addSelection(kDauTpcClsMin, v0SelectionNames.at(kDauTpcClsMin), config.dauTpcClustersMin.value, limits::kLowerLimit, true, true, false); + + this->setupSelectionHistogram(registry); + this->template setupFilterHistogram( + registry, + { + {v0FilterNames.at(kPtMin), mPtMin}, + {v0FilterNames.at(kPtMax), mPtMax}, + {v0FilterNames.at(kEtaMin), mEtaMin}, + {v0FilterNames.at(kEtaMax), mEtaMax}, + {v0FilterNames.at(kPhiMin), mPhiMin}, + {v0FilterNames.at(kPhiMax), mPhiMax}, + {v0FilterNames.at(kRejectionK0shortMass), mRejectK0shortHypothesis ? 1 : 0}, + {v0FilterNames.at(kK0shortMassMin), mMassK0shortLowerLimit}, + {v0FilterNames.at(kK0shortMassMax), mMassK0shortUpperLimit}, + {v0FilterNames.at(kRejectionLambdaMass), mRejectLambdaHypothesis ? 1 : 0}, + {v0FilterNames.at(kLambdaMassMin), mMassLambdaLowerLimit}, + {v0FilterNames.at(kLambdaMassMax), mMassLambdaUpperLimit}, + }); } template @@ -277,40 +322,90 @@ class V0Selection : public BaseSelectionevaluateObservable(kNegDaughTpcPion, negDaughter.tpcNSigmaPi()); this->evaluateObservable(kNegDaughTpcProton, negDaughter.tpcNSigmaPr()); - this->assembleBitmask(); + this->assembleBitmask(); } template bool checkFilters(const T& v0) const { - // check kinematics first - const bool kinematicsOk = - (v0.pt() > mPtMin && v0.pt() < mPtMax) && - (v0.eta() > mEtaMin && v0.eta() < mEtaMax) && - (v0.phi() > mPhiMin && v0.phi() < mPhiMax); - if (!kinematicsOk) { - return false; - } - // now check mass hypothesis - if constexpr (modes::isEqual(v0Type, modes::V0::kLambda)) { - return (v0.mLambda() > mMassLambdaLowerLimit && v0.mLambda() < mMassLambdaUpperLimit) && // inside Λ - (!mRejectK0shortHypothesis || (v0.mK0Short() < mMassK0shortLowerLimit || v0.mK0Short() > mMassK0shortUpperLimit)); // outside K0s - } + bool pass = true; + bool p = false; + + // kinematics + p = v0.pt() > mPtMin; + this->template fillFilter(kPtMin, p); + pass &= p; + + p = v0.pt() < mPtMax; + this->template fillFilter(kPtMax, p); + pass &= p; + + p = v0.eta() > mEtaMin; + this->template fillFilter(kEtaMin, p); + pass &= p; + + p = v0.eta() < mEtaMax; + this->template fillFilter(kEtaMax, p); + pass &= p; + + p = v0.phi() > mPhiMin; + this->template fillFilter(kPhiMin, p); + pass &= p; + + p = v0.phi() < mPhiMax; + this->template fillFilter(kPhiMax, p); + pass &= p; + + // mass hypothesis: signal window is a gating AND-cut (two bounds, both must pass); + // the competing hypothesis is a rejection OR-cut (pass if outside the window, or if + // rejection is disabled) — its own bin records whether the candidate cleared the + // rejection, while the min/max bins are diagnostic bounds on where the mass sits. + if constexpr (modes::isEqual(v0Type, modes::V0::kLambda) || modes::isEqual(v0Type, modes::V0::kAntiLambda)) { + float const mass = modes::isEqual(v0Type, modes::V0::kLambda) ? v0.mLambda() : v0.mAntiLambda(); + + p = mass > mMassLambdaLowerLimit; + this->template fillFilter(kLambdaMassMin, p); + pass &= p; + + p = mass < mMassLambdaUpperLimit; + this->template fillFilter(kLambdaMassMax, p); + pass &= p; - if constexpr (modes::isEqual(v0Type, modes::V0::kAntiLambda)) { - return (v0.mAntiLambda() > mMassLambdaLowerLimit && v0.mAntiLambda() < mMassLambdaUpperLimit) && // inside Λbar - (!mRejectK0shortHypothesis || (v0.mK0Short() < mMassK0shortLowerLimit || v0.mK0Short() > mMassK0shortUpperLimit)); // outside K0s + bool const belowK0s = v0.mK0Short() < mMassK0shortLowerLimit; + this->template fillFilter(kK0shortMassMin, belowK0s); + bool const aboveK0s = v0.mK0Short() > mMassK0shortUpperLimit; + this->template fillFilter(kK0shortMassMax, aboveK0s); + + p = !mRejectK0shortHypothesis || belowK0s || aboveK0s; + this->template fillFilter(kRejectionK0shortMass, p); + pass &= p; } if constexpr (modes::isEqual(v0Type, modes::V0::kK0short)) { - return (v0.mK0Short() > mMassK0shortLowerLimit && v0.mK0Short() < mMassK0shortUpperLimit) && // inside K0s - (!mRejectLambdaHypothesis || (v0.mLambda() < mMassLambdaLowerLimit || v0.mLambda() > mMassLambdaUpperLimit)) && // outside Λ - (!mRejectLambdaHypothesis || (v0.mAntiLambda() < mMassLambdaLowerLimit || v0.mAntiLambda() > mMassLambdaUpperLimit)); // outside Λbar + p = v0.mK0Short() > mMassK0shortLowerLimit; + this->template fillFilter(kK0shortMassMin, p); + pass &= p; + + p = v0.mK0Short() < mMassK0shortUpperLimit; + this->template fillFilter(kK0shortMassMax, p); + pass &= p; + + bool const belowLambda = v0.mLambda() < mMassLambdaLowerLimit; + bool const aboveLambda = v0.mLambda() > mMassLambdaUpperLimit; + bool const belowAntiLambda = v0.mAntiLambda() < mMassLambdaLowerLimit; + bool const aboveAntiLambda = v0.mAntiLambda() > mMassLambdaUpperLimit; + this->template fillFilter(kLambdaMassMin, belowLambda && belowAntiLambda); + this->template fillFilter(kLambdaMassMax, aboveLambda && aboveAntiLambda); + + p = !mRejectLambdaHypothesis || ((belowLambda || aboveLambda) && (belowAntiLambda || aboveAntiLambda)); + this->template fillFilter(kRejectionLambdaMass, p); + pass &= p; } - return false; - } - [[nodiscard]] bool passThroughAllV0s() const { return mPassThrough; } + this->template fillFilterSummary(pass); + + return this->isPassThrough() || pass; + } protected: float mMassK0shortLowerLimit = 0.483f; @@ -321,8 +416,6 @@ class V0Selection : public BaseSelection produceK0shortExtras{"produceK0shortExtras", -1, "Produce K0shortExtras (-1: auto; 0 off; 1 on)"}; }; -template +template class V0Builder { public: @@ -399,11 +492,11 @@ class V0Builder int64_t posDaughterIndex = 0; int64_t negDaughterIndex = 0; for (const auto& v0 : v0s) { - if (!mV0Selection.passThroughAllV0s() && !mV0Selection.checkFilters(v0)) { + if (!mV0Selection.checkFilters(v0)) { continue; } mV0Selection.applySelections(v0, tracks); - if (!mV0Selection.passThroughAllV0s() && !mV0Selection.passesAllRequiredSelections()) { + if (!mV0Selection.passesAllRequiredSelections()) { continue; } @@ -536,7 +629,7 @@ class V0Builder [[nodiscard]] bool fillAnyTable() const { return mFillAnyTable; } private: - V0Selection mV0Selection; + V0Selection mV0Selection; bool mFillAnyTable = false; bool mProduceLambdas = false; bool mProduceLambdaMasks = false; diff --git a/PWGCF/Femto/TableProducer/femtoProducer.cxx b/PWGCF/Femto/TableProducer/femtoProducer.cxx index ae91ecdf286..328e2f61bc1 100644 --- a/PWGCF/Femto/TableProducer/femtoProducer.cxx +++ b/PWGCF/Femto/TableProducer/femtoProducer.cxx @@ -92,12 +92,12 @@ struct FemtoProducer { collisionbuilder::ConfCollisionFilters confCollisionFilters; collisionbuilder::ConfCollisionBits confCollisionBits; collisionbuilder::ConfCollisionRctFlags confCollisionRctFlags; - collisionbuilder::CollisionBuilder collisionBuilder; + collisionbuilder::CollisionBuilder collisionBuilder; // track builder trackbuilder::TrackBuilderProducts trackBuilderProducts; trackbuilder::ConfTrackTables confTrackTables; - trackbuilder::TrackBuilder trackBuilder; + trackbuilder::TrackBuilder trackBuilder; trackbuilder::ConfTrackBits confTrackBits; trackbuilder::ConfTrackFilters confTrackFilters; @@ -106,28 +106,28 @@ struct FemtoProducer { v0builder::ConfV0Tables confV0Tables; v0builder::ConfV0Filters confV0Filters; v0builder::ConfK0shortBits confK0shortBits; - v0builder::V0Builder k0shortBuilder; + v0builder::V0Builder k0shortBuilder; v0builder::ConfLambdaBits confLambdaBits; - v0builder::V0Builder lambdaBuilder; - v0builder::V0Builder antilambdaBuilder; + v0builder::V0Builder lambdaBuilder; + v0builder::V0Builder antilambdaBuilder; // cascade builder cascadebuilder::CascadeBuilderProducts cascadeBuilderProducts; cascadebuilder::ConfCascadeTables confCascadeTables; cascadebuilder::ConfCascadeFilters confCascadeFilters; cascadebuilder::ConfXiBits confXiBits; - cascadebuilder::CascadeBuilder xiBuilder; + cascadebuilder::CascadeBuilder xiBuilder; cascadebuilder::ConfOmegaBits confOmegaBits; - cascadebuilder::CascadeBuilder omegaBuilder; + cascadebuilder::CascadeBuilder omegaBuilder; // kink builder kinkbuilder::KinkBuilderProducts kinkBuilderProducts; kinkbuilder::ConfKinkTables confKinkTables; kinkbuilder::ConfKinkFilters confKinkFilters; kinkbuilder::ConfSigmaBits confSigmaBits; - kinkbuilder::KinkBuilder sigmaBuilder; + kinkbuilder::KinkBuilder sigmaBuilder; kinkbuilder::ConfSigmaPlusBits confSigmaPlusBits; - kinkbuilder::KinkBuilder sigmaPlusBuilder; + kinkbuilder::KinkBuilder sigmaPlusBuilder; // mc builder mcbuilder::ConfMc confMc; From 0c78a6388e2831d8fe08dd061028a60d16be00a7 Mon Sep 17 00:00:00 2001 From: Anton Riedel Date: Fri, 10 Jul 2026 15:18:45 +0200 Subject: [PATCH 3/8] Feat: update cutculator --- PWGCF/Femto/Macros/cutculator.py | 42 ++- PWGCF/Femto/Macros/cutculator_gui.py | 432 ++++++++++++++++++++------- 2 files changed, 359 insertions(+), 115 deletions(-) diff --git a/PWGCF/Femto/Macros/cutculator.py b/PWGCF/Femto/Macros/cutculator.py index add45265ec6..b3bf0f2bef1 100755 --- a/PWGCF/Femto/Macros/cutculator.py +++ b/PWGCF/Femto/Macros/cutculator.py @@ -35,6 +35,21 @@ def parse_bin_label(label): return result +def is_filter_histogram(bins): + """ + Determine whether the parsed bins belong to a filter histogram (fixed, + non-selectable pre-filter values) rather than a selection/bitmask histogram. + Filter bins carry a "FilterName" key; selection bins carry "SelectionName". + """ + for b in bins: + if "FilterName" in b: + return True + if "SelectionName" in b: + return False + # no parseable bins at all; treat as selection histogram (existing behavior) + return False + + def format_value_with_comment(b): """Return Value plus optional (comment=...) suffix.""" val = b.get("Value", "") @@ -44,6 +59,20 @@ def format_value_with_comment(b): return val +def print_filter_values(bins): + """ + Filters are fixed values already applied when the producer ran — there is + nothing to select. Just print each filter's name and value. + """ + print("\n=======================================") + print("Filter values used in this histogram:") + print("=======================================\n") + for b in bins: + name = b.get("FilterName", "unknown") + value = b.get("FilterValue", "") + print(f" {name}: {value}") + + def ask_user_selection(group): """ Prompt user to select bin(s) for this selection group. @@ -188,7 +217,8 @@ def main(rootfile_path, tdir_path="femto-producer"): print(f"\nUsing histogram: {hname}") print(f"Histogram contains {nbins} bins.\n") - # parse all bins, ignoring the last 2 special bins + # parse all bins, ignoring the last 2 special bins ("All analyzed"/"All passed", + # present on both selection and filter histograms) bins = [] for i in range(1, nbins - 2 + 1): label = hist.GetXaxis().GetBinLabel(i) @@ -198,6 +228,16 @@ def main(rootfile_path, tdir_path="femto-producer"): bdict["_bin_index"] = i bins.append(bdict) + if not bins: + print("No parseable bins found in this histogram.") + return + + # filter histograms carry fixed, already-applied values — nothing to select, + # so just print them and skip the interactive/bitmask machinery entirely + if is_filter_histogram(bins): + print_filter_values(bins) + return + # group by SelectionName groups = {} for b in bins: diff --git a/PWGCF/Femto/Macros/cutculator_gui.py b/PWGCF/Femto/Macros/cutculator_gui.py index a5beb43ff2f..9aceb2cc312 100644 --- a/PWGCF/Femto/Macros/cutculator_gui.py +++ b/PWGCF/Femto/Macros/cutculator_gui.py @@ -38,6 +38,7 @@ ACCENT_OPT = "#a6e3a1" # green – optional ACCENT_REJ = "#f38ba8" # red – rejection / neutral ACCENT_ALWAYS = "#cba6f7" # purple – loosest/always-true (no bit set) +ACCENT_FILTER = "#f9e2af" # yellow – filter values (read-only) FG = "#cdd6f4" FG_DIM = "#6c7086" BORDER = "#45475a" @@ -49,6 +50,10 @@ FONT_MONO = ("JetBrains Mono", 11, "bold") FONT_SMALL = ("Inter", 9) +# Leading column width shared by every row type (checkbox glyph, loosest-bin +# spacer) so all rows' text starts at exactly the same x position. +CHECK_COL_WIDTH = 3 + # ── Helpers ─────────────────────────────────────────────────────────────────── def parse_bin_label(label): @@ -93,6 +98,37 @@ def has_only_skipped_minimal_bins(group): return bool(minimal) and all(b.get("BitPosition", "X").upper() == "X" for b in minimal) +def get_skipped_minimal_bins(group): + """ + Returns the minimal bins in a group whose BitPosition=="X" — i.e. the + loosest threshold(s) that are always in effect regardless of which + (if any) stricter minimal bit the user additionally selects. These never + get a checkbox, but their effect is always applied, so they must still + be surfaced in the summary. + """ + return [ + b + for b in group + if b.get("MinimalCut", "0") == "1" + and b.get("OptionalCut", "0") == "0" + and b.get("BitPosition", "X").upper() == "X" + ] + + +def is_filter_group(bins): + """ + Determine whether the parsed bins belong to a filter histogram (fixed, + non-selectable pre-filter values) rather than a selection/bitmask histogram. + Filter bins carry a "FilterName" key; selection bins carry "SelectionName". + """ + for b in bins: + if "FilterName" in b: + return True + if "SelectionName" in b: + return False + return False + + def load_bins_from_hist(hist): nbins = hist.GetNbinsX() bins = [] @@ -103,12 +139,18 @@ def load_bins_from_hist(hist): b = parse_bin_label(label) b["_bin_index"] = i bins.append(b) + + is_filter = is_filter_group(bins) + if is_filter: + # filter histograms are a flat list of fixed name/value pairs — no grouping needed + return bins, True + # group by SelectionName preserving order groups = {} for b in bins: name = b.get("SelectionName", f"unknown_{b['_bin_index']}") groups.setdefault(name, []).append(b) - return groups + return groups, False # ── Main Application ────────────────────────────────────────────────────────── @@ -117,8 +159,8 @@ def __init__(self, rootfile=None, tdir="femto-producer"): super().__init__() self.title("CutCulator") self.configure(bg=BG) - self.geometry("980x820") - self.minsize(780, 600) + self.geometry("1200x820") + self.minsize(900, 600) self.resizable(True, True) self._rootfile_path = rootfile @@ -126,7 +168,10 @@ def __init__(self, rootfile=None, tdir="femto-producer"): self._root_file = None self._hist = None self._groups = {} # SelectionName → list[bin_dict] + self._filter_bins = [] # list[bin_dict], used when the histogram is a filter histogram + self._is_filter_hist = False self._vars = {} # (SelectionName, idx) → BooleanVar + self._check_labels = {} # (SelectionName, idx) → Label (custom checkbox glyph) self._build_ui() @@ -163,25 +208,44 @@ def _build_ui(self): self._style_combobox() # ── legend ── - legend = tk.Frame(self, bg=BG, pady=4, padx=18) - legend.pack(fill="x") - for color, label in [ - (ACCENT, "Minimal"), - (ACCENT_OPT, "Optional"), - (ACCENT_REJ, "Neutral"), - (ACCENT_ALWAYS, "Loosest (no bit set)"), - ]: - dot = tk.Label(legend, text="●", font=FONT_BODY, bg=BG, fg=color) - dot.pack(side="left") - tk.Label(legend, text=label, font=FONT_SMALL, bg=BG, fg=FG_DIM).pack(side="left", padx=(2, 12)) + self._legend = tk.Frame(self, bg=BG, pady=4, padx=18) + self._legend.pack(fill="x") + self._build_legend_selection() + + # ── bottom result bar (packed to bottom BEFORE the main area so the + # main area fills remaining space correctly) ── + self._bottom = tk.Frame(self, bg=BG_CARD, pady=10, padx=18) + self._bottom.pack(fill="x", side="bottom") + + tk.Label(self._bottom, text="Bitmask:", font=FONT_HEAD, bg=BG_CARD, fg=FG).pack(side="left") + + self._lbl_dec = tk.Label(self._bottom, text="—", font=FONT_MONO, bg=BG_CARD, fg=ACCENT, width=14, anchor="w") + self._lbl_dec.pack(side="left", padx=8) + + self._lbl_hex = tk.Label( + self._bottom, text="—", font=FONT_MONO, bg=BG_CARD, fg=ACCENT_OPT, width=14, anchor="w" + ) + self._lbl_hex.pack(side="left", padx=8) + + self._lbl_bin = tk.Label(self._bottom, text="—", font=FONT_MONO, bg=BG_CARD, fg=FG_DIM, anchor="w") + self._lbl_bin.pack(side="left", padx=8) - # ── main paned area: selection list (top) + summary (bottom) ── - paned = tk.PanedWindow(self, orient="vertical", bg=BORDER, sashwidth=4, sashrelief="flat") - paned.pack(fill="both", expand=True, padx=12, pady=4) + copy_frame = tk.Frame(self._bottom, bg=BG_CARD) + copy_frame.pack(side="right") + self._make_button(copy_frame, "Copy Dec", lambda: self._copy(self._lbl_dec["text"]), ACCENT).pack( + side="left", padx=3 + ) + self._make_button(copy_frame, "Copy Hex", lambda: self._copy(self._lbl_hex["text"]), ACCENT_OPT).pack( + side="left", padx=3 + ) - # ── scrollable selection area ── - outer = tk.Frame(paned, bg=BG) - paned.add(outer, stretch="always", minsize=200) + # ── main paned area: selection cards (left) + summary sidebar (right) ── + self._paned = tk.PanedWindow(self, orient="horizontal", bg=BORDER, sashwidth=4, sashrelief="flat") + self._paned.pack(fill="both", expand=True, padx=12, pady=4) + + # ── scrollable selection area (left) ── + outer = tk.Frame(self._paned, bg=BG) + self._paned.add(outer, stretch="always", minsize=360) self._canvas = tk.Canvas(outer, bg=BG, highlightthickness=0, bd=0) vsb = ttk.Scrollbar(outer, orient="vertical", command=self._canvas.yview) @@ -197,22 +261,23 @@ def _build_ui(self): self._canvas.bind_all("", self._on_mousewheel) self._canvas.bind_all("", self._on_mousewheel) - # ── summary panel ── - summary_outer = tk.Frame(paned, bg=BG_CARD) - paned.add(summary_outer, stretch="never", minsize=120) + # ── summary sidebar (right) ── + self._summary_outer = tk.Frame(self._paned, bg=BG_CARD) + self._paned.add(self._summary_outer, stretch="never", minsize=280, width=320) - summary_hdr = tk.Frame(summary_outer, bg=BG_CARD, pady=5, padx=10) + summary_hdr = tk.Frame(self._summary_outer, bg=BG_CARD, pady=8, padx=10) summary_hdr.pack(fill="x") - tk.Label(summary_hdr, text="Selected cuts", font=FONT_HEAD, bg=BG_CARD, fg=FG).pack(side="left") + self._lbl_summary_title = tk.Label(summary_hdr, text="Selected cuts", font=FONT_HEAD, bg=BG_CARD, fg=FG) + self._lbl_summary_title.pack(anchor="w") self._lbl_summary_empty = tk.Label(summary_hdr, text="(none)", font=FONT_SMALL, bg=BG_CARD, fg=FG_DIM) - self._lbl_summary_empty.pack(side="left", padx=8) + self._lbl_summary_empty.pack(anchor="w", pady=(2, 0)) - tk.Frame(summary_outer, bg=BORDER, height=1).pack(fill="x") + tk.Frame(self._summary_outer, bg=BORDER, height=1).pack(fill="x") - summary_scroll_outer = tk.Frame(summary_outer, bg=BG_CARD) + summary_scroll_outer = tk.Frame(self._summary_outer, bg=BG_CARD) summary_scroll_outer.pack(fill="both", expand=True) - self._summary_canvas = tk.Canvas(summary_scroll_outer, bg=BG_CARD, highlightthickness=0, bd=0, height=100) + self._summary_canvas = tk.Canvas(summary_scroll_outer, bg=BG_CARD, highlightthickness=0, bd=0) summary_vsb = ttk.Scrollbar(summary_scroll_outer, orient="vertical", command=self._summary_canvas.yview) self._summary_canvas.configure(yscrollcommand=summary_vsb.set) summary_vsb.pack(side="right", fill="y") @@ -223,29 +288,31 @@ def _build_ui(self): self._summary_inner.bind("", self._on_summary_inner_configure) self._summary_canvas.bind("", self._on_summary_canvas_configure) - # ── bottom result bar ── - bottom = tk.Frame(self, bg=BG_CARD, pady=10, padx=18) - bottom.pack(fill="x", side="bottom") - - tk.Label(bottom, text="Bitmask:", font=FONT_HEAD, bg=BG_CARD, fg=FG).pack(side="left") - - self._lbl_dec = tk.Label(bottom, text="—", font=FONT_MONO, bg=BG_CARD, fg=ACCENT, width=14, anchor="w") - self._lbl_dec.pack(side="left", padx=8) - - self._lbl_hex = tk.Label(bottom, text="—", font=FONT_MONO, bg=BG_CARD, fg=ACCENT_OPT, width=14, anchor="w") - self._lbl_hex.pack(side="left", padx=8) - - self._lbl_bin = tk.Label(bottom, text="—", font=FONT_MONO, bg=BG_CARD, fg=FG_DIM, anchor="w") - self._lbl_bin.pack(side="left", padx=8) + def _build_legend_selection(self): + for w in self._legend.winfo_children(): + w.destroy() + for color, label in [ + (ACCENT, "Minimal"), + (ACCENT_OPT, "Optional"), + (ACCENT_REJ, "Neutral"), + (ACCENT_ALWAYS, "Loosest (no bit set)"), + ]: + dot = tk.Label(self._legend, text="●", font=FONT_BODY, bg=BG, fg=color) + dot.pack(side="left") + tk.Label(self._legend, text=label, font=FONT_SMALL, bg=BG, fg=FG_DIM).pack(side="left", padx=(2, 12)) - copy_frame = tk.Frame(bottom, bg=BG_CARD) - copy_frame.pack(side="right") - self._make_button(copy_frame, "Copy Dec", lambda: self._copy(self._lbl_dec["text"]), ACCENT).pack( - side="left", padx=3 - ) - self._make_button(copy_frame, "Copy Hex", lambda: self._copy(self._lbl_hex["text"]), ACCENT_OPT).pack( - side="left", padx=3 - ) + def _build_legend_filter(self): + for w in self._legend.winfo_children(): + w.destroy() + dot = tk.Label(self._legend, text="●", font=FONT_BODY, bg=BG, fg=ACCENT_FILTER) + dot.pack(side="left") + tk.Label( + self._legend, + text="Filter value (fixed, already applied — not selectable)", + font=FONT_SMALL, + bg=BG, + fg=FG_DIM, + ).pack(side="left", padx=(2, 12)) # ── Combobox styling ────────────────────────────────────────────────────── def _style_combobox(self): @@ -329,10 +396,101 @@ def _on_hist_selected(self, _e=None): if not hist: return self._hist = hist - self._groups = load_bins_from_hist(hist) self._vars = {} - self._build_selections() - self._update_bitmask() + self._check_labels = {} + + # Always start from a clean summary panel — forget()-ing the pane only + # hides it, it does not destroy previously built rows. + self._clear_summary() + + parsed, is_filter = load_bins_from_hist(hist) + self._is_filter_hist = is_filter + + if is_filter: + self._filter_bins = parsed + self._groups = {} + self._build_legend_filter() + self._build_filter_view() + self._set_bitmask_bar_visible(False) + self._set_summary_visible(False) + else: + self._groups = parsed + self._filter_bins = [] + self._build_legend_selection() + self._build_selections() + self._set_bitmask_bar_visible(True) + self._set_summary_visible(True) + self._update_bitmask() + + def _clear_summary(self): + """Destroy all summary rows and reset to the empty state. Called on every + histogram switch so stale rows from a previous selection histogram never + linger while a filter histogram (which never calls _rebuild_summary) is + being shown.""" + for w in self._summary_inner.winfo_children(): + w.destroy() + self._lbl_summary_empty.config(text="(none)") + self._on_summary_inner_configure() + + def _set_bitmask_bar_visible(self, visible): + # self._bottom is a plain pack()ed frame (never added to the PanedWindow), + # so toggling it with pack()/pack_forget() is fine here. + if visible: + self._bottom.pack(fill="x", side="bottom") + else: + self._bottom.pack_forget() + + def _set_summary_visible(self, visible): + # self._summary_outer IS managed by the PanedWindow, so it must be + # shown/hidden via the PanedWindow's own add()/forget() API — calling + # pack() on it directly conflicts with the PanedWindow geometry manager + # and produces broken layout (this was the bug: the summary pane would + # swallow the whole window and the selection cards would disappear). + panes = self._paned.panes() + summary_path = str(self._summary_outer) + if visible: + if summary_path not in panes: + self._paned.add(self._summary_outer, stretch="never", minsize=280, width=320) + else: + if summary_path in panes: + self._paned.forget(self._summary_outer) + + # ── Filter (read-only) view ─────────────────────────────────────────────── + def _build_filter_view(self): + for w in self._inner.winfo_children(): + w.destroy() + + card = tk.Frame(self._inner, bg=BG_CARD, bd=0, highlightthickness=1, highlightbackground=BORDER) + card.pack(fill="x", padx=10, pady=5, ipadx=8, ipady=6) + + hdr = tk.Frame(card, bg=BG_CARD) + hdr.pack(fill="x", padx=6, pady=(4, 2)) + tk.Label(hdr, text="Filter values", font=FONT_HEAD, bg=BG_CARD, fg=FG).pack(side="left") + tk.Label( + hdr, + text="fixed values already applied when the producer ran — nothing to select", + font=FONT_SMALL, + bg=BG_CARD, + fg=FG_DIM, + ).pack(side="left", padx=10) + + tk.Frame(card, bg=BORDER, height=1).pack(fill="x", padx=6, pady=2) + + rows_frame = tk.Frame(card, bg=BG_CARD) + rows_frame.pack(fill="x", padx=6, pady=4) + + for b in self._filter_bins: + name = b.get("FilterName", "unknown") + value = b.get("FilterValue", "") + + row = tk.Frame(rows_frame, bg=BG_CARD) + row.pack(fill="x", pady=1) + + tk.Label(row, text="●", font=FONT_BODY, bg=BG_CARD, fg=ACCENT_FILTER).pack(side="left", padx=(0, 4)) + tk.Label(row, text=name, font=FONT_BODY, bg=BG_CARD, fg=FG, anchor="w").pack(side="left") + tk.Label(row, text=value, font=FONT_BODY, bg=BG_CARD, fg=FG_DIM, anchor="e").pack(side="right", padx=4) + + self._on_inner_configure() # ── Selection cards ─────────────────────────────────────────────────────── def _build_selections(self): @@ -376,14 +534,12 @@ def _build_group_card(self, sel_name, group): ).pack(side="left", padx=10) else: # show the loosest (most permissive) minimal threshold as a hint - skipped_minimal = [ - b for b in group if b.get("MinimalCut", "0") == "1" and b.get("BitPosition", "X").upper() == "X" - ] + skipped_minimal = get_skipped_minimal_bins(group) if skipped_minimal: loosest_val = format_value_with_comment(skipped_minimal[0]) tk.Label( hdr, - text=f"minimal cut → loosest selection: {loosest_val}", + text=f"minimal cut → always applied floor: {loosest_val}", font=FONT_SMALL, bg=BG_CARD, fg=FG_DIM, @@ -415,6 +571,10 @@ def _build_group_card(self, sel_name, group): if b.get("MinimalCut", "0") == "1" and b.get("OptionalCut", "0") == "0": self._build_loosest_row(bins_frame, b) else: + # even when there are selectable minimal bins, the skipped-bit-position + # loosest bin(s) are still always in effect — show them as informational rows too + for b in get_skipped_minimal_bins(group): + self._build_loosest_row(bins_frame, b) for i, b in minimal: self._build_bin_row(bins_frame, sel_name, i, b, "minimal") for i, b in optional: @@ -423,21 +583,32 @@ def _build_group_card(self, sel_name, group): self._build_bin_row(bins_frame, sel_name, i, b, "neutral") def _build_loosest_row(self, parent, b): - """Informational row for a loosest/always-true cut — no checkbox, no bit.""" + """Informational row for a loosest/always-true cut — no checkbox, no bit. + + Uses the exact same widget type/width (a Label with width=CHECK_COL_WIDTH) + for its leading column as _build_bin_row's checkbox glyph, so the two row + types line up pixel-for-pixel. A native tk.Checkbutton's indicator box has + no queryable pixel width, so matching it with a plain spacer Label never + aligns reliably — using an identical Label on both sides is the only way + to guarantee it. + """ label_text = format_value_with_comment(b) row = tk.Frame(parent, bg=BG_CARD) row.pack(fill="x", pady=1) tk.Label(row, text="●", font=FONT_BODY, bg=BG_CARD, fg=ACCENT_ALWAYS).pack(side="left", padx=(0, 4)) + tk.Label(row, text=" ", font=FONT_BODY, bg=BG_CARD, fg=FG_DIM, width=CHECK_COL_WIDTH, anchor="w").pack( + side="left" + ) tk.Label( row, text=label_text, font=FONT_BODY, bg=BG_CARD, - fg=FG_DIM, # dimmed to signal non-interactive + fg=FG_DIM, + anchor="w", ).pack(side="left", fill="x", expand=True) - # no bit-position badge since it's "X" def _build_bin_row(self, parent, sel_name, idx, b, kind): color = {"minimal": ACCENT, "optional": ACCENT_OPT, "neutral": ACCENT_REJ}[kind] @@ -446,42 +617,44 @@ def _build_bin_row(self, parent, sel_name, idx, b, kind): var = tk.BooleanVar(value=False) self._vars[(sel_name, idx)] = var - row = tk.Frame(parent, bg=BG_CARD) + row = tk.Frame(parent, bg=BG_CARD, cursor="hand2") row.pack(fill="x", pady=1) - # coloured dot tk.Label(row, text="●", font=FONT_BODY, bg=BG_CARD, fg=color).pack(side="left", padx=(0, 4)) - # checkbox - cb = tk.Checkbutton( - row, - text=label_text, - variable=var, - font=FONT_BODY, - bg=BG_CARD, - fg=FG, - activebackground=BG_HOVER, - activeforeground=FG, - selectcolor=SEL_BG, - relief="flat", - bd=0, - highlightthickness=0, - cursor="hand2", - command=self._update_bitmask, - ) - cb.pack(side="left", fill="x", expand=True) + # Custom checkbox glyph, built from the SAME Label(width=CHECK_COL_WIDTH) + # pattern as the loosest-row spacer above — this is what actually fixes + # the alignment: both row types share one widget recipe for their + # leading column instead of trying to match a native Checkbutton's + # unmeasurable indicator box. + check_lbl = tk.Label(row, text="[ ]", font=FONT_BODY, bg=BG_CARD, fg=FG_DIM, width=CHECK_COL_WIDTH, anchor="w") + check_lbl.pack(side="left") + self._check_labels[(sel_name, idx)] = check_lbl + + text_lbl = tk.Label(row, text=label_text, font=FONT_BODY, bg=BG_CARD, fg=FG, anchor="w") + text_lbl.pack(side="left", fill="x", expand=True) # bit-position badge pos = b.get("BitPosition", "X") if pos.upper() != "X": tk.Label(row, text=f"bit {pos}", font=FONT_SMALL, bg=BG_CARD, fg=FG_DIM, width=8).pack(side="right", padx=4) + def toggle(_e=None): + var.set(not var.get()) + check_lbl.config(text="[x]" if var.get() else "[ ]", fg=color if var.get() else FG_DIM) + self._update_bitmask() + + for w in (row, check_lbl, text_lbl): + w.bind("", toggle) + # ── Bitmask computation + summary update ────────────────────────────────── def _update_bitmask(self): + if self._is_filter_hist: + return + bitmask = 0 - selected_entries = [] # list of (sel_name, label_text, bit_pos, color, is_loosest) + checked = [] # (sel_name, kind, pos_int, label_text, pos_str, color) - # User-selected cuts (from checkboxes) for (sel_name, idx), var in self._vars.items(): if not var.get(): continue @@ -492,16 +665,45 @@ def _update_bitmask(self): if pos.upper() != "X": bitmask |= 1 << int(pos) - label_text = format_value_with_comment(b) - selected_entries.append((sel_name, label_text, pos, color, False)) - # Loosest-only cuts: always shown in summary, never set a bit + pos_int = int(pos) if pos.upper() != "X" else -1 + label_text = format_value_with_comment(b) + checked.append((sel_name, kind, pos_int, label_text, pos, color)) + + # Within the same (sel_name, kind) group, multiple checked bits represent a + # threshold ladder for one observable, not independent alternatives — only + # the strictest one (highest BitPosition) is the meaningful cut, so collapse + # to that single entry for display. + tightest_by_group = {} + for sel_name, kind, pos_int, label_text, pos, color in checked: + key = (sel_name, kind) + if key not in tightest_by_group or pos_int > tightest_by_group[key][0]: + tightest_by_group[key] = (pos_int, label_text, pos, color) + + selected_entries = [ + (sel_name, label_text, pos, color, False) + for (sel_name, kind), (pos_int, label_text, pos, color) in tightest_by_group.items() + ] + + # Selection names that already have a checked "minimal" entry — for those, + # the checked threshold is strictly stricter than the always-applied floor + # (that's what makes it a selectable, higher-bit-position option in the + # first place), so the floor is logically subsumed and showing it alongside + # the checked value is redundant. Only surface the floor when nothing + # stricter has been chosen for that selection. + sel_names_with_checked_minimal = { + sel_name for (sel_name, kind) in tightest_by_group.keys() if kind == "minimal" + } + + # Loosest/always-applied bins: shown only when no stricter minimal cut for + # the same selection is currently checked — otherwise this entry is fully + # dominated by the checked one and would just duplicate the same cut. for sel_name, group in self._groups.items(): - if has_only_skipped_minimal_bins(group): - for b in group: - if b.get("MinimalCut", "0") == "1" and b.get("OptionalCut", "0") == "0": - label_text = format_value_with_comment(b) - selected_entries.append((sel_name, label_text, "X", ACCENT_ALWAYS, True)) + if sel_name in sel_names_with_checked_minimal: + continue + for b in get_skipped_minimal_bins(group): + label_text = format_value_with_comment(b) + selected_entries.append((sel_name, label_text, "X", ACCENT_ALWAYS, True)) self._lbl_dec.config(text=str(bitmask)) self._lbl_hex.config(text=hex(bitmask)) @@ -510,13 +712,12 @@ def _update_bitmask(self): self._rebuild_summary(selected_entries) def _rebuild_summary(self, entries): - """Rebuild the selected-cuts summary panel.""" - for w in self._summary_inner.winfo_children(): - w.destroy() + """Rebuild the selected-cuts summary sidebar. Narrow column → each value + gets its own row, stacked under the selection name, rather than packed + side-by-side in a single line.""" + self._clear_summary() if not entries: - self._lbl_summary_empty.config(text="(none)") - self._on_summary_inner_configure() return self._lbl_summary_empty.config(text="") @@ -528,50 +729,53 @@ def _rebuild_summary(self, entries): for sel_name, items in by_name.items(): block = tk.Frame(self._summary_inner, bg=BG_CARD) - block.pack(fill="x", padx=10, pady=(3, 0)) + block.pack(fill="x", padx=10, pady=(4, 2)) - # name on its own line — no truncation tk.Label( block, - text=f"{sel_name}:", + text=sel_name, font=FONT_BODY, bg=BG_CARD, fg=FG, anchor="w", + wraplength=280, + justify="left", ).pack(fill="x") - # values indented below the name - values_row = tk.Frame(block, bg=BG_CARD) - values_row.pack(fill="x", padx=(16, 0), pady=(0, 2)) - for label_text, pos, color, is_loosest in items: - entry_frame = tk.Frame(values_row, bg=BG_CARD) - entry_frame.pack(side="left", padx=(0, 12)) + entry_row = tk.Frame(block, bg=BG_CARD) + entry_row.pack(fill="x", padx=(14, 0), pady=1) - tk.Label(entry_frame, text="●", font=FONT_SMALL, bg=BG_CARD, fg=color).pack(side="left") + tk.Label(entry_row, text="●", font=FONT_SMALL, bg=BG_CARD, fg=color).pack(side="left") tk.Label( - entry_frame, + entry_row, text=label_text, font=FONT_SMALL, bg=BG_CARD, fg=FG_DIM if is_loosest else FG, - ).pack(side="left", padx=(2, 0)) + anchor="w", + wraplength=220, + justify="left", + ).pack(side="left", padx=(4, 0), fill="x", expand=True) + if is_loosest: tk.Label( - entry_frame, - text="(no bit)", + entry_row, + text="no bit", font=FONT_SMALL, bg=BG_CARD, fg=ACCENT_ALWAYS, - ).pack(side="left", padx=(4, 0)) + ).pack(side="right") elif pos.upper() != "X": tk.Label( - entry_frame, - text=f"[bit {pos}]", + entry_row, + text=f"bit {pos}", font=FONT_SMALL, bg=BG_CARD, fg=FG_DIM, - ).pack(side="left", padx=(4, 0)) + ).pack(side="right") + + tk.Frame(self._summary_inner, bg=BORDER, height=1).pack(fill="x", padx=10, pady=(4, 0)) self._on_summary_inner_configure() From 85c6436c7578b4480bd08334819f1b039779d9fe Mon Sep 17 00:00:00 2001 From: ALICE Action Bot Date: Fri, 10 Jul 2026 13:21:36 +0000 Subject: [PATCH 4/8] Please consider the following formatting changes --- PWGCF/Femto/Macros/cutculator.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 PWGCF/Femto/Macros/cutculator.py diff --git a/PWGCF/Femto/Macros/cutculator.py b/PWGCF/Femto/Macros/cutculator.py old mode 100755 new mode 100644 From 2c6903f01f6afb5867477b1e7734f3ca1e324781 Mon Sep 17 00:00:00 2001 From: Anton Riedel Date: Fri, 10 Jul 2026 15:31:29 +0200 Subject: [PATCH 5/8] Fix: fix linter error --- PWGCF/Femto/Macros/cutculator_gui.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/PWGCF/Femto/Macros/cutculator_gui.py b/PWGCF/Femto/Macros/cutculator_gui.py index 9aceb2cc312..ac166e36e68 100644 --- a/PWGCF/Femto/Macros/cutculator_gui.py +++ b/PWGCF/Femto/Macros/cutculator_gui.py @@ -19,6 +19,7 @@ import tkinter as tk from tkinter import ttk, filedialog, messagebox import argparse +from typing import Any, Dict, List try: import ROOT @@ -167,8 +168,8 @@ def __init__(self, rootfile=None, tdir="femto-producer"): self._tdir_path = tdir self._root_file = None self._hist = None - self._groups = {} # SelectionName → list[bin_dict] - self._filter_bins = [] # list[bin_dict], used when the histogram is a filter histogram + self._groups: Dict[str, List[Dict[str, Any]]] = {} # SelectionName → list[bin_dict] + self._filter_bins: List[Dict[str, Any]] = [] # list[bin_dict], used when the histogram is a filter histogram self._is_filter_hist = False self._vars = {} # (SelectionName, idx) → BooleanVar self._check_labels = {} # (SelectionName, idx) → Label (custom checkbox glyph) @@ -407,6 +408,11 @@ def _on_hist_selected(self, _e=None): self._is_filter_hist = is_filter if is_filter: + # load_bins_from_hist returns a flat list of bin dicts when is_filter + # is True; the assert lets the type checker narrow `parsed` from the + # dict|list union it infers from the function's two return shapes, + # instead of leaving self._groups statically ambiguous. + assert isinstance(parsed, list) self._filter_bins = parsed self._groups = {} self._build_legend_filter() @@ -414,6 +420,9 @@ def _on_hist_selected(self, _e=None): self._set_bitmask_bar_visible(False) self._set_summary_visible(False) else: + # load_bins_from_hist returns a dict of SelectionName -> bins when + # is_filter is False; same narrowing purpose as above. + assert isinstance(parsed, dict) self._groups = parsed self._filter_bins = [] self._build_legend_selection() @@ -627,7 +636,9 @@ def _build_bin_row(self, parent, sel_name, idx, b, kind): # the alignment: both row types share one widget recipe for their # leading column instead of trying to match a native Checkbutton's # unmeasurable indicator box. - check_lbl = tk.Label(row, text="[ ]", font=FONT_BODY, bg=BG_CARD, fg=FG_DIM, width=CHECK_COL_WIDTH, anchor="w") + check_lbl = tk.Label( + row, text="[ ]", font=FONT_BODY, bg=BG_CARD, fg=FG_DIM, width=CHECK_COL_WIDTH, anchor="w" + ) check_lbl.pack(side="left") self._check_labels[(sel_name, idx)] = check_lbl From 626ae194694bc42730b0707805afd74297323c73 Mon Sep 17 00:00:00 2001 From: Anton Riedel Date: Fri, 10 Jul 2026 15:38:54 +0200 Subject: [PATCH 6/8] Fix: fix linter error --- PWGCF/Femto/Macros/cutculator_gui.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/PWGCF/Femto/Macros/cutculator_gui.py b/PWGCF/Femto/Macros/cutculator_gui.py index ac166e36e68..227b6032ceb 100644 --- a/PWGCF/Femto/Macros/cutculator_gui.py +++ b/PWGCF/Femto/Macros/cutculator_gui.py @@ -409,10 +409,12 @@ def _on_hist_selected(self, _e=None): if is_filter: # load_bins_from_hist returns a flat list of bin dicts when is_filter - # is True; the assert lets the type checker narrow `parsed` from the - # dict|list union it infers from the function's two return shapes, - # instead of leaving self._groups statically ambiguous. - assert isinstance(parsed, list) + # is True. This check narrows `parsed` from the dict|list union the + # type checker infers from the function's two return shapes (same + # purpose an assert would serve, but unlike assert this is never + # stripped out under python -O, so it stays a real runtime guard). + if not isinstance(parsed, list): + raise TypeError("load_bins_from_hist() must return a list of bin dicts when is_filter is True") self._filter_bins = parsed self._groups = {} self._build_legend_filter() @@ -422,7 +424,8 @@ def _on_hist_selected(self, _e=None): else: # load_bins_from_hist returns a dict of SelectionName -> bins when # is_filter is False; same narrowing purpose as above. - assert isinstance(parsed, dict) + if not isinstance(parsed, dict): + raise TypeError("load_bins_from_hist() must return a dict of bins when is_filter is False") self._groups = parsed self._filter_bins = [] self._build_legend_selection() From a91217ec030ca32c10742f24a1ddbce0d23ac974 Mon Sep 17 00:00:00 2001 From: Anton Riedel Date: Fri, 10 Jul 2026 15:46:18 +0200 Subject: [PATCH 7/8] Feat: silence import error --- PWGCF/Femto/Macros/cutculator.py | 3 +-- PWGCF/Femto/Macros/cutculator_gui.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/PWGCF/Femto/Macros/cutculator.py b/PWGCF/Femto/Macros/cutculator.py index b3bf0f2bef1..7ce759182dc 100644 --- a/PWGCF/Femto/Macros/cutculator.py +++ b/PWGCF/Femto/Macros/cutculator.py @@ -15,8 +15,7 @@ @brief CutCulator (Compute bitmask for selecting particles in the Femto Framework) @author Anton Riedel , Technical University of Munich """ - -import ROOT +import ROOT # pylint: disable=import-error import argparse VALUE_DELIM = "___" diff --git a/PWGCF/Femto/Macros/cutculator_gui.py b/PWGCF/Femto/Macros/cutculator_gui.py index 227b6032ceb..e70b5b8f6c7 100644 --- a/PWGCF/Femto/Macros/cutculator_gui.py +++ b/PWGCF/Femto/Macros/cutculator_gui.py @@ -22,7 +22,7 @@ from typing import Any, Dict, List try: - import ROOT + import ROOT # pylint: disable=import-error ROOT.gROOT.SetBatch(True) except ImportError: @@ -639,9 +639,7 @@ def _build_bin_row(self, parent, sel_name, idx, b, kind): # the alignment: both row types share one widget recipe for their # leading column instead of trying to match a native Checkbutton's # unmeasurable indicator box. - check_lbl = tk.Label( - row, text="[ ]", font=FONT_BODY, bg=BG_CARD, fg=FG_DIM, width=CHECK_COL_WIDTH, anchor="w" - ) + check_lbl = tk.Label(row, text="[ ]", font=FONT_BODY, bg=BG_CARD, fg=FG_DIM, width=CHECK_COL_WIDTH, anchor="w") check_lbl.pack(side="left") self._check_labels[(sel_name, idx)] = check_lbl From aeed455b124e0a26fcd8db09c11b9d9f721565b8 Mon Sep 17 00:00:00 2001 From: Anton Riedel Date: Fri, 10 Jul 2026 16:22:31 +0200 Subject: [PATCH 8/8] Fix: fix includes --- PWGCF/Femto/Core/baseSelection.h | 1 - PWGCF/Femto/Core/selectionContainer.h | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/PWGCF/Femto/Core/baseSelection.h b/PWGCF/Femto/Core/baseSelection.h index 65d068bdce7..5c46c4747a3 100644 --- a/PWGCF/Femto/Core/baseSelection.h +++ b/PWGCF/Femto/Core/baseSelection.h @@ -30,7 +30,6 @@ #include #include #include -#include #include #include diff --git a/PWGCF/Femto/Core/selectionContainer.h b/PWGCF/Femto/Core/selectionContainer.h index 86792d6d705..cf3b7da0d11 100644 --- a/PWGCF/Femto/Core/selectionContainer.h +++ b/PWGCF/Femto/Core/selectionContainer.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include