diff --git a/PWGCF/Femto/Core/baseSelection.h b/PWGCF/Femto/Core/baseSelection.h index 947e766bb50..5e5b0deab97 100644 --- a/PWGCF/Femto/Core/baseSelection.h +++ b/PWGCF/Femto/Core/baseSelection.h @@ -125,20 +125,24 @@ class BaseSelection addSelection(observableIndex); } - /// \brief Add a static-value based selection for a specific observable. + /// \brief Add a range-based selection (either inside or outside the configured ranges) for a + /// specific observable. /// \param observableIndex Index of the observable. /// \param selectionName Name of the selection. - /// \param selectionValues Vector of threshold values. - /// \param limitType Type of limit (from limits::LimitType). + /// \param selectionRanges Vector of "lower;upper" range strings. /// \param skipMostPermissiveBit Whether to skip the loosest threshold when assembling the bitmask. /// \param isMinimalCut Whether this cut is mandatory (must be passed for the candidate to be accepted). /// \param isOptionalCut Whether this cut is optional (candidate is accepted if any optional cut passes). + /// \param limitType Either limits::kInsideRange (value must fall inside) or limits::kOutsideRange + /// (value must fall outside, e.g. rejecting a competing mass hypothesis). A + /// fully-open range entry (";") is exempt for either type -- see SelectionContainer. void addSelection(int observableIndex, std::string const& selectionName, std::vector const& selectionRanges, bool skipMostPermissiveBit, bool isMinimalCut, - bool isOptionalCut) + bool isOptionalCut, + limits::LimitType limitType = limits::kInsideRange) { // check index if (static_cast(observableIndex) >= NumObservables) { @@ -149,7 +153,8 @@ class BaseSelection selectionRanges, mPassThrough ? false : skipMostPermissiveBit, mPassThrough ? false : isMinimalCut, - mPassThrough ? false : isOptionalCut); + mPassThrough ? false : isOptionalCut, + limitType); addSelection(observableIndex); } @@ -248,6 +253,42 @@ class BaseSelection } } + /// \brief Evaluate the same observable for two independently-computed values of the same + /// candidate and store only the logical AND of the two resulting per-threshold bitmasks + /// \param observableIndex Index of the observable. + /// \param value1 First value to evaluate. + /// \param value2 Second value to evaluate. + void evaluateObservable(int observableIndex, T value1, T value2) + { + // if there are no selections configured, bail out + if (mSelectionContainers.at(observableIndex).isEmpty()) { + return; + } + // if a previous observable already failed a minimal selection, + // there is no point in evaluating further observables + if (!mPassesMinimalSelections) { + return; + } + auto& container = mSelectionContainers.at(observableIndex); + container.evaluate(value1); + auto const bitmask1 = container.getRawBitmask(); + container.evaluate(value2); + auto const bitmask2 = container.getRawBitmask(); + container.setBitmask((bitmask1 & bitmask2).to_ullong()); + // if any minimal selection is not fulfilled, the candidate is rejected + if (mHasMinimalSelection) { + if (!container.passesAsMinimalCut()) { + mPassesMinimalSelections = false; + } + } + // if any optional selection is fulfilled, the candidate is accepted + if (mHasOptionalSelection) { + if (container.passesAsOptionalCut()) { + mPassesOptionalSelections = true; + } + } + } + /// \brief Evaluate a single observable against its configured selections. /// \param observableIndex Index of the observable. /// \param values Vector of values of the observable. diff --git a/PWGCF/Femto/Core/cascadeBuilder.h b/PWGCF/Femto/Core/cascadeBuilder.h index c0976be17d1..a8ab5f6620c 100644 --- a/PWGCF/Femto/Core/cascadeBuilder.h +++ b/PWGCF/Femto/Core/cascadeBuilder.h @@ -48,19 +48,11 @@ struct ConfCascadeFilters : o2::framework::ConfigurableGroup { o2::framework::Configurable etaMax{"etaMax", 10.f, "Maximum eta"}; o2::framework::Configurable phiMin{"phiMin", 0.f, "Minimum phi"}; o2::framework::Configurable phiMax{"phiMax", 1.f * o2::constants::math::TwoPI, "Maximum phi"}; - o2::framework::Configurable massLambdaMin{"massLambdaMin", 1.0f, "Minimum mass of Lambda daughters in cascade decay"}; - o2::framework::Configurable massLambdaMax{"massLambdaMax", 1.2f, "Maximum mass of Lambda daughters in cascade decay"}; o2::framework::Configurable massXiMin{"massXiMin", 1.2f, "Minimum Xi mass"}; o2::framework::Configurable massXiMax{"massXiMax", 1.4f, "Maximum Xi mass"}; - o2::framework::Configurable rejectHypothesisOmega{"rejectHypothesisOmega", false, "Rejection of Omega hypothesis for Xi candidates"}; - o2::framework::Configurable rejectMassOmegaMin{"rejectMassOmegaMin", 1.668f, "Reject minimum Omega mass for Xi hypothesis"}; - o2::framework::Configurable rejectMassOmegaMax{"rejectMassOmegaMax", 1.676f, "Reject maximum Omega mass for Xi hypothesis"}; o2::framework::Configurable massOmegaMin{"massOmegaMin", 1.5f, "Minimum Omega mass"}; o2::framework::Configurable massOmegaMax{"massOmegaMax", 1.9f, "Maximum Omega mass"}; - o2::framework::Configurable rejectHypothesisXi{"rejectHypothesisXi", false, "Rejection of Xi hypothesis for Omega candidates"}; - o2::framework::Configurable rejectMassXiMin{"rejectMassXiMin", 1.317f, "Reject Minimum Xi mass for Omega hypothesis"}; - o2::framework::Configurable rejectMassXiMax{"rejectMassXiMax", 1.325f, "Reject Maximum Xi mass for Omega hypothesis"}; }; // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) @@ -74,6 +66,7 @@ struct ConfCascadeFilters : o2::framework::ConfigurableGroup { o2::framework::Configurable> lambdaTransRadMin{"lambdaTransRadMin", {0.9f}, "Minimum transverse radius (cm)"}; \ o2::framework::Configurable> lambdaDcaDauMax{"lambdaDcaDauMax", {0.5f}, "Maximum DCA between the daughters at decay vertex (cm)"}; \ o2::framework::Configurable> lambdaDcaToPvMin{"lambdaDcaToPvMin", {0.3f}, "Minimum DCA between the lambda and primary vertex"}; \ + o2::framework::Configurable> lambdaMassRanges{"lambdaMassRanges", {"1.0;1.2"}, "Lambda daughter mass ranges (lower;upper) to accept -- bit is set if the cascade's mLambda falls inside the range"}; \ o2::framework::Configurable> dauAbsEtaMax{"dauAbsEtaMax", {0.8f}, "Maximum |eta| of all daughters"}; \ o2::framework::Configurable> dauAbsDcaxyMin{"dauAbsDcaxyMin", {0.05f}, "Minimum |DCAxy| of the daughters and bachelor from primary vertex (cm)"}; \ o2::framework::Configurable> dauTpcClustersMin{"dauTpcClustersMin", {80.f}, "Minimum number of TPC clusters for daughter tracks"}; \ @@ -89,6 +82,7 @@ struct ConfXiBits : o2::framework::ConfigurableGroup { CASCADE_DEFAULT_BITS o2::framework::Configurable> bachelorTpcPion{"bachelorTpcPion", {5.f}, "Maximum |nsigma_Pion| TPC for bachelor tracks"}; o2::framework::Configurable> bachelorTofPion{"bachelorTofPion", {}, "Maximum |nsigma_Pion| TOF for bachelor tracks"}; + o2::framework::Configurable> rejectOmegaMassRanges{"rejectOmegaMassRanges", {"1.668;1.676"}, "Omega mass ranges (lower;upper) to reject -- bit is set if the candidate's mOmega falls outside the range"}; }; struct ConfOmegaBits : o2::framework::ConfigurableGroup { @@ -96,6 +90,7 @@ struct ConfOmegaBits : o2::framework::ConfigurableGroup { CASCADE_DEFAULT_BITS o2::framework::Configurable> bachelorTpcKaon{"bachelorTpcKaon", {5.f}, "Maximum |nsigma_Kaon| TPC for bachelor tracks"}; o2::framework::Configurable> bachelorTofKaon{"bachelorTofKaon", {}, "Maximum |nsigma_Kaon| TOF for bachelor tracks"}; + o2::framework::Configurable> rejectXiMassRanges{"rejectXiMassRanges", {"1.317;1.325"}, "Xi mass ranges (lower;upper) to reject -- bit is set if the candidate's mXi falls outside the range"}; }; #undef CASCADE_DEFAULT_BITS @@ -139,6 +134,11 @@ enum CascadeSels { kLambdaDcaDauMax, ///< Max. DCA between the lambda daughters at lambda decay vertex kLambdaTransRadMin, ///< Min. transverse radius of the lambda kLambdaDcaToPvMin, ///< Min. DCA of the lambda to the primary vertex + kLambdaMassRange, ///< Lambda daughter mass must fall inside the configured range(s) + + // rejection of competing mass hypotheses (range-based: bit is set if the candidate is outside the range) + kRejectOmegaMass, ///< Xi: reject Omega mass hypothesis + kRejectXiMass, ///< Omega: reject Xi mass hypothesis // selection for bachelor/daughters kDauAbsEtaMax, ///< Max. |eta| of daughter tracks @@ -173,6 +173,10 @@ const std::unordered_map cascadeSelectionNames = { {kLambdaDcaDauMax, "Lambda DCA Daughter Max"}, {kLambdaTransRadMin, "Lambda Transverse Radius Min"}, {kLambdaDcaToPvMin, "Lambda DCA to PV Min"}, + {kLambdaMassRange, "Lambda Daughter Mass Range"}, + + {kRejectOmegaMass, "Reject Omega mass hypothesis"}, + {kRejectXiMass, "Reject Xi mass hypothesis"}, {kDauAbsEtaMax, "Daughter Abs Eta Max"}, {kDauTpcClsMin, "Daughter TPC Clusters Min"}, @@ -195,14 +199,8 @@ enum CascadeFilters { kEtaMax, kPhiMin, kPhiMax, - kLambdaMassMin, - kLambdaMassMax, - kXiMassMin, - kXiMassMax, - kRejectionOmegaMass, - kOmegaMassMin, - kOmegaMassMax, - kRejectionXiMass, + kMassMin, ///< signal mass window lower bound, for this cascadeType's own mass hypothesis + kMassMax, ///< signal mass window upper bound, for this cascadeType's own mass hypothesis kCascadeFiltersMax }; @@ -215,14 +213,8 @@ const std::unordered_map cascadeFilterNames = { {kEtaMax, "etaMax"}, {kPhiMin, "phiMin"}, {kPhiMax, "phiMax"}, - {kLambdaMassMin, "lambdaMassMin"}, - {kLambdaMassMax, "lambdaMassMax"}, - {kXiMassMin, "xiMassMin"}, - {kXiMassMax, "xiMassMax"}, - {kRejectionOmegaMass, "rejectOmega"}, - {kOmegaMassMin, "omegaMassMin"}, - {kOmegaMassMax, "omegaMassMax"}, - {kRejectionXiMass, "rejectXi"}, + {kMassMin, "massMin"}, + {kMassMax, "massMax"}, }; /// \class FemtoDreamTrackCuts @@ -245,8 +237,6 @@ class CascadeSelection : public baseselection::BaseSelection const& rangeStrings, bool skipMostPermissiveBit, bool isMinimalCut, - bool isOptionalCut) + bool isOptionalCut, + limits::LimitType limitType = limits::kInsideRange) : mSelectionName(selectionName), - mLimitType(limits::kRange), + mLimitType(limitType), mSkipMostPermissiveBit(skipMostPermissiveBit), mIsMinimalCut(isMinimalCut), mIsOptionalCut(isOptionalCut) { + if (mLimitType != limits::kInsideRange && mLimitType != limits::kOutsideRange) { + LOG(fatal) << "Range-string selections only support kInsideRange or kOutsideRange"; + } if (rangeStrings.size() > sizeof(BitmaskType) * CHAR_BIT) { LOG(fatal) << "Too many selections for a single observable. Limit is " << sizeof(BitmaskType) * CHAR_BIT; } @@ -158,7 +170,9 @@ class SelectionContainer parseRangeString(rangeStr, lower, upper); mSelectionRanges.emplace_back(lower, upper); } - // ranges are sorted by their lenghts, i.e. from widest range to tightest range + // for kInsideRange, ranges are sorted from widest (most permissive) to tightest; for + // kOutsideRange it's the opposite -- the narrowest excluded region is the most permissive + // (fewer values get rejected) // in principle the ranges do not have to include each other, exepct this is configured as minimal cut, check is added at the end // to cover both cases, this also means we always check all ranges, when assembling the bit mask sortSelections(); @@ -167,15 +181,25 @@ class SelectionContainer mSelectionValues.push_back(mSelectionRanges[i].second - mSelectionRanges[i].first); } - // for minimal range cut, ranges must be strictly nested (each range contains all narrower ones) - // this is required for the early-exit logic in passesAsMinimalCut() to be correct + // for a minimal range cut, ranges must be strictly nested (each range contains all ranges that + // sort after it) -- this is required for the single-bit shortcut in passesAsMinimalCut() to be + // correct. For kInsideRange (sorted widest-first) that means range[i] must contain range[i+1]; + // for kOutsideRange (sorted narrowest-first) it's the reverse: range[i] must be contained in + // range[i+1]. A fully-open ("reject nothing") entry is the empty interval [max, -max], which is + // trivially contained in (or containing, for kInsideRange) anything, so this check already holds + // for it without any special-casing. if (isMinimalCut) { for (std::size_t i = 0; i + 1 < mSelectionRanges.size(); i++) { - if (mSelectionRanges[i].first > mSelectionRanges[i + 1].first || - mSelectionRanges[i].second < mSelectionRanges[i + 1].second) { + bool const nested = (mLimitType == limits::kOutsideRange) + ? (mSelectionRanges[i].first >= mSelectionRanges[i + 1].first && + mSelectionRanges[i].second <= mSelectionRanges[i + 1].second) + : (mSelectionRanges[i].first <= mSelectionRanges[i + 1].first && + mSelectionRanges[i].second >= mSelectionRanges[i + 1].second); + if (!nested) { LOG(fatal) << "Ranges for minimal cut " << selectionName << " are not nested. Range [" << mSelectionRanges[i].first << ";" << mSelectionRanges[i].second - << "] does not contain [" << mSelectionRanges[i + 1].first << ";" << mSelectionRanges[i + 1].second << "]"; + << "] is not " << (mLimitType == limits::kOutsideRange ? "contained in" : "containing") + << " [" << mSelectionRanges[i + 1].first << ";" << mSelectionRanges[i + 1].second << "]"; } } } @@ -318,7 +342,7 @@ class SelectionContainer } } break; - case (limits::kRange): + case (limits::kInsideRange): // ranges are sorted widest-first but a narrower range passing does not imply a wider one fails (no check on boundries), // so all ranges must be checked explicitly — no early exit for (std::size_t i = 0; i < mSelectionRanges.size(); i++) { @@ -327,6 +351,18 @@ class SelectionContainer } } break; + case (limits::kOutsideRange): + // ranges are sorted narrowest-first (most permissive -- smallest excluded region); same + // no-early-exit reasoning as kInsideRange applies, just with the pass condition inverted. + // A fully-open range string (";") is parsed as the empty interval [max, -max] (see + // parseRangeString()), so this plain check already evaluates true for every value -- no + // special-casing needed here to express "reject nothing". + for (std::size_t i = 0; i < mSelectionRanges.size(); i++) { + if (value < mSelectionRanges.at(i).first || value > mSelectionRanges.at(i).second) { + mBitmask.set(i); + } + } + break; case (limits::kEqual): // kEqual has no natural ordering, so all thresholds must be checked and we cannot bail early for (std::size_t i = 0; i < mSelectionValues.size(); i++) { @@ -385,6 +421,14 @@ class SelectionContainer mBitmask = std::bitset(bitmask); } + /// \brief Retrieve the raw (unshifted) internal bitmask, including the most-permissive-threshold + /// bit even if mSkipMostPermissiveBit is set. Used when two separate evaluate() calls for + /// the same observable need to be combined externally (e.g. logical AND) before the + /// result is written back via setBitmask() -- unlike getBitmask(), this never shifts, so + /// it stays the correct counterpart to setBitmask() (which also never shifts). + /// \return Bitset exactly as currently stored. + [[nodiscard]] std::bitset const& getRawBitmask() const { return mBitmask; } + /// \brief Reset the internal bitmask to zero. void reset() { mBitmask.reset(); } @@ -458,25 +502,65 @@ class SelectionContainer /// \brief Build a histogram bin label string encoding the full configuration of a single threshold. /// \param selectionIndex Index of the threshold within this container. /// \return Encoded label string. + /// \brief Describes an open (sentinel, i.e. omitted-by-the-user) side of a range, if any. + /// For kOutsideRange, a single omitted side flips which half of the "value < lower || + /// value > upper" OR-condition stays active, so the resulting satisfied/reject direction + /// is spelled out explicitly rather than named generically -- omitting the upper bound + /// there means the lower-bound inequality alone remains, i.e. "reject value < lower". + /// \param range Lower/upper bound pair for a kInsideRange/kOutsideRange threshold. + /// \return A human-readable description of the effective cut, or "" if both sides are bounded. + [[nodiscard]] std::string getOpenSideComment(std::pair const& range) const + { + bool const lowerOpen = std::abs(range.first) == std::numeric_limits::max(); + bool const upperOpen = std::abs(range.second) == std::numeric_limits::max(); + if (!lowerOpen && !upperOpen) { + return ""; + } + if (lowerOpen && upperOpen) { + return "no cut"; + } + std::ostringstream oss; + if (mLimitType == limits::kInsideRange) { + oss << "accept value " << (upperOpen ? ">= " : "<= ") << (upperOpen ? range.first : range.second); + } else { + oss << "reject value " << (upperOpen ? "< " : "> ") << (upperOpen ? range.first : range.second); + } + return oss.str(); + } + [[nodiscard]] std::string getBinLabel(int selectionIndex) const { std::ostringstream oss; // Determine value string std::string valueStr; + std::string autoComment; - if (mLimitType == limits::kRange) { - // Print actual lower;upper interval + if (mLimitType == limits::kInsideRange || mLimitType == limits::kOutsideRange) { + // Print the actual lower;upper interval -- kept as real, parseable numbers (not a word like + // "no cut") since this Value field is read by cutculator.py; an open side (lower and/or + // upper bound left at the sentinel, i.e. omitted by the user) is instead flagged via the + // Comment field below. const auto& range = mSelectionRanges.at(selectionIndex); std::ostringstream rangeStream; rangeStream << range.first << RangeDelimiter << range.second; valueStr = rangeStream.str(); + autoComment = getOpenSideComment(range); } else if (mSelectionFunctions.empty()) { valueStr = std::to_string(mSelectionValues.at(selectionIndex)); } else { valueStr = mSelectionFunctions.at(selectionIndex).GetExpFormula().Data(); } + // combine rather than override -- e.g. TOF PID selections attach a "minMomTofX = ..." comment + // to every threshold via addComments(), independent of whether a given threshold's range + // also happens to be open-sided + std::string const userComment = mComments.empty() ? "" : mComments.at(selectionIndex); + std::string comment = userComment; + if (!autoComment.empty()) { + comment = userComment.empty() ? autoComment : autoComment + "; " + userComment; + } + oss << "SelectionName" << ValueDelimiter << mSelectionName << SectionDelimiter << "LimitType" << ValueDelimiter << getLimitTypeAsString() << SectionDelimiter << "MinimalCut" << ValueDelimiter << (mIsMinimalCut ? "1" : "0") << SectionDelimiter @@ -486,7 +570,7 @@ class SelectionContainer << "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)); + << "Comment" << ValueDelimiter << (comment.empty() ? NoValue : comment); return oss.str(); } @@ -499,8 +583,16 @@ class SelectionContainer return std::string(mSelectionFunctions.at(selectionIndex).GetExpFormula().Data()); } if (!mSelectionRanges.empty()) { + const auto& range = mSelectionRanges.at(selectionIndex); + bool const lowerOpen = std::abs(range.first) == std::numeric_limits::max(); + bool const upperOpen = std::abs(range.second) == std::numeric_limits::max(); + if (lowerOpen && upperOpen) { + return std::string{"no cut"}; + } std::ostringstream oss; - oss << mSelectionRanges.at(selectionIndex).first << ";" << mSelectionRanges.at(selectionIndex).second; + oss << (lowerOpen ? "-inf" : std::to_string(range.first).c_str()) + << ";" + << (upperOpen ? "+inf" : std::to_string(range.second).c_str()); return oss.str(); } return std::to_string(mSelectionValues.at(selectionIndex)); @@ -559,13 +651,23 @@ class SelectionContainer case (limits::kAbsLowerLimit): std::sort(mSelectionValues.begin(), mSelectionValues.end(), [](T a, T b) { return a < b; }); break; - case (limits::kRange): + case (limits::kInsideRange): // sort by range width descending so the most permissive (widest) range comes first std::sort(mSelectionRanges.begin(), mSelectionRanges.end(), [](std::pair const& a, std::pair const& b) { return (a.second - a.first) > (b.second - b.first); }); break; + case (limits::kOutsideRange): + // opposite of kInsideRange: the most permissive (most likely to be satisfied) exclusion is + // the narrowest one, since it rejects the fewest values. A fully-open ("reject nothing") + // range parses to the empty interval [max, -max] (see parseRangeString()), whose "width" is + // the most negative possible value, so it naturally sorts first without any special-casing. + std::sort(mSelectionRanges.begin(), mSelectionRanges.end(), + [](std::pair const& a, std::pair const& b) { + return (a.second - a.first) < (b.second - b.first); + }); + break; default: break; } @@ -591,7 +693,12 @@ class SelectionContainer /// \brief Parse a range string of the form "lower;upper" into a lower and upper bound. /// Either bound may be omitted (e.g. ";1.0" or "0.5;"), in which case - /// -/+ numeric_limits::max() is used respectively. + /// -/+ numeric_limits::max() is used respectively. For kOutsideRange, a fully-open string + /// (";", both bounds omitted) is the one exception: it is parsed as the empty interval + /// [max, -max] instead of [-max, max] -- every value then trivially fails the normal + /// "value >= lower && value <= upper" check, i.e. every value counts as outside, which is + /// what "reject nothing" needs. [-max, max] would instead mean every value is inside, + /// i.e. nothing is ever outside, which would reject everything -- the opposite intent. /// \param rangeStr Input string to parse. /// \param lower Output lower bound. /// \param upper Output upper bound. @@ -604,6 +711,18 @@ class SelectionContainer std::string lowerStr = rangeStr.substr(0, pos); std::string upperStr = rangeStr.substr(pos + 1); + if (lowerStr.empty() && upperStr.empty() && mLimitType == limits::kOutsideRange) { + // kOutsideRange's "value < lower || value > upper" is the negation of kInsideRange's + // "value >= lower && value <= upper" -- substituting the same [-max, +max] that makes the + // latter trivially true for every value would make the former trivially FALSE for every + // value instead (nothing is < -max or > +max), i.e. "reject everything" rather than the + // intended "reject nothing". The empty interval [max, -max] is the encoding that keeps the + // disjunction trivially true for every value instead, giving the correct "exempt" meaning. + lower = std::numeric_limits::max(); + upper = -std::numeric_limits::max(); + return; + } + lower = lowerStr.empty() ? -std::numeric_limits::max() : static_cast(std::stod(lowerStr)); upper = upperStr.empty() ? std::numeric_limits::max() : static_cast(std::stod(upperStr)); @@ -615,7 +734,7 @@ class SelectionContainer std::string mSelectionName; std::vector mSelectionValues = {}; ///< Threshold values, sorted from most permissive to most restrictive std::vector mSelectionFunctions; ///< TF1 threshold functions (empty for static selections) - std::vector> mSelectionRanges = {}; ///< Lower and upper bounds for kRange selections, one pair per threshold + std::vector> mSelectionRanges = {}; ///< Lower and upper bounds for kInsideRange/kOutsideRange selections, one pair per threshold limits::LimitType mLimitType = limits::kLimitTypeLast; ///< Comparison type applied during evaluation bool mSkipMostPermissiveBit = false; ///< If true, the most permissive threshold does not occupy a bit in the global bitmask bool mIsMinimalCut = false; ///< If true, this selection is mandatory; failing it rejects the candidate diff --git a/PWGCF/Femto/Core/v0Builder.h b/PWGCF/Femto/Core/v0Builder.h index 491a8dee403..c753fd1edc6 100644 --- a/PWGCF/Femto/Core/v0Builder.h +++ b/PWGCF/Femto/Core/v0Builder.h @@ -51,14 +51,8 @@ struct ConfV0Filters : o2::framework::ConfigurableGroup { o2::framework::Configurable phiMax{"phiMax", 1.f * o2::constants::math::TwoPI, "Maximum phi"}; o2::framework::Configurable massMinLambda{"massMinLambda", 1.f, "Minimum mass for Lambda hypothesis"}; o2::framework::Configurable massMaxLambda{"massMaxLambda", 1.2f, "Maximum mass for Lambda hypothesis"}; - o2::framework::Configurable rejectHypothesisK0short{"rejectHypothesisK0short", true, "Rejection of K0short hypothesis for Lambda candidates"}; - o2::framework::Configurable rejectMassMinK0short{"rejectMassMinK0short", 0.475f, "Minimum mass to reject K0short hypothesis for Lambda candidates"}; - o2::framework::Configurable rejectMassMaxK0short{"rejectMassMaxK0short", 0.515f, "Maximum mass to reject K0short hypothesis for Lambda candidates"}; - o2::framework::Configurable massMinK0short{"massMinK0short", 0.45f, "Minimum mass for K0Short hypothesis"}; - o2::framework::Configurable massMaxK0short{"massMaxK0short", 0.53f, "Maximum mass for K0Short hypothesis"}; - o2::framework::Configurable rejectHypothesisLambda{"rejectHypothesisLambda", true, "Rejection of Lambda hypothesis for K0short candidates"}; - o2::framework::Configurable rejectMassMinLambda{"rejectMassMinLambda", 1.11f, "Minimum mass to reject Lambda hypothesis for K0short candidates"}; - o2::framework::Configurable rejectMassMaxLambda{"rejectMassMaxLambda", 1.12f, "Maximum mass to reject Lambda hypothesis for K0short candidates"}; + o2::framework::Configurable massMinK0short{"massMinK0short", 0.485f, "Minimum mass for K0Short hypothesis"}; + o2::framework::Configurable massMaxK0short{"massMaxK0short", 0.51f, "Maximum mass for K0Short hypothesis"}; }; // selections bits for all v0s @@ -88,6 +82,7 @@ struct ConfLambdaBits : o2::framework::ConfigurableGroup { o2::framework::Configurable> posDauTofProton{"posDauTofProton", {}, "Maximum |nsigma_Proton| TOF for positive daughter tracks"}; o2::framework::Configurable> negDauTofPion{"negDauTofPion", {}, "Maximum |nsigma_Pion| TOF for negative daughter tracks"}; o2::framework::Configurable> negDauTofProton{"negDauTofProton", {}, "Maximum |nsigma_Proton| TOF for negative daughter tracks"}; + o2::framework::Configurable> rejectK0shortMassRanges{"rejectK0shortMassRanges", {"0.475;0.515"}, "K0short mass ranges (lower;upper) to reject -- bit is set if the candidate's mK0Short falls outside the range"}; }; // derived selection bits for K0Short @@ -98,6 +93,7 @@ struct ConfK0shortBits : o2::framework::ConfigurableGroup { o2::framework::Configurable> negDauTpcPion{"negDauTpcPion", {5.f}, "Maximum |nsigma_Pion| TPC for negative daughter tracks"}; o2::framework::Configurable> posDauTofPion{"posDauTofPion", {}, "Maximum |nsigma_Pion| TOF for positive daughter tracks"}; o2::framework::Configurable> negDauTofPion{"negDauTofPion", {}, "Maximum |nsigma_Pion| TOF for negative daughter tracks"}; + o2::framework::Configurable> rejectLambdaMassRanges{"rejectLambdaMassRanges", {"1.11;1.12"}, "Lambda/AntiLambda mass ranges (lower;upper) to reject -- bit is set if the candidate's mLambda (resp. mAntiLambda) falls outside the range; same range for both since they share the same PDG mass"}; }; #undef V0_DEFAULT_BITS @@ -128,7 +124,7 @@ struct ConfLambdaSelection : o2::framework::ConfigurableGroup { template struct ConfK0shortSelection : o2::framework::ConfigurableGroup { std::string prefix = Prefix; - V0_DEFAULT_SELECTIONS(0.47, 0.51, 310) + V0_DEFAULT_SELECTIONS(0.485, 0.51, 310) o2::framework::Configurable sign{"sign", 0, "Dummy value. For compatibility with Lambda selection"}; }; @@ -174,6 +170,10 @@ enum V0Sels { kNegDaughTofPion, ///< TOF Pion PID for negative daughter kNegDaughTofProton, ///< TOF Proton PID for negative daughter + // rejection of competing mass hypotheses (range-based: bit is set if the candidate is outside the range) + kRejectK0shortMass, ///< Lambda/AntiLambda: reject K0short mass hypothesis + kRejectLambdaMass, ///< K0short: reject Lambda/AntiLambda mass hypothesis (single bit, see applySelections()) + kV0SelsMax }; @@ -199,7 +199,9 @@ const std::unordered_map v0SelectionNames = { {kPosDaughTofPion, "TOF Pion PID for positive daughter"}, {kPosDaughTofProton, "TOF Proton PID for positive daughter"}, {kNegDaughTofPion, "TOF Pion PID for negative daughter"}, - {kNegDaughTofProton, "TOF Proton PID for negative daughter"}}; + {kNegDaughTofProton, "TOF Proton PID for negative daughter"}, + {kRejectK0shortMass, "Reject K0short mass hypothesis"}, + {kRejectLambdaMass, "Reject Lambda/AntiLambda mass hypothesis"}}; // enum for all track filters (loose pre-selection, applied before quality/PID cuts) enum V0Filters { @@ -209,12 +211,8 @@ enum V0Filters { kEtaMax, kPhiMin, kPhiMax, - kRejectionK0shortMass, - kK0shortMassMin, - kK0shortMassMax, - kRejectionLambdaMass, - kLambdaMassMin, - kLambdaMassMax, + kMassMin, ///< signal mass window lower bound, for this v0Type's own mass hypothesis + kMassMax, ///< signal mass window upper bound, for this v0Type's own mass hypothesis kV0FiltersMax }; @@ -228,12 +226,8 @@ const std::unordered_map v0FilterNames = { {kEtaMax, "etaMax"}, {kPhiMin, "phiMin"}, {kPhiMax, "phiMax"}, - {kRejectionK0shortMass, "rejectK0short"}, - {kK0shortMassMin, "k0shortMassMin"}, - {kK0shortMassMax, "k0shortMassMax"}, - {kRejectionLambdaMass, "rejectLambda"}, - {kLambdaMassMin, "lambdaMassMin"}, - {kLambdaMassMax, "lambdaMassMax"}, + {kMassMin, "massMin"}, + {kMassMax, "massMax"}, }; /// \brief Cut class to contain and execute all cuts applied to v0s @@ -262,9 +256,8 @@ class V0Selection : public baseselection::BaseSelectionaddSelection(kRejectK0shortMass, v0SelectionNames.at(kRejectK0shortMass), config.rejectK0shortMassRanges.value, true, true, false, limits::kOutsideRange); if constexpr (modes::isEqual(v0Type, modes::V0::kLambda)) { this->addSelection(kPosDaughTpcProton, v0SelectionNames.at(kPosDaughTpcProton), config.posDauTpcProton.value, limits::kAbsUpperLimit, true, true, false); @@ -287,9 +280,8 @@ class V0Selection : public baseselection::BaseSelectionaddSelection(kRejectLambdaMass, v0SelectionNames.at(kRejectLambdaMass), config.rejectLambdaMassRanges.value, true, true, false, limits::kOutsideRange); 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); @@ -309,6 +301,10 @@ class V0Selection : public baseselection::BaseSelectionaddSelection(kDauTpcClsMin, v0SelectionNames.at(kDauTpcClsMin), config.dauTpcClustersMin.value, limits::kLowerLimit, true, true, false); this->setupSelectionHistogram(registry); + // signal mass window bins carry this v0Type's own mass hypothesis only -- otherwise e.g. the + // K0short filter histogram would also show Lambda's (irrelevant, default-valued) mass window + float const massLowerLimit = modes::isEqual(v0Type, modes::V0::kK0short) ? mMassK0shortLowerLimit : mMassLambdaLowerLimit; + float const massUpperLimit = modes::isEqual(v0Type, modes::V0::kK0short) ? mMassK0shortUpperLimit : mMassLambdaUpperLimit; this->template setupFilterHistogram( registry, { @@ -318,12 +314,8 @@ class V0Selection : public baseselection::BaseSelectionevaluateObservable(kRejectK0shortMass, v0candidate.mK0Short()); + } + if constexpr (modes::isEqual(v0Type, modes::V0::kK0short)) { + // K0short's V0Data carries both mLambda() and mAntiLambda(), but we only want a single + // rejection bit -- evaluate both against the configured range(s) and keep only the AND + // (bit set if BOTH independently fall outside), without assuming anything about where + // the configured range(s) are centered. See the two-value evaluateObservable() overload + // in baseSelection.h. + this->evaluateObservable(kRejectLambdaMass, v0candidate.mLambda(), v0candidate.mAntiLambda()); + } + this->assembleBitmask(); } @@ -430,49 +436,27 @@ class V0Selection : public baseselection::BaseSelectiontemplate 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. + // signal mass window: gating AND-cut (both bounds must pass). The competing hypothesis is no + // longer rejected here -- it's a stored bit instead 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); + this->template fillFilter(kMassMin, p); pass &= p; p = mass < mMassLambdaUpperLimit; - this->template fillFilter(kLambdaMassMax, p); - pass &= p; - - 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); + this->template fillFilter(kMassMax, p); pass &= p; } if constexpr (modes::isEqual(v0Type, modes::V0::kK0short)) { p = v0.mK0Short() > mMassK0shortLowerLimit; - this->template fillFilter(kK0shortMassMin, p); + this->template fillFilter(kMassMin, 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); + this->template fillFilter(kMassMax, p); pass &= p; } @@ -482,13 +466,11 @@ class V0Selection : public baseselection::BaseSelection