ATLAS Offline Software
Loading...
Searching...
No Matches
SegmentEdgeClassifierTool.cxx
Go to the documentation of this file.
2#include "InferenceUtils.h"
11#include "Acts/Utilities/Helpers.hpp"
13#include "GaudiKernel/SystemOfUnits.h"
14#include <nlohmann/json.hpp>
15#include <algorithm>
16#include <array>
17#include <cmath>
18#include <cstdint>
19#include <fstream>
20#include <mutex>
21#include <map>
22#include <optional>
23#include <sstream>
24#include <tuple>
25#include <unordered_map>
26#include <unordered_set>
27
28namespace {
30using SegmentBucketKey =
31 std::tuple<int, int, int>; // sector, chamberIndex, etaIndex
32
33SegmentBucketKey segmentBucketKey(const xAOD::MuonSegment& seg) {
34 return {seg.sector(), static_cast<int>(seg.chamberIndex()), seg.etaIndex()};
35}
36
40int layersInBucket(const MuonR4::SpacePointBucket& bucket) {
42 std::vector<unsigned int> uniqueLayers;
43 uniqueLayers.reserve(bucket.size());
44 for (const MuonR4::SpacePointBucket::value_type& sp : bucket) {
45 const unsigned int layNum = sorter.sectorLayerNum(*sp);
46 if (!Acts::rangeContainsValue(uniqueLayers, layNum)) {
47 uniqueLayers.push_back(layNum);
48 }
49 }
50 return static_cast<int>(uniqueLayers.size());
51}
52
58inline int sectorDistance(int a, int b, int mod) {
59 int d = std::abs(a - b);
60 return mod > 0 ? std::min(d, mod - d) : d;
61}
62
63std::optional<MuonML::SegmentNodeFeatureId> nodeFeatureIdFromName(const std::string& name) {
64 using FeatureId = MuonML::SegmentNodeFeatureId;
65 if (name == "segmentPositionX_m") return FeatureId::SegmentPositionX;
66 if (name == "segmentPositionY_m") return FeatureId::SegmentPositionY;
67 if (name == "segmentPositionZ_m") return FeatureId::SegmentPositionZ;
68 if (name == "segmentDirectionX") return FeatureId::SegmentDirectionX;
69 if (name == "segmentDirectionY") return FeatureId::SegmentDirectionY;
70 if (name == "segmentDirectionZ") return FeatureId::SegmentDirectionZ;
71 if (name == "bucket_chamberIndex") return FeatureId::BucketChamberIndex;
72 if (name == "bucket_layers") return FeatureId::BucketLayers;
73 if (name == "bucket_sector") return FeatureId::BucketSector;
74 if (name == "bucket_segments") return FeatureId::BucketSegments;
75 return std::nullopt;
76}
77
78float nodeFeatureValue(MuonML::SegmentNodeFeatureId feature,
79 const Amg::Vector3D& pos,
80 const Amg::Vector3D& dir,
81 const MuonML::BucketSegmentFeatures& bucket) {
82 using FeatureId = MuonML::SegmentNodeFeatureId;
83 switch (feature) {
84 case FeatureId::SegmentPositionX: return static_cast<float>(pos.x());
85 case FeatureId::SegmentPositionY: return static_cast<float>(pos.y());
86 case FeatureId::SegmentPositionZ: return static_cast<float>(pos.z());
87 case FeatureId::SegmentDirectionX: return static_cast<float>(dir.x());
88 case FeatureId::SegmentDirectionY: return static_cast<float>(dir.y());
89 case FeatureId::SegmentDirectionZ: return static_cast<float>(dir.z());
90 case FeatureId::BucketChamberIndex: return static_cast<float>(bucket.chamberIndex);
91 case FeatureId::BucketLayers: return static_cast<float>(bucket.layers);
92 case FeatureId::BucketSector: return static_cast<float>(bucket.sector);
93 case FeatureId::BucketSegments: return static_cast<float>(bucket.nSegments);
94 }
95 return 0.f;
96}
97}
98
99namespace MuonML {
100
102 if (m_sectorModulo.value() > 0 &&
103 2ULL * static_cast<unsigned long long>(m_maxDeltaSector.value()) + 1ULL >
104 static_cast<unsigned long long>(m_sectorModulo.value())) {
105 ATH_MSG_ERROR("MaxDeltaSector=" << m_maxDeltaSector.value()
106 << " spans duplicate sectors for SectorModulo="
107 << m_sectorModulo.value());
108 return StatusCode::FAILURE;
109 }
111
112 // Resolve node feature names from model metadata, matching the ONNX exporter.
113 {
114 Ort::AllocatorWithDefaultOptions allocator;
115 Ort::ModelMetadata meta = model().GetModelMetadata();
116 auto keys = meta.GetCustomMetadataMapKeysAllocated(allocator);
117 std::vector<std::string> keyList;
118 keyList.reserve(keys.size());
119 for (const auto& k : keys) keyList.emplace_back(k.get());
120
121 constexpr std::array<std::string_view, 4> candidates{
122 "x_feature_names", "node_feature_names", "feature_names", "input_feature_names"};
123 std::string usedKey;
124 std::vector<std::string> names;
125 for (std::string_view key : candidates) {
126 const std::string keyStr{key};
127 if (std::find(keyList.begin(), keyList.end(), keyStr) == keyList.end()) continue;
128 names = parseFeatureNames(meta.LookupCustomMetadataMapAllocated(keyStr.c_str(), allocator).get());
129 if (!names.empty()) {
130 usedKey = keyStr;
131 break;
132 }
133 }
134
135 if (names.empty()) {
137 ATH_MSG_WARNING("Model metadata has no usable node feature name key"
138 " (tried x_feature_names/node_feature_names/feature_names/input_feature_names)."
139 " Falling back to default training order.");
140 } else {
141 if (names.size() != kNodeFeatureCount) {
142 ATH_MSG_ERROR("Model metadata key '" << usedKey << "' has " << names.size()
143 << " features, expected " << kNodeFeatureCount);
144 return StatusCode::FAILURE;
145 }
146 for (const std::string& n : names) {
147 if (!nodeFeatureIdFromName(n).has_value()) {
148 ATH_MSG_ERROR("Unsupported node feature name in model metadata ('" << usedKey
149 << "'): '" << n << "'."
150 " Add mapping in SegmentEdgeClassifierTool::nodeFeatureValue().");
151 return StatusCode::FAILURE;
152 }
153 }
154 m_nodeFeatureNames = std::move(names);
155 ATH_MSG_DEBUG("Using node feature names from model metadata key '" << usedKey << "'.");
156 }
157
158 m_nodeFeatureIds.reserve(m_nodeFeatureNames.size());
159 for (const std::string& n : m_nodeFeatureNames) {
160 const auto id = nodeFeatureIdFromName(n);
161 if (!id.has_value()) {
162 ATH_MSG_ERROR("Internal feature-id resolution failed for node feature name '" << n << "'.");
163 return StatusCode::FAILURE;
164 }
165 m_nodeFeatureIds.push_back(*id);
166 }
167
168 std::ostringstream order;
169 order << "Node feature order:";
170 for (std::size_t i = 0; i < m_nodeFeatureNames.size(); ++i) {
171 order << " f" << i << "=" << m_nodeFeatureNames[i];
172 if (i + 1 < m_nodeFeatureNames.size()) order << ",";
173 }
174 ATH_MSG_DEBUG(order.str());
175 }
176
178 ATH_MSG_ERROR("Internal node feature setup has " << m_nodeFeatureNames.size()
179 << " entries, expected " << kNodeFeatureCount);
180 return StatusCode::FAILURE;
181 }
182 if (m_nodeFeatureIds.size() != kNodeFeatureCount) {
183 ATH_MSG_ERROR("Internal node feature id setup has " << m_nodeFeatureIds.size()
184 << " entries, expected " << kNodeFeatureCount);
185 return StatusCode::FAILURE;
186 }
187
188 m_cosMin = std::cos(m_maxDeltaThetaDeg.value() * Gaudi::Units::deg);
189
190 if (!m_debugDumpFile.value().empty()) {
191 std::ofstream out{m_debugDumpFile.value(), std::ios::out | std::ios::trunc};
192 if (!out) {
193 ATH_MSG_ERROR("Could not create segment-edge debug dump file: "
194 << m_debugDumpFile.value());
195 return StatusCode::FAILURE;
196 }
197
198 nlohmann::ordered_json metadata;
199 metadata["record_type"] = "metadata";
200 metadata["format_version"] = 1;
201 metadata["tool"] = "SegmentEdgeClassifierTool";
202 metadata["input_names"] = {m_inputNodeName.value(),
203 m_inputEdgeIndexName.value(),
204 m_inputEdgeAttrName.value()};
205 metadata["output_name"] = m_outputName.value();
206 metadata["x_feature_names"] = m_nodeFeatureNames;
207 metadata["edge_attr_feature_names"] = {
208 "deltaPositionX_m", "deltaPositionY_m", "deltaPositionZ_m",
209 "distance_m", "cos_opening_angle", "same_chamber", "same_sector"};
210 metadata["edge_index_layout"] = "row_major_2_by_E";
211 metadata["edge_order"] = "directed src_to_dst; row 0 then row 1";
212 metadata["max_delta_theta_deg"] = m_maxDeltaThetaDeg.value();
213 metadata["max_delta_sector"] = m_maxDeltaSector.value();
214 metadata["sector_modulo"] = m_sectorModulo.value();
215 metadata["debug_dump_max_events"] = m_debugDumpMaxEvents.value();
216 out << metadata.dump() << '\n';
217
218 ATH_MSG_INFO("Writing segment-edge ONNX debug dump to "
219 << m_debugDumpFile.value()
220 << " (DebugDumpMaxEvents="
221 << m_debugDumpMaxEvents.value() << ")");
222 }
223
225
226 return StatusCode::SUCCESS;
227}
228
229StatusCode SegmentEdgeClassifierTool::runGraphInference(const EventContext&, GraphRawData&) const {
230 ATH_MSG_ERROR("runGraphInference is not supported by SegmentEdgeClassifierTool. Use SegmentEdgeInferenceAlg + ISegmentEdgeClassifierTool methods.");
231 return StatusCode::FAILURE;
232}
233
235 const EventContext&, const xAOD::MuonSegmentContainer& segments,
236 SegmentEdgeGraph& graph) const {
237 graph = SegmentEdgeGraph{};
238 graph.segments.reserve(segments.size());
239 graph.nodeFeatures.reserve(segments.size() * kNodeFeatureCount);
240 // Evaluated once per event; short-circuits without touching the message
241 // service unless the property was explicitly enabled.
242 const bool truthDiag = m_enableTruthDiagnostics.value() && msgLvl(MSG::DEBUG);
243
244 /*
245 * Keep the original bucket multiplicity in the node feature even when the
246 * speed configuration retains only the best representatives of a bucket.
247 * This preserves the model's occupancy input while removing duplicate node
248 * and edge work before tensor construction.
249 */
250 std::map<SegmentBucketKey, std::vector<const xAOD::MuonSegment*>>
251 segmentsByBucket;
252 for (const xAOD::MuonSegment* segment : segments) {
253 segmentsByBucket[segmentBucketKey(*segment)].push_back(segment);
254 }
255
256 const InferenceUtils::SegmentQualityOrder betterSegment{};
257
258 std::unordered_set<const xAOD::MuonSegment*> retainedSegments;
259 retainedSegments.reserve(segments.size());
260 for (auto& [_, bucketSegments] : segmentsByBucket) {
261 std::ranges::sort(bucketSegments, betterSegment);
262 const std::size_t nKeep = m_maxSegmentsPerBucket.value() == 0
263 ? bucketSegments.size()
264 : std::min<std::size_t>(
265 bucketSegments.size(),
266 m_maxSegmentsPerBucket.value());
267 retainedSegments.insert(bucketSegments.begin(),
268 bucketSegments.begin() + nKeep);
269 }
270
271 std::vector<Amg::Vector3D> pos;
272 std::vector<Amg::Vector3D> dir;
273 std::vector<BucketSegmentFeatures> bucket;
274 pos.reserve(retainedSegments.size());
275 dir.reserve(retainedSegments.size());
276 bucket.reserve(retainedSegments.size());
277
278 for (const xAOD::MuonSegment* segment : segments) {
279 if (!retainedSegments.contains(segment)) continue;
280
281 const Amg::Vector3D position = segment->position();
282 const Amg::Vector3D direction = segment->direction();
283 const SegmentBucketKey key = segmentBucketKey(*segment);
284 const auto bucketIt = segmentsByBucket.find(key);
285 const int multiplicity =
286 bucketIt == segmentsByBucket.end()
287 ? 1
288 : static_cast<int>(bucketIt->second.size());
289
290 const int chamberIndex = static_cast<int>(segment->chamberIndex());
291 const int layers = layersInBucket(*MuonR4::detailedSegment(*segment)->parent()->parentBucket());
292 const int sector = segment->sector();
293
294 graph.segments.push_back(segment);
295 pos.emplace_back(position / Gaudi::Units::m);
296 dir.emplace_back(direction);
297 bucket.emplace_back(BucketSegmentFeatures{
298 chamberIndex, layers, sector, multiplicity});
299 for (const SegmentNodeFeatureId featureId : m_nodeFeatureIds) {
300 graph.nodeFeatures.push_back(
301 nodeFeatureValue(featureId, pos.back(), dir.back(), bucket.back()));
302 }
303 }
304 graph.nNodes = graph.segments.size();
305
306 if (pos.size() != graph.nNodes || dir.size() != graph.nNodes ||
307 bucket.size() != graph.nNodes) {
308 ATH_MSG_ERROR("Inconsistent vector sizes during graph building: nodes="
309 << graph.nNodes << ", pos=" << pos.size()
310 << ", dir=" << dir.size() << ", bucket=" << bucket.size());
311 return StatusCode::FAILURE;
312 }
313
314 if (graph.nNodes < 2) {
315 graph.nEdges = 0;
316 if (truthDiag) fillTruthDiagnostics(segments, retainedSegments, graph);
317 return StatusCode::SUCCESS;
318 }
319
320 const auto wrapRegularSector = [&](int sector) {
321 // MuonSegment::sector() is a regular sector number, not an ExpandedSector
322 // coordinate. Wrap it to [0, modulo); <= 0 disables wrapping.
323 if (m_sectorModulo.value() > 0) {
324 sector %= m_sectorModulo.value();
325 if (sector < 0) sector += m_sectorModulo.value();
326 }
327 return sector;
328 };
329
330 // The lookup key must use the same wrapping as the target sectors below:
331 // ATLAS sectors are 1-based (1..16), so a raw key of 16 can never match a
332 // wrapped target of 0, which silently dropped every edge into sector 16.
333 // The per-pair sectorDistance check below enforces the true circular
334 // distance on the raw sector numbers.
335 std::unordered_map<int, std::vector<std::size_t>> nodesBySector;
336 nodesBySector.reserve(graph.nNodes);
337 for (std::size_t node = 0; node < graph.nNodes; ++node) {
338 nodesBySector[wrapRegularSector(bucket[node].sector)].push_back(node);
339 }
340
341 std::unordered_map<int, std::vector<int>> targetSectorsBySourceSector;
342 targetSectorsBySourceSector.reserve(nodesBySector.size());
343 std::size_t sectorLocalEdgeUpperBound = 0;
344 for (const auto& [sourceSector, sourceNodes] : nodesBySector) {
345 std::vector<int> targetSectors;
346 targetSectors.reserve(2 * m_maxDeltaSector.value() + 1);
347 for (int delta = -m_maxDeltaSector.value();
348 delta <= m_maxDeltaSector.value(); ++delta) {
349 targetSectors.push_back(wrapRegularSector(sourceSector + delta));
350 }
351 for (const int targetSector : targetSectors) {
352 const auto found = nodesBySector.find(targetSector);
353 if (found == nodesBySector.end()) continue;
354 sectorLocalEdgeUpperBound += sourceNodes.size() * found->second.size();
355 if (targetSector == sourceSector) {
356 sectorLocalEdgeUpperBound -= sourceNodes.size();
357 }
358 }
359 targetSectorsBySourceSector.emplace(sourceSector,
360 std::move(targetSectors));
361 }
362
363 /*
364 * The model receives a directed graph, but the geometric candidate relation
365 * is undirected. Build each pair once, then emit both directions. With a
366 * non-zero input cap, each endpoint nominates its best candidates and the
367 * union is made bidirectional before inference; this preserves the message
368 * passing symmetry expected by the GNN.
369 */
370 struct UndirectedEdge {
371 std::size_t first{0};
372 std::size_t second{0};
373 float dx{0.f};
374 float dy{0.f};
375 float dz{0.f};
376 float distance{0.f};
377 float cosAngle{0.f};
378 };
379 const auto betterEdge = [](const UndirectedEdge& first,
380 const UndirectedEdge& second) {
381 const int cosOrder = InferenceUtils::compareFloatDescending(
382 first.cosAngle, second.cosAngle);
383 if (cosOrder != 0) {
384 return cosOrder < 0;
385 }
386
387 const int distanceOrder =
388 InferenceUtils::compareFloat(first.distance, second.distance);
389 if (distanceOrder != 0) {
390 return distanceOrder < 0;
391 }
392 if (first.first != second.first) return first.first < second.first;
393 return first.second < second.second;
394 };
395 const auto edgeKey = [](const UndirectedEdge& edge) {
396 return (static_cast<std::uint64_t>(edge.first) << 32) |
397 static_cast<std::uint64_t>(edge.second);
398 };
399
400 const unsigned int maxEdgesPerNode =
402 const unsigned int maxEdgesPerTargetChamber =
404 const bool usePreInferenceSelection =
405 maxEdgesPerNode != 0 || maxEdgesPerTargetChamber != 0;
406 std::vector<std::vector<UndirectedEdge>> bestEdgesByNode;
407 if (usePreInferenceSelection) {
408 bestEdgesByNode.resize(graph.nNodes);
409 const unsigned int reservePerNode =
410 maxEdgesPerNode != 0 ? maxEdgesPerNode : maxEdgesPerTargetChamber;
411 for (std::vector<UndirectedEdge>& edges : bestEdgesByNode) {
412 edges.reserve(reservePerNode);
413 }
414 } else {
415 graph.edgeIndex.reserve(2 * sectorLocalEdgeUpperBound);
416 graph.edgeFeatures.reserve(kEdgeFeatureCount * sectorLocalEdgeUpperBound);
417 }
418 const auto appendDirectedPair = [&](const UndirectedEdge& edge) {
419 graph.edgeIndex.push_back(static_cast<int64_t>(edge.first));
420 graph.edgeIndex.push_back(static_cast<int64_t>(edge.second));
421 graph.edgeFeatures.insert(
422 graph.edgeFeatures.end(),
423 {edge.dx, edge.dy, edge.dz, edge.distance, edge.cosAngle,
424 float(bucket[edge.first].chamberIndex ==
425 bucket[edge.second].chamberIndex),
426 float(bucket[edge.first].sector == bucket[edge.second].sector)});
427
428 graph.edgeIndex.push_back(static_cast<int64_t>(edge.second));
429 graph.edgeIndex.push_back(static_cast<int64_t>(edge.first));
430 graph.edgeFeatures.insert(
431 graph.edgeFeatures.end(),
432 {-edge.dx, -edge.dy, -edge.dz, edge.distance, edge.cosAngle,
433 float(bucket[edge.first].chamberIndex ==
434 bucket[edge.second].chamberIndex),
435 float(bucket[edge.first].sector == bucket[edge.second].sector)});
436 };
437
438 const auto retainForNode = [&](std::size_t node,
439 const UndirectedEdge& candidate) {
440 std::vector<UndirectedEdge>& retained = bestEdgesByNode[node];
441 const std::size_t other = candidate.first == node ? candidate.second
442 : candidate.first;
443 const int targetChamber = bucket[other].chamberIndex;
444
445 if (maxEdgesPerTargetChamber != 0) {
446 unsigned int sameChamberCount = 0;
447 auto worstSameChamber = retained.end();
448 for (auto it = retained.begin(); it != retained.end(); ++it) {
449 const std::size_t retainedOther =
450 it->first == node ? it->second : it->first;
451 if (bucket[retainedOther].chamberIndex != targetChamber) continue;
452 ++sameChamberCount;
453 if (worstSameChamber == retained.end() ||
454 betterEdge(*worstSameChamber, *it)) {
455 worstSameChamber = it;
456 }
457 }
458 if (sameChamberCount >= maxEdgesPerTargetChamber) {
459 if (!betterEdge(candidate, *worstSameChamber)) return;
460 *worstSameChamber = candidate;
461 } else {
462 retained.push_back(candidate);
463 }
464 } else {
465 retained.push_back(candidate);
466 }
467
468 if (maxEdgesPerNode != 0 && retained.size() > maxEdgesPerNode) {
469 auto worst = retained.begin();
470 for (auto it = std::next(retained.begin()); it != retained.end(); ++it) {
471 if (betterEdge(*worst, *it)) worst = it;
472 }
473 retained.erase(worst);
474 }
475 };
476
477 std::size_t candidatePairs = 0;
478 for (std::size_t first = 0; first < graph.nNodes; ++first) {
479 const auto sectorsIt =
480 targetSectorsBySourceSector.find(wrapRegularSector(bucket[first].sector));
481 if (sectorsIt == targetSectorsBySourceSector.end()) continue;
482 for (const int sector : sectorsIt->second) {
483 const auto targetIt = nodesBySector.find(sector);
484 if (targetIt == nodesBySector.end()) continue;
485
486 for (const std::size_t second : targetIt->second) {
487 // Every valid pair will be visited from the lower-index endpoint.
488 if (second <= first) continue;
489 if (sectorDistance(bucket[first].sector, bucket[second].sector,
490 m_sectorModulo.value()) >
491 m_maxDeltaSector.value()) {
492 continue;
493 }
495 bucket[first].chamberIndex == bucket[second].chamberIndex) {
496 continue;
497 }
498 const float cosAngle = static_cast<float>(dir[first].dot(dir[second]));
499 if (cosAngle < m_cosMin) continue;
500
501 const Amg::Vector3D delta = pos[second] - pos[first];
502 const UndirectedEdge candidate{
503 first,
504 second,
505 static_cast<float>(delta.x()),
506 static_cast<float>(delta.y()),
507 static_cast<float>(delta.z()),
508 static_cast<float>(delta.mag()),
509 cosAngle};
510 ++candidatePairs;
511
512 if (!usePreInferenceSelection) {
513 appendDirectedPair(candidate);
514 } else {
515 retainForNode(first, candidate);
516 retainForNode(second, candidate);
517 }
518 }
519 }
520 }
521 std::size_t retainedPairs = candidatePairs;
522 if (usePreInferenceSelection) {
523 const unsigned int selectedReservePerNode =
524 maxEdgesPerNode != 0 ? maxEdgesPerNode : maxEdgesPerTargetChamber;
525 std::unordered_set<std::uint64_t> selectedKeys;
526 selectedKeys.reserve(graph.nNodes * selectedReservePerNode);
527 std::vector<UndirectedEdge> selectedEdges;
528 selectedEdges.reserve(graph.nNodes * selectedReservePerNode);
529
530 for (const std::vector<UndirectedEdge>& nodeEdges : bestEdgesByNode) {
531 for (const UndirectedEdge& edge : nodeEdges) {
532 if (selectedKeys.insert(edgeKey(edge)).second) {
533 selectedEdges.push_back(edge);
534 }
535 }
536 }
537 std::sort(selectedEdges.begin(), selectedEdges.end(),
538 [](const UndirectedEdge& first,
539 const UndirectedEdge& second) {
540 if (first.first != second.first) {
541 return first.first < second.first;
542 }
543 return first.second < second.second;
544 });
545
546 retainedPairs = selectedEdges.size();
547 graph.edgeIndex.reserve(4 * retainedPairs);
548 graph.edgeFeatures.reserve(2 * kEdgeFeatureCount * retainedPairs);
549 for (const UndirectedEdge& edge : selectedEdges) {
550 appendDirectedPair(edge);
551 }
552 }
553 graph.nEdges = graph.edgeIndex.size() / 2;
554 const std::size_t nodesBeforeIsolatedNodeDrop = graph.nNodes;
555 if (m_dropIsolatedNodesBeforeInference.value() && graph.nEdges != 0) {
556 std::vector<unsigned char> active(graph.nNodes, 0);
557 for (const int64_t index : graph.edgeIndex) {
558 active[static_cast<std::size_t>(index)] = 1;
559 }
560 const std::size_t activeNodes =
561 std::count(active.begin(), active.end(), static_cast<unsigned char>(1));
562 if (activeNodes != graph.nNodes) {
563 std::vector<std::size_t> oldToNew(graph.nNodes, graph.nNodes);
564 std::vector<const xAOD::MuonSegment*> compactedSegments;
565 std::vector<float> compactedNodeFeatures;
566 compactedSegments.reserve(activeNodes);
567 compactedNodeFeatures.reserve(activeNodes * kNodeFeatureCount);
568 for (std::size_t oldNode = 0; oldNode < graph.nNodes; ++oldNode) {
569 if (!active[oldNode]) continue;
570 oldToNew[oldNode] = compactedSegments.size();
571 compactedSegments.push_back(graph.segments[oldNode]);
572 const auto featureBegin = graph.nodeFeatures.begin() +
573 oldNode * kNodeFeatureCount;
574 compactedNodeFeatures.insert(compactedNodeFeatures.end(),
575 featureBegin,
576 featureBegin + kNodeFeatureCount);
577 }
578 for (int64_t& index : graph.edgeIndex) {
579 index = static_cast<int64_t>(oldToNew[static_cast<std::size_t>(index)]);
580 }
581 graph.segments = std::move(compactedSegments);
582 graph.nodeFeatures = std::move(compactedNodeFeatures);
583 graph.nNodes = activeNodes;
584 }
585 }
586 if (truthDiag) fillTruthDiagnostics(segments, retainedSegments, graph);
587 ATH_MSG_DEBUG("buildGraph: input segments=" << segments.size()
588 << ", kept nodes=" << graph.nNodes
589 << ", nodes before isolated-node drop=" << nodesBeforeIsolatedNodeDrop
590 << ", bucket cap=" << m_maxSegmentsPerBucket.value()
591 << ", candidate pairs=" << candidatePairs
592 << ", retained pairs=" << retainedPairs
593 << ", built directed edges=" << graph.nEdges
594 << ", pre-inference node cap=" << m_maxEdgesPerNodeBeforeInference.value()
595 << ", per-target-chamber cap=" << maxEdgesPerTargetChamber
596 << ", drop same chamber=" << m_dropSameChamberEdgesBeforeInference.value()
597 << ", drop isolated nodes=" << m_dropIsolatedNodesBeforeInference.value()
598 << ", sector-local reserve=" << sectorLocalEdgeUpperBound);
599
600 // Job-summed diagnostics
601 if (msgLvl(MSG::DEBUG)) {
602 m_sumInputSegments += segments.size();
603 m_sumCandidatePairs += candidatePairs;
604 m_sumRetainedPairs += retainedPairs;
605 m_sumNodesBeforeIsolatedDrop += nodesBeforeIsolatedNodeDrop;
606 m_sumNodesAfterIsolatedDrop += graph.nNodes;
607 }
608 return StatusCode::SUCCESS;
609}
610
612 const xAOD::MuonSegmentContainer& segments,
613 const std::unordered_set<const xAOD::MuonSegment*>& bucketRetained,
614 SegmentEdgeGraph& graph) const {
615 std::unordered_map<const xAOD::MuonSegment*, std::int32_t> nodeOf;
616 nodeOf.reserve(graph.segments.size());
617 for (std::size_t node = 0; node < graph.segments.size(); ++node) {
618 nodeOf.emplace(graph.segments[node], static_cast<std::int32_t>(node));
619 }
620
621 graph.inputNodeIndex.assign(segments.size(), kDroppedAsIsolated);
622 std::unordered_map<std::int32_t, std::vector<std::uint32_t>> byTruth;
623 std::uint32_t inputIndex = 0;
624 for (const xAOD::MuonSegment* segment : segments) {
625 const auto found = nodeOf.find(segment);
626 if (found != nodeOf.end()) {
627 graph.inputNodeIndex[inputIndex] = found->second;
628 } else if (!bucketRetained.contains(segment)) {
629 graph.inputNodeIndex[inputIndex] = kDroppedByBucketCap;
630 }
631 if (const xAOD::TruthParticle* truthPart =
633 byTruth[static_cast<std::int32_t>(truthPart->index())].push_back(inputIndex);
634 }
635 ++inputIndex;
636 }
637
638 const auto pairKey = [](std::uint64_t first, std::uint64_t second) {
639 return first < second ? (first << 32) | second : (second << 32) | first;
640 };
641 std::unordered_set<std::uint64_t> scoredPairs;
642 scoredPairs.reserve(graph.nEdges);
643 for (std::size_t edge = 0; edge < graph.nEdges; ++edge) {
644 scoredPairs.insert(
645 pairKey(static_cast<std::uint64_t>(graph.edgeIndex[2 * edge]),
646 static_cast<std::uint64_t>(graph.edgeIndex[2 * edge + 1])));
647 }
648
649 // Same order as the pair loop in buildGraph(): bucket cap (node level),
650 // sector window, same-chamber drop, angle window; a pair that clears all of
651 // them but was not scored can only have been evicted by the edge caps.
652 for (const auto& entry : byTruth) {
653 const std::vector<std::uint32_t>& members = entry.second;
654 for (std::size_t x = 0; x < members.size(); ++x) {
655 for (std::size_t y = x + 1; y < members.size(); ++y) {
656 const std::uint32_t i = members[x];
657 const std::uint32_t j = members[y];
658 const xAOD::MuonSegment* first = segments[i];
659 const xAOD::MuonSegment* second = segments[j];
660 PairFate fate{i, j, PairGate::Scored, 0};
661 const int sectorDelta = sectorDistance(first->sector(), second->sector(),
662 m_sectorModulo.value());
663 fate.sectorDelta = static_cast<std::uint8_t>(std::min(sectorDelta, 255));
664 if (!bucketRetained.contains(first) || !bucketRetained.contains(second)) {
666 } else if (sectorDelta > m_maxDeltaSector.value()) {
668 } else if (m_dropSameChamberEdgesBeforeInference.value() &&
669 first->chamberIndex() == second->chamberIndex()) {
671 } else if (static_cast<float>(first->direction().dot(second->direction())) <
672 m_cosMin) {
674 } else {
675 const std::int32_t firstNode = graph.inputNodeIndex[i];
676 const std::int32_t secondNode = graph.inputNodeIndex[j];
677 const bool scored =
678 firstNode >= 0 && secondNode >= 0 &&
679 scoredPairs.contains(pairKey(static_cast<std::uint64_t>(firstNode),
680 static_cast<std::uint64_t>(secondNode)));
681 if (!scored) fate.gate = PairGate::EdgeCaps;
682 }
683 graph.truthPairFates.push_back(fate);
684 }
685 }
686 }
687}
688
691 "SegmentEdgeClassifierTool pre-ONNX pruning summary (job-summed, "
692 "independent of PairGateThreshold): "
693 << "inputSegments=" << m_sumInputSegments
694 << ", candidatePairs=" << m_sumCandidatePairs
695 << " (geometric pairs before MaxEdgesPerNodeBeforeInference/"
696 "MaxEdgesPerTargetChamberBeforeInference caps)"
697 << ", retainedPairs=" << m_sumRetainedPairs
698 << " (pairs actually sent to ONNX; MaxEdgesPerNodeBeforeInference="
700 << ", MaxEdgesPerTargetChamberBeforeInference="
702 << ", nodesBeforeIsolatedDrop=" << m_sumNodesBeforeIsolatedDrop
703 << ", nodesAfterIsolatedDrop=" << m_sumNodesAfterIsolatedDrop
704 << " (DropIsolatedNodesBeforeInference="
706 << "; nodes dropped here never reached ONNX or the pair-gate threshold)");
707 return StatusCode::SUCCESS;
708}
709
710StatusCode SegmentEdgeClassifierTool::classifyEdges(const EventContext& ctx,
711 const SegmentEdgeGraph& graph,
712 std::vector<SegmentEdgeScore>& scores) const {
713 scores.clear();
714 if (!graph.nNodes) return StatusCode::SUCCESS;
715 if (!graph.nEdges) {
716 ATH_CHECK(dumpDebugEvent(ctx, graph, scores));
717 return StatusCode::SUCCESS;
718 }
719
720 if (graph.nodeFeatures.size() != graph.nNodes * kNodeFeatureCount) {
721 ATH_MSG_ERROR("Unexpected node feature size " << graph.nodeFeatures.size()
722 << "; expected " << (graph.nNodes * kNodeFeatureCount));
723 return StatusCode::FAILURE;
724 }
725 if (graph.edgeIndex.size() != 2 * graph.nEdges) {
726 ATH_MSG_ERROR("Unexpected edge index size " << graph.edgeIndex.size()
727 << "; expected " << (2 * graph.nEdges));
728 return StatusCode::FAILURE;
729 }
730 if (graph.edgeFeatures.size() != graph.nEdges * kEdgeFeatureCount) {
731 ATH_MSG_ERROR("Unexpected edge feature size " << graph.edgeFeatures.size()
732 << "; expected " << (graph.nEdges * kEdgeFeatureCount));
733 return StatusCode::FAILURE;
734 }
735
736 GraphRawData raw{};
737 raw.graph = std::make_unique<InferenceGraph>();
738 raw.edgeIndexPacked.resize(2 * graph.nEdges);
739 for (std::size_t e = 0; e < graph.nEdges; ++e) {
740 raw.edgeIndexPacked[e] = graph.edgeIndex[2 * e];
741 raw.edgeIndexPacked[graph.nEdges + e] = graph.edgeIndex[2 * e + 1];
742 }
743
744 Ort::MemoryInfo memInfo = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
745
746 const std::vector<int64_t> nodeShape{static_cast<int64_t>(graph.nNodes), static_cast<int64_t>(kNodeFeatureCount)};
747 // The graph outlives the synchronous ONNX call below. Use its node
748 // buffer directly instead of allocating and copying featureLeaves per event.
749 ATLAS_THREAD_SAFE float* nodeFeaturesData =
750 const_cast<float*>(graph.nodeFeatures.data());
751 raw.graph->dataTensor.emplace_back(
752 Ort::Value::CreateTensor<float>(memInfo,
753 nodeFeaturesData,
754 graph.nodeFeatures.size(),
755 nodeShape.data(),
756 nodeShape.size()));
757
758 const std::vector<int64_t> edgeIndexShape{2, static_cast<int64_t>(graph.nEdges)};
759 raw.graph->dataTensor.emplace_back(
760 Ort::Value::CreateTensor<int64_t>(memInfo,
761 raw.edgeIndexPacked.data(),
762 raw.edgeIndexPacked.size(),
763 edgeIndexShape.data(),
764 edgeIndexShape.size()));
765
766 // ONNX Runtime's CreateTensor API takes a non-const pointer, but it does not
767 // mutate input buffers during inference. Avoid copying edge_attr every event.
768 ATLAS_THREAD_SAFE float* edgeFeaturesData = const_cast<float*>(graph.edgeFeatures.data());
769 const std::vector<int64_t> edgeAttrShape{static_cast<int64_t>(graph.nEdges), static_cast<int64_t>(kEdgeFeatureCount)};
770 raw.graph->dataTensor.emplace_back(
771 Ort::Value::CreateTensor<float>(memInfo,
772 edgeFeaturesData,
773 graph.edgeFeatures.size(),
774 edgeAttrShape.data(),
775 edgeAttrShape.size()));
776
777 const std::vector<const char*> inputNames{
778 m_inputNodeName.value().c_str(),
779 m_inputEdgeIndexName.value().c_str(),
780 m_inputEdgeAttrName.value().c_str()};
781 const std::vector<const char*> outputNames{m_outputName.value().c_str()};
782 ATH_MSG_DEBUG("classifyEdges: ONNX inputs shapes x=[" << nodeShape[0] << "," << nodeShape[1]
783 << "], edge_index=[" << edgeIndexShape[0] << "," << edgeIndexShape[1]
784 << "], edge_attr=[" << edgeAttrShape[0] << "," << edgeAttrShape[1] << "]");
785 ATH_CHECK(runNamedInference(raw, inputNames, outputNames));
786
787 if (raw.graph->dataTensor.size() <= inputNames.size()) {
788 ATH_MSG_ERROR("Missing ONNX output tensor for segment edge inference");
789 return StatusCode::FAILURE;
790 }
791
792 const Ort::Value& outTensor = raw.graph->dataTensor[inputNames.size()];
793 const auto outInfo = outTensor.GetTensorTypeAndShapeInfo();
794 const std::vector<int64_t> outShape = outInfo.GetShape();
795 const size_t outSize = outInfo.GetElementCount();
796 if (!outShape.empty()) {
797 ATH_MSG_DEBUG("classifyEdges: ONNX output rank=" << outShape.size()
798 << ", first dim=" << outShape.front()
799 << ", elements=" << outSize);
800 } else {
801 ATH_MSG_DEBUG("classifyEdges: ONNX scalar output, elements=" << outSize);
802 }
803 if (outSize < graph.nEdges) {
804 ATH_MSG_ERROR("ONNX logits tensor has " << outSize << " entries for " << graph.nEdges << " edges");
805 return StatusCode::FAILURE;
806 }
807
808 const float* logits = outTensor.GetTensorData<float>();
809 scores.reserve(graph.nEdges);
810 for (std::size_t e=0; e<graph.nEdges; ++e) {
811 const float l = logits[e];
812 scores.push_back({std::size_t(graph.edgeIndex[2 * e]),
813 std::size_t(graph.edgeIndex[2 * e + 1]),
814 l,
816 }
817
818 ATH_CHECK(dumpDebugEvent(ctx, graph, scores));
819 return StatusCode::SUCCESS;
820}
821
823 const EventContext& ctx,
824 const SegmentEdgeGraph& graph,
825 const std::vector<SegmentEdgeScore>& scores) const {
826 if (m_debugDumpFile.value().empty()) return StatusCode::SUCCESS;
827
828 std::lock_guard<std::mutex> lock{m_debugDumpMutex};
829 if (m_debugDumpMaxEvents.value() != 0 &&
830 m_debugDumpEvents.load(std::memory_order_relaxed) >=
831 m_debugDumpMaxEvents.value()) {
832 return StatusCode::SUCCESS;
833 }
834
835 if (graph.nodeFeatures.size() != graph.nNodes * kNodeFeatureCount ||
836 graph.edgeIndex.size() != graph.nEdges * 2 ||
837 graph.edgeFeatures.size() != graph.nEdges * kEdgeFeatureCount ||
838 scores.size() != graph.nEdges) {
839 ATH_MSG_ERROR("Cannot write segment-edge debug dump: inconsistent graph/output sizes"
840 << " nodes=" << graph.nNodes
841 << " nodeFeatures=" << graph.nodeFeatures.size()
842 << " edges=" << graph.nEdges
843 << " edgeIndex=" << graph.edgeIndex.size()
844 << " edgeFeatures=" << graph.edgeFeatures.size()
845 << " scores=" << scores.size());
846 return StatusCode::FAILURE;
847 }
848
849 nlohmann::json x = nlohmann::json::array();
850 x.get_ref<nlohmann::json::array_t&>().reserve(graph.nodeFeatures.size());
851 for (const float value : graph.nodeFeatures) {
852 x.push_back(std::isfinite(value) ? nlohmann::json(value)
853 : nlohmann::json(nullptr));
854 }
855
856 nlohmann::json edgeIndex = nlohmann::json::array();
857 edgeIndex.get_ref<nlohmann::json::array_t&>().reserve(graph.nEdges * 2);
858 // This is the actual ONNX [2,E] row-major buffer: all sources then all destinations.
859 for (std::size_t edge = 0; edge < graph.nEdges; ++edge) {
860 edgeIndex.push_back(graph.edgeIndex[2 * edge]);
861 }
862 for (std::size_t edge = 0; edge < graph.nEdges; ++edge) {
863 edgeIndex.push_back(graph.edgeIndex[2 * edge + 1]);
864 }
865
866 nlohmann::json edgeAttr = nlohmann::json::array();
867 edgeAttr.get_ref<nlohmann::json::array_t&>().reserve(graph.edgeFeatures.size());
868 for (const float value : graph.edgeFeatures) {
869 edgeAttr.push_back(std::isfinite(value) ? nlohmann::json(value)
870 : nlohmann::json(nullptr));
871 }
872
873 nlohmann::json logits = nlohmann::json::array();
874 nlohmann::json probabilities = nlohmann::json::array();
875 nlohmann::json edgeSrc = nlohmann::json::array();
876 nlohmann::json edgeDst = nlohmann::json::array();
877 logits.get_ref<nlohmann::json::array_t&>().reserve(scores.size());
878 probabilities.get_ref<nlohmann::json::array_t&>().reserve(scores.size());
879 edgeSrc.get_ref<nlohmann::json::array_t&>().reserve(scores.size());
880 edgeDst.get_ref<nlohmann::json::array_t&>().reserve(scores.size());
881 for (const SegmentEdgeScore& score : scores) {
882 edgeSrc.push_back(score.src);
883 edgeDst.push_back(score.dst);
884 logits.push_back(std::isfinite(score.logit) ? nlohmann::json(score.logit)
885 : nlohmann::json(nullptr));
886 probabilities.push_back(std::isfinite(score.probability)
887 ? nlohmann::json(score.probability)
888 : nlohmann::json(nullptr));
889 }
890
891 std::ofstream out{m_debugDumpFile.value(), std::ios::out | std::ios::app};
892 if (!out) {
893 ATH_MSG_ERROR("Could not append to segment-edge debug dump file: "
894 << m_debugDumpFile.value());
895 return StatusCode::FAILURE;
896 }
897
898 const unsigned int dumpIndex =
899 m_debugDumpEvents.fetch_add(1, std::memory_order_relaxed);
900 nlohmann::ordered_json event;
901 event["record_type"] = "event";
902 event["format_version"] = 1;
903 event["dump_index"] = dumpIndex;
904 event["run_number"] = ctx.eventID().run_number();
905 event["lumi_block"] = ctx.eventID().lumi_block();
906 event["event_number"] = ctx.eventID().event_number();
907 event["slot"] = ctx.slot();
908 event["n_nodes"] = graph.nNodes;
909 event["n_edges"] = graph.nEdges;
910 event["x_shape"] = {graph.nNodes, kNodeFeatureCount};
911 event["edge_index_shape"] = {2, graph.nEdges};
912 event["edge_attr_shape"] = {graph.nEdges, kEdgeFeatureCount};
913 event["logits_shape"] = {graph.nEdges};
914 event["x"] = std::move(x);
915 event["edge_index"] = std::move(edgeIndex);
916 event["edge_attr"] = std::move(edgeAttr);
917 event["edge_src"] = std::move(edgeSrc);
918 event["edge_dst"] = std::move(edgeDst);
919 event["logits"] = std::move(logits);
920 event["probabilities"] = std::move(probabilities);
921 out << event.dump() << '\n';
922
923 ATH_MSG_DEBUG("Wrote segment-edge debug event " << dumpIndex
924 << " to " << m_debugDumpFile.value());
925
926 return StatusCode::SUCCESS;
927}
928
929} // 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,...)
std::vector< std::vector< float > > scores
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 y
#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
std::atomic< std::size_t > m_sumInputSegments
Job-summed pre-ONNX pruning counters (see buildGraph()).
void fillTruthDiagnostics(const xAOD::MuonSegmentContainer &segments, const std::unordered_set< const xAOD::MuonSegment * > &bucketRetained, SegmentEdgeGraph &graph) const
MC-only diagnostics: record, for every input segment and every pair of input segments sharing a truth...
Gaudi::Property< unsigned int > m_debugDumpMaxEvents
Gaudi::Property< unsigned int > m_maxEdgesPerNodeBeforeInference
Gaudi::Property< unsigned int > m_maxEdgesPerTargetChamberBeforeInference
std::atomic< std::size_t > m_sumNodesBeforeIsolatedDrop
Gaudi::Property< std::string > m_outputName
std::atomic< std::size_t > m_sumRetainedPairs
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.
StatusCode finalize() override
Log a pre-ONNX candidate-edge pruning.
Gaudi::Property< unsigned int > m_maxSegmentsPerBucket
SG::ReadDecorHandleKey< xAOD::MuonSegmentContainer > m_truthLinkKey
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.
std::atomic< std::size_t > m_sumCandidatePairs
Gaudi::Property< bool > m_dropIsolatedNodesBeforeInference
StatusCode dumpDebugEvent(const EventContext &ctx, const SegmentEdgeGraph &graph, const std::vector< SegmentEdgeScore > &scores) const
std::atomic< std::size_t > m_sumNodesAfterIsolatedDrop
: 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
@ 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 kDroppedAsIsolated
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.
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".
TruthParticle_v1 TruthParticle
Typedef to implementation.
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.
One pair of input segments that share a truth particle (input-container indices).
std::uint8_t sectorDelta
std::vector< float > edgeFeatures
packed [E,7]: dpos(3), dist, cos, same_chamber, same_sector
std::vector< std::int32_t > inputNodeIndex
per input segment: node index, or kDropped*
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)
std::vector< PairFate > truthPairFates
every pair of input segments sharing a truth particle