ATLAS Offline Software
Loading...
Searching...
No Matches
SegmentEdgeInferenceAlg.cxx
Go to the documentation of this file.
1/*
2 Copyright (C) 2002-2025 CERN for the benefit of the ATLAS collaboration
3*/
5#include "InferenceUtils.h"
12#include "Acts/Utilities/Helpers.hpp"
13#include <algorithm>
14#include <cmath>
15#include <limits>
16#include <memory>
17#include <numeric>
18#include <unordered_set>
19#include <utility>
20
21namespace MuonML {
22
23namespace {
24
25std::uint64_t
26undirectedPairKey(std::size_t first, std::size_t second) {
27 if (first > second) std::swap(first, second);
28 const auto first32 = static_cast<std::uint32_t>(first);
29 const auto second32 = static_cast<std::uint32_t>(second);
30
31 return (static_cast<std::uint64_t>(first32) << 32) | static_cast<std::uint64_t>(second32);
32}
33
34std::pair<std::size_t, std::size_t>
35unpackPairKey(std::uint64_t key) {
36 const auto first = static_cast<std::uint32_t>(key >> 32);
37 const auto second = static_cast<std::uint32_t>(key);
38 return {first, second};
39}
40
41class DisjointSet {
42public:
43 explicit DisjointSet(std::size_t size) : m_parent(size), m_rank(size, 0) {
44 std::iota(m_parent.begin(), m_parent.end(), 0);
45 }
46
47 std::size_t find(std::size_t node) {
48 if (m_parent[node] != node) m_parent[node] = find(m_parent[node]);
49 return m_parent[node];
50 }
51
52 void unite(std::size_t first, std::size_t second) {
53 first = find(first);
54 second = find(second);
55 if (first == second) return;
56 if (m_rank[first] < m_rank[second]) std::swap(first, second);
57 m_parent[second] = first;
58 if (m_rank[first] == m_rank[second]) ++m_rank[first];
59 }
60
61private:
62 std::vector<std::size_t> m_parent;
63 std::vector<unsigned char> m_rank;
64};
65
66// Explicit radial ranking (0 = closest to the IP), independent of the
67// LayerIndex enum's underlying integer values. BarrelExtended is the
68// barrel's BI-equivalent chamber, so it ranks alongside Inner.
69int layerRadialRank(Muon::MuonStationIndex::LayerIndex layer) {
71 switch (layer) {
72 case Inner:
73 case BarrelExtended:
74 return 0;
75 case Middle:
76 return 1;
77 case Outer:
78 return 2;
79 case Extended:
80 return 3;
81 default:
82 return 4;
83 }
84}
85
86// Truth diagnostics only. Why a node that passed the threshold was not kept.
87constexpr unsigned char kReasonNotSelected = 0; // no selected pair (top-K / mutual gate)
88constexpr unsigned char kReasonChamberDedup = 1; // KeepBestSegmentPerChamber
89constexpr unsigned char kReasonComponentRejected = 2; // MinSegmentsPerComponent / no anchor
90
91// Loss categories; the order matches the table printed by finalize().
92enum LossCategory : std::uint8_t{
93 kNoPartnerSingleton = 0,
94 kNoPartnerSameChamber,
95 kBucketSelf,
96 kBucketPartners,
97 kSectorWindow,
98 kAngleWindow,
99 kEdgeCaps,
100 kBelowThreshold,
101 kDedupChamber,
102 kComponentRejected,
103 kNotSelected,
104 kNumLossCategories
105};
106
107enum class TruthRegion : std::uint8_t { kBarrel = 0, kTransition = 1, kEndcap = 2 };
108
109TruthRegion regionOf(double absEta) {
110 return absEta < 1.0 ? TruthRegion::kBarrel
111 : (absEta < 1.3 ? TruthRegion::kTransition : TruthRegion::kEndcap);
112}
113
114} // namespace
115
117 ATH_CHECK(m_segmentKey.initialize());
118 ATH_CHECK(m_pairGateDecorKey.initialize());
120 ATH_CHECK(m_edgeClassifier.retrieve());
121 m_truthDiagEnabled = m_edgeClassifier->enableTruthDiagnostics();
122 return StatusCode::SUCCESS;
123}
124
125StatusCode SegmentEdgeInferenceAlg::execute(const EventContext& ctx) const {
126 const xAOD::MuonSegmentContainer* segments{};
127 ATH_CHECK(SG::get(segments, m_segmentKey, ctx));
128 // Evaluated once per event; short-circuits without touching the message
129 // service unless the property was explicitly enabled.
130 const bool truthDiag = m_truthDiagEnabled && msgLvl(MSG::DEBUG);
131 ATH_MSG_DEBUG("Event " << ctx.eventID().event_number()
132 << ": input segments in '" << m_segmentKey.key()
133 << "' = " << segments->size());
134
135 if (truthDiag) {
136 std::size_t truthSegs = 0, bkgSegs = 0;
137 for (const xAOD::MuonSegment* seg : *segments) {
138 MuonR4::getTruthMatchedParticle(*seg) ? ++truthSegs : ++bkgSegs;
139 }
140 m_sumInputTruthSegments += truthSegs;
141 m_sumInputBkgSegments += bkgSegs;
142 }
143
144 SegmentEdgeGraph graph{};
145 std::vector<SegmentEdgeScore> scores{};
146 ATH_CHECK(m_edgeClassifier->buildGraph(ctx, *segments, graph));
147 ATH_MSG_DEBUG("Event " << ctx.eventID().event_number()
148 << ": built graph with nodes=" << graph.nNodes
149 << ", edges=" << graph.nEdges);
150
151 // Per-node truth-particle grouping key (-1 = unlabeled), used for the
152 // segment-level funnel and for the edge-level true/background split.
153 std::vector<int32_t> nodeTruthId;
154 if (truthDiag) {
155 nodeTruthId.assign(graph.nNodes, -1);
156 std::size_t truthSegs = 0, bkgSegs = 0;
157 for (std::size_t node = 0; node < graph.nNodes; ++node) {
158 const xAOD::TruthParticle* truthPart =
160 if (truthPart) {
161 nodeTruthId[node] = static_cast<int32_t>(truthPart->index());
162 ++truthSegs;
163 } else {
164 ++bkgSegs;
165 }
166 }
167 m_sumOnnxTruthSegments += truthSegs;
168 m_sumOnnxBkgSegments += bkgSegs;
169 }
170
171 ATH_CHECK(m_edgeClassifier->classifyEdges(ctx, graph, scores));
172 if (!scores.empty()) {
173 float minProb = std::numeric_limits<float>::max();
174 float maxProb = std::numeric_limits<float>::lowest();
175 for (const SegmentEdgeScore& score : scores) {
176 minProb = std::min(minProb, score.probability);
177 maxProb = std::max(maxProb, score.probability);
178 }
179 ATH_MSG_DEBUG("Event " << ctx.eventID().event_number()
180 << ": edge scores=" << scores.size()
181 << ", prob range=[" << minProb << ", " << maxProb << "]");
182 } else {
183 ATH_MSG_DEBUG("Event " << ctx.eventID().event_number()
184 << ": no edge scores produced");
185 }
186
187 // Symmetrise directed model outputs once. Everything downstream uses the
188 // score of an undirected segment association.
189 std::unordered_map<std::uint64_t, float> pairProbability;
190 pairProbability.reserve(scores.size());
191 for (const SegmentEdgeScore& score : scores) {
192 if (score.src >= graph.nNodes || score.dst >= graph.nNodes ||
193 score.src == score.dst) {
194 continue;
195 }
196 const std::uint64_t key = undirectedPairKey(score.src, score.dst);
197 auto [it, inserted] = pairProbability.emplace(key, score.probability);
198 if (!inserted) it->second = std::max(it->second, score.probability);
199 }
200
201 // Edge-level recall/false-positive check: candidate pairs that reached ONNX
202 // true pairs reach PairGateThreshold, versus how many background pairs do.
203 if (truthDiag) {
204 std::size_t trueTotal = 0, truePassed = 0, bkgTotal = 0, bkgPassed = 0;
205 for (const auto& [key, probability] : pairProbability) {
206 const auto [first, second] = unpackPairKey(key);
207 if (first >= graph.nNodes || second >= graph.nNodes) continue;
208 const bool isTrueEdge =
209 nodeTruthId[first] >= 0 && nodeTruthId[first] == nodeTruthId[second];
210 const bool passed = probability >= m_pairGateThreshold.value();
211 if (isTrueEdge) {
212 ++trueTotal;
213 truePassed += passed;
214 } else {
215 ++bkgTotal;
216 bkgPassed += passed;
217 }
218 }
219 m_sumTrueEdgesTotal += trueTotal;
220 m_sumTrueEdgesPassed += truePassed;
221 m_sumBkgEdgesTotal += bkgTotal;
222 m_sumBkgEdgesPassed += bkgPassed;
223 }
224
225 // Form a sparse, score-ranked topology. Mutual top-K associations retain
226 // locally consistent paths while suppressing one-sided bridges.
227 using WeightedEdge = std::pair<std::uint64_t, float>;
228 const auto betterWeightedEdge = [](const WeightedEdge& first,
229 const WeightedEdge& second) {
230 const int probabilityOrder = InferenceUtils::compareFloatDescending(
231 first.second, second.second);
232 if (probabilityOrder != 0) {
233 return probabilityOrder < 0;
234 }
235 return first.first < second.first;
236 };
237 std::vector<std::vector<WeightedEdge>> edgesByNode(graph.nNodes);
238 std::size_t thresholdPairs = 0;
239 for (const auto& [key, probability] : pairProbability) {
240 if (probability < m_pairGateThreshold.value()) continue;
241 const auto [first, second] = unpackPairKey(key);
242 if (first >= graph.nNodes || second >= graph.nNodes) continue;
243 edgesByNode[first].emplace_back(key, probability);
244 edgesByNode[second].emplace_back(key, probability);
245 ++thresholdPairs;
246 }
247
248 std::size_t thresholdedNodes = 0;
249 for (const std::vector<WeightedEdge>& nodeEdges : edgesByNode) {
250 thresholdedNodes += !nodeEdges.empty();
251 }
252
253 std::unordered_map<std::uint64_t, unsigned char> nominations;
254 nominations.reserve(thresholdPairs);
255 for (std::vector<WeightedEdge>& nodeEdges : edgesByNode) {
256 std::sort(nodeEdges.begin(), nodeEdges.end(), betterWeightedEdge);
257 if (m_maxEdgesPerNode.value() != 0 &&
258 nodeEdges.size() > m_maxEdgesPerNode.value()) {
259 nodeEdges.resize(m_maxEdgesPerNode.value());
260 }
261 for (const WeightedEdge& edge : nodeEdges) {
262 ++nominations[edge.first];
263 }
264 }
265
266 std::size_t mutualTopKPairs = 0;
267 std::size_t oneSidedTopKPairs = 0;
268 for (const auto& [_, count] : nominations) {
269 if (count == 2) {
270 ++mutualTopKPairs;
271 } else {
272 ++oneSidedTopKPairs;
273 }
274 }
275
276 std::unordered_set<std::uint64_t> selectedPairKeys;
277 selectedPairKeys.reserve(thresholdPairs);
278 if (m_useDegreeCappedComponents.value()) {
279 // Apply a score-ordered global degree cap when explicitly requested.
280 std::vector<WeightedEdge> acceptedPairs;
281 acceptedPairs.reserve(thresholdPairs);
282 for (const auto& [key, probability] : pairProbability) {
283 if (probability < m_pairGateThreshold.value()) continue;
284 acceptedPairs.emplace_back(key, probability);
285 }
286 std::sort(acceptedPairs.begin(), acceptedPairs.end(), betterWeightedEdge);
287
288 const unsigned int maxDegree = m_maxEdgesPerNode.value();
289 std::vector<unsigned int> degree(graph.nNodes, 0);
290 for (const WeightedEdge& edge : acceptedPairs) {
291 const auto [first, second] = unpackPairKey(edge.first);
292 if (maxDegree != 0 &&
293 (degree[first] >= maxDegree || degree[second] >= maxDegree)) {
294 continue;
295 }
296 selectedPairKeys.insert(edge.first);
297 ++degree[first];
298 ++degree[second];
299 }
300 } else {
301 for (const auto& [key, count] : nominations) {
302 if (m_requireMutualTopKEdges.value() && count != 2) continue;
303 selectedPairKeys.insert(key);
304 }
305 }
306
307 // A mutual top-K selection can leave an endpoint without an association.
308 // Add its best thresholded edge, at most once per orphaned endpoint.
309 std::size_t orphanRecoveryPairs = 0;
310 if (!m_useDegreeCappedComponents.value() &&
311 m_requireMutualTopKEdges.value() &&
312 m_recoverOrphanNodes.value()) {
313 std::vector<unsigned char> selectedNode(graph.nNodes, 0);
314 for (const std::uint64_t key : selectedPairKeys) {
315 const auto [first, second] = unpackPairKey(key);
316 if (first < graph.nNodes) selectedNode[first] = 1;
317 if (second < graph.nNodes) selectedNode[second] = 1;
318 }
319 for (std::size_t node = 0; node < graph.nNodes; ++node) {
320 if (selectedNode[node] || edgesByNode[node].empty()) continue;
321 const std::uint64_t key = edgesByNode[node].front().first;
322 const auto [first, second] = unpackPairKey(key);
323 if (first >= graph.nNodes || second >= graph.nNodes) continue;
324 if (selectedPairKeys.insert(key).second) ++orphanRecoveryPairs;
325 selectedNode[first] = 1;
326 selectedNode[second] = 1;
327 }
328 }
329
330 std::vector<std::uint64_t> selectedPairs{selectedPairKeys.begin(),
331 selectedPairKeys.end()};
332 std::sort(selectedPairs.begin(), selectedPairs.end());
333
334 DisjointSet components{graph.nNodes};
335 std::vector<bool> activeNode(graph.nNodes, false);
336 for (const std::uint64_t key : selectedPairs) {
337 const auto [first, second] = unpackPairKey(key);
338 components.unite(first, second);
339 activeNode[first] = true;
340 activeNode[second] = true;
341 }
342
343 std::unordered_map<std::size_t, std::vector<std::size_t>> byRoot;
344 byRoot.reserve(graph.nNodes);
345 for (std::size_t node = 0; node < graph.nNodes; ++node) {
346 if (activeNode[node]) byRoot[components.find(node)].push_back(node);
347 }
348
349 // Deterministic component IDs make debugging and validation reproducible.
350 std::vector<std::vector<std::size_t>> componentNodes;
351 componentNodes.reserve(byRoot.size());
352 for (auto& [_, nodes] : byRoot) {
353 std::sort(nodes.begin(), nodes.end());
354 componentNodes.push_back(std::move(nodes));
355 }
356 std::ranges::sort(componentNodes,
357 [](const auto& first, const auto& second) {
358 return first.front() < second.front();
359 });
360
362 decor{m_pairGateDecorKey, ctx};
363
364 std::vector<std::uint8_t> keptNode(graph.nNodes, false);
365 std::vector<std::uint8_t> nodeDropReason;
366 if (truthDiag) {
367 nodeDropReason.assign(graph.nNodes, kReasonNotSelected);
368 }
369
370 std::size_t topologyNodes = 0;
371 std::size_t retainedNodes = 0;
372 std::size_t chamberSuppressedNodes = 0;
373 std::size_t rejectedComponents = 0;
374 std::size_t componentsKept = 0;
375 std::size_t anchors = 0;
376 std::size_t nodesRejectedByMinComponent = 0;
377 unsigned nextComponentId = 1;
378 const InferenceUtils::SegmentQualityOrder betterSegment{};
379
380 const auto isBetterNode = [&](std::size_t candidate, std::size_t incumbent) {
381 if (betterSegment(graph.segments[candidate], graph.segments[incumbent])) {
382 return true;
383 }
384 if (betterSegment(graph.segments[incumbent], graph.segments[candidate])) {
385 return false;
386 }
387 return candidate < incumbent;
388 };
389
390 for (const std::vector<std::size_t>& rawNodes : componentNodes) {
391 topologyNodes += rawNodes.size();
392 std::vector<std::size_t> retained = rawNodes;
393
394 // The seeder resolves same-chamber alternatives while building a seed.
395 // Retaining only the highest-ranked representative is therefore optional.
396 if (m_keepBestSegmentPerChamber.value()) {
397 std::unordered_map<int, std::size_t> bestByChamber;
398 bestByChamber.reserve(rawNodes.size());
399 for (const std::size_t node : rawNodes) {
400 const int chamber =
401 static_cast<int>(graph.segments[node]->chamberIndex());
402 const auto found = bestByChamber.find(chamber);
403 if (found == bestByChamber.end() || isBetterNode(node, found->second)) {
404 bestByChamber[chamber] = node;
405 }
406 }
407 retained.clear();
408 retained.reserve(bestByChamber.size());
409 for (const auto& [_, node] : bestByChamber) retained.push_back(node);
410 std::sort(retained.begin(), retained.end());
411 chamberSuppressedNodes += rawNodes.size() - retained.size();
412 }
413
414 // Provisional reasons; nodes that end up kept are identified by keptNode.
415 if (truthDiag) {
416 for (const std::size_t node : rawNodes) {
417 nodeDropReason[node] = kReasonChamberDedup;
418 }
419 for (const std::size_t node : retained) {
420 nodeDropReason[node] = kReasonComponentRejected;
421 }
422 }
423
424 if (retained.size() < m_minSegmentsPerComponent.value()) {
425 nodesRejectedByMinComponent += retained.size();
426 ++rejectedComponents;
427 continue;
428 }
429
430 // Only ranked component members launch seeds. The edge score therefore
431 // reduces seed attempts directly rather than serving only as a label.
432 std::vector<std::size_t> rankedNodes{retained};
433 if (m_anchorInnermostLayer.value()) {
434 // Anchor only on the inner segment(s)
435 int bestRank = std::numeric_limits<int>::max();
436 for (const std::size_t node : rankedNodes) {
437 bestRank = std::min(
438 bestRank,
440 graph.segments[node]->chamberIndex())));
441 }
442 std::erase_if(rankedNodes, [&](std::size_t node) {
443 return layerRadialRank(Muon::MuonStationIndex::toLayerIndex(
444 graph.segments[node]->chamberIndex())) != bestRank;
445 });
446 } else {
447 std::ranges::sort(rankedNodes, isBetterNode);
448 const std::size_t nAnchors = m_seedAnchorsPerComponent.value() == 0
449 ? rankedNodes.size()
450 : std::min<std::size_t>(m_seedAnchorsPerComponent.value(),
451 rankedNodes.size());
452 rankedNodes.resize(nAnchors);
453 }
454 if (rankedNodes.empty()) {
455 ++rejectedComponents;
456 continue;
457 }
458 std::ranges::sort(rankedNodes);
459
460 const unsigned componentId = nextComponentId++;
461 for (const std::size_t node : retained) {
462 const bool isAnchor = Acts::rangeContainsValue(rankedNodes, node);
463 decor(*graph.segments[node]) = {
464 componentId, static_cast<unsigned int>(isAnchor)};
465 keptNode[node] = true;
466 }
467 retainedNodes += retained.size();
468 anchors += rankedNodes.size();
469 ++componentsKept;
470 }
471
472 if (truthDiag) {
473 std::size_t truthSegs = 0, bkgSegs = 0;
474 for (std::size_t node = 0; node < graph.nNodes; ++node) {
475 if (!keptNode[node]) {
476 continue;
477 }
478 nodeTruthId[node] >= 0 ? ++truthSegs : ++bkgSegs;
479 }
480 m_sumRetainedTruthSegments += truthSegs;
481 m_sumRetainedBkgSegments += bkgSegs;
482
483 std::vector<unsigned char> thresholded(graph.nNodes, 0);
484 for (std::size_t node = 0; node < graph.nNodes; ++node) {
485 thresholded[node] = !edgesByNode[node].empty();
486 }
487 classifyLostTruthSegments(*segments, graph, pairProbability, thresholded,
488 keptNode, nodeDropReason);
489 }
490
491 if (!m_filteredSegmentKey.empty()) {
492 auto connectedSegments =
493 std::make_unique<ConstDataVector<xAOD::MuonSegmentContainer>>(
495 connectedSegments->reserve(graph.nNodes);
496 for (std::size_t node = 0; node < graph.nNodes; ++node) {
497 if (!keptNode[node] || !graph.segments[node]) continue;
498 connectedSegments->push_back(graph.segments[node]);
499 }
500
501 const std::size_t nConnectedSegments = connectedSegments->size();
504 ATH_CHECK(connectedHandle.record(std::move(connectedSegments)));
505 ATH_MSG_DEBUG("Event " << ctx.eventID().event_number()
506 << ": wrote " << nConnectedSegments
507 << " ML-connected segment(s) to '"
508 << m_filteredSegmentKey.key() << "'");
509 }
510
511 ATH_MSG_DEBUG("Event " << ctx.eventID().event_number()
512 << ": ML components graphNodes=" << graph.nNodes
513 << ", thresholdedNodes=" << thresholdedNodes
514 << ", thresholdPairs=" << thresholdPairs
515 << ", mutualTopKPairs=" << mutualTopKPairs
516 << ", oneSidedTopKPairs=" << oneSidedTopKPairs
517 << ", selectedPairs=" << selectedPairKeys.size()
518 << ", components=" << componentsKept
519 << ", topologyNodes=" << topologyNodes
520 << ", retainedNodes=" << retainedNodes
521 << ", chamberSuppressedNodes=" << chamberSuppressedNodes
522 << ", nodesRejectedByMinComponent=" << nodesRejectedByMinComponent
523 << ", keepBestSegmentPerChamber=" << m_keepBestSegmentPerChamber.value()
524 << ", seedAnchors=" << anchors
525 << ", rejectedComponents=" << rejectedComponents
526 << ", threshold=" << m_pairGateThreshold.value()
527 << ", maxEdgesPerNode=" << m_maxEdgesPerNode.value()
528 << ", orphanRecoveryPairs=" << orphanRecoveryPairs
529 << ", mutualTopK=" << m_requireMutualTopKEdges.value()
530 << ", degreeCapped=" << m_useDegreeCappedComponents.value());
531
532 // Job-summed diagnostics
533 if (msgLvl(MSG::DEBUG)) {
534 m_sumGraphNodes += graph.nNodes;
535 m_sumThresholdedNodes += thresholdedNodes;
536 m_sumMutualTopKPairs += mutualTopKPairs;
537 m_sumOneSidedTopKPairs += oneSidedTopKPairs;
538 m_sumOrphanRecoveryPairs += orphanRecoveryPairs;
539 m_sumTopologyNodes += topologyNodes;
540 m_sumRetainedNodes += retainedNodes;
541 m_sumChamberSuppressedNodes += chamberSuppressedNodes;
542 m_sumNodesRejectedByMinComponent += nodesRejectedByMinComponent;
543 m_sumRejectedComponents += rejectedComponents;
544 m_sumComponentsKept += componentsKept;
545 }
546 return StatusCode::SUCCESS;
547}
548
550 const xAOD::MuonSegmentContainer& segments, const SegmentEdgeGraph& graph,
551 const std::unordered_map<std::uint64_t, float>& pairProbability,
552 const std::vector<unsigned char>& thresholded,
553 const std::vector<std::uint8_t>& keptNode,
554 const std::vector<unsigned char>& nodeDropReason) const {
555 static_assert(kNumLossCategories == TruthLossCounters::kCategories);
556 constexpr std::size_t kColumns = TruthLossCounters::kColumns;
557 const std::size_t nInput = segments.size();
558
559 if (graph.inputNodeIndex.size() != nInput) {
560 if (!m_warnedMissingToolDiagnostics.exchange(true)) {
561 ATH_MSG_WARNING("Truth diagnostics requested, but the edge classifier tool "
562 "did not provide its pre-ONNX fates; set "
563 "EnableTruthDiagnostics on the tool as well. Skipping the "
564 "lost-segment classification.");
565 }
566 return;
567 }
568
569 // Truth particle of every input segment and the segments grouped per particle.
570 std::vector<const xAOD::TruthParticle*> truthPart(nInput, nullptr);
571 std::vector<std::int32_t> truthId(nInput, -1);
572 std::unordered_map<std::int32_t, std::vector<std::uint32_t>> byTruth;
573 {
574 std::uint32_t inputIndex = 0;
575 for (const xAOD::MuonSegment* segment : segments) {
576 if (const xAOD::TruthParticle* part = MuonR4::getTruthMatchedParticle(*segment)) {
577 truthPart[inputIndex] = part;
578 truthId[inputIndex] = static_cast<std::int32_t>(part->index());
579 byTruth[truthId[inputIndex]].push_back(inputIndex);
580 }
581 ++inputIndex;
582 }
583 }
584 const auto isRetained = [&](std::size_t input) {
585 const std::int32_t node = graph.inputNodeIndex[input];
586 return node >= 0 && keptNode[static_cast<std::size_t>(node)];
587 };
588
589 // Per segment: how far its best cross-chamber true pair got (1 = bucket cap,
590 // 2 = sector window, 3 = angle window, 4 = edge caps, 5 = scored, 0 = only
591 // same-chamber partners) and the score / index of its best scored pair.
592 const auto rankOf = [](PairGate gate) -> unsigned char {
593 switch (gate) {
594 case PairGate::BucketCap: return 1;
595 case PairGate::SectorWindow: return 2;
596 case PairGate::AngleWindow: return 3;
597 case PairGate::EdgeCaps: return 4;
598 case PairGate::Scored: return 5;
599 case PairGate::SameChamber: break;
600 }
601 return 0;
602 };
603 std::vector<unsigned char> bestRank(nInput, 0);
604 std::vector<float> bestScore(nInput, -1.f);
605 std::vector<std::int32_t> bestScoredPair(nInput, -1);
606 for (std::size_t k = 0; k < graph.truthPairFates.size(); ++k) {
607 const PairFate& fate = graph.truthPairFates[k];
608 const unsigned char rank = rankOf(fate.gate);
609 if (rank == 0) continue;
610 const std::array<std::uint32_t, 2> ends{fate.first, fate.second};
611 for (const std::uint32_t end : ends) bestRank[end] = std::max(bestRank[end], rank);
612 if (fate.gate != PairGate::Scored) continue;
613 const std::int32_t firstNode = graph.inputNodeIndex[fate.first];
614 const std::int32_t secondNode = graph.inputNodeIndex[fate.second];
615 if (firstNode < 0 || secondNode < 0) continue;
616 const auto found = pairProbability.find(undirectedPairKey(
617 static_cast<std::size_t>(firstNode), static_cast<std::size_t>(secondNode)));
618 const float score = found != pairProbability.end() ? found->second : 0.f;
619 for (const std::uint32_t end : ends) {
620 if (score > bestScore[end]) {
621 bestScore[end] = score;
622 bestScoredPair[end] = static_cast<std::int32_t>(k);
623 }
624 }
625 }
626
627 // Seedability per truth muon: at least 2 distinct chambers in the input,
628 // fewer than 2 among the retained segments.
629 std::unordered_map<std::int32_t, bool> seedLostByTruth;
630 std::array<std::size_t, 4> muonsSeedable{};
631 std::array<std::size_t, 4> muonsSeedLost{};
632 for (const auto& entry : byTruth) {
633 std::unordered_set<int> inputChambers;
634 std::unordered_set<int> retainedChambers;
635 for (const std::uint32_t input : entry.second) {
636 const int chamber = static_cast<int>(segments[input]->chamberIndex());
637 inputChambers.insert(chamber);
638 if (isRetained(input)) retainedChambers.insert(chamber);
639 }
640 const bool seedable = inputChambers.size() >= 2;
641 const bool lost = seedable && retainedChambers.size() < 2;
642 seedLostByTruth[entry.first] = lost;
643 if (!seedable) continue;
644 const std::size_t region = 1 + static_cast<std::size_t>(
645 regionOf(std::abs(truthPart[entry.second.front()]->eta())));
646 ++muonsSeedable[0];
647 ++muonsSeedable[region];
648 if (lost) {
649 ++muonsSeedLost[0];
650 ++muonsSeedLost[region];
651 }
652 }
653
654 std::array<std::size_t, kNumLossCategories * kColumns> category{};
655 std::array<std::size_t, 5> scoreBin{};
656 std::array<std::size_t, 2> sectorDelta{};
657 std::array<std::size_t, 4> layerPair{};
658 std::array<std::size_t, 4> muonSegments{};
659 std::array<std::size_t, 3> precisionHits{};
660 for (std::size_t i = 0; i < nInput; ++i) {
661 if (truthId[i] < 0 || isRetained(i)) continue;
662 const std::vector<std::uint32_t>& members = byTruth[truthId[i]];
663 const std::int32_t node = graph.inputNodeIndex[i];
664
665 std::size_t cat{};
666 if (members.size() == 1) {
667 cat = kNoPartnerSingleton;
668 } else if (bestRank[i] == 0) {
669 cat = kNoPartnerSameChamber;
670 } else if (node == kDroppedByBucketCap) {
671 cat = kBucketSelf;
672 } else if (node >= 0 && thresholded[static_cast<std::size_t>(node)]) {
673 switch (nodeDropReason[static_cast<std::size_t>(node)]) {
674 case kReasonChamberDedup: cat = kDedupChamber; break;
675 case kReasonComponentRejected: cat = kComponentRejected; break;
676 default: cat = kNotSelected; break;
677 }
678 } else {
679 switch (bestRank[i]) {
680 case 5: cat = kBelowThreshold; break;
681 case 4: cat = kEdgeCaps; break;
682 case 3: cat = kAngleWindow; break;
683 case 2: cat = kSectorWindow; break;
684 default: cat = kBucketPartners; break;
685 }
686 }
687
688 const std::size_t region = 1 + static_cast<std::size_t>(
689 regionOf(std::abs(truthPart[i]->eta())));
690 ++category[cat * kColumns];
691 ++category[cat * kColumns + region];
692 if (seedLostByTruth[truthId[i]]) ++category[cat * kColumns + 4];
693
694 if (cat != kBelowThreshold) continue;
695 if (bestScoredPair[i] >= 0) {
696 const PairFate& fate = graph.truthPairFates[static_cast<std::size_t>(bestScoredPair[i])];
697 const float score = bestScore[i];
698 ++scoreBin[score < 1e-3f ? 0 : score < 1e-2f ? 1 : score < 2e-2f ? 2
699 : score < 5e-2f ? 3 : 4];
700 ++sectorDelta[fate.sectorDelta == 0 ? 0 : 1];
701 const std::uint32_t other = fate.first == i ? fate.second : fate.first;
702 int rankA = layerRadialRank(Muon::MuonStationIndex::toLayerIndex(
703 segments[i]->chamberIndex()));
704 int rankB = layerRadialRank(Muon::MuonStationIndex::toLayerIndex(
705 segments[other]->chamberIndex()));
706 if (rankA > rankB) std::swap(rankA, rankB);
707 ++layerPair[(rankA == 0 && rankB == 1) ? 0
708 : (rankA == 1 && rankB == 2) ? 1
709 : (rankA == 0 && rankB == 2) ? 2 : 3];
710 }
711 ++muonSegments[std::min<std::size_t>(members.size(), 5) - 2];
712 const unsigned int hits = segments[i]->nPrecisionHits();
713 ++precisionHits[hits <= 4 ? 0 : (hits <= 6 ? 1 : 2)];
714 }
715
716 for (std::size_t c = 0; c < category.size(); ++c) m_truthLoss.category[c] += category[c];
717 for (std::size_t c = 0; c < scoreBin.size(); ++c) m_truthLoss.scoreBin[c] += scoreBin[c];
718 for (std::size_t c = 0; c < sectorDelta.size(); ++c) m_truthLoss.sectorDelta[c] += sectorDelta[c];
719 for (std::size_t c = 0; c < layerPair.size(); ++c) m_truthLoss.layerPair[c] += layerPair[c];
720 for (std::size_t c = 0; c < muonSegments.size(); ++c) m_truthLoss.muonSegments[c] += muonSegments[c];
721 for (std::size_t c = 0; c < precisionHits.size(); ++c) m_truthLoss.precisionHits[c] += precisionHits[c];
722 for (std::size_t c = 0; c < muonsSeedable.size(); ++c) {
723 m_truthLoss.muonsSeedable[c] += muonsSeedable[c];
724 m_truthLoss.muonsSeedLost[c] += muonsSeedLost[c];
725 }
726}
727
730 "SegmentEdgeInferenceAlg post-ONNX selection summary (job-summed): "
731 << "graphNodes=" << m_sumGraphNodes
732 << ", thresholdedNodes=" << m_sumThresholdedNodes
733 << " (>=PairGateThreshold=" << m_pairGateThreshold.value() << ")"
734 << ", mutualTopKPairs=" << m_sumMutualTopKPairs
735 << ", oneSidedTopKPairs(rejected by RequireMutualTopKEdges="
737 << " (MaxEdgesPerNode=" << m_maxEdgesPerNode.value()
738 << "; a thresholded edge is dropped here purely by rank, "
739 "independent of PairGateThreshold)"
740 << ", orphanRecoveryPairs=" << m_sumOrphanRecoveryPairs
741 << ", chamberSuppressedNodes=" << m_sumChamberSuppressedNodes
742 << " (KeepBestSegmentPerChamber=" << m_keepBestSegmentPerChamber.value() << ")"
743 << ", nodesRejectedByMinComponent=" << m_sumNodesRejectedByMinComponent
744 << " (MinSegmentsPerComponent=" << m_minSegmentsPerComponent.value() << ")"
745 << ", rejectedComponents=" << m_sumRejectedComponents
746 << ", componentsKept=" << m_sumComponentsKept
747 << ", topologyNodes=" << m_sumTopologyNodes
748 << ", retainedNodes=" << m_sumRetainedNodes);
749
750 if (m_truthDiagEnabled) {
752 "SegmentEdgeInferenceAlg truth-vs-background funnel (job-summed; "
753 "truth segment = getTruthMatchedParticle(seg) != nullptr, matching "
754 "SegmentDumperAlg::m_segmentHasTruth, no isMuon() filter, no G4 "
755 "pseudo-label fallback): "
756 << "input: truth=" << m_sumInputTruthSegments
757 << " bkg=" << m_sumInputBkgSegments
758 << "; reachesONNX: truth=" << m_sumOnnxTruthSegments
759 << " bkg=" << m_sumOnnxBkgSegments
760 << "; retained: truth=" << m_sumRetainedTruthSegments
761 << " bkg=" << m_sumRetainedBkgSegments
762 << "; edge-level (same truth-particle index on both endpoints) at "
763 "PairGateThreshold=" << m_pairGateThreshold.value()
764 << ": trueEdges passed/total=" << m_sumTrueEdgesPassed << "/"
766 << ", bkgEdges passed/total=" << m_sumBkgEdgesPassed << "/"
768
769 constexpr std::array<const char*, TruthLossCounters::kCategories> names{
770 "A1 no possible partner: singleton muon segment",
771 "A2 no possible partner: same-chamber partners only",
772 "B0 never reached ONNX: segment dropped by MaxSegmentsPerBucket",
773 "B1 never reached ONNX: partners dropped by MaxSegmentsPerBucket",
774 "B2 never reached ONNX: outside sector window (MaxDeltaSector)",
775 "B3 never reached ONNX: outside angle window (MaxDeltaThetaDeg)",
776 "B4 never reached ONNX: pre-ONNX edge caps",
777 "C true pair(s) scored below PairGateThreshold",
778 "D1 passed threshold, removed: KeepBestSegmentPerChamber",
779 "D2 passed threshold, removed: component rejected (MinSegmentsPerComponent/anchors)",
780 "D3 passed threshold, removed: not selected by top-K/mutual gate"};
781 constexpr std::size_t kColumns = TruthLossCounters::kColumns;
782 ATH_MSG_DEBUG("Lost truth segments by stage (job-summed; truth segment = "
783 "input segment with a truth particle, lost = not in the filtered "
784 "container). Columns: all | barrel | transition | endcap | "
785 "belonging to a muon that lost seedability");
786 std::size_t lostTotal = 0;
787 for (std::size_t c = 0; c < names.size(); ++c) {
788 const auto value = [&](std::size_t col) { return m_truthLoss.category[c * kColumns + col].load(); };
789 lostTotal += value(0);
790 ATH_MSG_DEBUG(" " << names[c] << ": " << value(0) << " | " << value(1)
791 << " | " << value(2) << " | " << value(3) << " | " << value(4));
792 }
793 ATH_MSG_DEBUG(" check: sum of categories=" << lostTotal
794 << ", input truth - retained truth="
796 ATH_MSG_DEBUG(" truth muons with >=2 input chambers (all|barrel|transition|endcap): "
797 << m_truthLoss.muonsSeedable[0].load() << "|" << m_truthLoss.muonsSeedable[1].load()
798 << "|" << m_truthLoss.muonsSeedable[2].load() << "|" << m_truthLoss.muonsSeedable[3].load()
799 << "; of which <2 chambers retained: "
800 << m_truthLoss.muonsSeedLost[0].load() << "|" << m_truthLoss.muonsSeedLost[1].load()
801 << "|" << m_truthLoss.muonsSeedLost[2].load() << "|" << m_truthLoss.muonsSeedLost[3].load());
802 ATH_MSG_DEBUG(" category C descriptors: best true-pair score <1e-3/1e-3..1e-2/1e-2..2e-2/2e-2..5e-2/>=5e-2: "
803 << m_truthLoss.scoreBin[0].load() << "/" << m_truthLoss.scoreBin[1].load() << "/"
804 << m_truthLoss.scoreBin[2].load() << "/" << m_truthLoss.scoreBin[3].load() << "/"
805 << m_truthLoss.scoreBin[4].load()
806 << "; pair sector delta same/adjacent: " << m_truthLoss.sectorDelta[0].load() << "/"
807 << m_truthLoss.sectorDelta[1].load()
808 << "; layer pair Inner-Middle/Middle-Outer/Inner-Outer/other: "
809 << m_truthLoss.layerPair[0].load() << "/" << m_truthLoss.layerPair[1].load() << "/"
810 << m_truthLoss.layerPair[2].load() << "/" << m_truthLoss.layerPair[3].load()
811 << "; muon input segments 2/3/4/5+: " << m_truthLoss.muonSegments[0].load() << "/"
812 << m_truthLoss.muonSegments[1].load() << "/" << m_truthLoss.muonSegments[2].load() << "/"
813 << m_truthLoss.muonSegments[3].load()
814 << "; nPrecisionHits <=4/5-6/>=7: " << m_truthLoss.precisionHits[0].load() << "/"
815 << m_truthLoss.precisionHits[1].load() << "/" << m_truthLoss.precisionHits[2].load());
816 }
817 return StatusCode::SUCCESS;
818}
819
820} // namespace MuonML
Scalar eta() const
pseudorapidity method
#define ATH_CHECK
Evaluate an expression and check for errors.
#define ATH_MSG_DEBUG(x,...)
#define ATH_MSG_WARNING(x,...)
DataVector adapter that acts like it holds const pointers.
std::vector< std::vector< float > > scores
bool passed(DecisionID id, const DecisionIDContainer &)
checks if required decision ID is in the set of IDs in the container
Handle class for reading from StoreGate.
Handle class for recording to StoreGate.
size_t size() const
Number of registered mappings.
static const Attributes_t empty
bool msgLvl(const MSG::Level lvl) const
size_type size() const noexcept
Returns the number of elements in the collection.
std::atomic< std::size_t > m_sumOnnxBkgSegments
std::atomic< std::size_t > m_sumChamberSuppressedNodes
Gaudi::Property< bool > m_keepBestSegmentPerChamber
std::atomic< std::size_t > m_sumRetainedNodes
Gaudi::Property< bool > m_anchorInnermostLayer
SG::WriteDecorHandleKey< xAOD::MuonSegmentContainer > m_pairGateDecorKey
Per-segment payload consumed by MlMsTrackSeeder: [componentId, isSeedAnchor] Empty means that the seg...
Gaudi::Property< bool > m_useDegreeCappedComponents
std::atomic< std::size_t > m_sumOrphanRecoveryPairs
ToolHandle< ISegmentEdgeClassifierTool > m_edgeClassifier
StatusCode finalize() override
Log of the post-ONNX rank/mutuality and min-component-size selection.
Gaudi::Property< unsigned int > m_maxEdgesPerNode
std::atomic< std::size_t > m_sumThresholdedNodes
std::atomic< std::size_t > m_sumRetainedBkgSegments
Gaudi::Property< bool > m_requireMutualTopKEdges
std::atomic< std::size_t > m_sumOneSidedTopKPairs
std::atomic< std::size_t > m_sumInputBkgSegments
void classifyLostTruthSegments(const xAOD::MuonSegmentContainer &segments, const SegmentEdgeGraph &graph, const std::unordered_map< std::uint64_t, float > &pairProbability, const std::vector< unsigned char > &thresholded, const std::vector< std::uint8_t > &keptNode, const std::vector< unsigned char > &nodeDropReason) const
MC-only: classify every truth-labeled input segment that did not make it into the filtered container ...
std::atomic< std::size_t > m_sumTrueEdgesPassed
bool m_truthDiagEnabled
Whether truth-particle matching is available/enabled this job.
std::atomic< std::size_t > m_sumBkgEdgesPassed
StatusCode execute(const EventContext &ctx) const override
std::atomic< std::size_t > m_sumBkgEdgesTotal
std::atomic< std::size_t > m_sumGraphNodes
Job-summed post-ONNX selection counters (see execute()).
Gaudi::Property< unsigned int > m_minSegmentsPerComponent
std::atomic< std::size_t > m_sumOnnxTruthSegments
std::atomic< std::size_t > m_sumInputTruthSegments
std::atomic< std::size_t > m_sumNodesRejectedByMinComponent
std::atomic< std::size_t > m_sumTrueEdgesTotal
std::atomic< std::size_t > m_sumRetainedTruthSegments
SG::WriteHandleKey< ConstDataVector< xAOD::MuonSegmentContainer > > m_filteredSegmentKey
Gaudi::Property< bool > m_recoverOrphanNodes
std::atomic< std::size_t > m_sumMutualTopKPairs
std::atomic< std::size_t > m_sumRejectedComponents
SG::ReadHandleKey< xAOD::MuonSegmentContainer > m_segmentKey
Gaudi::Property< unsigned int > m_seedAnchorsPerComponent
Gaudi::Property< float > m_pairGateThreshold
std::atomic< std::size_t > m_sumTopologyNodes
std::atomic< std::size_t > m_sumComponentsKept
StatusCode record(std::unique_ptr< T > data)
Record a const object to the store.
Definition node.h:24
Auxiliary class to instantiate WriteDecorHandles.The handles can be created in an empty state.
std::string find(const std::string &s)
return a remapped string
Definition hcg.cxx:140
int count(std::string s, const std::string &regx)
count how many occurances of a regx are in a string
Definition hcg.cxx:148
bool first
Definition DeMoScan.py:534
int compareFloatDescending(float first, float second)
Three-way descending comparison which also orders NaN last.
PairGate
Gate that stopped (or did not stop) a pair of input segments from reaching ONNX.
@ Scored
the pair was sent to ONNX
@ SameChamber
same-chamber pair dropped by design
@ AngleWindow
cos(opening angle) below MaxDeltaThetaDeg
@ EdgeCaps
candidate evicted by the pre-ONNX per-node / per-target-chamber caps
@ SectorWindow
|delta sector| > MaxDeltaSector
@ BucketCap
an endpoint was dropped by MaxSegmentsPerBucket
constexpr std::int32_t kDroppedByBucketCap
Values of SegmentEdgeGraph::inputNodeIndex for input segments without a node.
const xAOD::TruthParticle * getTruthMatchedParticle(const xAOD::MuonSegment &segment)
Returns the particle truth-matched to the segment.
LayerIndex
enum to classify the different layers in the muon spectrometer
LayerIndex toLayerIndex(ChIndex index)
convert ChIndex into LayerIndex
@ VIEW_ELEMENTS
this data object is a view, it does not own its elmts
const T * get(const ReadCondHandleKey< T > &key, const EventContext &ctx)
Convenience function to retrieve an object given a ReadCondHandleKey.
bool absEta(const xAOD::TauJet &tau, float &out)
void sort(typename DataModel_detail::iterator< DVL > beg, typename DataModel_detail::iterator< DVL > end)
Specialization of sort for DataVector/List.
void swap(ElementLinkVector< DOBJ > &lhs, ElementLinkVector< DOBJ > &rhs)
std::size_t erase_if(T_container &container, T_Func pred)
MuonSegmentContainer_v1 MuonSegmentContainer
Definition of the current "MuonSegment container version".
TruthParticle_v1 TruthParticle
Typedef to implementation.
MuonSegment_v1 MuonSegment
Reference the current persistent version:
Common quality ordering for segment representatives.
One pair of input segments that share a truth particle (input-container indices).
std::uint32_t first
std::uint32_t second
std::uint8_t sectorDelta
std::vector< std::int32_t > inputNodeIndex
per input segment: node index, or kDropped*
std::vector< const xAOD::MuonSegment_v1 * > segments
std::vector< PairFate > truthPairFates
every pair of input segments sharing a truth particle