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);
64 return segmentTheta - pivotTheta;
67struct ProjectionSegment {
68 std::array<const Coincidence*, 3> points{};
70 std::uint8_t stationMask{0};
71 std::uint8_t summedQuality{0};
72 std::uint8_t nStations{0};
78 float outputResidual{0.F};
79 float consistency{0.F};
80 std::uint16_t pivotChannel{0};
84void appendValidationSegments(
const GroupKey& groupKey,
85 const std::vector<ProjectionSegment>& segments,
86 OutputSegments& output) {
88 for (
const ProjectionSegment& segment : segments) {
90 value.subdetectorId = groupKey.subDetectorId;
91 value.triggerSector = groupKey.triggerSector;
92 value.bcTag = groupKey.bcTag;
93 value.projection = segment.isStrip ? Projection::Strip : Projection::Wire;
94 value.stationMask = segment.stationMask;
95 value.summedQuality = segment.summedQuality;
96 value.nStations = segment.nStations;
97 value.eta = segment.eta;
98 value.phi = segment.phi;
99 value.residual = segment.residual;
100 value.outputResidual = segment.outputResidual;
101 value.consistency = segment.consistency;
102 value.pivotChannel = segment.pivotChannel;
103 output.emplace_back(value);
107const Coincidence* projectionPivot(
const ProjectionSegment& segment) {
108 if (segment.points[2] !=
nullptr)
return segment.points[2];
109 if (segment.points[1] !=
nullptr)
return segment.points[1];
110 return segment.points[0];
113ProjectionSegment makeSegment(
const Coincidence* m1,
const Coincidence* m2,
114 const Coincidence* m3,
const bool isStrip) {
115 ProjectionSegment segment;
116 segment.points = {
m1,
m2,
m3};
118 for (
const Coincidence* point : segment.points) {
119 if (point ==
nullptr)
continue;
120 segment.stationMask |= stationBit(point->station);
121 segment.summedQuality =
static_cast<std::uint8_t
>(
122 segment.summedQuality + point->observedLayers);
126 const Coincidence* pivot = projectionPivot(segment);
127 if (pivot !=
nullptr) {
128 segment.eta = pivot->eta;
129 segment.phi = pivot->phi;
130 segment.pivotChannel = pivot->channel;
133 std::vector<const Coincidence*> points;
134 for (
const Coincidence* point : segment.points) {
135 if (point !=
nullptr) points.emplace_back(point);
137 if (points.size() >= 2U) {
138 const float first = measuringCoordinate(*points.front());
139 const float last = measuringCoordinate(*points.back());
140 segment.residual = coordinateDifference(isStrip, first, last);
141 segment.outputResidual =
isStrip
143 : physicalDeltaTheta(*points.front(),
145 segment.consistency = std::abs(segment.residual);
147 if (m1 !=
nullptr && m2 !=
nullptr && m3 !=
nullptr) {
148 const float c1 = measuringCoordinate(*m1);
149 const float c2 = measuringCoordinate(*m2);
150 const float c3 = measuringCoordinate(*m3);
151 const float predictedM1 =
isStrip
152 ?
c2 -
static_cast<float>(
155 segment.consistency = std::abs(
156 coordinateDifference(isStrip, c1, predictedM1));
157 segment.residual = coordinateDifference(isStrip, c1, c3);
158 segment.outputResidual =
isStrip
160 : physicalDeltaTheta(*m1, *m3);
166 for (
const Coincidence* point : points) {
167 sinPhi += std::sin(point->phi);
168 cosPhi += std::cos(point->phi);
170 segment.phi = std::atan2(sinPhi, cosPhi);
171 }
else if (!points.empty()) {
173 for (
const Coincidence* point : points) etaSum += point->eta;
174 segment.eta = etaSum /
static_cast<float>(points.size());
179int coordinateKey(
const Coincidence* point,
const bool isStrip) {
180 if (point ==
nullptr)
return 0;
181 const float coordinate =
isStrip ? point->phi : point->eta;
182 return static_cast<int>(std::lround(100000.F * coordinate));
185using SegmentKey = std::tuple<std::uint8_t, bool, int, int, int>;
187SegmentKey segmentKey(
const ProjectionSegment& segment) {
188 return {segment.stationMask, segment.isStrip,
189 coordinateKey(segment.points[0], segment.isStrip),
190 coordinateKey(segment.points[1], segment.isStrip),
191 coordinateKey(segment.points[2], segment.isStrip)};
194bool betterSegment(
const ProjectionSegment& lhs,
195 const ProjectionSegment& rhs) {
196 if (lhs.nStations != rhs.nStations)
return lhs.nStations > rhs.nStations;
197 if (lhs.summedQuality != rhs.summedQuality) {
198 return lhs.summedQuality > rhs.summedQuality;
200 const bool lhsHasM1 = (lhs.stationMask & 0x1U) != 0U;
201 const bool rhsHasM1 = (rhs.stationMask & 0x1U) != 0U;
202 if (lhsHasM1 != rhsHasM1)
return lhsHasM1;
203 const bool lhsHasM3 = (lhs.stationMask & 0x4U) != 0U;
204 const bool rhsHasM3 = (rhs.stationMask & 0x4U) != 0U;
205 if (lhsHasM3 != rhsHasM3)
return lhsHasM3;
206 if (lhs.consistency != rhs.consistency) {
207 return lhs.consistency < rhs.consistency;
209 return segmentKey(lhs) < segmentKey(rhs);
212void addSegment(std::vector<ProjectionSegment>& segments,
213 std::set<SegmentKey>& keys,
const Coincidence* m1,
214 const Coincidence* m2,
const Coincidence* m3,
215 const bool isStrip, Statistics& statistics) {
216 const ProjectionSegment segment = makeSegment(m1, m2, m3, isStrip);
217 if (!
keys.insert(segmentKey(segment)).second) {
218 ++statistics.nDuplicateProjectionSegments;
221 segments.emplace_back(segment);
224void retainProjectionWorkingSet(std::vector<ProjectionSegment>& segments,
225 const std::size_t maximum,
226 Statistics& statistics) {
228 if (maximum == 0U || segments.size() <= maximum)
return;
230 std::array<bool, 8> retainedMask{};
231 std::vector<ProjectionSegment> retained;
232 retained.reserve(maximum);
233 for (
const ProjectionSegment& segment : segments) {
234 if (segment.stationMask >= retainedMask.size() ||
235 retainedMask[segment.stationMask]) {
238 retained.emplace_back(segment);
239 retainedMask[segment.stationMask] =
true;
240 if (retained.size() == maximum)
break;
242 for (
const ProjectionSegment& segment : segments) {
243 if (retained.size() == maximum)
break;
244 const bool alreadyRetained = std::any_of(
245 retained.begin(), retained.end(),
246 [&segment](
const ProjectionSegment& selected) {
247 return segmentKey(selected) == segmentKey(segment);
249 if (!alreadyRetained) retained.emplace_back(segment);
251 statistics.nLimitedProjectionSegments += segments.size() - retained.size();
252 segments = std::move(retained);
256std::vector<ProjectionSegment> buildProjectionSegments(
257 const std::array<std::vector<const Coincidence*>, 3>& points,
258 const bool isStrip,
const Config& config, Statistics& statistics) {
259 std::vector<ProjectionSegment> segments;
260 std::set<SegmentKey>
keys;
262 for (
const Coincidence* m1 : points[0]) {
263 for (
const Coincidence* m2 : points[1]) {
264 for (
const Coincidence* m3 : points[2]) {
265 addSegment(segments, keys, m1, m2, m3, isStrip, statistics);
269 for (
const Coincidence* m1 : points[0]) {
270 for (
const Coincidence* m3 : points[2]) {
271 addSegment(segments, keys, m1,
nullptr, m3, isStrip, statistics);
273 for (
const Coincidence* m2 : points[1]) {
274 addSegment(segments, keys, m1, m2,
nullptr, isStrip, statistics);
277 for (
const Coincidence* m2 : points[1]) {
278 for (
const Coincidence* m3 : points[2]) {
279 addSegment(segments, keys,
nullptr, m2, m3, isStrip, statistics);
283 retainProjectionWorkingSet(segments,
284 config.maxSegmentCombinationsPerGroup,
289std::uint8_t quality(
const ProjectionSegment& segment,
const Station station) {
290 const Coincidence* point = segment.points[stationIndex(station)];
291 return point ==
nullptr ? 0U : point->observedLayers;
295 const Coincidence* wire{
nullptr};
296 const Coincidence* strip{
nullptr};
297 std::size_t stationIndex{0U};
300 float coordinateResidual{0.F};
302 explicit operator bool()
const {
return wire !=
nullptr && strip !=
nullptr; }
305PositionPair selectPositionPair(
const ProjectionSegment& wire,
306 const ProjectionSegment&
strip,
311 const std::size_t station =
index - 1U;
312 const Coincidence* wirePoint = wire.points[station];
313 const Coincidence* stripPoint =
strip.points[station];
314 if (wirePoint ==
nullptr || stripPoint ==
nullptr)
continue;
316 const float deltaEta = std::abs(wirePoint->eta - stripPoint->eta);
317 const float deltaPhi = std::abs(
static_cast<float>(
319 if (
config.maxPivotWireStripDeltaEta >= 0.F &&
320 deltaEta >
config.maxPivotWireStripDeltaEta) {
323 if (
config.maxPivotWireStripDeltaPhi >= 0.F &&
333struct CandidatePair {
334 const ProjectionSegment* wire{
nullptr};
335 const ProjectionSegment* strip{
nullptr};
336 PositionPair position{};
337 std::uint8_t positionStationMask{0};
338 std::uint8_t combinedStationMask{0};
341int bendScoreMilli(
const CandidatePair&
pair) {
342 const float bend = std::sqrt(
343 pair.wire->residual *
pair.wire->residual +
344 0.25F *
pair.strip->residual *
pair.strip->residual);
345 return static_cast<int>(std::lround(1000.F * bend));
348bool betterCandidatePair(
const CandidatePair& lhs,
const CandidatePair& rhs) {
349 const std::uint16_t lhsQuality =
static_cast<std::uint16_t
>(
350 lhs.wire->summedQuality + lhs.strip->summedQuality);
351 const std::uint16_t rhsQuality =
static_cast<std::uint16_t
>(
352 rhs.wire->summedQuality + rhs.strip->summedQuality);
353 if (lhsQuality != rhsQuality)
return lhsQuality > rhsQuality;
354 const unsigned int lhsStations =
355 std::popcount(
static_cast<unsigned int>(lhs.combinedStationMask));
356 const unsigned int rhsStations =
357 std::popcount(
static_cast<unsigned int>(rhs.combinedStationMask));
358 if (lhsStations != rhsStations)
return lhsStations > rhsStations;
359 if (lhs.position.stationIndex != rhs.position.stationIndex) {
360 return lhs.position.stationIndex > rhs.position.stationIndex;
362 const int lhsBend = bendScoreMilli(lhs);
363 const int rhsBend = bendScoreMilli(rhs);
364 if (lhsBend != rhsBend)
return lhsBend < rhsBend;
365 if (lhs.position.coordinateResidual != rhs.position.coordinateResidual) {
366 return lhs.position.coordinateResidual < rhs.position.coordinateResidual;
368 if (lhs.position.wire->eta != rhs.position.wire->eta) {
369 return lhs.position.wire->eta < rhs.position.wire->eta;
371 return lhs.position.strip->phi < rhs.position.strip->phi;
374using CandidateKey = std::tuple<
375 std::uint16_t, std::uint16_t, std::uint16_t, std::uint16_t,
376 std::int16_t, std::uint16_t, std::uint16_t, std::int16_t,
377 std::uint16_t, std::uint8_t, std::uint8_t, std::uint8_t, int,
int>;
379CandidateKey candidateKey(
const GroupKey& group,
const CandidatePair&
pair) {
380 return {
group.subDetectorId,
383 pair.position.wire->detectorSector,
384 pair.position.wire->stationEta,
385 pair.position.wire->stationPhi,
386 pair.position.strip->detectorSector,
387 pair.position.strip->stationEta,
388 pair.position.strip->stationPhi,
389 pair.wire->stationMask,
390 pair.strip->stationMask,
391 pair.positionStationMask,
392 static_cast<int>(std::lround(100000.F *
pair.position.wire->eta)),
393 static_cast<int>(std::lround(100000.F *
pair.position.strip->phi))};
396void pruneLocalCandidateBins(std::vector<CandidatePair>& pairs,
398 Statistics& statistics) {
399 if (
config.maxCandidatesPerLocalBin == 0U ||
400 config.localCandidateEtaBinWidth <= 0.F ||
401 config.localCandidatePhiBinWidth <= 0.F ||
pairs.empty()) {
405 std::map<std::tuple<int, int, std::size_t>, std::size_t> keptPerBin;
406 std::vector<CandidatePair> retained;
407 retained.reserve(
pairs.size());
408 for (
const CandidatePair&
pair : pairs) {
409 const float phi =
static_cast<float>(
411 const int etaBin =
static_cast<int>(std::floor(
412 pair.position.wire->eta /
config.localCandidateEtaBinWidth));
413 const int phiBin =
static_cast<int>(std::floor(
414 (
phi + std::numbers::pi_v<float>) /
415 config.localCandidatePhiBinWidth));
417 std::make_tuple(etaBin, phiBin,
pair.position.stationIndex);
418 std::size_t&
count = keptPerBin[
key];
420 ++statistics.nLimitedCandidates;
423 retained.emplace_back(
pair);
426 pairs = std::move(retained);
429bool sameChamber(
const Coincidence& lhs,
const Coincidence& rhs) {
430 return lhs.station == rhs.station && lhs.stationEta == rhs.stationEta &&
431 lhs.stationPhi == rhs.stationPhi;
434bool sameCandidateChamberPath(
const CandidatePair& lhs,
435 const CandidatePair& rhs) {
436 unsigned int commonStations = 0U;
437 for (std::size_t station = 0U; station < lhs.wire->points.size(); ++station) {
438 const Coincidence* lhsPoint = lhs.wire->points[station];
439 const Coincidence* rhsPoint = rhs.wire->points[station];
440 if (lhsPoint ==
nullptr || rhsPoint ==
nullptr)
continue;
441 if (!sameChamber(*lhsPoint, *rhsPoint))
return false;
444 return commonStations >= 2U;
447bool sameCoarseLocalPosition(
const CandidatePair& lhs,
448 const CandidatePair& rhs,
450 if (!sameCandidateChamberPath(lhs, rhs))
return false;
451 if (
config.localDuplicateEtaWindow <= 0.F ||
452 config.localDuplicatePhiWindow <= 0.F) {
453 return lhs.position.wire->eta == rhs.position.wire->eta &&
454 lhs.position.strip->phi == rhs.position.strip->phi;
456 return std::abs(lhs.position.wire->eta - rhs.position.wire->eta) <
457 config.localDuplicateEtaWindow &&
459 rhs.position.strip->phi)) <
460 config.localDuplicatePhiWindow;
463void suppressLocalPositionDuplicates(std::vector<CandidatePair>& pairs,
465 Statistics& statistics) {
466 if (
config.maxCandidatesPerLocalPosition == 0U ||
pairs.empty())
return;
468 std::vector<CandidatePair> retained;
469 retained.reserve(
pairs.size());
470 for (
const CandidatePair&
pair : pairs) {
471 std::size_t
count = 0U;
472 for (
const CandidatePair& selected : retained) {
473 if (sameCoarseLocalPosition(
pair, selected, config)) {
475 if (
count >=
config.maxCandidatesPerLocalPosition)
break;
478 if (
count >=
config.maxCandidatesPerLocalPosition) {
479 ++statistics.nLocalDuplicateCandidates;
482 retained.emplace_back(
pair);
484 pairs = std::move(retained);
487void retainCandidateWorkingSet(std::vector<CandidatePair>& pairs,
488 const std::size_t maximum,
489 Statistics& statistics) {
491 if (maximum == 0U ||
pairs.size() <= maximum)
return;
493 std::array<bool, 8> retainedMask{};
494 std::vector<CandidatePair> retained;
495 retained.reserve(maximum);
496 for (
const CandidatePair&
pair : pairs) {
497 if (
pair.positionStationMask >= retainedMask.size() ||
498 retainedMask[
pair.positionStationMask]) {
501 retained.emplace_back(
pair);
502 retainedMask[
pair.positionStationMask] =
true;
503 if (retained.size() == maximum)
break;
505 for (
const CandidatePair&
pair : pairs) {
506 if (retained.size() == maximum)
break;
507 const bool alreadyRetained = std::any_of(
508 retained.begin(), retained.end(),
509 [&
pair](
const CandidatePair& selected) {
510 return selected.wire == pair.wire && selected.strip == pair.strip &&
511 selected.position.stationIndex == pair.position.stationIndex;
513 if (!alreadyRetained) retained.emplace_back(
pair);
515 statistics.nLimitedCandidates +=
pairs.size() - retained.size();
516 pairs = std::move(retained);
520void countPositionMask(
const std::uint8_t mask, Statistics& statistics) {
521 if (mask == 0x7U) ++statistics.nM1M2M3Candidates;
522 else if (mask == 0x3U) ++statistics.nM1M2Candidates;
523 else if (mask == 0x5U) ++statistics.nM1M3Candidates;
524 else if (mask == 0x6U) ++statistics.nM2M3Candidates;
525 else if (mask == 0x1U) ++statistics.nM1OnlyPositionCandidates;
526 else if (mask == 0x2U) ++statistics.nM2OnlyPositionCandidates;
527 else if (mask == 0x4U) ++statistics.nM3OnlyPositionCandidates;
545 if (validationSegments !=
nullptr) validationSegments->clear();
548 std::array<std::array<std::vector<const Coincidence*>, 3>, 2>;
549 std::map<GroupKey, Projections> grouped;
550 for (
const Coincidence& coincidence : coincidences) {
551 if (stationBit(coincidence.station) == 0U)
continue;
552 const std::size_t projection = coincidence.isStrip ? 1U : 0U;
553 grouped[coincidence.key][projection][stationIndex(coincidence.station)]
554 .emplace_back(&coincidence);
557 std::set<CandidateKey> candidateKeys;
558 for (
const auto& [groupKey, projections] : grouped) {
559 const std::vector<ProjectionSegment> wireSegments =
560 buildProjectionSegments(projections[0],
false,
m_config, statistics);
561 const std::vector<ProjectionSegment> stripSegments =
562 buildProjectionSegments(projections[1],
true,
m_config, statistics);
565 if (validationSegments !=
nullptr) {
566 appendValidationSegments(groupKey, wireSegments, *validationSegments);
567 appendValidationSegments(groupKey, stripSegments, *validationSegments);
570 std::vector<CandidatePair> pairs;
571 pairs.reserve(wireSegments.size() * stripSegments.size());
572 for (
const ProjectionSegment& wire : wireSegments) {
573 if (
m_config.maxCandidateDThetaAbs >= 0.F &&
574 std::abs(wire.residual) >
m_config.maxCandidateDThetaAbs) {
578 for (
const ProjectionSegment&
strip : stripSegments) {
579 if (
m_config.maxCandidateDPhiAbs >= 0.F &&
584 const PositionPair position =
590 const std::uint8_t positionMask =
static_cast<std::uint8_t
>(
591 wire.stationMask &
strip.stationMask);
592 const std::uint8_t combinedMask =
static_cast<std::uint8_t
>(
593 wire.stationMask |
strip.stationMask);
594 if (std::popcount(
static_cast<unsigned int>(combinedMask)) < 2) {
599 CandidatePair{&wire, &
strip, position, positionMask, combinedMask});
603 pruneLocalCandidateBins(pairs,
m_config, statistics);
604 suppressLocalPositionDuplicates(pairs,
m_config, statistics);
605 retainCandidateWorkingSet(pairs,
606 m_config.maxSegmentCombinationsPerGroup,
609 for (
const CandidatePair&
pair : pairs) {
610 const CandidateKey keyValue = candidateKey(groupKey,
pair);
611 if (!candidateKeys.insert(keyValue).second) {
616 const ProjectionSegment& wire = *
pair.wire;
617 const ProjectionSegment&
strip = *
pair.strip;
620 candidate.
sectorId = groupKey.triggerSector;
622 candidate.
bcTag = groupKey.bcTag;
623 candidate.
eta =
pair.position.wire->eta;
624 candidate.
phi =
pair.position.strip->phi;
641 candidate.
m1Eta = wire.points[0]->eta;
647 candidate.
m2Eta = wire.points[1]->eta;
653 candidate.
m3Eta = wire.points[2]->eta;
661 static_cast<unsigned int>(
pair.combinedStationMask)) +
662 wire.summedQuality +
strip.summedQuality);
663 candidates.emplace_back(candidate);
667 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.