Skip to content

Commit 206b677

Browse files
MatchCosmics: compare same-side TPC-only legs refitted at a common time
A TPC-only track has no time measurement: its time0 is the tracker's guess (GPUTPCGMTrackParam::ShiftZ: z = 0 at the beam line if the helix can come from it, otherwise the latest cluster at z = 0.521 * x, then clamped to the drift volume). For a cosmic, which does not come from the beam line, the two legs get independent guesses, typically tens of mus apart, so their clusters were distortion-corrected at different z and the legs disagree (y, snp, q/pt) although they are one track. For legs on opposite sides the time comes from z continuity, but legs on the same side were compared as tracked. New MatchCosmicsParams refitSameSideAtCommonTime (default false, not in the preset physics-v1, see below): after the cheap cuts (tgl, q/pt, same half), same-side TPC-only legs are refitted at the centre of their brackets' overlap (the time the refit of the winners uses) and compared there (MatchCosmics::refitSeedAtTime: TPC refit from the outer parameters, then the propagation to the DCA as for the seeds; energy loss along the muon's flight as in refitWinners). The TPC refitter is created once per TF in process() and shared with refitWinners. The matcher debug tree writes the legs compared and a commonTime flag. Cosmic MC with PbPb-like distortions (190 TFs, preset physics-v1, off -> on): correctly matched muons 6848 -> 7324 of 8639 (79.3 -> 84.8 %); same side 77.0 -> 84.2 %, its loss to the crude chi2 cut 6.9 -> 0.8 %; A×C unchanged; 0 wrong-muon pairs in both; matcher +12 ms per TF (~100 refits). With the true MC time instead of the window centre the same-side pass rate of the chi2 cut would be 99.2 % (centre 98.0 %, legs at their own time0s 84.7 %). PbPb 2025 (4 TFs): ~530 refits per TF, +15 ms per TF. Not in the preset: in PbPb (568041, 310 CTFs, 16541 TFs) it adds mostly pairs of collision tracks, whose time0 was about right and which the refit at the window centre mis-corrects: +352 same-side cosmics but only +5 TOF-tagged (2.6 % of the new ones vs 15.3 % of the common ones), and ~35 A×C cosmics lose a leg to a refitted same-side pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9bd67e9 commit 206b677

3 files changed

Lines changed: 100 additions & 41 deletions

File tree

‎Detectors/GlobalTracking/include/GlobalTracking/MatchCosmics.h‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ class VDriftCorrFact;
4141
namespace gpu
4242
{
4343
class TPCFastTransformPOD;
44-
}
44+
class GPUO2InterfaceRefit;
45+
} // namespace gpu
4546
namespace globaltracking
4647
{
4748

@@ -137,6 +138,7 @@ class MatchCosmics
137138
private:
138139
void updateTimeDependentParams();
139140
RejFlag checkPair(int i, int j);
141+
bool refitSeedAtTime(const TrackSeed& seed, float timeMUS, TrackSeed& out);
140142
void registerMatch(int i, int j, float chi2, float tCommon = 0.f, float tCommonErr = -1.f);
141143
void suppressMatch(int partner0, int partner1);
142144
void createSeeds(const o2::globaltracking::RecoContainer& data);
@@ -164,6 +166,9 @@ class MatchCosmics
164166
float mQ2PtCutoff = 1e9;
165167
float mQ2PtCutoffOppositeSides = 1e9;
166168
const MatchCosmicsParams* mMatchParams = nullptr;
169+
const o2::globaltracking::RecoContainer* mRecoData = nullptr; ///< inputs of the TF being processed
170+
o2::gpu::GPUO2InterfaceRefit* mTPCRefitter = nullptr; ///< TPC refitter of the TF being processed (owned by process())
171+
size_t mNRefitsCommonTime = 0; ///< seeds refitted at the common time of a same-side pair in this TF
167172

168173
std::vector<o2d::TrackCosmics> mCosmicTracks;
169174
std::vector<o2::MCCompLabel> mCosmicTracksLbl;

‎Detectors/GlobalTracking/include/GlobalTracking/MatchCosmicsParams.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ struct MatchCosmicsParams : public o2::conf::ConfigurableParamHelper<MatchCosmic
4141
float minSeedDCAxyNSigma = 0.f; // use only tracks with |DCA_xy| >= this * sigma(DCA_xy) (0: no cut; poorly measured collision tracks)
4242
bool constrainTPCOnlyZ = false; // TPC-only legs: test z at a common time (same side, or a leg with known time), else require the time implied by z continuity in both brackets
4343
bool vetoSameHalf = false; // reject pairs whose two legs lie on the same side of the closest approach (two pieces of one leg)
44+
bool refitSameSideAtCommonTime = false; // TPC-only legs on the same side: compare them refitted at the centre of their brackets' overlap instead of at their own time0s
4445
float nSigmaTError = 4.f; // number of sigmas on track time error for matching (except for TPC which provides an interval)
4546
float tpcExtraZError2 = 1.f; // extra error^2 on the TPC-only track Z coordinate
4647
float fiducialRIP = 1.0f; // consider track having |Y@x=0|< this as passing DCA cut (if requested)

‎Detectors/GlobalTracking/src/MatchCosmics.cxx‎

Lines changed: 93 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,17 @@ void MatchCosmics::process(const o2::globaltracking::RecoContainer& data)
111111
}
112112
}
113113
}
114+
// TPC refitter of this TF: same-side TPC-only legs compared at a common time (checkPair) and the refit of the winners
115+
std::unique_ptr<o2::gpu::GPUO2InterfaceRefit> tpcRefitter;
116+
if (data.inputsTPCclusters) {
117+
tpcRefitter = std::make_unique<o2::gpu::GPUO2InterfaceRefit>(&data.inputsTPCclusters->clusterIndex, mTPCCorrMaps, mBz, data.getTPCTracksClusterRefs().data(), 0,
118+
data.clusterShMapTPC.data(), data.occupancyMapTPC.data(), data.occupancyMapTPC.size(), nullptr,
119+
o2::base::Propagator::Instance());
120+
}
121+
mTPCRefitter = tpcRefitter.get();
122+
mRecoData = &data;
123+
mNRefitsCommonTime = 0;
124+
114125
// sort in time bracket lower edge, putting rejected tracks in the end
115126
std::vector<int> sortID(ntr);
116127
std::iota(sortID.begin(), sortID.end(), 0);
@@ -134,6 +145,11 @@ void MatchCosmics::process(const o2::globaltracking::RecoContainer& data)
134145

