32constexpr std::uint8_t stationBit(
const Station station) {
33 if (station == Station::M1)
return 0x1U;
34 if (station == Station::M2)
return 0x2U;
35 if (station == Station::M3)
return 0x4U;
39constexpr std::size_t stationIndex(
const Station station) {
40 if (station == Station::M1)
return 0U;
41 if (station == Station::M2)
return 1U;
45float measuringCoordinate(
const Coincidence& point) {
46 return point.isStrip ? point.phi : point.eta;
49float coordinateDifference(
const bool isStrip,
const float lhs,
57float physicalDeltaTheta(
const Coincidence& inner,
58 const Coincidence& outer) {
59 const float deltaR = outer.r - inner.r;
60 const float deltaZ = outer.z - inner.z;
61 if (
deltaR == 0.F && deltaZ == 0.F)
return 0.F;
62 const float segmentTheta = std::atan2(
deltaR, deltaZ);
63 const float pivotTheta = std::atan2(outer.r, outer.z);
65 return static_cast<float>(
69struct ProjectionSegment {
70 std::array<const Coincidence*, 3> points{};
72 std::uint8_t stationMask{0};
73 std::uint8_t summedQuality{0};
74 std::uint8_t nStations{0};
80 float outputResidual{0.F};
81 float consistency{0.F};
82 std::uint16_t pivotChannel{0};
86void appendValidationSegments(
const GroupKey& groupKey,
87 const std::vector<ProjectionSegment>& segments,
88 OutputSegments& output) {
90 for (
const ProjectionSegment& segment : segments) {
92 value.subdetectorId = groupKey.subDetectorId;
93 value.triggerSector = groupKey.triggerSector;
94 value.bcTag = groupKey.bcTag;
95 value.projection = segment.isStrip ? Projection::Strip : Projection::Wire;
96 value.stationMask = segment.stationMask;
97 value.summedQuality = segment.summedQuality;
98 value.nStations = segment.nStations;
99 value.eta = segment.eta;
100 value.phi = segment.phi;
101 value.residual = segment.residual;
102 value.outputResidual = segment.outputResidual;
103 value.consistency = segment.consistency;
104 value.pivotChannel = segment.pivotChannel;
105 output.emplace_back(value);
109const Coincidence* projectionPivot(
const ProjectionSegment& segment) {
110 if (segment.points[2] !=
nullptr)
return segment.points[2];
111 if (segment.points[1] !=
nullptr)
return segment.points[1];
112 return segment.points[0];
115ProjectionSegment makeSegment(
const Coincidence* m1,
const Coincidence* m2,
116 const Coincidence* m3,
const bool isStrip) {
117 ProjectionSegment segment;
118 segment.points = {
m1,
m2,
m3};
120 for (
const Coincidence* point : segment.points) {
121 if (point ==
nullptr)
continue;
122 segment.stationMask |= stationBit(point->station);
123 segment.summedQuality =
static_cast<std::uint8_t
>(
124 segment.summedQuality + point->observedLayers);
128 const Coincidence* pivot = projectionPivot(segment);
129 if (pivot !=
nullptr) {
130 segment.eta = pivot->eta;
131 segment.phi = pivot->phi;
132 segment.pivotChannel = pivot->channel;
135 std::vector<const Coincidence*> points;
136 for (
const Coincidence* point : segment.points) {
137 if (point !=
nullptr) points.emplace_back(point);
139 if (points.size() >= 2U) {
140 const float first = measuringCoordinate(*points.front());
141 const float last = measuringCoordinate(*points.back());
142 segment.residual = coordinateDifference(isStrip, first, last);
143 segment.outputResidual =
isStrip
145 : physicalDeltaTheta(*points.front(),
147 segment.consistency = std::abs(segment.residual);
149 if (m1 !=
nullptr && m2 !=
nullptr && m3 !=
nullptr) {
150 const float c1 = measuringCoordinate(*m1);
151 const float c2 = measuringCoordinate(*m2);
152 const float c3 = measuringCoordinate(*m3);
153 const float predictedM1 =
isStrip
154 ?
c2 -
static_cast<float>(
157 segment.consistency = std::abs(
158 coordinateDifference(isStrip, c1, predictedM1));
159 segment.residual = coordinateDifference(isStrip, c1, c3);
160 segment.outputResidual =
isStrip
162 : physicalDeltaTheta(*m1, *m3);
168 for (
const Coincidence* point : points) {
169 sinPhi += std::sin(point->phi);
170 cosPhi += std::cos(point->phi);
172 segment.phi = std::atan2(sinPhi, cosPhi);
173 }
else if (!points.empty()) {
175 for (
const Coincidence* point : points) etaSum += point->eta;
176 segment.eta = etaSum /
static_cast<float>(points.size());
181int coordinateKey(
const Coincidence* point,
const bool isStrip) {
182 if (point ==
nullptr)
return 0;
183 const float coordinate =
isStrip ? point->phi : point->eta;
184 return static_cast<int>(std::lround(100000.F * coordinate));
187using SegmentKey = std::tuple<std::uint8_t, bool, int, int, int>;
189SegmentKey segmentKey(
const ProjectionSegment& segment) {
190 return {segment.stationMask, segment.isStrip,
191 coordinateKey(segment.points[0], segment.isStrip),
192 coordinateKey(segment.points[1], segment.isStrip),
193 coordinateKey(segment.points[2], segment.isStrip)};
196bool betterSegment(
const ProjectionSegment& lhs,
197 const ProjectionSegment& rhs) {
198 if (lhs.nStations != rhs.nStations)
return lhs.nStations > rhs.nStations;
199 if (lhs.summedQuality != rhs.summedQuality) {
200 return lhs.summedQuality > rhs.summedQuality;
202 const bool lhsHasM1 = (lhs.stationMask & 0x1U) != 0U;
203 const bool rhsHasM1 = (rhs.stationMask & 0x1U) != 0U;
204 if (lhsHasM1 != rhsHasM1)
return lhsHasM1;
205 const bool lhsHasM3 = (lhs.stationMask & 0x4U) != 0U;
206 const bool rhsHasM3 = (rhs.stationMask & 0x4U) != 0U;
207 if (lhsHasM3 != rhsHasM3)
return lhsHasM3;
208 if (lhs.consistency != rhs.consistency) {
209 return lhs.consistency < rhs.consistency;
211 return segmentKey(lhs) < segmentKey(rhs);
214void addSegment(std::vector<ProjectionSegment>& segments,
215 std::set<SegmentKey>& keys,
const Coincidence* m1,
216 const Coincidence* m2,
const Coincidence* m3,
217 const bool isStrip, Statistics& statistics) {
218 const ProjectionSegment segment = makeSegment(m1, m2, m3, isStrip);
219 if (!
keys.insert(segmentKey(segment)).second) {
220 ++statistics.nDuplicateProjectionSegments;
223 segments.emplace_back(segment);
226void retainProjectionWorkingSet(std::vector<ProjectionSegment>& segments,
227 const std::size_t maximum,
228 Statistics& statistics) {
230 if (maximum == 0U || segments.size() <= maximum)
return;
232 std::array<bool, 8> retainedMask{};
233 std::vector<ProjectionSegment> retained;
234 retained.reserve(maximum);
235 for (
const ProjectionSegment& segment : segments) {
236 if (segment.stationMask >= retainedMask.size() ||
237 retainedMask[segment.stationMask]) {
240 retained.emplace_back(segment);
241 retainedMask[segment.stationMask] =
true;
242 if (retained.size() == maximum)
break;
244 for (
const ProjectionSegment& segment : segments) {
245 if (retained.size() == maximum)
break;
246 const bool alreadyRetained = std::any_of(
247 retained.begin(), retained.end(),
248 [&segment](
const ProjectionSegment& selected) {
249 return segmentKey(selected) == segmentKey(segment);
251 if (!alreadyRetained) retained.emplace_back(segment);
253 statistics.nLimitedProjectionSegments += segments.size() - retained.size();
254 segments = std::move(retained);
258std::vector<ProjectionSegment> buildProjectionSegments(
259 const std::array<std::vector<const Coincidence*>, 3>& points,
260 const bool isStrip,
const Config& config, Statistics& statistics) {
261 std::vector<ProjectionSegment> segments;
262 std::set<SegmentKey>
keys;
264 for (
const Coincidence* m1 : points[0]) {
265 for (
const Coincidence* m2 : points[1]) {
266 for (
const Coincidence* m3 : points[2]) {
267 addSegment(segments, keys, m1, m2, m3, isStrip, statistics);
271 for (
const Coincidence* m1 : points[0]) {
272 for (
const Coincidence* m3 : points[2]) {
273 addSegment(segments, keys, m1,
nullptr, m3, isStrip, statistics);
275 for (
const Coincidence* m2 : points[1]) {
276 addSegment(segments, keys, m1, m2,
nullptr, isStrip, statistics);
279 for (
const Coincidence* m2 : points[1]) {
280 for (
const Coincidence* m3 : points[2]) {
281 addSegment(segments, keys,
nullptr, m2, m3, isStrip, statistics);
285 retainProjectionWorkingSet(segments,
286 config.maxSegmentCombinationsPerGroup,
291std::uint8_t quality(
const ProjectionSegment& segment,
const Station station) {
292 const Coincidence* point = segment.points[stationIndex(station)];
293 return point ==
nullptr ? 0U : point->observedLayers;
297 const Coincidence* wire{
nullptr};
298 const Coincidence* strip{
nullptr};
299 std::size_t stationIndex{0U};
302 float coordinateResidual{0.F};
304 explicit operator bool()
const {
return wire !=
nullptr && strip !=
nullptr; }
307PositionPair selectPositionPair(
const ProjectionSegment& wire,
308 const ProjectionSegment&
strip,
313 const std::size_t station =
index - 1U;
314 const Coincidence* wirePoint = wire.points[station];
315 const Coincidence* stripPoint =
strip.points[station];
316 if (wirePoint ==
nullptr || stripPoint ==
nullptr)
continue;
318 const float deltaEta = std::abs(wirePoint->eta - stripPoint->eta);
319 const float deltaPhi = std::abs(
static_cast<float>(
321 if (
config.maxPivotWireStripDeltaEta >= 0.F &&
322 deltaEta >
config.maxPivotWireStripDeltaEta) {
325 if (
config.maxPivotWireStripDeltaPhi >= 0.F &&
335struct CandidatePair {
336 const ProjectionSegment* wire{
nullptr};
337 const ProjectionSegment* strip{
nullptr};
338 PositionPair position{};
339 std::uint8_t positionStationMask{0};
340 std::uint8_t combinedStationMask{0};
343int bendScoreMilli(
const CandidatePair&
pair) {
344 const float bend = std::sqrt(
345 pair.wire->residual *
pair.wire->residual +
346 0.25F *
pair.strip->residual *
pair.strip->residual);
347 return static_cast<int>(std::lround(1000.F * bend));
350bool betterCandidatePair(
const CandidatePair& lhs,
const CandidatePair& rhs) {
351 const std::uint16_t lhsQuality =
static_cast<std::uint16_t
>(
352 lhs.wire->summedQuality + lhs.strip->summedQuality);
353 const std::uint16_t rhsQuality =
static_cast<std::uint16_t
>(
354 rhs.wire->summedQuality + rhs.strip->summedQuality);
355 if (lhsQuality != rhsQuality)
return lhsQuality > rhsQuality;
356 const unsigned int lhsStations =
357 std::popcount(
static_cast<unsigned int>(lhs.combinedStationMask));
358 const unsigned int rhsStations =
359 std::popcount(
static_cast<unsigned int>(rhs.combinedStationMask));
360 if (lhsStations != rhsStations)
return lhsStations > rhsStations;
361 if (lhs.position.stationIndex != rhs.position.stationIndex) {
362 return lhs.position.stationIndex > rhs.position.stationIndex;
364 const int lhsBend = bendScoreMilli(lhs);
365 const int rhsBend = bendScoreMilli(rhs);
366 if (lhsBend != rhsBend)
return lhsBend < rhsBend;
367 if (lhs.position.coordinateResidual != rhs.position.coordinateResidual) {
368 return lhs.position.coordinateResidual < rhs.position.coordinateResidual;
370 if (lhs.position.wire->eta != rhs.position.wire->eta) {
371 return lhs.position.wire->eta < rhs.position.wire->eta;
373 return lhs.position.strip->phi < rhs.position.strip->phi;
376using CandidateKey = std::tuple<
377 std::uint16_t, std::uint16_t, std::uint16_t, std::uint16_t,
378 std::int16_t, std::uint16_t, std::uint16_t, std::int16_t,
379 std::uint16_t, std::uint8_t, std::uint8_t, std::uint8_t, int,
int>;
381CandidateKey candidateKey(
const GroupKey& group,
const CandidatePair&
pair) {
382 return {
group.subDetectorId,
385 pair.position.wire->detectorSector,
386 pair.position.wire->stationEta,
387 pair.position.wire->stationPhi,
388 pair.position.strip->detectorSector,
389 pair.position.strip->stationEta,
390 pair.position.strip->stationPhi,
391 pair.wire->stationMask,
392 pair.strip->stationMask,
393 pair.positionStationMask,
394 static_cast<int>(std::lround(100000.F *
pair.position.wire->eta)),
395 static_cast<int>(std::lround(100000.F *
pair.position.strip->phi))};
398void pruneLocalCandidateBins(std::vector<CandidatePair>& pairs,
400 Statistics& statistics) {
401 if (
config.maxCandidatesPerLocalBin == 0U ||
402 config.localCandidateEtaBinWidth <= 0.F ||
403 config.localCandidatePhiBinWidth <= 0.F ||
pairs.empty()) {
407 std::map<std::tuple<int, int, std::size_t>, std::size_t> keptPerBin;
408 std::vector<CandidatePair> retained;
409 retained.reserve(
pairs.size());
410 for (
const CandidatePair&
pair : pairs) {
411 const float phi =
static_cast<float>(
413 const int etaBin =
static_cast<int>(std::floor(
414 pair.position.wire->eta /
config.localCandidateEtaBinWidth));
415 const int phiBin =
static_cast<int>(std::floor(
416 (
phi + std::numbers::pi_v<float>) /
417 config.localCandidatePhiBinWidth));
419 std::make_tuple(etaBin, phiBin,
pair.position.stationIndex);
420 std::size_t&
count = keptPerBin[
key];
422 ++statistics.nLimitedCandidates;
425 retained.emplace_back(
pair);
428 pairs = std::move(retained);
431bool sameChamber(
const Coincidence& lhs,
const Coincidence& rhs) {
432 return lhs.station == rhs.station && lhs.stationEta == rhs.stationEta &&
433 lhs.stationPhi == rhs.stationPhi;
436bool sameCandidateChamberPath(
const CandidatePair& lhs,
437 const CandidatePair& rhs) {
438 unsigned int commonStations = 0U;
439 for (std::size_t station = 0U; station < lhs.wire->points.size(); ++station) {
440 const Coincidence* lhsPoint = lhs.wire->points[station];
441 const Coincidence* rhsPoint = rhs.wire->points[station];
442 if (lhsPoint ==
nullptr || rhsPoint ==
nullptr)
continue;
443 if (!sameChamber(*lhsPoint, *rhsPoint))
return false;
446 return commonStations >= 2U;
449bool sameCoarseLocalPosition(
const CandidatePair& lhs,
450 const CandidatePair& rhs,
452 if (!sameCandidateChamberPath(lhs, rhs))
return false;
453 if (
config.localDuplicateEtaWindow <= 0.F ||
454 config.localDuplicatePhiWindow <= 0.F) {
455 return lhs.position.wire->eta == rhs.position.wire->eta &&
456 lhs.position.strip->phi == rhs.position.strip->phi;
458 return std::abs(lhs.position.wire->eta - rhs.position.wire->eta) <
459 config.localDuplicateEtaWindow &&
461 rhs.position.strip->phi)) <
462 config.localDuplicatePhiWindow;
465void suppressLocalPositionDuplicates(std::vector<CandidatePair>& pairs,
467 Statistics& statistics) {
468 if (
config.maxCandidatesPerLocalPosition == 0U ||
pairs.empty())
return;
470 std::vector<CandidatePair> retained;
471 retained.reserve(
pairs.size());
472 for (
const CandidatePair&
pair : pairs) {
473 std::size_t
count = 0U;
474 for (
const CandidatePair& selected : retained) {
475 if (sameCoarseLocalPosition(
pair, selected, config)) {
477 if (
count >=
config.maxCandidatesPerLocalPosition)
break;
480 if (
count >=
config.maxCandidatesPerLocalPosition) {
481 ++statistics.nLocalDuplicateCandidates;
484 retained.emplace_back(
pair);
486 pairs = std::move(retained);
489void retainCandidateWorkingSet(std::vector<CandidatePair>& pairs,
490 const std::size_t maximum,
491 Statistics& statistics) {
493 if (maximum == 0U ||
pairs.size() <= maximum)
return;
495 std::array<bool, 8> retainedMask{};
496 std::vector<CandidatePair> retained;
497 retained.reserve(maximum);
498 for (
const CandidatePair&
pair : pairs) {
499 if (
pair.positionStationMask >= retainedMask.size() ||
500 retainedMask[
pair.positionStationMask]) {
503 retained.emplace_back(
pair);
504 retainedMask[
pair.positionStationMask] =
true;
505 if (retained.size() == maximum)
break;
507 for (
const CandidatePair&
pair : pairs) {
508 if (retained.size() == maximum)
break;
509 const bool alreadyRetained = std::any_of(
510 retained.begin(), retained.end(),
511 [&
pair](
const CandidatePair& selected) {
512 return selected.wire == pair.wire && selected.strip == pair.strip &&
513 selected.position.stationIndex == pair.position.stationIndex;
515 if (!alreadyRetained) retained.emplace_back(
pair);
517 statistics.nLimitedCandidates +=
pairs.size() - retained.size();
518 pairs = std::move(retained);
522void countPositionMask(
const std::uint8_t mask, Statistics& statistics) {
523 if (mask == 0x7U) ++statistics.nM1M2M3Candidates;
524 else if (mask == 0x3U) ++statistics.nM1M2Candidates;
525 else if (mask == 0x5U) ++statistics.nM1M3Candidates;
526 else if (mask == 0x6U) ++statistics.nM2M3Candidates;
527 else if (mask == 0x1U) ++statistics.nM1OnlyPositionCandidates;
528 else if (mask == 0x2U) ++statistics.nM2OnlyPositionCandidates;
529 else if (mask == 0x4U) ++statistics.nM3OnlyPositionCandidates;
547 if (validationSegments !=
nullptr) validationSegments->clear();
550 std::array<std::array<std::vector<const Coincidence*>, 3>, 2>;
551 std::map<GroupKey, Projections> grouped;
552 for (
const Coincidence& coincidence : coincidences) {
553 if (stationBit(coincidence.station) == 0U)
continue;
554 const std::size_t projection = coincidence.isStrip ? 1U : 0U;
555 grouped[coincidence.key][projection][stationIndex(coincidence.station)]
556 .emplace_back(&coincidence);
559 std::set<CandidateKey> candidateKeys;
560 for (
const auto& [groupKey, projections] : grouped) {
561 const std::vector<ProjectionSegment> wireSegments =
562 buildProjectionSegments(projections[0],
false,
m_config, statistics);
563 const std::vector<ProjectionSegment> stripSegments =
564 buildProjectionSegments(projections[1],
true,
m_config, statistics);
567 if (validationSegments !=
nullptr) {
568 appendValidationSegments(groupKey, wireSegments, *validationSegments);
569 appendValidationSegments(groupKey, stripSegments, *validationSegments);
572 std::vector<CandidatePair> pairs;
573 pairs.reserve(wireSegments.size() * stripSegments.size());
574 for (
const ProjectionSegment& wire : wireSegments) {
575 if (
m_config.maxCandidateDThetaAbs >= 0.F &&
576 std::abs(wire.residual) >
m_config.maxCandidateDThetaAbs) {
580 for (
const ProjectionSegment&
strip : stripSegments) {
581 if (
m_config.maxCandidateDPhiAbs >= 0.F &&
586 const PositionPair position =
592 const std::uint8_t positionMask =
static_cast<std::uint8_t
>(
593 wire.stationMask &
strip.stationMask);
594 const std::uint8_t combinedMask =
static_cast<std::uint8_t
>(
595 wire.stationMask |
strip.stationMask);
596 if (std::popcount(
static_cast<unsigned int>(combinedMask)) < 2) {
601 CandidatePair{&wire, &
strip, position, positionMask, combinedMask});
605 pruneLocalCandidateBins(pairs,
m_config, statistics);
606 suppressLocalPositionDuplicates(pairs,
m_config, statistics);
607 retainCandidateWorkingSet(pairs,
608 m_config.maxSegmentCombinationsPerGroup,
611 for (
const CandidatePair&
pair : pairs) {
612 const CandidateKey keyValue = candidateKey(groupKey,
pair);
613 if (!candidateKeys.insert(keyValue).second) {
618 const ProjectionSegment& wire = *
pair.wire;
619 const ProjectionSegment&
strip = *
pair.strip;
622 candidate.
sectorId = groupKey.triggerSector;
624 candidate.
bcTag = groupKey.bcTag;
625 candidate.
eta =
pair.position.wire->eta;
626 candidate.
phi =
pair.position.strip->phi;
643 candidate.
m1Eta = wire.points[0]->eta;
649 candidate.
m2Eta = wire.points[1]->eta;
655 candidate.
m3Eta = wire.points[2]->eta;
663 static_cast<unsigned int>(
pair.combinedStationMask)) +
664 wire.summedQuality +
strip.summedQuality);
665 candidates.emplace_back(candidate);
669 return StatusCode::SUCCESS;
Scalar deltaPhi(const MatrixBase< Derived > &vec) const
Scalar deltaR(const MatrixBase< Derived > &vec) const
Scalar phi() const
phi method
SegmentReconstructionConfig m_config
StatusCode build(const StationCoincidenceContainer &coincidences, TgcL0CandidateContainer &candidates, SegmentStatistics &statistics, TgcL0SegmentContainer *validationSegments=nullptr) const
SegmentReconstruction(SegmentReconstructionConfig config=SegmentReconstructionConfig{})
int count(std::string s, const std::string ®x)
count how many occurances of a regx are in a string
std::vector< StationCoincidence > StationCoincidenceContainer
std::vector< TgcL0Segment > TgcL0SegmentContainer
TgcL0SegmentProjection
Projection represented by a transient TGC segment.
std::vector< TgcL0Candidate > TgcL0CandidateContainer
Event-local candidate collection.
double deltaEta(const I4Momentum &p1, const I4Momentum &p2)
Computes efficiently .
void stable_sort(DataModel_detail::iterator< DVL > beg, DataModel_detail::iterator< DVL > end)
Specialization of stable_sort for DataVector/List.
double deltaPhi(double phiA, double phiB)
delta Phi in range [-pi,pi[
setSAddress setEtaMS setDirPhiMS setDirZMS setBarrelRadius setEndcapAlpha setEndcapRadius setPhiMap phiBin
setSAddress setEtaMS setDirPhiMS setDirZMS setBarrelRadius setEndcapAlpha setEndcapRadius setInterceptInner setEtaMap etaBin
setBGCode setTAP setLVL2ErrorBits bool
Event-local candidate used by the TGC simulation tools.
std::int16_t m1StationEta
Offline chamber identifiers retained for overlap diagnostics.
std::uint8_t m2StripQuality
std::uint8_t m1StripQuality
Station-coincidence qualities for strip projections.
std::int16_t m2StationEta
float deltaPhi
Signed azimuthal-angle residual, in radians.
std::uint16_t m1StationPhi
std::uint8_t m2WireQuality
std::uint8_t positionStationMask
Alias of stationMask retained for explicit validation/debug use.
std::int16_t m3StationEta
std::uint8_t m1WireQuality
Station-coincidence qualities for wire projections.
std::uint16_t readoutSector
Run-3 detector/readout sector retained for diagnostics.
float m1Eta
Reconstructed station positions used only by validation/debug code.
std::uint16_t bcTag
Bunch-crossing tag.
std::uint8_t stripStationMask
Stations used by the strip projection segment.
std::uint8_t m3StripQuality
std::uint8_t m3WireQuality
std::uint8_t stationMask
Stations with a complete two-dimensional wire-and-strip position.
std::uint16_t m3StationPhi
std::uint16_t sectorId
Trigger Sector identifier.
std::uint16_t m2StationPhi
std::uint8_t wireStationMask
Stations used by the wire projection segment.
float eta
Pseudorapidity at the TGC pivot plane.
float phi
Azimuth at the TGC pivot plane, in radians.
std::uint16_t pivotStripChannel
std::uint16_t subdetectorId
Subdetector identifier.
std::uint8_t selectorPriority
Track-Selector priority.
std::uint16_t pivotWireChannel
Representative pivot channels retained for diagnostics.
float deltaTheta
Signed polar-angle residual, in radians.
std::uint8_t wireQuality
Wire quality.
Event-local hardware processing key.
std::size_t nStripSegments
std::size_t nDuplicateCandidates
std::size_t nWireSegments
std::size_t nRejectedPairs
Event-local projection segment optionally exposed for validation.