133 <<
"' = " << segments->
size());
136 std::size_t truthSegs = 0, bkgSegs = 0;
145 std::vector<SegmentEdgeScore>
scores{};
148 <<
": built graph with nodes=" << graph.
nNodes
149 <<
", edges=" << graph.
nEdges);
153 std::vector<int32_t> nodeTruthId;
155 nodeTruthId.assign(graph.
nNodes, -1);
156 std::size_t truthSegs = 0, bkgSegs = 0;
161 nodeTruthId[
node] =
static_cast<int32_t
>(truthPart->index());
173 float minProb = std::numeric_limits<float>::max();
174 float maxProb = std::numeric_limits<float>::lowest();
176 minProb = std::min(minProb, score.probability);
177 maxProb = std::max(maxProb, score.probability);
180 <<
": edge scores=" <<
scores.size()
181 <<
", prob range=[" << minProb <<
", " << maxProb <<
"]");
184 <<
": no edge scores produced");
189 std::unordered_map<std::uint64_t, float> pairProbability;
190 pairProbability.reserve(
scores.size());
192 if (score.src >= graph.
nNodes || score.dst >= graph.
nNodes ||
193 score.src == score.dst) {
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);
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];
227 using WeightedEdge = std::pair<std::uint64_t, float>;
228 const auto betterWeightedEdge = [](
const WeightedEdge& first,
229 const WeightedEdge& second) {
231 first.second, second.second);
232 if (probabilityOrder != 0) {
233 return probabilityOrder < 0;
235 return first.first < second.first;
237 std::vector<std::vector<WeightedEdge>> edgesByNode(graph.
nNodes);
238 std::size_t thresholdPairs = 0;
239 for (
const auto& [key, probability] : pairProbability) {
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);
248 std::size_t thresholdedNodes = 0;
249 for (
const std::vector<WeightedEdge>& nodeEdges : edgesByNode) {
250 thresholdedNodes += !nodeEdges.empty();
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);
261 for (
const WeightedEdge& edge : nodeEdges) {
262 ++nominations[edge.first];
266 std::size_t mutualTopKPairs = 0;
267 std::size_t oneSidedTopKPairs = 0;
268 for (
const auto& [_,
count] : nominations) {
276 std::unordered_set<std::uint64_t> selectedPairKeys;
277 selectedPairKeys.reserve(thresholdPairs);
280 std::vector<WeightedEdge> acceptedPairs;
281 acceptedPairs.reserve(thresholdPairs);
282 for (
const auto& [key, probability] : pairProbability) {
284 acceptedPairs.emplace_back(key, probability);
286 std::sort(acceptedPairs.begin(), acceptedPairs.end(), betterWeightedEdge);
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)) {
296 selectedPairKeys.insert(edge.first);
301 for (
const auto& [key,
count] : nominations) {
303 selectedPairKeys.insert(key);
309 std::size_t orphanRecoveryPairs = 0;
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;
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;
330 std::vector<std::uint64_t> selectedPairs{selectedPairKeys.begin(),
331 selectedPairKeys.end()};
332 std::sort(selectedPairs.begin(), selectedPairs.end());
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;
343 std::unordered_map<std::size_t, std::vector<std::size_t>> byRoot;
344 byRoot.reserve(graph.
nNodes);
346 if (activeNode[
node]) byRoot[components.find(
node)].push_back(
node);
350 std::vector<std::vector<std::size_t>> componentNodes;
351 componentNodes.reserve(byRoot.size());
352 for (
auto& [_, nodes] : byRoot) {
354 componentNodes.push_back(std::move(nodes));
356 std::ranges::sort(componentNodes,
357 [](
const auto& first,
const auto& second) {
358 return first.front() < second.front();
364 std::vector<std::uint8_t> keptNode(graph.
nNodes,
false);
365 std::vector<std::uint8_t> nodeDropReason;
367 nodeDropReason.assign(graph.
nNodes, kReasonNotSelected);
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;
380 const auto isBetterNode = [&](std::size_t candidate, std::size_t incumbent) {
387 return candidate < incumbent;
390 for (
const std::vector<std::size_t>& rawNodes : componentNodes) {
391 topologyNodes += rawNodes.size();
392 std::vector<std::size_t> retained = rawNodes;
397 std::unordered_map<int, std::size_t> bestByChamber;
398 bestByChamber.reserve(rawNodes.size());
399 for (
const std::size_t
node : rawNodes) {
402 const auto found = bestByChamber.find(chamber);
403 if (found == bestByChamber.end() || isBetterNode(
node, found->second)) {
404 bestByChamber[chamber] =
node;
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();
416 for (
const std::size_t
node : rawNodes) {
417 nodeDropReason[
node] = kReasonChamberDedup;
419 for (
const std::size_t
node : retained) {
420 nodeDropReason[
node] = kReasonComponentRejected;
425 nodesRejectedByMinComponent += retained.size();
426 ++rejectedComponents;
432 std::vector<std::size_t> rankedNodes{retained};
435 int bestRank = std::numeric_limits<int>::max();
436 for (
const std::size_t
node : rankedNodes) {
447 std::ranges::sort(rankedNodes, isBetterNode);
452 rankedNodes.resize(nAnchors);
454 if (rankedNodes.empty()) {
455 ++rejectedComponents;
458 std::ranges::sort(rankedNodes);
460 const unsigned componentId = nextComponentId++;
461 for (
const std::size_t
node : retained) {
462 const bool isAnchor = Acts::rangeContainsValue(rankedNodes,
node);
464 componentId,
static_cast<unsigned int>(isAnchor)};
465 keptNode[
node] =
true;
467 retainedNodes += retained.size();
468 anchors += rankedNodes.size();
473 std::size_t truthSegs = 0, bkgSegs = 0;
475 if (!keptNode[
node]) {
478 nodeTruthId[
node] >= 0 ? ++truthSegs : ++bkgSegs;
483 std::vector<unsigned char> thresholded(graph.
nNodes, 0);
485 thresholded[
node] = !edgesByNode[
node].empty();
488 keptNode, nodeDropReason);
492 auto connectedSegments =
493 std::make_unique<ConstDataVector<xAOD::MuonSegmentContainer>>(
495 connectedSegments->reserve(graph.
nNodes);
501 const std::size_t nConnectedSegments = connectedSegments->size();
506 <<
": wrote " << nConnectedSegments
507 <<
" ML-connected segment(s) to '"
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
524 <<
", seedAnchors=" << anchors
525 <<
", rejectedComponents=" << rejectedComponents
528 <<
", orphanRecoveryPairs=" << orphanRecoveryPairs
546 return StatusCode::SUCCESS;
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 {
557 const std::size_t nInput = segments.
size();
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.");
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;
574 std::uint32_t inputIndex = 0;
577 truthPart[inputIndex] = part;
578 truthId[inputIndex] =
static_cast<std::int32_t
>(part->index());
579 byTruth[truthId[inputIndex]].push_back(inputIndex);
584 const auto isRetained = [&](std::size_t input) {
586 return node >= 0 && keptNode[
static_cast<std::size_t
>(
node)];
592 const auto rankOf = [](
PairGate gate) ->
unsigned char {
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);
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);
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);
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);
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())));
647 ++muonsSeedable[region];
650 ++muonsSeedLost[region];
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]];
666 if (members.size() == 1) {
667 cat = kNoPartnerSingleton;
668 }
else if (bestRank[i] == 0) {
669 cat = kNoPartnerSameChamber;
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;
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;
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];
694 if (cat != kBelowThreshold)
continue;
695 if (bestScoredPair[i] >= 0) {
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];
703 segments[i]->chamberIndex()));
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];
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)];
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];
730 "SegmentEdgeInferenceAlg post-ONNX selection summary (job-summed): "
735 <<
", oneSidedTopKPairs(rejected by RequireMutualTopKEdges="
738 <<
"; a thresholded edge is dropped here purely by rank, "
739 "independent of PairGateThreshold)"
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): "
762 <<
"; edge-level (same truth-particle index on both endpoints) at "
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"};
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));
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());
817 return StatusCode::SUCCESS;