135146
selectWinners();
136147
refitWinners(data);
148+
if (mNRefitsCommonTime) {
149+
LOGP(info, "{} seeds refitted at the common time of same-side pairs", mNRefitsCommonTime);
150+
}
151+
mTPCRefitter = nullptr;
152+
mRecoData = nullptr;
137153

138154
mTFCount++;
139155
}
@@ -144,16 +160,7 @@ void MatchCosmics::refitWinners(const o2::globaltracking::RecoContainer& data)
144160
LOG(info) << "Refitting " << mWinners.size() << " winner matches";
145161
int count = 0;
146162
auto tpcTBinMUSInv = 1. / mTPCTBinMUS;
147-
const auto& tpcClusRefs = data.getTPCTracksClusterRefs();
148-
const auto& tpcClusShMap = data.clusterShMapTPC;
149-
const auto& tpcClusOccMap = data.occupancyMapTPC;
150-
std::unique_ptr<o2::gpu::GPUO2InterfaceRefit> tpcRefitter;
151-
if (data.inputsTPCclusters) {
152-
tpcRefitter = std::make_unique<o2::gpu::GPUO2InterfaceRefit>(&data.inputsTPCclusters->clusterIndex,
153-
mTPCCorrMaps, mBz,
154-
tpcClusRefs.data(), 0, tpcClusShMap.data(),
155-
tpcClusOccMap.data(), tpcClusOccMap.size(), nullptr, o2::base::Propagator::Instance());
156-
}
163+
auto* tpcRefitter = mTPCRefitter; // created in process()
157164

158165
const auto& itsClusters = prepareITSClusters(data);
159166
// RS FIXME: this is probably a temporary solution, since ITS tracking over boundaries will likely change the TrackITS format
@@ -461,6 +468,9 @@ MatchCosmics::RejFlag MatchCosmics::checkPair(int i, int j)
461468
float chi2 = 1.e9f;
462469
float tCommon = 0.f; // time fixed by z continuity of TPC-only legs on opposite sides, used by the refit
463470
float tCommonErr = -1.f; // its error (< 0: not fixed)
471+
TrackSeed seed0Common; // same-side TPC-only legs refitted at a common time (refitSameSideAtCommonTime)
472+
TrackSeed seed1Common;
473+
bool commonTime = false;
464474

