diff --git a/Common/Core/TPCVDriftManager.h b/Common/Core/TPCVDriftManager.h index d7c5b25be19..21f98ebffaf 100644 --- a/Common/Core/TPCVDriftManager.h +++ b/Common/Core/TPCVDriftManager.h @@ -37,10 +37,18 @@ class TPCVDriftManager mCCDB = ccdb; } + // Use the TPC side flags (with legacy-data fallback) instead of the tgl-sign-based correction. + // Off by default so that existing analyses see no change in results until this is explicitly + // enabled for testing. Tasks using TPCVDriftManager can expose this via their own Configurable. + void setUseSideBasedCorrection(bool value) noexcept + { + mUseSideBasedCorrection = value; + } + void update(uint64_t timestamp) noexcept { // Keep the object we already have if it is still valid for this timestamp. - // firstTime/lastTime are the first and last timestamps of the TFs the correction + // firstTime/lastTime are the first and last timestamps of the TF the correction // was derived from, so the validity range is closed on both ends. if (mVD != nullptr && timestamp >= static_cast(mVD->firstTime) && timestamp <= static_cast(mVD->lastTime)) { return; @@ -115,7 +123,7 @@ class TPCVDriftManager float dTime = tTB - trackExtra.trackTime(); float dDrift = dTime * mTPCVDriftNS; float dDriftErr = tTBErr * mTPCVDriftNS; - if (dDriftErr < 0.f || dDrift > 250.f) { // we cannot move a track outside the drift volume + if (dDriftErr < 0.f || dDrift > mMaxDriftCm) { // we cannot move a track outside the drift volume if (mOutside < mWarningLimit) { LOGP(warn, "Skipping correction outside of tpc volume with dDrift={} +- {}", dDrift, dDriftErr); const auto trackBC = trackExtra.template collision_as().template foundBC_as().globalBC(); @@ -131,7 +139,54 @@ class TPCVDriftManager } // impose new Z coordinate - track.setZ(track.getZ() + ((track.getTgl() < 0.) ? -dDrift : dDrift)); + float zShift = 0.f; + if (!mUseSideBasedCorrection) { + // Legacy behaviour (default): infer the side from tgl alone. + zShift = (track.getTgl() < 0.f) ? -dDrift : dDrift; + } else { + const auto sides = (trackExtra.flags() & (o2::aod::track::TrackFlags::TPCSideA | o2::aod::track::TrackFlags::TPCSideC)); + if (sides == o2::aod::track::TrackFlags::TPCSideA) { + zShift = dDrift; + } else if (sides == o2::aod::track::TrackFlags::TPCSideC) { + zShift = -dDrift; + } else if (sides == 0) { + // Fallback for datasets produced before the TPC side flags were introduced (Feb. 2026). + o2::aod::track::extensions::TPCTimeErrEncoding tEnc; + tEnc.encoding.timeErr = trackExtra.trackTimeRes(); + const float dFwd = tEnc.getDeltaTFwd(); + const float dBwd = tEnc.getDeltaTBwd(); + // Equal, small forward/backward margins mean the track is bounded on both ends, + // i.e. it crosses the CE: it cannot be moved and is already corrected elsewhere. + const bool crossesCE = (dFwd == dBwd) && (dFwd < mMaxCECrossingDeltaTNS); + if (!crossesCE) { + const bool zPositive = track.getZ() > 0.f; + const bool tglPositive = track.getTgl() > 0.f; + int side = 0; // +1 = A, -1 = C, 0 = undetermined -> leave uncorrected + if (zPositive == tglPositive) { + // Consistent sign: the track converges to Z=0 at the beamline by construction. + side = tglPositive ? 1 : -1; + } else if (dBwd == 0.f && dFwd > 0.f) { + // Bounded backward at the CE with room forward: no clusters on the opposite + // side, so trust the measured Z rather than the (here inverted) tgl. + side = zPositive ? 1 : -1; + } else if (dBwd > 0.f) { + // Large tgl track bounded at the readout side instead: trust tgl. + side = tglPositive ? 1 : -1; + } + // else: degenerate case, track touches both CE and readout -> cannot be deduced/moved. + // (in practice unreachable here: dFwd==dBwd==0 would already satisfy crossesCE above, + // since dFwd/dBwd are always >= 0; kept explicit to mirror the reference logic 1:1.) + + if (side > 0) { + zShift = dDrift; + } else if (side < 0) { + zShift = -dDrift; + } + } + } + // else: track has clusters on both sides (crossed the CE) and is already corrected elsewhere + } + track.setZ(track.getZ() + zShift); if constexpr (std::is_base_of_v) { track.setCov(track.getSigmaZ2() + dDriftErr * dDriftErr, o2::track::kSigZ2); } @@ -148,6 +203,7 @@ class TPCVDriftManager private: bool mValid{false}; + bool mUseSideBasedCorrection{false}; // off by default: preserves legacy tgl-sign behaviour // Factors float mTPCVDriftNS{0.f}; // drift velocity in cm/ns @@ -156,6 +212,8 @@ class TPCVDriftManager o2::ccdb::BasicCCDBManager* mCCDB{}; // reference to initialized ccdb manager static constexpr unsigned int mWarningLimit{10}; + static constexpr float mMaxDriftCm{250.f}; // TPC drift volume half-length in cm + static constexpr float mMaxCECrossingDeltaTNS{1000.f}; // ~1 us, ballpark forward/backward time margin of a CE-crossing track // Counters unsigned int mCalls{0}; // total number of calls