@@ -37,10 +37,18 @@ class TPCVDriftManager
3737 mCCDB = ccdb;
3838 }
3939
40+ // Use the TPC side flags (with legacy-data fallback) instead of the tgl-sign-based correction.
41+ // Off by default so that existing analyses see no change in results until this is explicitly
42+ // enabled for testing. Tasks using TPCVDriftManager can expose this via their own Configurable.
43+ void setUseSideBasedCorrection (bool value) noexcept
44+ {
45+ mUseSideBasedCorrection = value;
46+ }
47+
4048 void update (uint64_t timestamp) noexcept
4149 {
4250 // Keep the object we already have if it is still valid for this timestamp.
43- // firstTime/lastTime are the first and last timestamps of the TFs the correction
51+ // firstTime/lastTime are the first and last timestamps of the TF the correction
4452 // was derived from, so the validity range is closed on both ends.
4553 if (mVD != nullptr && timestamp >= static_cast <uint64_t >(mVD ->firstTime ) && timestamp <= static_cast <uint64_t >(mVD ->lastTime )) {
4654 return ;
@@ -136,7 +144,7 @@ class TPCVDriftManager
136144 float dTime = tTB - trackExtra.trackTime ();
137145 float dDrift = dTime * mTPCVDriftNS ;
138146 float dDriftErr = tTBErr * mTPCVDriftNS ;
139- if (dDriftErr < 0 .f || dDrift > 250 . f ) { // we cannot move a track outside the drift volume
147+ if (dDriftErr < 0 .f || dDrift > mMaxDriftCm ) { // we cannot move a track outside the drift volume
140148 if (mOutside < mWarningLimit ) {
141149 LOGP (warn, " Skipping correction outside of tpc volume with dDrift={} +- {}" , dDrift, dDriftErr);
142150 const auto trackBC = trackExtra.template collision_as <Collisions>().template foundBC_as <BCs>().globalBC ();
@@ -152,7 +160,54 @@ class TPCVDriftManager
152160 }
153161
154162 // impose new Z coordinate
155- track.setZ (track.getZ () + ((track.getTgl () < 0 .) ? -dDrift : dDrift));
163+ float zShift = 0 .f ;
164+ if (!mUseSideBasedCorrection ) {
165+ // Legacy behaviour (default): infer the side from tgl alone.
166+ zShift = (track.getTgl () < 0 .f ) ? -dDrift : dDrift;
167+ } else {
168+ const auto sides = (trackExtra.flags () & (o2::aod::track::TrackFlags::TPCSideA | o2::aod::track::TrackFlags::TPCSideC));
169+ if (sides == o2::aod::track::TrackFlags::TPCSideA) {
170+ zShift = dDrift;
171+ } else if (sides == o2::aod::track::TrackFlags::TPCSideC) {
172+ zShift = -dDrift;
173+ } else if (sides == 0 ) {
174+ // Fallback for datasets produced before the TPC side flags were introduced (Feb. 2026).
175+ o2::aod::track::extensions::TPCTimeErrEncoding tEnc;
176+ tEnc.encoding .timeErr = trackExtra.trackTimeRes ();
177+ const float dFwd = tEnc.getDeltaTFwd ();
178+ const float dBwd = tEnc.getDeltaTBwd ();
179+ // Equal, small forward/backward margins mean the track is bounded on both ends,
180+ // i.e. it crosses the CE: it cannot be moved and is already corrected elsewhere.
181+ const bool crossesCE = (dFwd == dBwd) && (dFwd < mMaxCECrossingDeltaTNS );
182+ if (!crossesCE) {
183+ const bool zPositive = track.getZ () > 0 .f ;
184+ const bool tglPositive = track.getTgl () > 0 .f ;
185+ int side = 0 ; // +1 = A, -1 = C, 0 = undetermined -> leave uncorrected
186+ if (zPositive == tglPositive) {
187+ // Consistent sign: the track converges to Z=0 at the beamline by construction.
188+ side = tglPositive ? 1 : -1 ;
189+ } else if (dBwd == 0 .f && dFwd > 0 .f ) {
190+ // Bounded backward at the CE with room forward: no clusters on the opposite
191+ // side, so trust the measured Z rather than the (here inverted) tgl.
192+ side = zPositive ? 1 : -1 ;
193+ } else if (dBwd > 0 .f ) {
194+ // Large tgl track bounded at the readout side instead: trust tgl.
195+ side = tglPositive ? 1 : -1 ;
196+ }
197+ // else: degenerate case, track touches both CE and readout -> cannot be deduced/moved.
198+ // (in practice unreachable here: dFwd==dBwd==0 would already satisfy crossesCE above,
199+ // since dFwd/dBwd are always >= 0; kept explicit to mirror the reference logic 1:1.)
200+
201+ if (side > 0 ) {
202+ zShift = dDrift;
203+ } else if (side < 0 ) {
204+ zShift = -dDrift;
205+ }
206+ }
207+ }
208+ // else: track has clusters on both sides (crossed the CE) and is already corrected elsewhere
209+ }
210+ track.setZ (track.getZ () + zShift);
156211 if constexpr (std::is_base_of_v<o2::track::TrackParCov, Track>) {
157212 track.setCov (track.getSigmaZ2 () + dDriftErr * dDriftErr, o2::track::kSigZ2 );
158213 }
@@ -169,6 +224,7 @@ class TPCVDriftManager
169224
170225 private:
171226 bool mValid {false };
227+ bool mUseSideBasedCorrection {false }; // off by default: preserves legacy tgl-sign behaviour
172228 // Factors
173229 float mTPCVDriftNS {0 .f }; // drift velocity in cm/ns
174230
@@ -177,6 +233,8 @@ class TPCVDriftManager
177233 o2::ccdb::BasicCCDBManager* mCCDB {}; // reference to initialized ccdb manager
178234
179235 static constexpr unsigned int mWarningLimit {10 };
236+ static constexpr float mMaxDriftCm {250 .f }; // TPC drift volume half-length in cm
237+ static constexpr float mMaxCECrossingDeltaTNS {1000 .f }; // ~1 us, ballpark forward/backward time margin of a CE-crossing track
180238
181239 // Counters
182240 unsigned int mCalls {0 }; // total number of calls
0 commit comments