465475
// check
466476
// 1) crude check on tgl and q/pt (if B!=0). Note: back-to-back tracks will have mutually params (see TrackPar::invertParam)
@@ -504,49 +514,58 @@ MatchCosmics::RejFlag MatchCosmics::checkPair(int i, int j)
504514
break;
505515
}
506516
}
507-
o2::track::TrackParCov seed1Inv = seed1;
517+
// TPC-only legs on the same side: the tracker transformed each leg's clusters with its own time0, a guess (the two legs of a cosmic
518+
// get guesses tens of mus apart), so the legs were distortion-corrected at different z and disagree although they are one track;
519+
// compare them refitted at one common time, the centre of their brackets' overlap (the time the refit of the winners uses)
520+
if (mMatchParams->refitSameSideAtCommonTime && seed0.tpcSide != 0 && seed0.tpcSide == seed1.tpcSide) { // tpcSide != 0: TPC-only one-side legs
521+
const float tPair = seed0.tBracket.getOverlap(seed1.tBracket).mean();
522+
commonTime = refitSeedAtTime(seed0, tPair, seed0Common) && refitSeedAtTime(seed1, tPair, seed1Common);
523+
}
524+
const TrackSeed& leg0 = commonTime ? seed0Common : seed0;
525+
const TrackSeed& leg1 = commonTime ? seed1Common : seed1;
526+
o2::track::TrackParCov seed1Inv = leg1;
508527
seed1Inv.invert();
509528
for (int i = 0; i < o2::track::kNParams; i++) { // add systematic error
510529
seed1Inv.updateCov(mMatchParams->systSigma2[i], o2::track::DiagMap[i]);
511530
}
512531

