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 <bit>
15#include <cmath>
16#include <limits>
17#include <memory>
18#include <numeric>
19#include <sstream>
20#include <unordered_set>
21#include <utility>
22
23namespace MuonML {
24
25namespace {
26
27std::uint64_t
28undirectedPairKey(std::size_t first, std::size_t second) {
29 if (first > second) std::swap(first, second);
30 const auto first32 = static_cast<std::uint32_t>(first);
31 const auto second32 = static_cast<std::uint32_t>(second);
32
33 return (static_cast<std::uint64_t>(first32) << 32) | static_cast<std::uint64_t>(second32);
34}
35
36std::pair<std::size_t, std::size_t>
37unpackPairKey(std::uint64_t key) {
38 const auto first = static_cast<std::uint32_t>(key >> 32);
39 const auto second = static_cast<std::uint32_t>(key);
40 return {first, second};
41}
42
43class DisjointSet {
44public:
45 explicit DisjointSet(std::size_t size) : m_parent(size), m_rank(size, 0) {
46 std::iota(m_parent.begin(), m_parent.end(), 0);
47 }
48
49 std::size_t find(std::size_t node) {
50 if (m_parent[node] != node) m_parent[node] = find(m_parent[node]);
51 return m_parent[node];
52 }
53
54 void unite(std::size_t first, std::size_t second) {
55 first = find(first);
56 second = find(second);
57 if (first == second) return;
58 if (m_rank[first] < m_rank[second]) std::swap(first, second);
59 m_parent[second] = first;
60 if (m_rank[first] == m_rank[second]) ++m_rank[first];
61 }
62
63private:
64 std::vector<std::size_t> m_parent;
65 std::vector<unsigned char> m_rank;
66};
67
68// Explicit radial ranking (0 = closest to the IP), independent of the
69// LayerIndex enum's underlying integer values. BarrelExtended is the
70// barrel's BI-equivalent chamber, so it ranks alongside Inner.
71int layerRadialRank(Muon::MuonStationIndex::LayerIndex layer) {
73 switch (layer) {
74 case Inner:
75 case BarrelExtended:
76 return 0;
77 case Middle:
78 return 1;
79 case Outer:
80 return 2;
81 case Extended:
82 return 3;
83 default:
84 return 4;
85 }
86}
87
88// Truth diagnostics only. Why a node that passed the threshold was not kept.
89constexpr unsigned char kReasonNotSelected = 0; // no selected pair (top-K / mutual gate)
90constexpr unsigned char kReasonChamberDedup = 1; // KeepBestSegmentPerChamber
91constexpr unsigned char kReasonComponentRejected = 2; // MinSegmentsPerComponent / no anchor
92
93// Loss categories; the order matches the table printed by finalize().
94enum LossCategory : std::uint8_t{
95 kNoPartnerSingleton = 0,
96 kNoPartnerSameChamber,
97 kBucketSelf,
98 kBucketPartners,
99 kSectorWindow,
100 kAngleWindow,
101 kEdgeCaps,
102 kBelowThreshold,
103 kDedupChamber,
104 kComponentRejected,
105 kNotSelected,
106 kNumLossCategories
107};
108
109enum class TruthRegion : std::uint8_t { kBarrel = 0, kTransition = 1, kEndcap = 2 };
110
111TruthRegion regionOf(double absEta) {
112 return absEta < 1.0 ? TruthRegion::kBarrel
113 : (absEta < 1.3 ? TruthRegion::kTransition : TruthRegion::kEndcap);
114}
115
116} // namespace
117
119 ATH_CHECK(m_segmentKey.initialize());
120 ATH_CHECK(m_pairGateDecorKey.initialize());
123 ATH_CHECK(m_edgeClassifier.retrieve());
124 m_truthDiagEnabled = m_edgeClassifier->enableTruthDiagnostics();
125 return StatusCode::SUCCESS;
126}
127
128StatusCode SegmentEdgeInferenceAlg::execute(const EventContext& ctx) const {
129 const xAOD::MuonSegmentContainer* segments{};
130 ATH_CHECK(SG::get(segments, m_segmentKey, ctx));
131 // Evaluated once per event; short-circuits without touching the message
132 // service unless the property was explicitly enabled.
133 const bool truthDiag = m_truthDiagEnabled && msgLvl(MSG::DEBUG);
134 ATH_MSG_DEBUG("Event " << ctx.eventID().event_number()
135 << ": input segments in '" << m_segmentKey.key()
136 << "' = " << segments->size());
137
138 if (truthDiag) {
139 std::size_t truthSegs = 0, bkgSegs = 0;
140 for (const xAOD::MuonSegment* seg : *segments) {
141 MuonR4::getTruthMatchedParticle(*seg) ? ++truthSegs : ++bkgSegs;
142 }
143 m_sumInputTruthSegments += truthSegs;
144 m_sumInputBkgSegments += bkgSegs;
145 }
146
147 SegmentEdgeGraph graph{};
148 std::vector<SegmentEdgeScore> scores{};
149 ATH_CHECK(m_edgeClassifier->buildGraph(ctx, *segments, graph));
150 ATH_MSG_DEBUG("Event " << ctx.eventID().event_number()
151 << ": built graph with nodes=" << graph.nNodes
152 << ", edges=" << graph.nEdges);
153
154 // Per-node truth-particle grouping key (-1 = unlabeled), used for the
155 // segment-level funnel and for the edge-level true/background split.
156 std::vector<int32_t> nodeTruthId;
157 // Truth-particle id -> |eta|, filled alongside nodeTruthId.
158 std::unordered_map<int32_t, double> truthIdToAbsEta;
159 if (truthDiag) {
160 nodeTruthId.assign(graph.nNodes, -1);
161 std::size_t truthSegs = 0, bkgSegs = 0;
162 for (std::size_t node = 0; node < graph.nNodes; ++node) {
163 const xAOD::TruthParticle* truthPart =
165 if (truthPart) {
166 nodeTruthId[node] = static_cast<int32_t>(truthPart->index());
167 truthIdToAbsEta.emplace(nodeTruthId[node], std::abs(truthPart->eta()));
168 ++truthSegs;
169 } else {
170 ++bkgSegs;
171 }
172 }
173 m_sumOnnxTruthSegments += truthSegs;
174 m_sumOnnxBkgSegments += bkgSegs;
175 }
176
177 ATH_CHECK(m_edgeClassifier->classifyEdges(ctx, graph, scores));
178 if (!scores.empty()) {
179 float minProb = std::numeric_limits<float>::max();
180 float maxProb = std::numeric_limits<float>::lowest();
181 for (const SegmentEdgeScore& score : scores) {
182 minProb = std::min(minProb, score.probability);
183 maxProb = std::max(maxProb, score.probability);
184 }
185 ATH_MSG_DEBUG("Event " << ctx.eventID().event_number()
186 << ": edge scores=" << scores.size()
187 << ", prob range=[" << minProb << ", " << maxProb << "]");
188 } else {
189 ATH_MSG_DEBUG("Event " << ctx.eventID().event_number()
190 << ": no edge scores produced");
191 }
192
193 // Symmetrise directed model outputs once. Everything downstream uses the
194 // score of an undirected segment association.
195 std::unordered_map<std::uint64_t, float> pairProbability;
196 pairProbability.reserve(scores.size());
197 for (const SegmentEdgeScore& score : scores) {
198 if (score.src >= graph.nNodes || score.dst >= graph.nNodes ||
199 score.src == score.dst) {
200 continue;
201 }
202 const std::uint64_t key = undirectedPairKey(score.src, score.dst);
203 auto [it, inserted] = pairProbability.emplace(key, score.probability);
204 if (!inserted) it->second = std::max(it->second, score.probability);
205 }
206
207 // Edge-level recall/false-positive check: candidate pairs that reached ONNX
208 // true pairs reach PairGateThreshold, versus how many background pairs do.
209 if (truthDiag) {
210 std::size_t trueTotal = 0, truePassed = 0, bkgTotal = 0, bkgPassed = 0;
211 for (const auto& [key, probability] : pairProbability) {
212 const auto [first, second] = unpackPairKey(key);
213 if (first >= graph.nNodes || second >= graph.nNodes) continue;
214 const bool isTrueEdge =
215 nodeTruthId[first] >= 0 && nodeTruthId[first] == nodeTruthId[second];
216 const bool passed = probability >= m_pairGateThreshold.value();
217 if (isTrueEdge) {
218 ++trueTotal;
219 truePassed += passed;
220 } else {
221 ++bkgTotal;
222 bkgPassed += passed;
223 }
224 }
225 m_sumTrueEdgesTotal += trueTotal;
226 m_sumTrueEdgesPassed += truePassed;
227 m_sumBkgEdgesTotal += bkgTotal;
228 m_sumBkgEdgesPassed += bkgPassed;
229 }
230
231 // Form a sparse, score-ranked topology. Mutual top-K associations retain
232 // locally consistent paths while suppressing one-sided bridges.
233 using WeightedEdge = std::pair<std::uint64_t, float>;
234 const auto betterWeightedEdge = [](const WeightedEdge& first,
235 const WeightedEdge& second) {
236 const int probabilityOrder = InferenceUtils::compareFloatDescending(
237 first.second, second.second);
238 if (probabilityOrder != 0) {
239 return probabilityOrder < 0;
240 }
241 return first.first < second.first;
242 };
243 std::vector<std::vector<WeightedEdge>> edgesByNode(graph.nNodes);
244 std::size_t thresholdPairs = 0;
245 for (const auto& [key, probability] : pairProbability) {
246 if (probability < m_pairGateThreshold.value()) continue;
247 const auto [first, second] = unpackPairKey(key);
248 if (first >= graph.nNodes || second >= graph.nNodes) continue;
249 edgesByNode[first].emplace_back(key, probability);
250 edgesByNode[second].emplace_back(key, probability);
251 ++thresholdPairs;
252 }
253
254 std::size_t thresholdedNodes = 0;
255 for (const std::vector<WeightedEdge>& nodeEdges : edgesByNode) {
256 thresholdedNodes += !nodeEdges.empty();
257 }
258
259 std::vector<Muon::MuonStationIndex::LayerIndex> nodeLayer(graph.nNodes);
260 for (std::size_t node = 0; node < graph.nNodes; ++node) {
261 nodeLayer[node] = Muon::MuonStationIndex::toLayerIndex(graph.segments[node]->chamberIndex());
262 }
263 const unsigned int maxEdgesPerLayer = m_maxEdgesPerNodePerLayer.value();
264 std::unordered_map<std::uint64_t, unsigned char> nominations;
265 nominations.reserve(thresholdPairs);
266 for (std::size_t node = 0; node < graph.nNodes; ++node) {
267 std::vector<WeightedEdge>& nodeEdges = edgesByNode[node];
268 std::sort(nodeEdges.begin(), nodeEdges.end(), betterWeightedEdge);
269 if (maxEdgesPerLayer != 0) {
270 // Best edges towards each layer: a segment keeps its partner in every neighbouring
271 // layer, and same-layer edges do not compete with cross-layer ones.
272 std::array<unsigned int, Acts::toUnderlying(Muon::MuonStationIndex::LayerIndex::LayerIndexMax) + 1> taken{};
273 std::erase_if(nodeEdges, [&](const WeightedEdge& edge) {
274 const auto [first, second] = unpackPairKey(edge.first);
275 const std::size_t other = first == node ? second : first;
276 const auto layer = static_cast<std::size_t>(Acts::toUnderlying(nodeLayer[other]) + 1);
277 return taken[layer]++ >= maxEdgesPerLayer;
278 });
279 } else if (m_maxEdgesPerNode.value() != 0 &&
280 nodeEdges.size() > m_maxEdgesPerNode.value()) {
281 nodeEdges.resize(m_maxEdgesPerNode.value());
282 }
283 for (const WeightedEdge& edge : nodeEdges) {
284 ++nominations[edge.first];
285 }
286 }
287
288 std::size_t mutualTopKPairs = 0;
289 std::size_t oneSidedTopKPairs = 0;
290 for (const auto& [_, count] : nominations) {
291 if (count == 2) {
292 ++mutualTopKPairs;
293 } else {
294 ++oneSidedTopKPairs;
295 }
296 }
297
298 std::unordered_set<std::uint64_t> selectedPairKeys;
299 selectedPairKeys.reserve(thresholdPairs);
300 if (m_useDegreeCappedComponents.value()) {
301 // Apply a score-ordered global degree cap when explicitly requested.
302 std::vector<WeightedEdge> acceptedPairs;
303 acceptedPairs.reserve(thresholdPairs);
304 for (const auto& [key, probability] : pairProbability) {
305 if (probability < m_pairGateThreshold.value()) continue;
306 acceptedPairs.emplace_back(key, probability);
307 }
308 std::sort(acceptedPairs.begin(), acceptedPairs.end(), betterWeightedEdge);
309
310 const unsigned int maxDegree = m_maxEdgesPerNode.value();
311 std::vector<unsigned int> degree(graph.nNodes, 0);
312 for (const WeightedEdge& edge : acceptedPairs) {
313 const auto [first, second] = unpackPairKey(edge.first);
314 if (maxDegree != 0 &&
315 (degree[first] >= maxDegree || degree[second] >= maxDegree)) {
316 continue;
317 }
318 selectedPairKeys.insert(edge.first);
319 ++degree[first];
320 ++degree[second];
321 }
322 } else {
323 for (const auto& [key, count] : nominations) {
324 if (m_requireMutualTopKEdges.value() && count != 2) continue;
325 selectedPairKeys.insert(key);
326 }
327 }
328
329 // A mutual top-K selection can leave an endpoint without an association.
330 // Add its best thresholded edge, at most once per orphaned endpoint.
331 std::size_t orphanRecoveryPairs = 0;
332 if (!m_useDegreeCappedComponents.value() &&
333 m_requireMutualTopKEdges.value() &&
334 m_recoverOrphanNodes.value()) {
335 std::vector<unsigned char> selectedNode(graph.nNodes, 0);
336 for (const std::uint64_t key : selectedPairKeys) {
337 const auto [first, second] = unpackPairKey(key);
338 if (first < graph.nNodes) selectedNode[first] = 1;
339 if (second < graph.nNodes) selectedNode[second] = 1;
340 }
341 for (std::size_t node = 0; node < graph.nNodes; ++node) {
342 if (selectedNode[node] || edgesByNode[node].empty()) continue;
343 const std::uint64_t key = edgesByNode[node].front().first;
344 const auto [first, second] = unpackPairKey(key);
345 if (first >= graph.nNodes || second >= graph.nNodes) continue;
346 if (selectedPairKeys.insert(key).second) ++orphanRecoveryPairs;
347 selectedNode[first] = 1;
348 selectedNode[second] = 1;
349 }
350 }
351
352 std::vector<std::uint64_t> selectedPairs{selectedPairKeys.begin(),
353 selectedPairKeys.end()};
354 std::sort(selectedPairs.begin(), selectedPairs.end());
355
356 // Truth diagnostics only: true cross-layer edges above threshold that the selection keeps,
357 // barrel / endcap (by the first endpoint's chamber)
358 if (truthDiag) {
359 for (const auto& [key, probability] : pairProbability) {
360 if (probability < m_pairGateThreshold.value()) continue;
361 const auto [first, second] = unpackPairKey(key);
362 if (first >= graph.nNodes || second >= graph.nNodes) continue;
363 if (nodeTruthId[first] < 0 || nodeTruthId[first] != nodeTruthId[second]) continue;
364 if (nodeLayer[first] == nodeLayer[second]) continue;
365 const std::size_t region =
366 Muon::MuonStationIndex::isBarrel(graph.segments[first]->chamberIndex()) ? 0 : 1;
367 ++m_sumTrueCrossLayerEdges[region];
368 if (selectedPairKeys.contains(key)) ++m_sumTrueCrossLayerSelected[region];
369 }
370 }
371
372 // Truth diagnostics only: best selected-edge score per node, so that the
373 // strongest edge holding a component together is a max over its nodes.
374 std::vector<float> nodeBestSelectedScore;
375 if (truthDiag) {
376 nodeBestSelectedScore.assign(graph.nNodes, -1.f);
377 for (const std::uint64_t key : selectedPairs) {
378 const auto found = pairProbability.find(key);
379 if (found == pairProbability.end()) continue;
380 const auto [first, second] = unpackPairKey(key);
381 nodeBestSelectedScore[first] = std::max(nodeBestSelectedScore[first], found->second);
382 nodeBestSelectedScore[second] = std::max(nodeBestSelectedScore[second], found->second);
383 }
384 }
385
386 // Innermost-layer anchoring with an anchor cap: rank the inner candidates by
387 // their strongest selected edge towards another layer, i.e. by how firmly
388 // the model links them outwards. Same-chamber alternatives otherwise all
389 // launch seeds built from the same outer segments.
390 const auto layerOf = [&graph](std::size_t node) {
391 return Muon::MuonStationIndex::toLayerIndex(graph.segments[node]->chamberIndex());
392 };
393 std::vector<float> nodeBestCrossLayerScore;
394 std::vector<unsigned int> nodeCrossLayerEdges; // truth diagnostics only
395 {
396 nodeBestCrossLayerScore.assign(graph.nNodes, -1.f);
397 if (truthDiag) nodeCrossLayerEdges.assign(graph.nNodes, 0);
398 for (const std::uint64_t key : selectedPairs) {
399 const auto [first, second] = unpackPairKey(key);
400 if (layerOf(first) == layerOf(second)) continue;
401 const auto found = pairProbability.find(key);
402 if (found == pairProbability.end()) continue;
403 nodeBestCrossLayerScore[first] = std::max(nodeBestCrossLayerScore[first], found->second);
404 nodeBestCrossLayerScore[second] = std::max(nodeBestCrossLayerScore[second], found->second);
405 if (truthDiag) {
406 ++nodeCrossLayerEdges[first];
407 ++nodeCrossLayerEdges[second];
408 }
409 }
410 }
411
412
413 DisjointSet components{graph.nNodes};
414 std::vector<bool> activeNode(graph.nNodes, false);
415 for (const std::uint64_t key : selectedPairs) {
416 const auto [first, second] = unpackPairKey(key);
417 components.unite(first, second);
418 activeNode[first] = true;
419 activeNode[second] = true;
420 }
421
422 std::unordered_map<std::size_t, std::vector<std::size_t>> byRoot;
423 byRoot.reserve(graph.nNodes);
424 for (std::size_t node = 0; node < graph.nNodes; ++node) {
425 if (activeNode[node]) byRoot[components.find(node)].push_back(node);
426 }
427
428 // Deterministic component IDs make debugging and validation reproducible.
429 std::vector<std::vector<std::size_t>> componentNodes;
430 componentNodes.reserve(byRoot.size());
431 for (auto& [_, nodes] : byRoot) {
432 std::sort(nodes.begin(), nodes.end());
433 componentNodes.push_back(std::move(nodes));
434 }
435 std::ranges::sort(componentNodes,
436 [](const auto& first, const auto& second) {
437 return first.front() < second.front();
438 });
439
441 decor{m_pairGateDecorKey, ctx};
443 scoreDecor{m_componentScoreDecorKey, ctx};
444
445 std::vector<std::uint8_t> keptNode(graph.nNodes, false);
446 std::vector<std::uint8_t> nodeDropReason;
447 if (truthDiag) {
448 nodeDropReason.assign(graph.nNodes, kReasonNotSelected);
449 }
450
451 // Group-purity: which kept-component id(s) each truth muon's
452 // segments landed in, and whether any of those components were mixed
453 std::unordered_map<int32_t, std::unordered_set<unsigned>> muonComponents;
454 std::unordered_map<int32_t, bool> muonEverMixed;
455 // D8: weak clean components and, per truth muon, the number of strong multi-layer components
456 std::vector<int32_t> weakCleanMuons;
457 std::unordered_map<int32_t, unsigned int> strongComponentsPerMuon;
458 const auto sizeBin = [](std::size_t size) -> std::size_t {
459 if (size <= 4) return size - 2; // 2, 3, 4 -> 0, 1, 2
460 if (size < 10) return 3; // 5-9
461 if (size < 20) return 4; // 10-19
462 return 5; // 20+
463 };
464
465 std::size_t topologyNodes = 0;
466 std::size_t retainedNodes = 0;
467 std::size_t chamberSuppressedNodes = 0;
468 std::size_t rejectedComponents = 0;
469 std::size_t componentsKept = 0;
470 std::size_t anchors = 0;
471 std::size_t nodesRejectedByMinComponent = 0;
472 unsigned nextComponentId = 1;
473 const InferenceUtils::SegmentQualityOrder betterSegment{};
474
475 const auto isBetterNode = [&](std::size_t candidate, std::size_t incumbent) {
476 if (betterSegment(graph.segments[candidate], graph.segments[incumbent])) {
477 return true;
478 }
479 if (betterSegment(graph.segments[incumbent], graph.segments[candidate])) {
480 return false;
481 }
482 return candidate < incumbent;
483 };
484
485 for (const std::vector<std::size_t>& rawNodes : componentNodes) {
486 topologyNodes += rawNodes.size();
487 std::vector<std::size_t> retained = rawNodes;
488
489 // The seeder resolves same-chamber alternatives while building a seed.
490 // Retaining only the highest-ranked representative is therefore optional.
491 if (m_keepBestSegmentPerChamber.value()) {
492 std::unordered_map<int, std::size_t> bestByChamber;
493 bestByChamber.reserve(rawNodes.size());
494 for (const std::size_t node : rawNodes) {
495 const int chamber =
496 static_cast<int>(graph.segments[node]->chamberIndex());
497 const auto found = bestByChamber.find(chamber);
498 if (found == bestByChamber.end() || isBetterNode(node, found->second)) {
499 bestByChamber[chamber] = node;
500 }
501 }
502 retained.clear();
503 retained.reserve(bestByChamber.size());
504 for (const auto& [_, node] : bestByChamber) retained.push_back(node);
505 std::sort(retained.begin(), retained.end());
506 chamberSuppressedNodes += rawNodes.size() - retained.size();
507 }
508
509 // Provisional reasons; nodes that end up kept are identified by keptNode.
510 if (truthDiag) {
511 for (const std::size_t node : rawNodes) {
512 nodeDropReason[node] = kReasonChamberDedup;
513 }
514 for (const std::size_t node : retained) {
515 nodeDropReason[node] = kReasonComponentRejected;
516 }
517 }
518
519 if (retained.size() < m_minSegmentsPerComponent.value()) {
520 nodesRejectedByMinComponent += retained.size();
521 ++rejectedComponents;
522 continue;
523 }
524
525 // Only ranked component members launch seeds. The edge score therefore
526 // reduces seed attempts directly rather than serving only as a label.
527 std::vector<std::size_t> rankedNodes{retained};
528 if (m_anchorInnermostLayer.value()) {
529 // Anchor only on the inner segment(s)
530 int bestRank = std::numeric_limits<int>::max();
531 for (const std::size_t node : rankedNodes) {
532 bestRank = std::min(
533 bestRank,
535 graph.segments[node]->chamberIndex())));
536 }
537 std::erase_if(rankedNodes, [&](std::size_t node) {
538 return layerRadialRank(Muon::MuonStationIndex::toLayerIndex(
539 graph.segments[node]->chamberIndex())) != bestRank;
540 });
541 // Rank the inner anchors (strongest cross-layer edge first, segment quality as
542 // tie-break) and cap them.
543 if (m_seedAnchorsPerComponent.value() != 0) {
544 std::ranges::sort(rankedNodes, [&](std::size_t first, std::size_t second) {
546 nodeBestCrossLayerScore[first], nodeBestCrossLayerScore[second]);
547 return order != 0 ? order < 0 : isBetterNode(first, second);
548 });
549 if (rankedNodes.size() > m_seedAnchorsPerComponent.value()) {
550 rankedNodes.resize(m_seedAnchorsPerComponent.value());
551 }
552 }
553 } else {
554 std::ranges::sort(rankedNodes, isBetterNode);
555 const std::size_t nAnchors = m_seedAnchorsPerComponent.value() == 0
556 ? rankedNodes.size()
557 : std::min<std::size_t>(m_seedAnchorsPerComponent.value(),
558 rankedNodes.size());
559 rankedNodes.resize(nAnchors);
560 }
561 if (rankedNodes.empty()) {
562 ++rejectedComponents;
563 continue;
564 }
565 // rankedNodes stays in rank order (best anchor first): the decoration
566 // carries the rank so that the seeder can try anchors best-first.
567
568 const unsigned componentId = nextComponentId++;
569 float componentCrossScore = -1.f;
570 for (const std::size_t node : rawNodes) {
571 componentCrossScore = std::max(componentCrossScore, nodeBestCrossLayerScore[node]);
572 }
573 for (const std::size_t node : retained) {
574 scoreDecor(*graph.segments[node]) = {componentCrossScore, nodeBestCrossLayerScore[node]};
575 const auto anchorIt = std::ranges::find(rankedNodes, node);
576 const bool isAnchor = anchorIt != rankedNodes.end();
577 decor(*graph.segments[node]) = {
578 componentId,
579 isAnchor ? static_cast<unsigned int>(anchorIt - rankedNodes.begin()) + 1 : 0u};
580 keptNode[node] = true;
581 }
582 retainedNodes += retained.size();
583 anchors += rankedNodes.size();
584 ++componentsKept;
585
586 // Group purity: does this kept segments from more than one truth muon?
587 if (truthDiag) {
588 std::unordered_set<int32_t> componentTruthIds;
589 for (const std::size_t node : retained) {
590 if (nodeTruthId[node] >= 0) componentTruthIds.insert(nodeTruthId[node]);
591 }
592 const std::size_t outcome = componentTruthIds.empty() ? 2
593 : componentTruthIds.size() == 1 ? 0 : 1; // clean/mixed/bkgOnly
594 ++m_groupPurity.componentsByOutcome[outcome];
595 ++m_groupPurity.componentSizeHist[sizeBin(retained.size())];
596
597 // Component descriptors that a component-level cut could use: the
598 // strongest selected edge among the nodes that formed it (before the
599 // chamber dedup), its retained size and its number of distinct layers.
600 float strongest = -1.f;
601 for (const std::size_t node : rawNodes) {
602 strongest = std::max(strongest, nodeBestSelectedScore[node]);
603 }
604 const std::size_t scoreBin = strongest < 0.05f ? 0 : strongest < 0.1f ? 1
605 : strongest < 0.3f ? 2 : strongest < 0.5f ? 3 : 4;
606 std::uint32_t layerMask = 0;
607 for (const std::size_t node : retained) {
608 // +1 so that LayerUnknown (-1) maps onto bit 0
609 layerMask |= 1u << (Acts::toUnderlying(Muon::MuonStationIndex::toLayerIndex(
610 graph.segments[node]->chamberIndex())) + 1);
611 }
612 const std::size_t layerBin =
613 std::min<std::size_t>(std::popcount(layerMask), GroupPurityCounters::kLayerBins) - 1;
614 ++m_groupPurity.scoreByOutcome[outcome * GroupPurityCounters::kScoreBins + scoreBin];
615 ++m_groupPurity.sizeByOutcome[outcome * GroupPurityCounters::kSizeBins + sizeBin(retained.size())];
616 ++m_groupPurity.layersByOutcome[outcome * GroupPurityCounters::kLayerBins + layerBin];
617
618 // Components that can seed (>= 2 layers)
619 if (std::popcount(layerMask) >= 2) {
620 float crossScore = -1.f;
621 unsigned int crossEdges = 0;
622 for (const std::size_t node : rawNodes) {
623 crossScore = std::max(crossScore, nodeBestCrossLayerScore[node]);
624 crossEdges += nodeCrossLayerEdges[node];
625 }
626 crossEdges /= 2;
627 const std::size_t crossBin = crossScore < 0.05f ? 0 : crossScore < 0.1f ? 1 : crossScore < 0.3f ? 2
628 : crossScore < 0.5f ? 3 : crossScore < 0.8f ? 4 : 5;
629 ++m_groupPurity.crossScoreByOutcome[outcome * GroupPurityCounters::kCrossScoreBins + crossBin];
630 ++m_groupPurity.crossEdgesByOutcome[outcome * GroupPurityCounters::kCrossEdgeBins +
631 std::min<std::size_t>(crossEdges, 3)];
632 if (crossScore < 0.05f) {
633 if (outcome == 0) weakCleanMuons.push_back(*componentTruthIds.begin());
634 } else {
635 for (const int32_t id : componentTruthIds) ++strongComponentsPerMuon[id];
636 }
637 }
638 const bool mixed = componentTruthIds.size() >= 2;
639 if (mixed) {
640 m_groupPurity.mixedComponentSegments += retained.size();
641 const std::uint64_t packed =
642 (static_cast<std::uint64_t>(componentTruthIds.size()) << 32) | retained.size();
643 std::uint64_t current = m_groupPurity.worstMixedPacked.load(std::memory_order_relaxed);
644 while (packed > current && !m_groupPurity.worstMixedPacked.compare_exchange_weak(
645 current, packed, std::memory_order_relaxed)) {}
646 }
647 for (const int32_t id : componentTruthIds) {
648 muonComponents[id].insert(componentId);
649 if (mixed) muonEverMixed[id] = true;
650 }
651 }
652 }
653
654 if (truthDiag) {
655 std::size_t truthSegs = 0, bkgSegs = 0;
656 for (std::size_t node = 0; node < graph.nNodes; ++node) {
657 if (!keptNode[node]) continue;
658 nodeTruthId[node] >= 0 ? ++truthSegs : ++bkgSegs;
659 }
660 m_sumRetainedTruthSegments += truthSegs;
661 m_sumRetainedBkgSegments += bkgSegs;
662
663 std::vector<unsigned char> thresholded(graph.nNodes, 0);
664 for (std::size_t node = 0; node < graph.nNodes; ++node) {
665 thresholded[node] = !edgesByNode[node].empty();
666 }
667 classifyLostTruthSegments(*segments, graph, pairProbability, thresholded,
668 keptNode, nodeDropReason);
669 classifyGroupPurity(muonComponents, muonEverMixed, truthIdToAbsEta);
670 for (const int32_t id : weakCleanMuons) {
671 ++(strongComponentsPerMuon[id] > 0 ? m_groupPurity.weakCleanWithOther : m_groupPurity.weakCleanAlone);
672 }
673 }
674
675 if (!m_filteredSegmentKey.empty()) {
676 auto connectedSegments =
677 std::make_unique<ConstDataVector<xAOD::MuonSegmentContainer>>(
679 connectedSegments->reserve(graph.nNodes);
680 for (std::size_t node = 0; node < graph.nNodes; ++node) {
681 if (!keptNode[node] || !graph.segments[node]) continue;
682 connectedSegments->push_back(graph.segments[node]);
683 }
684
685 const std::size_t nConnectedSegments = connectedSegments->size();
688 ATH_CHECK(connectedHandle.record(std::move(connectedSegments)));
689 ATH_MSG_DEBUG("Event " << ctx.eventID().event_number()
690 << ": wrote " << nConnectedSegments
691 << " ML-connected segment(s) to '"
692 << m_filteredSegmentKey.key() << "'");
693 }
694
695 ATH_MSG_DEBUG("Event " << ctx.eventID().event_number()
696 << ": ML components graphNodes=" << graph.nNodes
697 << ", thresholdedNodes=" << thresholdedNodes
698 << ", thresholdPairs=" << thresholdPairs
699 << ", mutualTopKPairs=" << mutualTopKPairs
700 << ", oneSidedTopKPairs=" << oneSidedTopKPairs
701 << ", selectedPairs=" << selectedPairKeys.size()
702 << ", components=" << componentsKept
703 << ", topologyNodes=" << topologyNodes
704 << ", retainedNodes=" << retainedNodes
705 << ", chamberSuppressedNodes=" << chamberSuppressedNodes
706 << ", nodesRejectedByMinComponent=" << nodesRejectedByMinComponent
707 << ", keepBestSegmentPerChamber=" << m_keepBestSegmentPerChamber.value()
708 << ", seedAnchors=" << anchors
709 << ", rejectedComponents=" << rejectedComponents
710 << ", threshold=" << m_pairGateThreshold.value()
711 << ", maxEdgesPerNode=" << m_maxEdgesPerNode.value()
712 << ", orphanRecoveryPairs=" << orphanRecoveryPairs
713 << ", mutualTopK=" << m_requireMutualTopKEdges.value()
714 << ", degreeCapped=" << m_useDegreeCappedComponents.value());
715
716 // Job-summed diagnostics
717 if (msgLvl(MSG::DEBUG)) {
718 m_sumGraphNodes += graph.nNodes;
719 m_sumThresholdedNodes += thresholdedNodes;
720 m_sumMutualTopKPairs += mutualTopKPairs;
721 m_sumOneSidedTopKPairs += oneSidedTopKPairs;
722 m_sumOrphanRecoveryPairs += orphanRecoveryPairs;
723 m_sumTopologyNodes += topologyNodes;
724 m_sumRetainedNodes += retainedNodes;
725 m_sumChamberSuppressedNodes += chamberSuppressedNodes;
726 m_sumNodesRejectedByMinComponent += nodesRejectedByMinComponent;
727 m_sumRejectedComponents += rejectedComponents;
728 m_sumComponentsKept += componentsKept;
729 }
730 return StatusCode::SUCCESS;
731}
732
734 const xAOD::MuonSegmentContainer& segments, const SegmentEdgeGraph& graph,
735 const std::unordered_map<std::uint64_t, float>& pairProbability,
736 const std::vector<unsigned char>& thresholded,
737 const std::vector<std::uint8_t>& keptNode,
738 const std::vector<unsigned char>& nodeDropReason) const {
739 static_assert(kNumLossCategories == TruthLossCounters::kCategories);
740 constexpr std::size_t kColumns = TruthLossCounters::kColumns;
741 const std::size_t nInput = segments.size();
742
743 if (graph.inputNodeIndex.size() != nInput) {
744 if (!m_warnedMissingToolDiagnostics.exchange(true)) {
745 ATH_MSG_WARNING("Truth diagnostics requested, but the edge classifier tool "
746 "did not provide its pre-ONNX fates; set "
747 "EnableTruthDiagnostics on the tool as well. Skipping the "
748 "lost-segment classification.");
749 }
750 return;
751 }
752
753 // Truth particle of every input segment and the segments grouped per particle.
754 std::vector<const xAOD::TruthParticle*> truthPart(nInput, nullptr);
755 std::vector<std::int32_t> truthId(nInput, -1);
756 std::unordered_map<std::int32_t, std::vector<std::uint32_t>> byTruth;
757 {
758 std::uint32_t inputIndex = 0;
759 for (const xAOD::MuonSegment* segment : segments) {
760 if (const xAOD::TruthParticle* part = MuonR4::getTruthMatchedParticle(*segment)) {
761 truthPart[inputIndex] = part;
762 truthId[inputIndex] = static_cast<std::int32_t>(part->index());
763 byTruth[truthId[inputIndex]].push_back(inputIndex);
764 }
765 ++inputIndex;
766 }
767 }
768 const auto isRetained = [&](std::size_t input) {
769 const std::int32_t node = graph.inputNodeIndex[input];
770 return node >= 0 && keptNode[static_cast<std::size_t>(node)];
771 };
772
773 // Per segment: how far its best cross-chamber true pair got (1 = bucket cap,
774 // 2 = sector window, 3 = angle window, 4 = edge caps, 5 = scored, 0 = only
775 // same-chamber partners) and the score / index of its best scored pair.
776 const auto rankOf = [](PairGate gate) -> unsigned char {
777 switch (gate) {
778 case PairGate::BucketCap: return 1;
779 case PairGate::SectorWindow: return 2;
780 case PairGate::AngleWindow: return 3;
781 case PairGate::EdgeCaps: return 4;
782 case PairGate::Scored: return 5;
783 case PairGate::SameChamber: break;
784 }
785 return 0;
786 };
787 std::vector<unsigned char> bestRank(nInput, 0);
788 std::vector<float> bestScore(nInput, -1.f);
789 std::vector<std::int32_t> bestScoredPair(nInput, -1);
790 for (std::size_t k = 0; k < graph.truthPairFates.size(); ++k) {
791 const PairFate& fate = graph.truthPairFates[k];
792 const unsigned char rank = rankOf(fate.gate);
793 if (rank == 0) continue;
794 const std::array<std::uint32_t, 2> ends{fate.first, fate.second};
795 for (const std::uint32_t end : ends) bestRank[end] = std::max(bestRank[end], rank);
796 if (fate.gate != PairGate::Scored) continue;
797 const std::int32_t firstNode = graph.inputNodeIndex[fate.first];
798 const std::int32_t secondNode = graph.inputNodeIndex[fate.second];
799 if (firstNode < 0 || secondNode < 0) continue;
800 const auto found = pairProbability.find(undirectedPairKey(
801 static_cast<std::size_t>(firstNode), static_cast<std::size_t>(secondNode)));
802 const float score = found != pairProbability.end() ? found->second : 0.f;
803 for (const std::uint32_t end : ends) {
804 if (score > bestScore[end]) {
805 bestScore[end] = score;
806 bestScoredPair[end] = static_cast<std::int32_t>(k);
807 }
808 }
809 }
810
811 // Seedability per truth muon: at least 2 distinct chambers in the input,
812 // fewer than 2 among the retained segments.
813 std::unordered_map<std::int32_t, bool> seedLostByTruth;
814 std::array<std::size_t, 4> muonsSeedable{};
815 std::array<std::size_t, 4> muonsSeedLost{};
816 for (const auto& entry : byTruth) {
817 std::unordered_set<int> inputChambers;
818 std::unordered_set<int> retainedChambers;
819 for (const std::uint32_t input : entry.second) {
820 const int chamber = static_cast<int>(segments[input]->chamberIndex());
821 inputChambers.insert(chamber);
822 if (isRetained(input)) retainedChambers.insert(chamber);
823 }
824 const bool seedable = inputChambers.size() >= 2;
825 const bool lost = seedable && retainedChambers.size() < 2;
826 seedLostByTruth[entry.first] = lost;
827 if (!seedable) continue;
828 const std::size_t region = 1 + static_cast<std::size_t>(
829 regionOf(std::abs(truthPart[entry.second.front()]->eta())));
830 ++muonsSeedable[0];
831 ++muonsSeedable[region];
832 if (lost) {
833 ++muonsSeedLost[0];
834 ++muonsSeedLost[region];
835 }
836 }
837
838 std::array<std::size_t, kNumLossCategories * kColumns> category{};
839 std::array<std::size_t, 5> scoreBin{};
840 std::array<std::size_t, 2> sectorDelta{};
841 std::array<std::size_t, 4> layerPair{};
842 std::array<std::size_t, 4> muonSegments{};
843 std::array<std::size_t, 3> precisionHits{};
844 for (std::size_t i = 0; i < nInput; ++i) {
845 if (truthId[i] < 0 || isRetained(i)) continue;
846 const std::vector<std::uint32_t>& members = byTruth[truthId[i]];
847 const std::int32_t node = graph.inputNodeIndex[i];
848
849 std::size_t cat{};
850 if (members.size() == 1) {
851 cat = kNoPartnerSingleton;
852 } else if (bestRank[i] == 0) {
853 cat = kNoPartnerSameChamber;
854 } else if (node == kDroppedByBucketCap) {
855 cat = kBucketSelf;
856 } else if (node >= 0 && thresholded[static_cast<std::size_t>(node)]) {
857 switch (nodeDropReason[static_cast<std::size_t>(node)]) {
858 case kReasonChamberDedup: cat = kDedupChamber; break;
859 case kReasonComponentRejected: cat = kComponentRejected; break;
860 default: cat = kNotSelected; break;
861 }
862 } else {
863 switch (bestRank[i]) {
864 case 5: cat = kBelowThreshold; break;
865 case 4: cat = kEdgeCaps; break;
866 case 3: cat = kAngleWindow; break;
867 case 2: cat = kSectorWindow; break;
868 default: cat = kBucketPartners; break;
869 }
870 }
871
872 const std::size_t region = 1 + static_cast<std::size_t>(
873 regionOf(std::abs(truthPart[i]->eta())));
874 ++category[cat * kColumns];
875 ++category[cat * kColumns + region];
876 if (seedLostByTruth[truthId[i]]) ++category[cat * kColumns + 4];
877
878 if (cat != kBelowThreshold) continue;
879 if (bestScoredPair[i] >= 0) {
880 const PairFate& fate = graph.truthPairFates[static_cast<std::size_t>(bestScoredPair[i])];
881 const float score = bestScore[i];
882 ++scoreBin[score < 1e-3f ? 0 : score < 1e-2f ? 1 : score < 2e-2f ? 2
883 : score < 5e-2f ? 3 : 4];
884 ++sectorDelta[fate.sectorDelta == 0 ? 0 : 1];
885 const std::uint32_t other = fate.first == i ? fate.second : fate.first;
886 int rankA = layerRadialRank(Muon::MuonStationIndex::toLayerIndex(
887 segments[i]->chamberIndex()));
888 int rankB = layerRadialRank(Muon::MuonStationIndex::toLayerIndex(
889 segments[other]->chamberIndex()));
890 if (rankA > rankB) std::swap(rankA, rankB);
891 ++layerPair[(rankA == 0 && rankB == 1) ? 0
892 : (rankA == 1 && rankB == 2) ? 1
893 : (rankA == 0 && rankB == 2) ? 2 : 3];
894 }
895 ++muonSegments[std::min<std::size_t>(members.size(), 5) - 2];
896 const unsigned int hits = segments[i]->nPrecisionHits();
897 ++precisionHits[hits <= 4 ? 0 : (hits <= 6 ? 1 : 2)];
898 }
899
900 for (std::size_t c = 0; c < category.size(); ++c) m_truthLoss.category[c] += category[c];
901 for (std::size_t c = 0; c < scoreBin.size(); ++c) m_truthLoss.scoreBin[c] += scoreBin[c];
902 for (std::size_t c = 0; c < sectorDelta.size(); ++c) m_truthLoss.sectorDelta[c] += sectorDelta[c];
903 for (std::size_t c = 0; c < layerPair.size(); ++c) m_truthLoss.layerPair[c] += layerPair[c];
904 for (std::size_t c = 0; c < muonSegments.size(); ++c) m_truthLoss.muonSegments[c] += muonSegments[c];
905 for (std::size_t c = 0; c < precisionHits.size(); ++c) m_truthLoss.precisionHits[c] += precisionHits[c];
906 for (std::size_t c = 0; c < muonsSeedable.size(); ++c) {
907 m_truthLoss.muonsSeedable[c] += muonsSeedable[c];
908 m_truthLoss.muonsSeedLost[c] += muonsSeedLost[c];
909 }
910}
911
913 const std::unordered_map<std::int32_t, std::unordered_set<unsigned>>& muonComponents,
914 const std::unordered_map<std::int32_t, bool>& muonEverMixed,
915 const std::unordered_map<std::int32_t, double>& truthIdToAbsEta) const {
916 // Only muons with at least one retained segment in some kept component
917 // appear in muonComponents; fully-lost muons are already covered by
918 // classifyLostTruthSegments()/muonsSeedLost above, not double-counted here.
919 std::array<std::size_t, GroupPurityCounters::kColumns * 3> outcome{};
920 for (const auto& [id, componentIds] : muonComponents) {
921 const auto mixedIt = muonEverMixed.find(id);
922 const bool everMixed = mixedIt != muonEverMixed.end() && mixedIt->second;
923 // 0 = clean (one component, alone), 1 = mixed (shared a component with
924 // another muon), 2 = fragmented (split across components, never mixed).
925 const std::size_t result = everMixed ? 1 : (componentIds.size() >= 2 ? 2 : 0);
926 const auto etaIt = truthIdToAbsEta.find(id);
927 const std::size_t region = 1 + (etaIt != truthIdToAbsEta.end()
928 ? static_cast<std::size_t>(regionOf(etaIt->second)) : 0);
929 ++outcome[0 * 3 + result];
930 ++outcome[region * 3 + result];
931 }
932 for (std::size_t i = 0; i < outcome.size(); ++i) m_groupPurity.muonOutcome[i] += outcome[i];
933}
934
936 // The job-summed counters are only filled, and the summaries only built, at DEBUG level.
937 if (!msgLvl(MSG::DEBUG)) return StatusCode::SUCCESS;
939 "SegmentEdgeInferenceAlg post-ONNX selection summary (job-summed): "
940 << "graphNodes=" << m_sumGraphNodes
941 << ", thresholdedNodes=" << m_sumThresholdedNodes
942 << " (>=PairGateThreshold=" << m_pairGateThreshold.value() << ")"
943 << ", mutualTopKPairs=" << m_sumMutualTopKPairs
944 << ", oneSidedTopKPairs(rejected by RequireMutualTopKEdges="
946 << " (MaxEdgesPerNode=" << m_maxEdgesPerNode.value()
947 << ", MaxEdgesPerNodePerLayer=" << m_maxEdgesPerNodePerLayer.value() << " (0 = global top-K)"
948 << "; a thresholded edge is dropped here purely by rank, "
949 "independent of PairGateThreshold)"
950 << ", orphanRecoveryPairs=" << m_sumOrphanRecoveryPairs
951 << ", chamberSuppressedNodes=" << m_sumChamberSuppressedNodes
952 << " (KeepBestSegmentPerChamber=" << m_keepBestSegmentPerChamber.value() << ")"
953 << ", nodesRejectedByMinComponent=" << m_sumNodesRejectedByMinComponent
954 << " (MinSegmentsPerComponent=" << m_minSegmentsPerComponent.value() << ")"
955 << ", rejectedComponents=" << m_sumRejectedComponents
956 << ", componentsKept=" << m_sumComponentsKept
957 << ", topologyNodes=" << m_sumTopologyNodes
958 << ", retainedNodes=" << m_sumRetainedNodes);
959
960 if (m_truthDiagEnabled) {
961 ATH_MSG_DEBUG("True cross-layer edges above PairGateThreshold kept by the selection, selected/total: barrel="
962 << m_sumTrueCrossLayerSelected[0] << "/" << m_sumTrueCrossLayerEdges[0]
963 << ", endcap=" << m_sumTrueCrossLayerSelected[1] << "/" << m_sumTrueCrossLayerEdges[1]);
965 "SegmentEdgeInferenceAlg truth-vs-background funnel (job-summed; "
966 "truth segment = getTruthMatchedParticle(seg) != nullptr, matching "
967 "SegmentDumperAlg::m_segmentHasTruth, no isMuon() filter, no G4 "
968 "pseudo-label fallback): "
969 << "input: truth=" << m_sumInputTruthSegments
970 << " bkg=" << m_sumInputBkgSegments
971 << "; reachesONNX: truth=" << m_sumOnnxTruthSegments
972 << " bkg=" << m_sumOnnxBkgSegments
973 << "; retained: truth=" << m_sumRetainedTruthSegments
974 << " bkg=" << m_sumRetainedBkgSegments
975 << "; edge-level (same truth-particle index on both endpoints) at "
976 "PairGateThreshold=" << m_pairGateThreshold.value()
977 << ": trueEdges passed/total=" << m_sumTrueEdgesPassed << "/"
979 << ", bkgEdges passed/total=" << m_sumBkgEdgesPassed << "/"
981
982 constexpr std::array<const char*, TruthLossCounters::kCategories> names{
983 "A1 no possible partner: singleton muon segment",
984 "A2 no possible partner: same-chamber partners only",
985 "B0 never reached ONNX: segment dropped by MaxSegmentsPerBucket",
986 "B1 never reached ONNX: partners dropped by MaxSegmentsPerBucket",
987 "B2 never reached ONNX: outside sector window (MaxDeltaSector)",
988 "B3 never reached ONNX: outside angle window (MaxDeltaThetaDeg)",
989 "B4 never reached ONNX: pre-ONNX edge caps",
990 "C true pair(s) scored below PairGateThreshold",
991 "D1 passed threshold, removed: KeepBestSegmentPerChamber",
992 "D2 passed threshold, removed: component rejected (MinSegmentsPerComponent/anchors)",
993 "D3 passed threshold, removed: not selected by top-K/mutual gate"};
994 constexpr std::size_t kColumns = TruthLossCounters::kColumns;
995 ATH_MSG_DEBUG("Lost truth segments by stage (job-summed; truth segment = "
996 "input segment with a truth particle, lost = not in the filtered "
997 "container). Columns: all | barrel | transition | endcap | "
998 "belonging to a muon that lost seedability");
999 std::size_t lostTotal = 0;
1000 for (std::size_t c = 0; c < names.size(); ++c) {
1001 const auto value = [&](std::size_t col) { return m_truthLoss.category[c * kColumns + col].load(); };
1002 lostTotal += value(0);
1003 ATH_MSG_DEBUG(" " << names[c] << ": " << value(0) << " | " << value(1)
1004 << " | " << value(2) << " | " << value(3) << " | " << value(4));
1005 }
1006 ATH_MSG_DEBUG(" check: sum of categories=" << lostTotal
1007 << ", input truth - retained truth="
1009 ATH_MSG_DEBUG(" truth muons with >=2 input chambers (all|barrel|transition|endcap): "
1010 << m_truthLoss.muonsSeedable[0].load() << "|" << m_truthLoss.muonsSeedable[1].load()
1011 << "|" << m_truthLoss.muonsSeedable[2].load() << "|" << m_truthLoss.muonsSeedable[3].load()
1012 << "; of which <2 chambers retained: "
1013 << m_truthLoss.muonsSeedLost[0].load() << "|" << m_truthLoss.muonsSeedLost[1].load()
1014 << "|" << m_truthLoss.muonsSeedLost[2].load() << "|" << m_truthLoss.muonsSeedLost[3].load());
1015 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: "
1016 << m_truthLoss.scoreBin[0].load() << "/" << m_truthLoss.scoreBin[1].load() << "/"
1017 << m_truthLoss.scoreBin[2].load() << "/" << m_truthLoss.scoreBin[3].load() << "/"
1018 << m_truthLoss.scoreBin[4].load()
1019 << "; pair sector delta same/adjacent: " << m_truthLoss.sectorDelta[0].load() << "/"
1020 << m_truthLoss.sectorDelta[1].load()
1021 << "; layer pair Inner-Middle/Middle-Outer/Inner-Outer/other: "
1022 << m_truthLoss.layerPair[0].load() << "/" << m_truthLoss.layerPair[1].load() << "/"
1023 << m_truthLoss.layerPair[2].load() << "/" << m_truthLoss.layerPair[3].load()
1024 << "; muon input segments 2/3/4/5+: " << m_truthLoss.muonSegments[0].load() << "/"
1025 << m_truthLoss.muonSegments[1].load() << "/" << m_truthLoss.muonSegments[2].load() << "/"
1026 << m_truthLoss.muonSegments[3].load()
1027 << "; nPrecisionHits <=4/5-6/>=7: " << m_truthLoss.precisionHits[0].load() << "/"
1028 << m_truthLoss.precisionHits[1].load() << "/" << m_truthLoss.precisionHits[2].load());
1029
1031 "SegmentEdgeInferenceAlg grouping purity (job-summed; components cover "
1032 "only kept components, i.e. those that survived MinSegmentsPerComponent "
1033 "and appear in the output): "
1034 << "clean=" << m_groupPurity.componentsByOutcome[0].load()
1035 << " mixed=" << m_groupPurity.componentsByOutcome[1].load()
1036 << " bkgOnly=" << m_groupPurity.componentsByOutcome[2].load()
1037 << "; segments inside mixed components=" << m_groupPurity.mixedComponentSegments.load()
1038 << "; component size histogram (2/3/4/5-9/10-19/20+): "
1039 << m_groupPurity.componentSizeHist[0].load() << "/" << m_groupPurity.componentSizeHist[1].load() << "/"
1040 << m_groupPurity.componentSizeHist[2].load() << "/" << m_groupPurity.componentSizeHist[3].load() << "/"
1041 << m_groupPurity.componentSizeHist[4].load() << "/" << m_groupPurity.componentSizeHist[5].load());
1042
1043 // Kept components per outcome, binned by a candidate component-level cut variable.
1044 const auto printByOutcome = [&](const char* label, const auto& counters, std::size_t nBins) {
1045 constexpr std::array<const char*, 3> outcomes{"clean", "mixed", "bkgOnly"};
1046 std::stringstream sstr{};
1047 for (std::size_t outcome = 0; outcome < outcomes.size(); ++outcome) {
1048 sstr << " " << outcomes[outcome] << "=";
1049 for (std::size_t bin = 0; bin < nBins; ++bin) {
1050 sstr << (bin ? "/" : "") << counters[outcome * nBins + bin].load();
1051 }
1052 }
1053 ATH_MSG_DEBUG(" kept components by " << label << ":" << sstr.str());
1054 };
1055 printByOutcome("strongest selected-edge score (<0.05/0.05-0.1/0.1-0.3/0.3-0.5/>=0.5)",
1056 m_groupPurity.scoreByOutcome, GroupPurityCounters::kScoreBins);
1057 printByOutcome("retained segments (2/3/4/5-9/10-19/20+)",
1058 m_groupPurity.sizeByOutcome, GroupPurityCounters::kSizeBins);
1059 printByOutcome("distinct layers (1/2/3/4+)",
1060 m_groupPurity.layersByOutcome, GroupPurityCounters::kLayerBins);
1061 printByOutcome("strongest selected cross-layer edge, components with >=2 layers "
1062 "(<0.05/0.05-0.1/0.1-0.3/0.3-0.5/0.5-0.8/>=0.8)",
1063 m_groupPurity.crossScoreByOutcome, GroupPurityCounters::kCrossScoreBins);
1064 printByOutcome("number of selected cross-layer edges, components with >=2 layers (0/1/2/3+)",
1065 m_groupPurity.crossEdgesByOutcome, GroupPurityCounters::kCrossEdgeBins);
1066 ATH_MSG_DEBUG(" D8 clean components with >=2 layers and strongest cross-layer edge < 0.05: "
1067 << "their muon has another component with >=2 layers and a stronger edge="
1068 << m_groupPurity.weakCleanWithOther.load()
1069 << ", it is the muon's only such component=" << m_groupPurity.weakCleanAlone.load());
1070
1071 const std::uint64_t worstMixed = m_groupPurity.worstMixedPacked.load();
1072 ATH_MSG_DEBUG(" worst mixed component this job: "
1073 << (worstMixed >> 32) << " distinct truth muons in "
1074 << (worstMixed & 0xffffffffull) << " segments"
1075 << (worstMixed == 0 ? " (no mixed components found)" : ""));
1076
1078 " truth-muon grouping outcome (all|barrel|transition|endcap; muons with "
1079 ">=1 retained segment only, fully-lost muons excluded -- see muonsSeedLost above): "
1080 << "clean=" << m_groupPurity.muonOutcome[0 * 3 + 0].load() << "|"
1081 << m_groupPurity.muonOutcome[1 * 3 + 0].load() << "|"
1082 << m_groupPurity.muonOutcome[2 * 3 + 0].load() << "|"
1083 << m_groupPurity.muonOutcome[3 * 3 + 0].load()
1084 << "; mixed=" << m_groupPurity.muonOutcome[0 * 3 + 1].load() << "|"
1085 << m_groupPurity.muonOutcome[1 * 3 + 1].load() << "|"
1086 << m_groupPurity.muonOutcome[2 * 3 + 1].load() << "|"
1087 << m_groupPurity.muonOutcome[3 * 3 + 1].load()
1088 << "; fragmented=" << m_groupPurity.muonOutcome[0 * 3 + 2].load() << "|"
1089 << m_groupPurity.muonOutcome[1 * 3 + 2].load() << "|"
1090 << m_groupPurity.muonOutcome[2 * 3 + 2].load() << "|"
1091 << m_groupPurity.muonOutcome[3 * 3 + 2].load());
1092 }
1093 return StatusCode::SUCCESS;
1094}
1095
1096} // 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
void classifyGroupPurity(const std::unordered_map< std::int32_t, std::unordered_set< unsigned > > &muonComponents, const std::unordered_map< std::int32_t, bool > &muonEverMixed, const std::unordered_map< std::int32_t, double > &truthIdToAbsEta) const
MC-only: given the per-muon component memberships and mixed-status gathered while the per-component c...
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, anchorRank + 1] Empty means that the s...
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
Gaudi::Property< unsigned int > m_maxEdgesPerNodePerLayer
SG::WriteDecorHandleKey< xAOD::MuonSegmentContainer > m_componentScoreDecorKey
Per-segment ML scores for MlMsTrackSeeder: [strongest selected cross-layer edge of the segment's comp...
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.
virtual double eta() const override final
The pseudorapidity ( ) of the particle.
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
std::string label(const std::string &format, int i)
Definition label.h:19
bool first
Definition DeMoScan.py:534
MuonR4::SegmentQualitySorter SegmentQualityOrder
Common quality ordering for segment representatives, shared with MlMsTrackSeeder so that graph constr...
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.
bool isBarrel(const ChIndex index)
Returns true if the chamber index points to a barrel chamber.
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)
MuonSegmentContainer_v1 MuonSegmentContainer
Definition of the current "MuonSegment container version".
TruthParticle_v1 TruthParticle
Typedef to implementation.
MuonSegment_v1 MuonSegment
Reference the current persistent version:
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
static constexpr std::size_t kCrossScoreBins
Components spanning >= 2 layers (the only ones that can seed), per outcome: