ATLAS Offline Software
Loading...
Searching...
No Matches
SegmentEdgeClassifierTool.cxx
Go to the documentation of this file.
2#include "InferenceUtils.h"
10#include "Acts/Utilities/Helpers.hpp"
12#include "GaudiKernel/SystemOfUnits.h"
13#include <nlohmann/json.hpp>
14#include <algorithm>
15#include <array>
16#include <cmath>
17#include <cstdint>
18#include <fstream>
19#include <mutex>
20#include <map>
21#include <optional>
22#include <sstream>
23#include <tuple>
24#include <unordered_map>
25#include <unordered_set>
26
27namespace {
29using SegmentBucketKey =
30 std::tuple<int, int, int>; // sector, chamberIndex, etaIndex
31
32SegmentBucketKey segmentBucketKey(const xAOD::MuonSegment& seg) {
33 return {seg.sector(), static_cast<int>(seg.chamberIndex()), seg.etaIndex()};
34}
35
39int layersInBucket(const MuonR4::SpacePointBucket& bucket) {
41 std::vector<unsigned int> uniqueLayers;
42 uniqueLayers.reserve(bucket.size());
43 for (const MuonR4::SpacePointBucket::value_type& sp : bucket) {
44 const unsigned int layNum = sorter.sectorLayerNum(*sp);
45 if (!Acts::rangeContainsValue(uniqueLayers, layNum)) {
46 uniqueLayers.push_back(layNum);
47 }
48 }
49 return static_cast<int>(uniqueLayers.size());
50}
51
57inline int sectorDistance(int a, int b, int mod) {
58 int d = std::abs(a - b);
59 return mod > 0 ? std::min(d, mod - d) : d;
60}
61
62std::optional<MuonML::SegmentNodeFeatureId> nodeFeatureIdFromName(const std::string& name) {
63 using FeatureId = MuonML::SegmentNodeFeatureId;
64 if (name == "segmentPositionX_m") return FeatureId::SegmentPositionX;
65 if (name == "segmentPositionY_m") return FeatureId::SegmentPositionY;
66 if (name == "segmentPositionZ_m") return FeatureId::SegmentPositionZ;
67 if (name == "segmentDirectionX") return FeatureId::SegmentDirectionX;
68 if (name == "segmentDirectionY") return FeatureId::SegmentDirectionY;
69 if (name == "segmentDirectionZ") return FeatureId::SegmentDirectionZ;
70 if (name == "bucket_chamberIndex") return FeatureId::BucketChamberIndex;
71 if (name == "bucket_layers") return FeatureId::BucketLayers;
72 if (name == "bucket_sector") return FeatureId::BucketSector;
73 if (name == "bucket_segments") return FeatureId::BucketSegments;
74 return std::nullopt;
75}
76
77float nodeFeatureValue(MuonML::SegmentNodeFeatureId feature,
78 const Amg::Vector3D& pos,
79 const Amg::Vector3D& dir,
80 const MuonML::BucketSegmentFeatures& bucket) {
81 using FeatureId = MuonML::SegmentNodeFeatureId;
82 switch (feature) {
83 case FeatureId::SegmentPositionX: return static_cast<float>(pos.x());
84 case FeatureId::SegmentPositionY: return static_cast<float>(pos.y());
85 case FeatureId::SegmentPositionZ: return static_cast<float>(pos.z());
86 case FeatureId::SegmentDirectionX: return static_cast<float>(dir.x());
87 case FeatureId::SegmentDirectionY: return static_cast<float>(dir.y());
88 case FeatureId::SegmentDirectionZ: return static_cast<float>(dir.z());
89 case FeatureId::BucketChamberIndex: return static_cast<float>(bucket.chamberIndex);
90 case FeatureId::BucketLayers: return static_cast<float>(bucket.layers);
91 case FeatureId::BucketSector: return static_cast<float>(bucket.sector);
92 case FeatureId::BucketSegments: return static_cast<float>(bucket.nSegments);
93 }
94 return 0.f;
95}
96}
97
98namespace MuonML {
99
101 if (m_sectorModulo.value() > 0 &&
102 2ULL * static_cast<unsigned long long>(m_maxDeltaSector.value()) + 1ULL >
103 static_cast<unsigned long long>(m_sectorModulo.value())) {
104 ATH_MSG_ERROR("MaxDeltaSector=" << m_maxDeltaSector.value()
105 << " spans duplicate sectors for SectorModulo="
106 << m_sectorModulo.value());
107 return StatusCode::FAILURE;
108 }
110
111 // Resolve node feature names from model metadata, matching the ONNX exporter.
112 {
113 Ort::AllocatorWithDefaultOptions allocator;
114 Ort::ModelMetadata meta = model().GetModelMetadata();
115 auto keys = meta.GetCustomMetadataMapKeysAllocated(allocator);
116 std::vector<std::string> keyList;
117 keyList.reserve(keys.size());
118 for (const auto& k : keys) keyList.emplace_back(k.get());
119
120 constexpr std::array<std::string_view, 4> candidates{
121 "x_feature_names", "node_feature_names", "feature_names", "input_feature_names"};
122 std::string usedKey;
123 std::vector<std::string> names;
124 for (std::string_view key : candidates) {
125 const std::string keyStr{key};
126 if (std::find(keyList.begin(), keyList.end(), keyStr) == keyList.end()) continue;
127 names = parseFeatureNames(meta.LookupCustomMetadataMapAllocated(keyStr.c_str(), allocator).get());
128 if (!names.empty()) {
129 usedKey = keyStr;
130 break;
131 }
132 }
133
134 if (names.empty()) {
136 ATH_MSG_WARNING("Model metadata has no usable node feature name key"
137 " (tried x_feature_names/node_feature_names/feature_names/input_feature_names)."
138 " Falling back to default training order.");
139 } else {
140 if (names.size() != kNodeFeatureCount) {
141 ATH_MSG_ERROR("Model metadata key '" << usedKey << "' has " << names.size()
142 << " features, expected " << kNodeFeatureCount);
143 return StatusCode::FAILURE;
144 }
145 for (const std::string& n : names) {
146 if (!nodeFeatureIdFromName(n).has_value()) {
147 ATH_MSG_ERROR("Unsupported node feature name in model metadata ('" << usedKey
148 << "'): '" << n << "'."
149 " Add mapping in SegmentEdgeClassifierTool::nodeFeatureValue().");
150 return StatusCode::FAILURE;
151 }
152 }
153 m_nodeFeatureNames = std::move(names);
154 ATH_MSG_DEBUG("Using node feature names from model metadata key '" << usedKey << "'.");
155 }
156
157 m_nodeFeatureIds.reserve(m_nodeFeatureNames.size());
158 for (const std::string& n : m_nodeFeatureNames) {
159 const auto id = nodeFeatureIdFromName(n);
160 if (!id.has_value()) {
161 ATH_MSG_ERROR("Internal feature-id resolution failed for node feature name '" << n << "'.");
162 return StatusCode::FAILURE;
163 }
164 m_nodeFeatureIds.push_back(*id);
165 }
166
167 std::ostringstream order;
168 order << "Node feature order:";
169 for (std::size_t i = 0; i < m_nodeFeatureNames.size(); ++i) {
170 order << " f" << i << "=" << m_nodeFeatureNames[i];
171 if (i + 1 < m_nodeFeatureNames.size()) order << ",";
172 }
173 ATH_MSG_DEBUG(order.str());
174 }
175
177 ATH_MSG_ERROR("Internal node feature setup has " << m_nodeFeatureNames.size()
178 << " entries, expected " << kNodeFeatureCount);
179 return StatusCode::FAILURE;
180 }
181 if (m_nodeFeatureIds.size() != kNodeFeatureCount) {
182 ATH_MSG_ERROR("Internal node feature id setup has " << m_nodeFeatureIds.size()
183 << " entries, expected " << kNodeFeatureCount);
184 return StatusCode::FAILURE;
185 }
186
187 m_cosMin = std::cos(m_maxDeltaThetaDeg.value() * Gaudi::Units::deg);
188
189 if (!m_debugDumpFile.value().empty()) {
190 std::ofstream out{m_debugDumpFile.value(), std::ios::out | std::ios::trunc};
191 if (!out) {
192 ATH_MSG_ERROR("Could not create segment-edge debug dump file: "
193 << m_debugDumpFile.value());
194 return StatusCode::FAILURE;
195 }
196
197 nlohmann::ordered_json metadata;
198 metadata["record_type"] = "metadata";
199 metadata["format_version"] = 1;
200 metadata["tool"] = "SegmentEdgeClassifierTool";
201 metadata["input_names"] = {m_inputNodeName.value(),
202 m_inputEdgeIndexName.value(),
203 m_inputEdgeAttrName.value()};
204 metadata["output_name"] = m_outputName.value();
205 metadata["x_feature_names"] = m_nodeFeatureNames;
206 metadata["edge_attr_feature_names"] = {
207 "deltaPositionX_m", "deltaPositionY_m", "deltaPositionZ_m",
208 "distance_m", "cos_opening_angle", "same_chamber", "same_sector"};
209 metadata["edge_index_layout"] = "row_major_2_by_E";
210 metadata["edge_order"] = "directed src_to_dst; row 0 then row 1";
211 metadata["max_delta_theta_deg"] = m_maxDeltaThetaDeg.value();
212 metadata["max_delta_sector"] = m_maxDeltaSector.value();
213 metadata["sector_modulo"] = m_sectorModulo.value();
214 metadata["debug_dump_max_events"] = m_debugDumpMaxEvents.value();
215 out << metadata.dump() << '\n';
216
217 ATH_MSG_INFO("Writing segment-edge ONNX debug dump to "
218 << m_debugDumpFile.value()
219 << " (DebugDumpMaxEvents="
220 << m_debugDumpMaxEvents.value() << ")");
221 }
222
223 return StatusCode::SUCCESS;
224}
225
226StatusCode SegmentEdgeClassifierTool::runGraphInference(const EventContext&, GraphRawData&) const {
227 ATH_MSG_ERROR("runGraphInference is not supported by SegmentEdgeClassifierTool. Use SegmentEdgeInferenceAlg + ISegmentEdgeClassifierTool methods.");
228 return StatusCode::FAILURE;
229}
230
232 const EventContext&, const xAOD::MuonSegmentContainer& segments,
233 SegmentEdgeGraph& graph) const {
234 graph = SegmentEdgeGraph{};
235 graph.segments.reserve(segments.size());
236 graph.nodeFeatures.reserve(segments.size() * kNodeFeatureCount);
237
238 /*
239 * Keep the original bucket multiplicity in the node feature even when the
240 * speed configuration retains only the best representatives of a bucket.
241 * This preserves the model's occupancy input while removing duplicate node
242 * and edge work before tensor construction.
243 */
244 std::map<SegmentBucketKey, std::vector<const xAOD::MuonSegment*>>
245 segmentsByBucket;
246 for (const xAOD::MuonSegment* segment : segments) {
247 segmentsByBucket[segmentBucketKey(*segment)].push_back(segment);
248 }
249
250 const InferenceUtils::SegmentQualityOrder betterSegment{};
251
252 std::unordered_set<const xAOD::MuonSegment*> retainedSegments;
253 retainedSegments.reserve(segments.size());
254 for (auto& [_, bucketSegments] : segmentsByBucket) {
255 std::ranges::sort(bucketSegments, betterSegment);
256 const std::size_t nKeep = m_maxSegmentsPerBucket.value() == 0
257 ? bucketSegments.size()
258 : std::min<std::size_t>(
259 bucketSegments.size(),
260 m_maxSegmentsPerBucket.value());
261 retainedSegments.insert(bucketSegments.begin(),
262 bucketSegments.begin() + nKeep);
263 }
264
265 std::vector<Amg::Vector3D> pos;
266 std::vector<Amg::Vector3D> dir;
267 std::vector<BucketSegmentFeatures> bucket;
268 pos.reserve(retainedSegments.size());
269 dir.reserve(retainedSegments.size());
270 bucket.reserve(retainedSegments.size());
271
272 for (const xAOD::MuonSegment* segment : segments) {
273 if (!retainedSegments.contains(segment)) continue;
274
275 const Amg::Vector3D position = segment->position();
276 const Amg::Vector3D direction = segment->direction();
277 const SegmentBucketKey key = segmentBucketKey(*segment);
278 const auto bucketIt = segmentsByBucket.find(key);
279 const int multiplicity =
280 bucketIt == segmentsByBucket.end()
281 ? 1
282 : static_cast<int>(bucketIt->second.size());
283
284 const int chamberIndex = static_cast<int>(segment->chamberIndex());
285 const int layers = layersInBucket(*MuonR4::detailedSegment(*segment)->parent()->parentBucket());
286 const int sector = segment->sector();
287
288 graph.segments.push_back(segment);
289 pos.emplace_back(position / Gaudi::Units::m);
290 dir.emplace_back(direction);
291 bucket.emplace_back(BucketSegmentFeatures{
292 chamberIndex, layers, sector, multiplicity});
293 for (const SegmentNodeFeatureId featureId : m_nodeFeatureIds) {
294 graph.nodeFeatures.push_back(
295 nodeFeatureValue(featureId, pos.back(), dir.back(), bucket.back()));
296 }
297 }
298 graph.nNodes = graph.segments.size();
299
300 if (pos.size() != graph.nNodes || dir.size() != graph.nNodes ||
301 bucket.size() != graph.nNodes) {
302 ATH_MSG_ERROR("Inconsistent vector sizes during graph building: nodes="
303 << graph.nNodes << ", pos=" << pos.size()
304 << ", dir=" << dir.size() << ", bucket=" << bucket.size());
305 return StatusCode::FAILURE;
306 }
307
308 if (graph.nNodes < 2) {
309 graph.nEdges = 0;
310 return StatusCode::SUCCESS;
311 }
312
313 const auto wrapRegularSector = [&](int sector) {
314 // MuonSegment::sector() is a regular sector number, not an ExpandedSector
315 // coordinate. Wrap it to [0, modulo); <= 0 disables wrapping.
316 if (m_sectorModulo.value() > 0) {
317 sector %= m_sectorModulo.value();
318 if (sector < 0) sector += m_sectorModulo.value();
319 }
320 return sector;
321 };
322
323 // The lookup key must use the same wrapping as the target sectors below:
324 // ATLAS sectors are 1-based (1..16), so a raw key of 16 can never match a
325 // wrapped target of 0, which silently dropped every edge into sector 16.
326 // The per-pair sectorDistance check below enforces the true circular
327 // distance on the raw sector numbers.
328 std::unordered_map<int, std::vector<std::size_t>> nodesBySector;
329 nodesBySector.reserve(graph.nNodes);
330 for (std::size_t node = 0; node < graph.nNodes; ++node) {
331 nodesBySector[wrapRegularSector(bucket[node].sector)].push_back(node);
332 }
333
334 std::unordered_map<int, std::vector<int>> targetSectorsBySourceSector;
335 targetSectorsBySourceSector.reserve(nodesBySector.size());
336 std::size_t sectorLocalEdgeUpperBound = 0;
337 for (const auto& [sourceSector, sourceNodes] : nodesBySector) {
338 std::vector<int> targetSectors;
339 targetSectors.reserve(2 * m_maxDeltaSector.value() + 1);
340 for (int delta = -m_maxDeltaSector.value();
341 delta <= m_maxDeltaSector.value(); ++delta) {
342 targetSectors.push_back(wrapRegularSector(sourceSector + delta));
343 }
344 for (const int targetSector : targetSectors) {
345 const auto found = nodesBySector.find(targetSector);
346 if (found == nodesBySector.end()) continue;
347 sectorLocalEdgeUpperBound += sourceNodes.size() * found->second.size();
348 if (targetSector == sourceSector) {
349 sectorLocalEdgeUpperBound -= sourceNodes.size();
350 }
351 }
352 targetSectorsBySourceSector.emplace(sourceSector,
353 std::move(targetSectors));
354 }
355
356 /*
357 * The model receives a directed graph, but the geometric candidate relation
358 * is undirected. Build each pair once, then emit both directions. With a
359 * non-zero input cap, each endpoint nominates its best candidates and the
360 * union is made bidirectional before inference; this preserves the message
361 * passing symmetry expected by the GNN.
362 */
363 struct UndirectedEdge {
364 std::size_t first{0};
365 std::size_t second{0};
366 float dx{0.f};
367 float dy{0.f};
368 float dz{0.f};
369 float distance{0.f};
370 float cosAngle{0.f};
371 };
372 const auto betterEdge = [](const UndirectedEdge& first,
373 const UndirectedEdge& second) {
374 const int cosOrder = InferenceUtils::compareFloatDescending(
375 first.cosAngle, second.cosAngle);
376 if (cosOrder != 0) {
377 return cosOrder < 0;
378 }
379
380 const int distanceOrder =
381 InferenceUtils::compareFloat(first.distance, second.distance);
382 if (distanceOrder != 0) {
383 return distanceOrder < 0;
384 }
385 if (first.first != second.first) return first.first < second.first;
386 return first.second < second.second;
387 };
388 const auto edgeKey = [](const UndirectedEdge& edge) {
389 return (static_cast<std::uint64_t>(edge.first) << 32) |
390 static_cast<std::uint64_t>(edge.second);
391 };
392
393 const unsigned int maxEdgesPerNode =
395 const unsigned int maxEdgesPerTargetChamber =
397 const bool usePreInferenceSelection =
398 maxEdgesPerNode != 0 || maxEdgesPerTargetChamber != 0;
399 std::vector<std::vector<UndirectedEdge>> bestEdgesByNode;
400 if (usePreInferenceSelection) {
401 bestEdgesByNode.resize(graph.nNodes);
402 const unsigned int reservePerNode =
403 maxEdgesPerNode != 0 ? maxEdgesPerNode : maxEdgesPerTargetChamber;
404 for (std::vector<UndirectedEdge>& edges : bestEdgesByNode) {
405 edges.reserve(reservePerNode);
406 }
407 } else {
408 graph.edgeIndex.reserve(2 * sectorLocalEdgeUpperBound);
409 graph.edgeFeatures.reserve(kEdgeFeatureCount * sectorLocalEdgeUpperBound);
410 }
411 const auto appendDirectedPair = [&](const UndirectedEdge& edge) {
412 graph.edgeIndex.push_back(static_cast<int64_t>(edge.first));
413 graph.edgeIndex.push_back(static_cast<int64_t>(edge.second));
414 graph.edgeFeatures.insert(
415 graph.edgeFeatures.end(),
416 {edge.dx, edge.dy, edge.dz, edge.distance, edge.cosAngle,
417 float(bucket[edge.first].chamberIndex ==
418 bucket[edge.second].chamberIndex),
419 float(bucket[edge.first].sector == bucket[edge.second].sector)});
420
421 graph.edgeIndex.push_back(static_cast<int64_t>(edge.second));
422 graph.edgeIndex.push_back(static_cast<int64_t>(edge.first));
423 graph.edgeFeatures.insert(
424 graph.edgeFeatures.end(),
425 {-edge.dx, -edge.dy, -edge.dz, edge.distance, edge.cosAngle,
426 float(bucket[edge.first].chamberIndex ==
427 bucket[edge.second].chamberIndex),
428 float(bucket[edge.first].sector == bucket[edge.second].sector)});
429 };
430
431 const auto retainForNode = [&](std::size_t node,
432 const UndirectedEdge& candidate) {
433 std::vector<UndirectedEdge>& retained = bestEdgesByNode[node];
434 const std::size_t other = candidate.first == node ? candidate.second
435 : candidate.first;
436 const int targetChamber = bucket[other].chamberIndex;
437
438 if (maxEdgesPerTargetChamber != 0) {
439 unsigned int sameChamberCount = 0;
440 auto worstSameChamber = retained.end();
441 for (auto it = retained.begin(); it != retained.end(); ++it) {
442 const std::size_t retainedOther =
443 it->first == node ? it->second : it->first;
444 if (bucket[retainedOther].chamberIndex != targetChamber) continue;
445 ++sameChamberCount;
446 if (worstSameChamber == retained.end() ||
447 betterEdge(*worstSameChamber, *it)) {
448 worstSameChamber = it;
449 }
450 }
451 if (sameChamberCount >= maxEdgesPerTargetChamber) {
452 if (!betterEdge(candidate, *worstSameChamber)) return;
453 *worstSameChamber = candidate;
454 } else {
455 retained.push_back(candidate);
456 }
457 } else {
458 retained.push_back(candidate);
459 }
460
461 if (maxEdgesPerNode != 0 && retained.size() > maxEdgesPerNode) {
462 auto worst = retained.begin();
463 for (auto it = std::next(retained.begin()); it != retained.end(); ++it) {
464 if (betterEdge(*worst, *it)) worst = it;
465 }
466 retained.erase(worst);
467 }
468 };
469
470 std::size_t candidatePairs = 0;
471 for (std::size_t first = 0; first < graph.nNodes; ++first) {
472 const auto sectorsIt =
473 targetSectorsBySourceSector.find(wrapRegularSector(bucket[first].sector));
474 if (sectorsIt == targetSectorsBySourceSector.end()) continue;
475 for (const int sector : sectorsIt->second) {
476 const auto targetIt = nodesBySector.find(sector);
477 if (targetIt == nodesBySector.end()) continue;
478
479 for (const std::size_t second : targetIt->second) {
480 // Every valid pair will be visited from the lower-index endpoint.
481 if (second <= first) continue;
482 if (sectorDistance(bucket[first].sector, bucket[second].sector,
483 m_sectorModulo.value()) >
484 m_maxDeltaSector.value()) {
485 continue;
486 }
488 bucket[first].chamberIndex == bucket[second].chamberIndex) {
489 continue;
490 }
491 const float cosAngle = static_cast<float>(dir[first].dot(dir[second]));
492 if (cosAngle < m_cosMin) continue;
493
494 const Amg::Vector3D delta = pos[second] - pos[first];
495 const UndirectedEdge candidate{
496 first,
497 second,
498 static_cast<float>(delta.x()),
499 static_cast<float>(delta.y()),
500 static_cast<float>(delta.z()),
501 static_cast<float>(delta.mag()),
502 cosAngle};
503 ++candidatePairs;
504
505 if (!usePreInferenceSelection) {
506 appendDirectedPair(candidate);
507 } else {
508 retainForNode(first, candidate);
509 retainForNode(second, candidate);
510 }
511 }
512 }
513 }
514 std::size_t retainedPairs = candidatePairs;
515 if (usePreInferenceSelection) {
516 const unsigned int selectedReservePerNode =
517 maxEdgesPerNode != 0 ? maxEdgesPerNode : maxEdgesPerTargetChamber;
518 std::unordered_set<std::uint64_t> selectedKeys;
519 selectedKeys.reserve(graph.nNodes * selectedReservePerNode);
520 std::vector<UndirectedEdge> selectedEdges;
521 selectedEdges.reserve(graph.nNodes * selectedReservePerNode);
522
523 for (const std::vector<UndirectedEdge>& nodeEdges : bestEdgesByNode) {
524 for (const UndirectedEdge& edge : nodeEdges) {
525 if (selectedKeys.insert(edgeKey(edge)).second) {
526 selectedEdges.push_back(edge);
527 }
528 }
529 }
530 std::sort(selectedEdges.begin(), selectedEdges.end(),
531 [](const UndirectedEdge& first,
532 const UndirectedEdge& second) {
533 if (first.first != second.first) {
534 return first.first < second.first;
535 }
536 return first.second < second.second;
537 });
538
539 retainedPairs = selectedEdges.size();
540 graph.edgeIndex.reserve(4 * retainedPairs);
541 graph.edgeFeatures.reserve(2 * kEdgeFeatureCount * retainedPairs);
542 for (const UndirectedEdge& edge : selectedEdges) {
543 appendDirectedPair(edge);
544 }
545 }
546 graph.nEdges = graph.edgeIndex.size() / 2;
547 const std::size_t nodesBeforeIsolatedNodeDrop = graph.nNodes;
548 if (m_dropIsolatedNodesBeforeInference.value() && graph.nEdges != 0) {
549 std::vector<unsigned char> active(graph.nNodes, 0);
550 for (const int64_t index : graph.edgeIndex) {
551 active[static_cast<std::size_t>(index)] = 1;
552 }
553 const std::size_t activeNodes =
554 std::count(active.begin(), active.end(), static_cast<unsigned char>(1));
555 if (activeNodes != graph.nNodes) {
556 std::vector<std::size_t> oldToNew(graph.nNodes, graph.nNodes);
557 std::vector<const xAOD::MuonSegment*> compactedSegments;
558 std::vector<float> compactedNodeFeatures;
559 compactedSegments.reserve(activeNodes);
560 compactedNodeFeatures.reserve(activeNodes * kNodeFeatureCount);
561 for (std::size_t oldNode = 0; oldNode < graph.nNodes; ++oldNode) {
562 if (!active[oldNode]) continue;
563 oldToNew[oldNode] = compactedSegments.size();
564 compactedSegments.push_back(graph.segments[oldNode]);
565 const auto featureBegin = graph.nodeFeatures.begin() +
566 oldNode * kNodeFeatureCount;
567 compactedNodeFeatures.insert(compactedNodeFeatures.end(),
568 featureBegin,
569 featureBegin + kNodeFeatureCount);
570 }
571 for (int64_t& index : graph.edgeIndex) {
572 index = static_cast<int64_t>(oldToNew[static_cast<std::size_t>(index)]);
573 }
574 graph.segments = std::move(compactedSegments);
575 graph.nodeFeatures = std::move(compactedNodeFeatures);
576 graph.nNodes = activeNodes;
577 }
578 }
579 ATH_MSG_DEBUG("buildGraph: input segments=" << segments.size()
580 << ", kept nodes=" << graph.nNodes
581 << ", nodes before isolated-node drop=" << nodesBeforeIsolatedNodeDrop
582 << ", bucket cap=" << m_maxSegmentsPerBucket.value()
583 << ", candidate pairs=" << candidatePairs
584 << ", retained pairs=" << retainedPairs
585 << ", built directed edges=" << graph.nEdges
586 << ", pre-inference node cap=" << m_maxEdgesPerNodeBeforeInference.value()
587 << ", per-target-chamber cap=" << maxEdgesPerTargetChamber
588 << ", drop same chamber=" << m_dropSameChamberEdgesBeforeInference.value()
589 << ", drop isolated nodes=" << m_dropIsolatedNodesBeforeInference.value()
590 << ", sector-local reserve=" << sectorLocalEdgeUpperBound);
591 return StatusCode::SUCCESS;
592}
593
594StatusCode SegmentEdgeClassifierTool::classifyEdges(const EventContext& ctx,
595 const SegmentEdgeGraph& graph,
596 std::vector<SegmentEdgeScore>& scores) const {
597 scores.clear();
598 if (!graph.nNodes) return StatusCode::SUCCESS;
599 if (!graph.nEdges) {
600 ATH_CHECK(dumpDebugEvent(ctx, graph, scores));
601 return StatusCode::SUCCESS;
602 }
603
604 if (graph.nodeFeatures.size() != graph.nNodes * kNodeFeatureCount) {
605 ATH_MSG_ERROR("Unexpected node feature size " << graph.nodeFeatures.size()
606 << "; expected " << (graph.nNodes * kNodeFeatureCount));
607 return StatusCode::FAILURE;
608 }
609 if (graph.edgeIndex.size() != 2 * graph.nEdges) {
610 ATH_MSG_ERROR("Unexpected edge index size " << graph.edgeIndex.size()
611 << "; expected " << (2 * graph.nEdges));
612 return StatusCode::FAILURE;
613 }
614 if (graph.edgeFeatures.size() != graph.nEdges * kEdgeFeatureCount) {
615 ATH_MSG_ERROR("Unexpected edge feature size " << graph.edgeFeatures.size()
616 << "; expected " << (graph.nEdges * kEdgeFeatureCount));
617 return StatusCode::FAILURE;
618 }
619
620 GraphRawData raw{};
621 raw.graph = std::make_unique<InferenceGraph>();
622 raw.edgeIndexPacked.resize(2 * graph.nEdges);
623 for (std::size_t e = 0; e < graph.nEdges; ++e) {
624 raw.edgeIndexPacked[e] = graph.edgeIndex[2 * e];
625 raw.edgeIndexPacked[graph.nEdges + e] = graph.edgeIndex[2 * e + 1];
626 }
627
628 Ort::MemoryInfo memInfo = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
629
630 const std::vector<int64_t> nodeShape{static_cast<int64_t>(graph.nNodes), static_cast<int64_t>(kNodeFeatureCount)};
631 // The graph outlives the synchronous ONNX call below. Use its node
632 // buffer directly instead of allocating and copying featureLeaves per event.
633 ATLAS_THREAD_SAFE float* nodeFeaturesData =
634 const_cast<float*>(graph.nodeFeatures.data());
635 raw.graph->dataTensor.emplace_back(
636 Ort::Value::CreateTensor<float>(memInfo,
637 nodeFeaturesData,
638 graph.nodeFeatures.size(),
639 nodeShape.data(),
640 nodeShape.size()));
641
642 const std::vector<int64_t> edgeIndexShape{2, static_cast<int64_t>(graph.nEdges)};
643 raw.graph->dataTensor.emplace_back(
644 Ort::Value::CreateTensor<int64_t>(memInfo,
645 raw.edgeIndexPacked.data(),
646 raw.edgeIndexPacked.size(),
647 edgeIndexShape.data(),
648 edgeIndexShape.size()));
649
650 // ONNX Runtime's CreateTensor API takes a non-const pointer, but it does not
651 // mutate input buffers during inference. Avoid copying edge_attr every event.
652 ATLAS_THREAD_SAFE float* edgeFeaturesData = const_cast<float*>(graph.edgeFeatures.data());
653 const std::vector<int64_t> edgeAttrShape{static_cast<int64_t>(graph.nEdges), static_cast<int64_t>(kEdgeFeatureCount)};
654 raw.graph->dataTensor.emplace_back(
655 Ort::Value::CreateTensor<float>(memInfo,
656 edgeFeaturesData,
657 graph.edgeFeatures.size(),
658 edgeAttrShape.data(),
659 edgeAttrShape.size()));
660
661 const std::vector<const char*> inputNames{
662 m_inputNodeName.value().c_str(),
663 m_inputEdgeIndexName.value().c_str(),
664 m_inputEdgeAttrName.value().c_str()};
665 const std::vector<const char*> outputNames{m_outputName.value().c_str()};
666 ATH_MSG_DEBUG("classifyEdges: ONNX inputs shapes x=[" << nodeShape[0] << "," << nodeShape[1]
667 << "], edge_index=[" << edgeIndexShape[0] << "," << edgeIndexShape[1]
668 << "], edge_attr=[" << edgeAttrShape[0] << "," << edgeAttrShape[1] << "]");
669 ATH_CHECK(runNamedInference(raw, inputNames, outputNames));
670
671 if (raw.graph->dataTensor.size() <= inputNames.size()) {
672 ATH_MSG_ERROR("Missing ONNX output tensor for segment edge inference");
673 return StatusCode::FAILURE;
674 }
675
676 const Ort::Value& outTensor = raw.graph->dataTensor[inputNames.size()];
677 const auto outInfo = outTensor.GetTensorTypeAndShapeInfo();
678 const std::vector<int64_t> outShape = outInfo.GetShape();
679 const size_t outSize = outInfo.GetElementCount();
680 if (!outShape.empty()) {
681 ATH_MSG_DEBUG("classifyEdges: ONNX output rank=" << outShape.size()
682 << ", first dim=" << outShape.front()
683 << ", elements=" << outSize);
684 } else {
685 ATH_MSG_DEBUG("classifyEdges: ONNX scalar output, elements=" << outSize);
686 }
687 if (outSize < graph.nEdges) {
688 ATH_MSG_ERROR("ONNX logits tensor has " << outSize << " entries for " << graph.nEdges << " edges");
689 return StatusCode::FAILURE;
690 }
691
692 const float* logits = outTensor.GetTensorData<float>();
693 scores.reserve(graph.nEdges);
694 for (std::size_t e=0; e<graph.nEdges; ++e) {
695 const float l = logits[e];
696 scores.push_back({std::size_t(graph.edgeIndex[2 * e]),
697 std::size_t(graph.edgeIndex[2 * e + 1]),
698 l,
700 }
701
702 ATH_CHECK(dumpDebugEvent(ctx, graph, scores));
703 return StatusCode::SUCCESS;
704}
705
707 const EventContext& ctx,
708 const SegmentEdgeGraph& graph,
709 const std::vector<SegmentEdgeScore>& scores) const {
710 if (m_debugDumpFile.value().empty()) return StatusCode::SUCCESS;
711
712 std::lock_guard<std::mutex> lock{m_debugDumpMutex};
713 if (m_debugDumpMaxEvents.value() != 0 &&
714 m_debugDumpEvents.load(std::memory_order_relaxed) >=
715 m_debugDumpMaxEvents.value()) {
716 return StatusCode::SUCCESS;
717 }
718
719 if (graph.nodeFeatures.size() != graph.nNodes * kNodeFeatureCount ||
720 graph.edgeIndex.size() != graph.nEdges * 2 ||
721 graph.edgeFeatures.size() != graph.nEdges * kEdgeFeatureCount ||
722 scores.size() != graph.nEdges) {
723 ATH_MSG_ERROR("Cannot write segment-edge debug dump: inconsistent graph/output sizes"
724 << " nodes=" << graph.nNodes
725 << " nodeFeatures=" << graph.nodeFeatures.size()
726 << " edges=" << graph.nEdges
727 << " edgeIndex=" << graph.edgeIndex.size()
728 << " edgeFeatures=" << graph.edgeFeatures.size()
729 << " scores=" << scores.size());
730 return StatusCode::FAILURE;
731 }
732
733 nlohmann::json x = nlohmann::json::array();
734 x.get_ref<nlohmann::json::array_t&>().reserve(graph.nodeFeatures.size());
735 for (const float value : graph.nodeFeatures) {
736 x.push_back(std::isfinite(value) ? nlohmann::json(value)
737 : nlohmann::json(nullptr));
738 }
739
740 nlohmann::json edgeIndex = nlohmann::json::array();
741 edgeIndex.get_ref<nlohmann::json::array_t&>().reserve(graph.nEdges * 2);
742 // This is the actual ONNX [2,E] row-major buffer: all sources then all destinations.
743 for (std::size_t edge = 0; edge < graph.nEdges; ++edge) {
744 edgeIndex.push_back(graph.edgeIndex[2 * edge]);
745 }
746 for (std::size_t edge = 0; edge < graph.nEdges; ++edge) {
747 edgeIndex.push_back(graph.edgeIndex[2 * edge + 1]);
748 }
749
750 nlohmann::json edgeAttr = nlohmann::json::array();
751 edgeAttr.get_ref<nlohmann::json::array_t&>().reserve(graph.edgeFeatures.size());
752 for (const float value : graph.edgeFeatures) {
753 edgeAttr.push_back(std::isfinite(value) ? nlohmann::json(value)
754 : nlohmann::json(nullptr));
755 }
756
757 nlohmann::json logits = nlohmann::json::array();
758 nlohmann::json probabilities = nlohmann::json::array();
759 nlohmann::json edgeSrc = nlohmann::json::array();
760 nlohmann::json edgeDst = nlohmann::json::array();
761 logits.get_ref<nlohmann::json::array_t&>().reserve(scores.size());
762 probabilities.get_ref<nlohmann::json::array_t&>().reserve(scores.size());
763 edgeSrc.get_ref<nlohmann::json::array_t&>().reserve(scores.size());
764 edgeDst.get_ref<nlohmann::json::array_t&>().reserve(scores.size());
765 for (const SegmentEdgeScore& score : scores) {
766 edgeSrc.push_back(score.src);
767 edgeDst.push_back(score.dst);
768 logits.push_back(std::isfinite(score.logit) ? nlohmann::json(score.logit)
769 : nlohmann::json(nullptr));
770 probabilities.push_back(std::isfinite(score.probability)
771 ? nlohmann::json(score.probability)
772 : nlohmann::json(nullptr));
773 }
774
775 std::ofstream out{m_debugDumpFile.value(), std::ios::out | std::ios::app};
776 if (!out) {
777 ATH_MSG_ERROR("Could not append to segment-edge debug dump file: "
778 << m_debugDumpFile.value());
779 return StatusCode::FAILURE;
780 }
781
782 const unsigned int dumpIndex =
783 m_debugDumpEvents.fetch_add(1, std::memory_order_relaxed);
784 nlohmann::ordered_json event;
785 event["record_type"] = "event";
786 event["format_version"] = 1;
787 event["dump_index"] = dumpIndex;
788 event["run_number"] = ctx.eventID().run_number();
789 event["lumi_block"] = ctx.eventID().lumi_block();
790 event["event_number"] = ctx.eventID().event_number();
791 event["slot"] = ctx.slot();
792 event["n_nodes"] = graph.nNodes;
793 event["n_edges"] = graph.nEdges;
794 event["x_shape"] = {graph.nNodes, kNodeFeatureCount};
795 event["edge_index_shape"] = {2, graph.nEdges};
796 event["edge_attr_shape"] = {graph.nEdges, kEdgeFeatureCount};
797 event["logits_shape"] = {graph.nEdges};
798 event["x"] = std::move(x);
799 event["edge_index"] = std::move(edgeIndex);
800 event["edge_attr"] = std::move(edgeAttr);
801 event["edge_src"] = std::move(edgeSrc);
802 event["edge_dst"] = std::move(edgeDst);
803 event["logits"] = std::move(logits);
804 event["probabilities"] = std::move(probabilities);
805 out << event.dump() << '\n';
806
807 ATH_MSG_DEBUG("Wrote segment-edge debug event " << dumpIndex
808 << " to " << m_debugDumpFile.value());
809
810 return StatusCode::SUCCESS;
811}
812
813} // namespace MuonML
#define ATH_CHECK
Evaluate an expression and check for errors.
#define ATH_MSG_DEBUG(x,...)
#define ATH_MSG_ERROR(x,...)
#define ATH_MSG_WARNING(x,...)
#define ATH_MSG_INFO(x,...)
virtual void lock()=0
Interface to allow an object to lock itself when made const in SG.
static Double_t sp
static Double_t a
#define x
Define macros for attributes used to control the static checker.
#define ATLAS_THREAD_SAFE
size_type size() const noexcept
Returns the number of elements in the collection.
static constexpr std::array< std::string_view, kNodeFeatureCount > kDefaultNodeFeatureNames
static constexpr std::size_t kEdgeFeatureCount
static std::vector< std::string > parseFeatureNames(const std::string &raw)
StatusCode runNamedInference(GraphRawData &graphData, const std::vector< const char * > &inputNames, const std::vector< const char * > &outputNames) const
Generic named inference, for tools with different I/O conventions.
static constexpr std::size_t kNodeFeatureCount
Gaudi::Property< unsigned int > m_debugDumpMaxEvents
Gaudi::Property< unsigned int > m_maxEdgesPerNodeBeforeInference
Gaudi::Property< unsigned int > m_maxEdgesPerTargetChamberBeforeInference
Gaudi::Property< std::string > m_outputName
std::atomic< unsigned int > m_debugDumpEvents
Gaudi::Property< std::string > m_inputEdgeAttrName
StatusCode runGraphInference(const EventContext &ctx, GraphRawData &graphData) const override
Not supported by this tool; returns FAILURE.
Gaudi::Property< unsigned int > m_maxSegmentsPerBucket
StatusCode classifyEdges(const EventContext &ctx, const SegmentEdgeGraph &graph, std::vector< SegmentEdgeScore > &scores) const override
Run ONNX inference on graph and populate scores with logit and probability for each edge; called afte...
Gaudi::Property< std::string > m_debugDumpFile
StatusCode buildGraph(const EventContext &ctx, const xAOD::MuonSegmentContainer &segments, SegmentEdgeGraph &graph) const override
Build a GNN graph from segments, computing node and edge features and storing the graph structure in ...
std::vector< std::string > m_nodeFeatureNames
Node feature order expected by the model metadata (resolved at initialize).
Gaudi::Property< std::string > m_inputEdgeIndexName
Gaudi::Property< bool > m_dropSameChamberEdgesBeforeInference
Gaudi::Property< std::string > m_inputNodeName
std::vector< SegmentNodeFeatureId > m_nodeFeatureIds
StatusCode initialize() override
Retrieve the ONNX model and resolve node feature ordering from metadata.
StatusCode dumpDebugEvent(const EventContext &ctx, const SegmentEdgeGraph &graph, const std::vector< SegmentEdgeScore > &scores) const
: The muon space point bucket represents a collection of points that will bre processed together in t...
The SpacePointPerLayerSorter sort two given space points by their layer Identifier.
Definition node.h:24
::Muon::MuonStationIndex::ChIndex chamberIndex() const
Returns the chamber index.
int etaIndex() const
Returns the eta index, which corresponds to stationEta in the offline identifiers (and the ).
Eigen::Matrix< double, 3, 1 > Vector3D
str index
Definition DeMoScan.py:362
int compareFloat(float first, float second)
Three-way float comparison which orders NaN after all numeric values.
int compareFloatDescending(float first, float second)
Three-way descending comparison which also orders NaN last.
SegmentNodeFeatureId
Identifier for each node feature in segment-based GNNs.
Definition MuonMLEvent.h:28
const Segment * detailedSegment(const xAOD::MuonSegment &seg)
Helper function to navigate from the xAOD::MuonSegment to the MuonR4::Segment.
Definition index.py:1
-diff
void sort(typename DataModel_detail::iterator< DVL > beg, typename DataModel_detail::iterator< DVL > end)
Specialization of sort for DataVector/List.
MuonSegmentContainer_v1 MuonSegmentContainer
Definition of the current "MuonSegment container version".
MuonSegment_v1 MuonSegment
Reference the current persistent version:
Segment features derived from or stored in bucket metadata.
int chamberIndex
Muon chamber index of the segment.
int sector
Sector number (typically 0–15).
int layers
Total number of active layers in the segment.
int nSegments
Count of segments in the same chamber/sector/eta group.
Helper struct to ship the Graph from the space point buckets to ONNX.
Definition GraphData.h:25
EdgeCounterVec_t edgeIndexPacked
Packed edge index buffer (kept alive for ONNX tensors that reference it) This stores [srcEdges,...
Definition GraphData.h:42
std::unique_ptr< InferenceGraph > graph
Pointer to the graph to be parsed to ONNX.
Definition GraphData.h:46
Common quality ordering for segment representatives.
std::vector< float > edgeFeatures
packed [E,7]: dpos(3), dist, cos, same_chamber, same_sector
std::vector< int64_t > edgeIndex
packed edge pairs [src0,dst0,src1,dst1,...]
std::vector< const xAOD::MuonSegment_v1 * > segments
std::vector< float > nodeFeatures
packed [N,10]: pos_m(3), dir_u(3), bucket(4)