513-
if (!seed1Inv.rotate(seed0.getAlpha()) ||
514-
!o2::base::Propagator::Instance()->PropagateToXBxByBz(seed1Inv, seed0.getX(), mMatchParams->maxSnp, mMatchParams->maxStep, mMatchParams->matCorr)) {
532+
if (!seed1Inv.rotate(leg0.getAlpha()) ||
533+
!o2::base::Propagator::Instance()->PropagateToXBxByBz(seed1Inv, leg0.getX(), mMatchParams->maxSnp, mMatchParams->maxStep, mMatchParams->matCorr)) {
515534
rej = RejProp;
516535
break;
517536
}
518-
auto dSnp = seed0.getSnp() - seed1Inv.getSnp();
519-
if (dSnp * dSnp > (seed0.getSigmaSnp2() + seed1Inv.getSigmaSnp2()) * mMatchParams->crudeNSigma2Cut[o2::track::kSnp]) {
537+
auto dSnp = leg0.getSnp() - seed1Inv.getSnp();
538+
if (dSnp * dSnp > (leg0.getSigmaSnp2() + seed1Inv.getSigmaSnp2()) * mMatchParams->crudeNSigma2Cut[o2::track::kSnp]) {
520539
rej = RejSnp;
521540
break;
522541
}
523-
auto dY = seed0.getY() - seed1Inv.getY();
524-
if (dY * dY > (seed0.getSigmaY2() + seed1Inv.getSigmaY2()) * mMatchParams->crudeNSigma2Cut[o2::track::kY]) {
542+
auto dY = leg0.getY() - seed1Inv.getY();
543+
if (dY * dY > (leg0.getSigmaY2() + seed1Inv.getSigmaY2()) * mMatchParams->crudeNSigma2Cut[o2::track::kY]) {
525544
rej = RejY;
526545
break;
527546
}
528-
bool ignoreZ = seed0.origID.getSource() == o2d::GlobalTrackID::TPC || seed1.origID.getSource() == o2d::GlobalTrackID::TPC;
547+
bool ignoreZ = leg0.origID.getSource() == o2d::GlobalTrackID::TPC || leg1.origID.getSource() == o2d::GlobalTrackID::TPC;
529548
if (ignoreZ && mMatchParams->constrainTPCOnlyZ) {
530549
// a TPC-only track with clusters on one side has z relative to its time0: z(t) = z + side * vD * (t - tRef); a CE-crossing or
531550
// non-TPC-only track has an absolute z (side 0). Bring both legs to a common time where possible and test z; for legs on opposite
532551
// sides, z continuity fixes the common time, which must lie in both time brackets.
533-
const int side0 = seed0.tpcSide;
534-
const int side1 = seed1.tpcSide;
535-
const float sigZ2 = (seed0.getSigmaZ2() + seed1Inv.getSigmaZ2()) * mMatchParams->crudeNSigma2Cut[o2::track::kZ];
552+
const int side0 = leg0.tpcSide;
553+
const int side1 = leg1.tpcSide;
554+
const float sigZ2 = (leg0.getSigmaZ2() + seed1Inv.getSigmaZ2()) * mMatchParams->crudeNSigma2Cut[o2::track::kZ];
536555
if (side0 == 0 || side1 == 0 || side0 == side1) {
537-
float dZ = seed0.getZ() - seed1Inv.getZ();
556+
float dZ = leg0.getZ() - seed1Inv.getZ();
538557
float dZTimeTol = 0.f; // the time of a non-TPC absolute leg is only known within its bracket (tRef is the bracket centre)
539558
if (side0 != 0 && side1 != 0) { // same side: the z offset is fixed by the difference of the reference times
540-
dZ -= side0 * mTPCVDrift * (seed0.tRef - seed1.tRef);
541-
} else if (side1 != 0) { // seed0 absolute: move seed1 to the time of seed0
542-
dZ -= side1 * mTPCVDrift * (seed0.tRef - seed1.tRef);
543-
if (seed0.origID.getSource() != o2d::GlobalTrackID::TPC) {
544-
dZTimeTol = 0.5f * mTPCVDrift * seed0.tBracket.delta();
559+
dZ -= side0 * mTPCVDrift * (leg0.tRef - leg1.tRef);
560+
} else if (side1 != 0) { // leg0 absolute: move leg1 to the time of leg0
561+
dZ -= side1 * mTPCVDrift * (leg0.tRef - leg1.tRef);
562+
if (leg0.origID.getSource() != o2d::GlobalTrackID::TPC) {
563+
dZTimeTol = 0.5f * mTPCVDrift * leg0.tBracket.delta();
545564
}
546-
} else if (side0 != 0) { // seed1 absolute: move seed0 to the time of seed1
547-
dZ += side0 * mTPCVDrift * (seed1.tRef - seed0.tRef);
548-
if (seed1.origID.getSource() != o2d::GlobalTrackID::TPC) {
549-
dZTimeTol = 0.5f * mTPCVDrift * seed1.tBracket.delta();
565+
} else if (side0 != 0) { // leg1 absolute: move leg0 to the time of leg1
566+
dZ += side0 * mTPCVDrift * (leg1.tRef - leg0.tRef);
567+
if (leg1.origID.getSource() != o2d::GlobalTrackID::TPC) {
568+
dZTimeTol = 0.5f * mTPCVDrift * leg1.tBracket.delta();
550569
}
551570
}
552571
const float dZTol = std::sqrt(sigZ2) + dZTimeTol;
@@ -555,20 +574,20 @@ MatchCosmics::RejFlag MatchCosmics::checkPair(int i, int j)
555574
break;
556575
}
557576
} else { // opposite sides
558-
const float t = 0.5f * (side0 * (seed1Inv.getZ() - seed0.getZ()) / mTPCVDrift + seed0.tRef + seed1.tRef);
577+
const float t = 0.5f * (side0 * (seed1Inv.getZ() - leg0.getZ()) / mTPCVDrift + leg0.tRef + leg1.tRef);
559578
const float tTol = std::sqrt(sigZ2) / (2.f * mTPCVDrift);
560-
if (t < std::max(seed0.tBracket.getMin(), seed1.tBracket.getMin()) - tTol || t > std::min(seed0.tBracket.getMax(), seed1.tBracket.getMax()) + tTol) {
579+
if (t < std::max(leg0.tBracket.getMin(), leg1.tBracket.getMin()) - tTol || t > std::min(leg0.tBracket.getMax(), leg1.tBracket.getMax()) + tTol) {
561580
rej = RejZ;
562581
break;
563582
}
564583
tCommon = t;
565-
tCommonErr = std::sqrt(seed0.getSigmaZ2() + seed1Inv.getSigmaZ2()) / (2.f * mTPCVDrift);
584+
tCommonErr = std::sqrt(leg0.getSigmaZ2() + seed1Inv.getSigmaZ2()) / (2.f * mTPCVDrift);
566585
}
567586
}
568587
// the z of a TPC-only leg refers to its own time0: it is tested above at a common time with constrainTPCOnlyZ, otherwise ignored
569588
if (!ignoreZ) { // both legs have an absolute z
570-
auto dZ = seed0.getZ() - seed1Inv.getZ();
571-
if (dZ * dZ > (seed0.getSigmaZ2() + seed1Inv.getSigmaZ2()) * mMatchParams->crudeNSigma2Cut[o2::track::kZ]) {
589+
auto dZ = leg0.getZ() - seed1Inv.getZ();
590+
if (dZ * dZ > (leg0.getSigmaZ2() + seed1Inv.getSigmaZ2()) * mMatchParams->crudeNSigma2Cut[o2::track::kZ]) {
572591
rej = RejZ;
573592
break;
574593
}
@@ -580,7 +599,7 @@ MatchCosmics::RejFlag MatchCosmics::checkPair(int i, int j)
580599
seed1Inv.setCov(0., o2::track::CovarMap[o2::track::kZ][o2::track::kQ2Pt]);
581600
}
582601
// calculate chi2 (expensive)
583-
chi2 = seed0.getPredictedChi2(seed1Inv);
602+
chi2 = leg0.getPredictedChi2(seed1Inv);
584603
if (chi2 > mMatchParams->crudeChi2Cut) {
585604
rej = RejChi2;
586605
break;
@@ -594,12 +613,14 @@ MatchCosmics::RejFlag MatchCosmics::checkPair(int i, int j)
594613

595614
#ifdef _ALLOW_DEBUG_TREES_
596615
if (mDBGOut && ((rej == Accept && isDebugFlag(MatchTreeAccOnly)) || isDebugFlag(MatchTreeAll))) {
597-
auto seed1I = seed1;
616+
const TrackSeed& dbgLeg0 = commonTime ? seed0Common : seed0; // the legs compared (refitted at the common time if they were)
617+
auto seed1I = commonTime ? seed1Common : seed1;
598618
seed1I.invert();
599-
if (seed1I.rotate(seed0.getAlpha()) && o2::base::Propagator::Instance()->PropagateToXBxByBz(seed1I, seed0.getX(), mMatchParams->maxSnp, mMatchParams->maxStep, mMatchParams->matCorr)) {
619+
if (seed1I.rotate(dbgLeg0.getAlpha()) && o2::base::Propagator::Instance()->PropagateToXBxByBz(seed1I, dbgLeg0.getX(), mMatchParams->maxSnp, mMatchParams->maxStep, mMatchParams->matCorr)) {
600620
int rejI = int(rej);
621+
int commonTimeI = commonTime;
601622
(*mDBGOut) << "match"
602-
<< "tf=" << mTFCount << "seed0=" << seed0 << "seed1=" << seed1I << "chi2Match=" << chi2 << "rej=" << rejI
623+
<< "tf=" << mTFCount << "seed0=" << dbgLeg0 << "seed1=" << seed1I << "chi2Match=" << chi2 << "rej=" << rejI << "commonTime=" << commonTimeI
603624
<< "side0=" << int(seed0.tpcSide) << "side1=" << int(seed1.tpcSide) << "tCommon=" << tCommon << "tCommonErr=" << tCommonErr << "\n";
604625
}
605626
}
@@ -608,6 +629,38 @@ MatchCosmics::RejFlag MatchCosmics::checkPair(int i, int j)
608629
return rej;
609630
}
610631

632+
//________________________________________________________
633+
bool MatchCosmics::refitSeedAtTime(const TrackSeed& seed, float timeMUS, TrackSeed& out)
634+
{
635+
// TPC-only seed refitted with its clusters transformed at the time timeMUS, then treated as the seeds in createSeeds and process() (muon,
636+
// extra z error, propagation to the DCA to the beam line); energy loss along the muon's flight in the refit and the propagation, as in
637+
// refitWinners. Its z then refers to timeMUS
638+
if (!mTPCRefitter || !mRecoData) {
639+
return false;
640+
}
641+
const auto& tpcTrack = mRecoData->getTPCTrack(seed.origID);
642+
o2::track::TrackParCov trk = tpcTrack.getParamOut();
643+
trk.setPID(o2::track::PID::Muon, true);
644+
trk.resetCovariance();
645+
// the muon flies downward: inward along a leg pointing up (top leg) is along its flight (loss), along the bottom leg against it (gain)
646+
std::array<float, 3> momentum{};
647+
trk.getPxPyPzGlo(momentum);
648+
const int eLossSign = momentum[1] > 0.f ? ELossLoss : ELossGain;
649+
if (mTPCRefitter->RefitTrackAsTrackParCov(trk, tpcTrack.getClusterRef(), timeMUS / mTPCTBinMUS, nullptr, false, true, eLossSign) < 0) {
650+
return false;
651+
}
652+
trk.setCov(mMatchParams->tpcExtraZError2 + trk.getSigmaZ2(), o2::track::kSigZ2);
653+
const o2::dataformats::VertexBase v;
654+
if (!o2::base::Propagator::Instance()->propagateToDCABxByBz(v, trk, mMatchParams->maxStep, mMatchParams->matCorr, nullptr, nullptr, eLossSign)) {
655+
return false;
656+
}
657+
out = seed;
658+
static_cast<o2::track::TrackParCov&>(out) = trk;
659+
out.tRef = timeMUS;
660+
mNRefitsCommonTime++;
661+
return true;
662+
}
663+
611664
//________________________________________________________
612665
void MatchCosmics::registerMatch(int i, int j, float chi2, float tCommon, float tCommonErr)
613666
{

0 commit comments

Comments
 (0)