ATLAS Offline Software
Loading...
Searching...
No Matches
PixelNNMonitorAlg.cxx
Go to the documentation of this file.
1/*
2 Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
3*/
4
5#include "PixelNNMonitorAlg.h"
6
12#include "InDetSimEvent/SiHit.h"
17#include "GaudiKernel/SystemOfUnits.h"
18
19#include <algorithm>
20#include <cmath>
21#include <limits>
22#include <map>
23#include <numeric>
24#include <span>
25#include <unordered_map>
26#include <vector>
27
28namespace {
29 // Reproduce the on-track split decision applied to a number-net output: the
30 // NnPixelClusterSplitProbTool posterior with its default uniform prior,
31 // which reduces to probs[i]/sum, then the ambiguity score-processor
32 // threshold (m_sharedProbCut2 = 0.3) that sets isSplit, which is what the
33 // on-track cluster tool actually uses. Returns 1/2/3; invalid input
34 // (a negative probability or non-positive sum) returns 1.
35 int splitDecision(std::span<const double, 3> probs) {
36 if (probs[0] < 0. || probs[1] < 0. || probs[2] < 0.) { return 1; }
37 const double sum = probs[0] + probs[1] + probs[2];
38 if (sum <= 0.) { return 1; }
39 constexpr double cut = 0.3; // m_sharedProbCut / m_sharedProbCut2
40 if (probs[2] / sum >= cut) { return 3; } // splitProbability(3)
41 if (probs[1] / sum >= cut) { return 2; } // splitProbability(2)
42 return 1;
43 }
44}
45
46namespace InDet {
47
48PixelNNMonitorAlg::PixelNNMonitorAlg(const std::string& name, ISvcLocator* pSvcLocator)
49 : AthMonitorAlgorithm(name, pSvcLocator) {}
50
53 ATH_CHECK(m_pixelClusterKey.initialize());
54 ATH_CHECK(m_trackCollectionKey.initialize());
55 ATH_CHECK(m_beamSpotKey.initialize());
56 ATH_CHECK(m_siHitKey.initialize(m_doTruth));
57 ATH_CHECK(m_splitProbKey.initialize(!m_splitProbKey.key().empty()));
58 ATH_CHECK(detStore()->retrieve(m_pixelID, "PixelID"));
59 ATH_CHECK(m_nnFactory.retrieve());
60 ATH_CHECK(m_lorentzTool.retrieve());
61 return StatusCode::SUCCESS;
62}
63
64
65std::vector<Amg::Vector2D> PixelNNMonitorAlg::truthPositions(
66 const InDet::PixelCluster& cluster,
67 const InDetDD::SiDetectorElement& element,
68 const std::vector<std::vector<const SiHit*>>& siHitsByHash) const {
69
70 std::vector<Amg::Vector2D> result;
71 const IdentifierHash hash = element.identifyHash();
72 if (hash >= siHitsByHash.size()) { return result; }
73
74 // One truth position per particle: average the mid-plane crossings of its
75 // geometry-matched SiHits (within +/-1 pixel of a cluster cell).
76 std::map<int, std::pair<Amg::Vector2D, int>> byParticle;
77 for (const SiHit* sh : siHitsByHash[hash]) {
78 HepGeom::Point3D<double> avg = sh->localStartPosition() + sh->localEndPosition();
79 avg *= 0.5;
80 Amg::Vector2D p = element.hitLocalToLocal(avg.z(), avg.y());
81 InDetDD::SiCellId diode = element.cellIdOfPosition(p);
82 if (!diode.isValid()) { continue; }
83 bool match = false;
84 for (const auto& rid : cluster.rdoList()) {
85 if (std::abs(static_cast<int>(diode.etaIndex()) - m_pixelID->eta_index(rid)) <= 1 &&
86 std::abs(static_cast<int>(diode.phiIndex()) - m_pixelID->phi_index(rid)) <= 1) {
87 match = true; break;
88 }
89 }
90 if (!match) { continue; }
91 const int id = HepMC::uniqueID(sh->particleLink());
92 // try_emplace with a zeroed vector: Amg::Vector2D (Eigen) is NOT
93 // zero-initialised by default, so byParticle[id] would accumulate garbage.
94 auto& e = byParticle.try_emplace(id, Amg::Vector2D::Zero(), 0).first->second;
95 e.first += p; e.second += 1;
96 }
97 for (const auto& [id, e] : byParticle) {
98 result.emplace_back(e.first / static_cast<double>(e.second));
99 }
100 return result;
101}
102
103StatusCode PixelNNMonitorAlg::fillHistograms(const EventContext& ctx) const {
104 using namespace Monitored;
105
106 const auto & numberGroup = getGroup("PixelNNNumber");
107 const auto & posSummary = getGroup("PixelNNPosSummary");
108 const auto & splitGroup = getGroup("PixelNNSplitFrac");
109
111 if (!pixelClusters.isValid()) {
112 ATH_MSG_WARNING("Could not retrieve PixelClusterContainer");
113 return StatusCode::SUCCESS;
114 }
115
116 // SiHits indexed by wafer hash, for truth positions.
117 std::vector<std::vector<const SiHit*>> siHitsByHash;
118 if (m_doTruth && m_pixelID) {
119 siHitsByHash.resize(m_pixelID->wafer_hash_max());
121 if (siHits.isValid()) {
122 for (const SiHit& h : *siHits) {
123 if (!h.isPixel()) { continue; }
124 Identifier wid = m_pixelID->wafer_id(h.getBarrelEndcap(), h.getLayerDisk(),
125 h.getPhiModule(), h.getEtaModule());
126 IdentifierHash wh = m_pixelID->wafer_hash(wid);
127 if (wh < m_pixelID->wafer_hash_max()) siHitsByHash[wh].push_back(&h);
128 }
129 } else {
130 ATH_MSG_DEBUG("SiHit collection not available; truth plots disabled this event");
131 }
132 }
133
134 // On-track clusters -> track parameters + surface (POSITION-net input only).
135 // A cluster may carry several tracks (one per particle in a merged cluster);
136 // each track is evaluated separately with its own incidence angle.
137 struct OnTrackInfo { const Trk::TrackParameters* params; const Trk::Surface* surface; };
138 std::unordered_map<Identifier::value_type, std::vector<OnTrackInfo>> onTrackClusters;
140 if (tracks.isValid()) {
141 for (const Trk::Track* track : *tracks) {
142 if (!track) { continue; }
143 for (const auto* tsos : *track->trackStateOnSurfaces()) {
144 if (!tsos || !tsos->type(Trk::TrackStateOnSurface::Measurement)) { continue; }
145 const auto* rio = dynamic_cast<const Trk::RIO_OnTrack*>(tsos->measurementOnTrack());
146 if (!rio || !rio->prepRawData()) { continue; }
147 const auto* pixClus = dynamic_cast<const InDet::PixelCluster*>(rio->prepRawData());
148 if (!pixClus || !tsos->trackParameters()) { continue; }
149 onTrackClusters[pixClus->identify().get_compact()].push_back(
150 {tsos->trackParameters(), &rio->associatedSurface()});
151 }
152 }
153 }
154
155 // Final reco split decision (isSplit, set by the ambiguity solver), for the
156 // reco curve of the split-fraction profiles.
157 const Trk::ClusterSplitProbabilityContainer* splitProbs = nullptr;
158 if (!m_splitProbKey.key().empty()) {
160 if (h.isValid()) splitProbs = h.cptr();
161 }
162
163 int nClusters = 0;
164
165 // Per-event extremes of the position-net outputs (filled for every evaluated
166 // on-track cluster, independent of truth, so they also monitor data).
167 double evtMinErrX = std::numeric_limits<double>::max();
168 double evtMaxErrX = 0.;
169 double evtMinErrY = std::numeric_limits<double>::max();
170 double evtMaxErrY = 0.;
171 double evtMaxAbsDeltaX = 0.;
172 double evtMaxAbsDeltaY = 0.;
173 double evtMaxProb2 = 0.;
174 bool evtHasPos = false;
175
176 for (const auto* coll : *pixelClusters) {
177 if (!coll) { continue; }
178 for (const auto* cluster : *coll) {
179 if (!cluster) { continue; }
180 const InDetDD::SiDetectorElement* element = cluster->detectorElement();
181 if (!element) { continue; }
182 ++nClusters;
183
184 const double eta = cluster->globalPosition().eta();
185 const int nCell = static_cast<int>(cluster->rdoList().size());
186
187 // True particle positions and multiplicity.
188 std::vector<Amg::Vector2D> truths;
189 if (m_doTruth && !siHitsByHash.empty()) {
190 truths = truthPositions(*cluster, *element, siHitsByHash);
191 }
192 const int trueN = static_cast<int>(truths.size());
193 const bool haveTruth = (trueN >= 1 && trueN <= 3);
194
195 // The with-track NN is only exercised for clusters on a track: the
196 // ambiguity solver re-evaluates the split prob with the track, and the
197 // on-track tool makes the position measurement. Off-track clusters are
198 // not part of this comparison.
199 auto it = onTrackClusters.find(cluster->identify().get_compact());
200 if (it == onTrackClusters.end() || it->second.empty()) { continue; }
201
202 // ---- Number net WITH track (as in the ambiguity solver). The predicted
203 // multiplicity is the reco split decision (with-track split prob +
204 // ambiguity map) = numberOfSubclusters, i.e. exactly what the
205 // on-track position call uses. ----
206 const Trk::Surface* surf0 = it->second.front().surface;
207 const Trk::TrackParameters* tp0 = it->second.front().params;
208 std::vector<double> probs =
209 m_nnFactory->estimateNumberOfParticles(*cluster, *surf0, *tp0);
210 if (probs.size() < 3) { continue; }
211
212 // Predicted multiplicity = the reco split decision applied to THIS
213 // (ONNX) number net's output, so the whole chain is the model under test.
214 const int predN = splitDecision(std::span<const double, 3>(probs.data(), 3));
215
216 // Reconstructed TRACK incidence angles fed to the with-track NN
217 // (reproduces addTrackInfoToInput). Written to the per-cluster track-angle
218 // dump so the offline eval can use the real track angle.
219 // Reference path length used by NnClusterizationFactory::addTrackInfoToInput.
220 constexpr double refPathLength = 0.250;
221 const Amg::Vector3D particleDir = tp0->momentum().unit();
222 Amg::Vector3D localIntersection = surf0->transform().inverse().linear() * particleDir;
223 const double cosTheta = std::cos(localIntersection.theta());
224 // Direction in the module plane: no well-defined incidence angle.
225 if (std::abs(cosTheta) < 1e-6) { continue; }
226 localIntersection *= refPathLength / cosTheta;
227 const double trkTheta = std::atan2(localIntersection.y(), refPathLength);
228 double trkPhi = std::atan2(localIntersection.x(), refPathLength);
229 const double tanl = m_lorentzTool->getTanLorentzAngle(element->identifyHash(), ctx);
230 trkPhi = std::atan(std::tan(trkPhi) - tanl);
231
232 evtMaxProb2 = std::max(evtMaxProb2, probs[1]);
233 auto monPredN = Scalar<int> ("predN", predN);
234 auto monProb1 = Scalar<float>("prob1", probs[0]);
235 auto monProb2 = Scalar<float>("prob2", probs[1]);
236 auto monProb3 = Scalar<float>("prob3", probs[2]);
237 fill(numberGroup, monPredN, monProb1, monProb2, monProb3);
238
239 // ---- Split fraction vs track kinematics: NN (network argmax >= 2) and
240 // reco (ambiguity-solver isSplit) on every on-track cluster, so
241 // these also fill on data; truth (trueN >= 2) on MC only. ----
242 double leadPt = 0.;
243 for (const OnTrackInfo& t : it->second)
244 if (t.params) leadPt = std::max(leadPt, t.params->momentum().perp());
245 const int nnArgmax = 1 + static_cast<int>(
246 std::max_element(probs.begin(), probs.begin() + 3) - probs.begin());
247
248 auto monPt = Scalar<float>("trackPt", leadPt / Gaudi::Units::GeV);
249 auto monPhi = Scalar<float>("trkPhi", trkPhi);
250 auto monTheta = Scalar<float>("trkTheta", trkTheta);
251 auto monClusEta = Scalar<float>("clusEta", eta);
252 auto monNn = Scalar<int> ("nnSplit", nnArgmax >= 2 ? 1 : 0);
253 fill(splitGroup, monPt, monPhi, monTheta, monClusEta, monNn);
254 if (splitProbs) {
255 const auto& sp = splitProbs->splitProbability(cluster);
256 auto monReco = Scalar<int>("recoSplit", sp.isSplit() ? 1 : 0);
257 fill(splitGroup, monPt, monPhi, monTheta, monClusEta, monReco);
258 }
259 if (haveTruth) {
260 auto monTruth = Scalar<int>("truthSplit", trueN >= 2 ? 1 : 0);
261 fill(splitGroup, monPt, monPhi, monTheta, monClusEta, monTruth);
262 }
263
264 if (haveTruth) {
265 auto monTrueN = Scalar<int> ("trueN", trueN);
266 auto monPredNc = Scalar<int> ("predNconf", predN);
267 auto monIsCorrect = Scalar<int> ("isCorrect", trueN == predN ? 1 : 0);
268 auto monEta = Scalar<float>("eta", eta);
269 auto monNCell = Scalar<int> ("nCell", nCell);
270 auto monProbMulti = Scalar<float>("probMulti", probs[1] + probs[2]);
271 fill(numberGroup, monTrueN, monPredNc, monIsCorrect, monEta, monNCell, monProbMulti);
272 }
273
274
275 // ---- Position net WITH track at the predicted multiplicity. The
276 // wide-range and extreme-value monitoring fills for every evaluated
277 // cluster (truth-free, so also on data); the residuals and pulls
278 // additionally require the multiplicity to be predicted correctly
279 // (predN == trueN) on simulation. ----
280 const int numberOfSubclusters = predN;
281
282 const auto & posDQ = getGroup("PixelNNPosDQ");
283 const auto & posGroup = getGroup("PixelNNPosN" + std::to_string(numberOfSubclusters));
284 for (const OnTrackInfo& trk : it->second) {
285 if (!trk.params || !trk.surface) { continue; }
286 std::vector<Amg::MatrixX> errors;
287 std::vector<Amg::Vector2D> positions = m_nnFactory->estimatePositions(
288 *cluster, *trk.surface, *trk.params, errors, numberOfSubclusters);
289 if (static_cast<int>(positions.size()) != numberOfSubclusters ||
290 static_cast<int>(errors.size()) != numberOfSubclusters) { continue; }
291
292 // Wide-range and precision monitoring of every sub-cluster prediction,
293 // with the offset taken against the cluster position (no truth needed).
294 for (int i = 0; i < numberOfSubclusters; ++i) {
295 if (errors[i].rows() < 2) { continue; }
296 const double sigX = std::sqrt(errors[i](0, 0));
297 const double sigY = std::sqrt(errors[i](1, 1));
298 if (sigX <= 0 || sigY <= 0) { continue; }
299 const double dX = positions[i][Trk::locX] - cluster->localPosition()[Trk::locX];
300 const double dY = positions[i][Trk::locY] - cluster->localPosition()[Trk::locY];
301 auto monDeltaX = Scalar<float>("posDeltaXWide", dX);
302 auto monDeltaY = Scalar<float>("posDeltaYWide", dY);
303 auto monErrXWide = Scalar<float>("posErrXWide", sigX);
304 auto monErrYWide = Scalar<float>("posErrYWide", sigY);
305 auto monPrecX = Scalar<float>("posPrecX", 1.0 / (sigX * sigX));
306 auto monPrecY = Scalar<float>("posPrecY", 1.0 / (sigY * sigY));
307 fill(posDQ, monDeltaX, monDeltaY, monErrXWide, monErrYWide, monPrecX, monPrecY);
308 evtMinErrX = std::min(evtMinErrX, sigX);
309 evtMaxErrX = std::max(evtMaxErrX, sigX);
310 evtMinErrY = std::min(evtMinErrY, sigY);
311 evtMaxErrY = std::max(evtMaxErrY, sigY);
312 evtMaxAbsDeltaX = std::max(evtMaxAbsDeltaX, std::abs(dX));
313 evtMaxAbsDeltaY = std::max(evtMaxAbsDeltaY, std::abs(dY));
314 evtHasPos = true;
315 }
316
317 if (!haveTruth || predN != trueN) { continue; }
318
319 // Pick the sub-cluster the track uses (tool logic: chi2 distance to the
320 // track local position, weighted by the track covariance).
321 const Amg::Vector2D trkPos = trk.params->localPosition();
322 // Fallback local uncertainties when the track has no covariance [mm].
323 constexpr double fallbackErrX = 0.01;
324 constexpr double fallbackErrY = 0.05;
325 Amg::Vector2D trkErr(fallbackErrX, fallbackErrY);
326 if (trk.params->covariance()) {
327 trkErr = Amg::Vector2D(std::sqrt((*trk.params->covariance())(0, 0)),
328 std::sqrt((*trk.params->covariance())(1, 1)));
329 }
330 int sub = 0;
331 double best = std::numeric_limits<double>::max();
332 for (int i = 0; i < numberOfSubclusters; ++i) {
333 double d = std::pow(trkPos[0] - positions[i][0], 2) / trkErr[0]
334 + std::pow(trkPos[1] - positions[i][1], 2) / trkErr[1];
335 if (d < best) { best = d; sub = i; }
336 }
337 if (errors[sub].rows() < 2) { continue; }
338 const double errX = std::sqrt(errors[sub](0, 0));
339 const double errY = std::sqrt(errors[sub](1, 1));
340 if (errX <= 0 || errY <= 0) { continue; }
341
342 // Assign the N sub-clusters to the N truths globally (min total squared
343 // distance), then the track's sub-cluster gets ITS matched truth. This
344 // avoids two sub-clusters grabbing the same truth in multi-particle
345 // clusters. (trueN == numberOfSubclusters here, since predN==trueN.)
346 std::vector<int> perm(numberOfSubclusters);
347 std::iota(perm.begin(), perm.end(), 0);
348 std::vector<int> bestPerm = perm;
349 double bestDist = std::numeric_limits<double>::max();
350 do {
351 double dsum = 0.;
352 for (int i = 0; i < numberOfSubclusters; ++i)
353 dsum += (positions[i] - truths[perm[i]]).squaredNorm();
354 if (dsum < bestDist) { bestDist = dsum; bestPerm = perm; }
355 } while (std::next_permutation(perm.begin(), perm.end()));
356 const int tr = bestPerm[sub];
357
358 const double resX = positions[sub][Trk::locX] - truths[tr][Trk::locX];
359 const double resY = positions[sub][Trk::locY] - truths[tr][Trk::locY];
360
361 auto monResX = Scalar<float>("resX", resX / Gaudi::Units::micrometer);
362 auto monResY = Scalar<float>("resY", resY / Gaudi::Units::micrometer);
363 auto monPullX = Scalar<float>("pullX", resX / errX);
364 auto monPullY = Scalar<float>("pullY", resY / errY);
365 auto monErrX = Scalar<float>("errX", errX / Gaudi::Units::micrometer);
366 auto monErrY = Scalar<float>("errY", errY / Gaudi::Units::micrometer);
367 auto monEta = Scalar<float>("eta", eta);
368 auto monNCell = Scalar<int> ("nCell", nCell);
369 fill(posGroup, monResX, monResY, monPullX, monPullY, monErrX, monErrY, monEta, monNCell);
370
371 auto monPosN = Scalar<int>("posN", numberOfSubclusters);
372 fill(posSummary, monPosN, monPullX, monPullY, monErrX, monErrY);
373 }
374 }
375 }
376
377 auto monNClusters = Scalar<int>("nClusters", nClusters);
378 fill(numberGroup, monNClusters);
379
380 if (evtHasPos) {
381 const auto & extremes = getGroup("PixelNNExtremes");
382 auto monMinErrX = Scalar<float>("evtMinErrX", evtMinErrX);
383 auto monMaxErrX = Scalar<float>("evtMaxErrX", evtMaxErrX);
384 auto monMinErrY = Scalar<float>("evtMinErrY", evtMinErrY);
385 auto monMaxErrY = Scalar<float>("evtMaxErrY", evtMaxErrY);
386 auto monMaxDX = Scalar<float>("evtMaxAbsDeltaX", evtMaxAbsDeltaX);
387 auto monMaxDY = Scalar<float>("evtMaxAbsDeltaY", evtMaxAbsDeltaY);
388 auto monMaxP2 = Scalar<float>("evtMaxProb2", evtMaxProb2);
389 fill(extremes, monMinErrX, monMaxErrX, monMinErrY, monMaxErrY, monMaxDX, monMaxDY, monMaxP2);
390 }
391 return StatusCode::SUCCESS;
392}
393
394} // namespace InDet
Scalar eta() const
pseudorapidity method
#define ATH_CHECK
Evaluate an expression and check for errors.
#define ATH_MSG_WARNING(x)
#define ATH_MSG_DEBUG(x)
static Double_t sp
Header file to be included by clients of the Monitored infrastructure.
This is an Identifier helper class for the Pixel subdetector.
size_t size() const
Number of registered mappings.
const ServiceHandle< StoreGateSvc > & detStore() const
Header file for AthHistogramAlgorithm.
const ToolHandle< GenericMonitoringTool > & getGroup(const std::string &name) const
Get a specific monitoring tool from the tool handle array.
virtual StatusCode initialize() override
initialize
AthMonitorAlgorithm(const std::string &name, ISvcLocator *pSvcLocator)
Constructor.
This is a "hash" representation of an Identifier.
Identifier for the strip or pixel cell.
Definition SiCellId.h:29
int phiIndex() const
Get phi index. Equivalent to strip().
Definition SiCellId.h:122
bool isValid() const
Test if its in a valid state.
Definition SiCellId.h:136
int etaIndex() const
Get eta index.
Definition SiCellId.h:114
Class to hold geometrical description of a silicon detector element.
SiCellId cellIdOfPosition(const Amg::Vector2D &localPos) const
As in previous method but returns SiCellId.
virtual IdentifierHash identifyHash() const override final
identifier hash (inline)
Amg::Vector2D hitLocalToLocal(double xEta, double xPhi) const
Simulation/Hit local frame to reconstruction local frame.
virtual StatusCode initialize() override
initialize
std::vector< Amg::Vector2D > truthPositions(const InDet::PixelCluster &cluster, const InDetDD::SiDetectorElement &element, const std::vector< std::vector< const SiHit * > > &siHitsByHash) const
True particle positions (local mm) in the cluster, one per particle, from the matching Geant4 SiHits ...
ToolHandle< NnClusterizationFactory > m_nnFactory
ToolHandle< ISiLorentzAngleTool > m_lorentzTool
Gaudi::Property< bool > m_doTruth
SG::ReadCondHandleKey< InDet::BeamSpotData > m_beamSpotKey
SG::ReadHandleKey< Trk::ClusterSplitProbabilityContainer > m_splitProbKey
SG::ReadHandleKey< InDet::PixelClusterContainer > m_pixelClusterKey
SG::ReadHandleKey< SiHitCollection > m_siHitKey
SG::ReadHandleKey< TrackCollection > m_trackCollectionKey
virtual StatusCode fillHistograms(const EventContext &ctx) const override
adds event to the monitoring histograms
PixelNNMonitorAlg(const std::string &name, ISvcLocator *pSvcLocator)
Declare a monitored scalar variable.
virtual bool isValid() override final
Can the handle be successfully dereferenced?
Definition SiHit.h:19
Container to associate Cluster with cluster splitting probabilities.
const ProbabilityInfo & splitProbability(const PrepRawData *cluster) const
const Amg::Vector3D & momentum() const
Access method for the momentum.
const std::vector< Identifier > & rdoList() const
return the List of rdo identifiers (pointers)
Class to handle RIO On Tracks ROT) for InDet and Muons, it inherits from the common MeasurementBase.
Definition RIO_OnTrack.h:70
Abstract Base Class for tracking surfaces.
Definition Surface.h:79
const Amg::Transform3D & transform() const
Returns HepGeom::Transform3D by reference.
@ Measurement
This is a measurement, and will at least contain a Trk::MeasurementBase.
void fill(const ToolHandle< GenericMonitoringTool > &groupHandle, std::vector< std::reference_wrapper< Monitored::IMonitoredVariable > > &&variables) const
Fills a vector of variables to a group by reference.
bool match(std::string s1, std::string s2)
match the individual directories of two strings
Definition hcg.cxx:359
Eigen::Matrix< double, 2, 1 > Vector2D
Eigen::Matrix< double, 3, 1 > Vector3D
int uniqueID(const T &p)
Primary Vertex Finder.
Generic monitoring tool for athena components.
@ locY
local cartesian
Definition ParamDefs.h:38
@ locX
Definition ParamDefs.h:37
ParametersBase< TrackParametersDim, Charged > TrackParameters