From faef6b7cc4a9b9ec64715ffcd2ef2cdc4886c76f Mon Sep 17 00:00:00 2001 From: Matthias Kleiner Date: Wed, 4 Nov 2026 14:54:38 +0100 Subject: [PATCH 1/2] TPC SCD: round instead of truncate when packing the unbinned residuals UnbinnedResid packed y, z, dy, dz and tgSlp into shorts with a plain cast, i.e. truncated towards zero: every value moved towards zero by up to one step (15 um for y, 6 um for dy). The decoded cluster position y + dy then depends on the sign of dy below the step size, and since the cluster positions lie on the ClusterNative 1/64-pad grid, binning residuals by the cluster position on sub-pad scales splits the clusters of a grid point by the sign of dy: a comb of up to +-0.8 mm in the mean residual (seen identically in PbPb data and no-distortion MC; snapping the decoded position back to the 1/64-pad grid removes it). Pack with rounding instead (UnbinnedResid::pack); unclamped tgSlp stays at |tgSlp| <= 0x7fff - 1, so isTgSlpClamped() is unchanged. Same rounding for the vertex x and the TRD tracklet slope. No format change. Co-Authored-By: Claude Opus 5.5 --- .../include/SpacePoints/TrackInterpolation.h | 20 ++++++++++++++----- .../SpacePoints/src/TrackInterpolation.cxx | 6 +++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Detectors/TPC/calibration/SpacePoints/include/SpacePoints/TrackInterpolation.h b/Detectors/TPC/calibration/SpacePoints/include/SpacePoints/TrackInterpolation.h index 38b7fd3784462..df938d2429fe7 100644 --- a/Detectors/TPC/calibration/SpacePoints/include/SpacePoints/TrackInterpolation.h +++ b/Detectors/TPC/calibration/SpacePoints/include/SpacePoints/TrackInterpolation.h @@ -18,6 +18,8 @@ #define ALICEO2_TPC_TRACKINTERPOLATION_H_ #include +#include +#include #include "CommonDataFormat/EvIndex.h" #include "CommonDataFormat/RangeReference.h" #include "ReconstructionDataFormats/Track.h" @@ -77,11 +79,11 @@ struct TPCClusterResiduals { struct UnbinnedResid { UnbinnedResid() = default; UnbinnedResid(float dyIn, float dzIn, float tgSlpIn, float yIn, float zIn, - unsigned char rowIn, unsigned char secIn, short chanIn = -1, bool rejFlag = false) : dy(static_cast(dyIn * 0x7fff / param::MaxResid)), - dz(static_cast(dzIn * 0x7fff / param::MaxResid)), - tgSlp(static_cast(tgSlpIn * 0x7fff / param::MaxTgSlp)), - y(static_cast(yIn * 0x7fff / param::MaxY)), - z(static_cast(zIn * 0x7fff / param::MaxZ)), + unsigned char rowIn, unsigned char secIn, short chanIn = -1, bool rejFlag = false) : dy(pack(dyIn, param::MaxResid)), + dz(pack(dzIn, param::MaxResid)), + tgSlp(pack(tgSlpIn, param::MaxTgSlp, std::abs(tgSlpIn) < param::MaxTgSlp ? 0x7fff - 1 : 0x7fff)), + y(pack(yIn, param::MaxY)), + z(pack(zIn, param::MaxZ)), row(rowIn), sec(secIn), channel(chanIn), @@ -96,6 +98,14 @@ struct UnbinnedResid { short channel{-1}; ///< extra channel info (ITS chip ID, TRD chamber, TOF main pad within the sector) bool rejected{false}; ///< residual is flagged as rejected in the validateTrack + /// value (|value| <= maximum) packed into a short, rounded to the nearest of the 0x7fff steps per maximum and limited to +-limit. + /// A plain cast would truncate towards zero, i.e. move every value towards zero by up to one step: the decoded cluster position + /// y + dy then depends on the sign of dy below the step size, which biases residuals binned by the cluster position on sub-pad scales + static short pack(float value, float maximum, int limit = 0x7fff) + { + return static_cast(std::clamp(std::lround(value * 0x7fff / maximum), -limit, limit)); + } + /// true if tgSlp was saturated at +-param::MaxTgSlp (scdcalib.clampTgSlp): unclamped values have |tgSlp| <= 0x7fff - 1 bool isTgSlpClamped() const { return tgSlp == 0x7fff || tgSlp == -0x7fff; } /// tgSlp marker of a position-only TPC cluster (scdcalib.keepClustersOnPropFail): no reference track at this cluster, diff --git a/Detectors/TPC/calibration/SpacePoints/src/TrackInterpolation.cxx b/Detectors/TPC/calibration/SpacePoints/src/TrackInterpolation.cxx index 17237473e85ae..138ca8d015f43 100644 --- a/Detectors/TPC/calibration/SpacePoints/src/TrackInterpolation.cxx +++ b/Detectors/TPC/calibration/SpacePoints/src/TrackInterpolation.cxx @@ -913,7 +913,7 @@ void TrackInterpolation::interpolateTrack(int iSeed) auto dy = yv - trkWorkITS.getY(); auto dz = zv - trkWorkITS.getZ(); if ((std::abs(dy) < param::MaxResid) && (std::abs(dz) < param::MaxResid) && (std::abs(trkWorkITS.getY()) < param::MaxY) && (std::abs(trkWorkITS.getZ()) < param::MaxZ) && std::abs(xv) < param::MaxVtxX) { - short compXV = static_cast(xv * 0x7fff / param::MaxVtxX); + short compXV = UnbinnedResid::pack(xv, param::MaxVtxX); mClRes.emplace_back(dy, dz, alpha / TMath::Pi(), trkWorkITS.getY(), trkWorkITS.getZ(), 190, -1, compXV); if (!gidTable[GTrackID::ITSTPC].isIndexSet()) { LOGP(fatal, "ITS-TPC seed index is not set for TOF track"); @@ -985,7 +985,7 @@ int TrackInterpolation::processTRDLayer(const o2::trd::TrackTRD& trkTRD, int iLa if (trkData) { auto slope = trdSP.getDy(); if (std::abs(slope) < param::MaxTRDSlope) { - trkData->TRDTrkltSlope[iLayer] = slope * 0x7fff / param::MaxTRDSlope; + trkData->TRDTrkltSlope[iLayer] = UnbinnedResid::pack(slope, param::MaxTRDSlope); } } if (trk64) { @@ -1308,7 +1308,7 @@ void TrackInterpolation::extrapolateTrack(int iSeed) auto dy = yv - trkWorkITS.getY(); auto dz = zv - trkWorkITS.getZ(); if ((std::abs(dy) < param::MaxResid) && (std::abs(dz) < param::MaxResid) && (std::abs(trkWorkITS.getY()) < param::MaxY) && (std::abs(trkWorkITS.getZ()) < param::MaxZ) && std::abs(xv) < param::MaxVtxX) { - short compXV = static_cast(xv * 0x7fff / param::MaxVtxX); + short compXV = UnbinnedResid::pack(xv, param::MaxVtxX); mClRes.emplace_back(dy, dz, alpha / TMath::Pi(), trkWorkITS.getY(), trkWorkITS.getZ(), 190, -1, compXV); if (!gidTableFull[GTrackID::ITSTPC].isIndexSet()) { LOGP(fatal, "ITS-TPC seed index is not set for TOF track"); From 261ba2d9744ae326d700348aaad761cceb9fc82b Mon Sep 17 00:00:00 2001 From: Matthias Kleiner Date: Wed, 4 Nov 2026 16:01:05 +0100 Subject: [PATCH 2/2] TPC SCD: rename UnbinnedResid::pack to packToShort, document the fixed 0x7fff scale Co-Authored-By: Claude Opus 5.5 --- .../include/SpacePoints/TrackInterpolation.h | 20 ++++++++++--------- .../SpacePoints/src/TrackInterpolation.cxx | 6 +++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Detectors/TPC/calibration/SpacePoints/include/SpacePoints/TrackInterpolation.h b/Detectors/TPC/calibration/SpacePoints/include/SpacePoints/TrackInterpolation.h index df938d2429fe7..9a47ffca0c96a 100644 --- a/Detectors/TPC/calibration/SpacePoints/include/SpacePoints/TrackInterpolation.h +++ b/Detectors/TPC/calibration/SpacePoints/include/SpacePoints/TrackInterpolation.h @@ -79,11 +79,11 @@ struct TPCClusterResiduals { struct UnbinnedResid { UnbinnedResid() = default; UnbinnedResid(float dyIn, float dzIn, float tgSlpIn, float yIn, float zIn, - unsigned char rowIn, unsigned char secIn, short chanIn = -1, bool rejFlag = false) : dy(pack(dyIn, param::MaxResid)), - dz(pack(dzIn, param::MaxResid)), - tgSlp(pack(tgSlpIn, param::MaxTgSlp, std::abs(tgSlpIn) < param::MaxTgSlp ? 0x7fff - 1 : 0x7fff)), - y(pack(yIn, param::MaxY)), - z(pack(zIn, param::MaxZ)), + unsigned char rowIn, unsigned char secIn, short chanIn = -1, bool rejFlag = false) : dy(packToShort(dyIn, param::MaxResid)), + dz(packToShort(dzIn, param::MaxResid)), + tgSlp(packToShort(tgSlpIn, param::MaxTgSlp, std::abs(tgSlpIn) < param::MaxTgSlp ? 0x7fff - 1 : 0x7fff)), + y(packToShort(yIn, param::MaxY)), + z(packToShort(zIn, param::MaxZ)), row(rowIn), sec(secIn), channel(chanIn), @@ -98,10 +98,12 @@ struct UnbinnedResid { short channel{-1}; ///< extra channel info (ITS chip ID, TRD chamber, TOF main pad within the sector) bool rejected{false}; ///< residual is flagged as rejected in the validateTrack - /// value (|value| <= maximum) packed into a short, rounded to the nearest of the 0x7fff steps per maximum and limited to +-limit. - /// A plain cast would truncate towards zero, i.e. move every value towards zero by up to one step: the decoded cluster position - /// y + dy then depends on the sign of dy below the step size, which biases residuals binned by the cluster position on sub-pad scales - static short pack(float value, float maximum, int limit = 0x7fff) + /// value (|value| <= maximum) packed into a signed short, rounded to the nearest step. The scale is fixed at 0x7fff steps per maximum + /// (decoded everywhere as short * maximum / 0x7fff); limit only clips the result to +-limit (e.g. 0x7fff - 1 for an unclamped tgSlp, to + /// keep 0x7fff as the clamp marker), it does not change the scale. A plain cast would truncate towards zero, i.e. move every value + /// towards zero by up to one step: the decoded cluster position y + dy then depends on the sign of dy below the step size, which + /// biases residuals binned by the cluster position on sub-pad scales + static short packToShort(float value, float maximum, int limit = 0x7fff) { return static_cast(std::clamp(std::lround(value * 0x7fff / maximum), -limit, limit)); } diff --git a/Detectors/TPC/calibration/SpacePoints/src/TrackInterpolation.cxx b/Detectors/TPC/calibration/SpacePoints/src/TrackInterpolation.cxx index 138ca8d015f43..64d6156365517 100644 --- a/Detectors/TPC/calibration/SpacePoints/src/TrackInterpolation.cxx +++ b/Detectors/TPC/calibration/SpacePoints/src/TrackInterpolation.cxx @@ -913,7 +913,7 @@ void TrackInterpolation::interpolateTrack(int iSeed) auto dy = yv - trkWorkITS.getY(); auto dz = zv - trkWorkITS.getZ(); if ((std::abs(dy) < param::MaxResid) && (std::abs(dz) < param::MaxResid) && (std::abs(trkWorkITS.getY()) < param::MaxY) && (std::abs(trkWorkITS.getZ()) < param::MaxZ) && std::abs(xv) < param::MaxVtxX) { - short compXV = UnbinnedResid::pack(xv, param::MaxVtxX); + short compXV = UnbinnedResid::packToShort(xv, param::MaxVtxX); mClRes.emplace_back(dy, dz, alpha / TMath::Pi(), trkWorkITS.getY(), trkWorkITS.getZ(), 190, -1, compXV); if (!gidTable[GTrackID::ITSTPC].isIndexSet()) { LOGP(fatal, "ITS-TPC seed index is not set for TOF track"); @@ -985,7 +985,7 @@ int TrackInterpolation::processTRDLayer(const o2::trd::TrackTRD& trkTRD, int iLa if (trkData) { auto slope = trdSP.getDy(); if (std::abs(slope) < param::MaxTRDSlope) { - trkData->TRDTrkltSlope[iLayer] = UnbinnedResid::pack(slope, param::MaxTRDSlope); + trkData->TRDTrkltSlope[iLayer] = UnbinnedResid::packToShort(slope, param::MaxTRDSlope); } } if (trk64) { @@ -1308,7 +1308,7 @@ void TrackInterpolation::extrapolateTrack(int iSeed) auto dy = yv - trkWorkITS.getY(); auto dz = zv - trkWorkITS.getZ(); if ((std::abs(dy) < param::MaxResid) && (std::abs(dz) < param::MaxResid) && (std::abs(trkWorkITS.getY()) < param::MaxY) && (std::abs(trkWorkITS.getZ()) < param::MaxZ) && std::abs(xv) < param::MaxVtxX) { - short compXV = UnbinnedResid::pack(xv, param::MaxVtxX); + short compXV = UnbinnedResid::packToShort(xv, param::MaxVtxX); mClRes.emplace_back(dy, dz, alpha / TMath::Pi(), trkWorkITS.getY(), trkWorkITS.getZ(), 190, -1, compXV); if (!gidTableFull[GTrackID::ITSTPC].isIndexSet()) { LOGP(fatal, "ITS-TPC seed index is not set for TOF track");