2 Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
4#ifndef NNCLSUTERCALIBRATORTOOLIMPL_ICC
5#define NNCLSUTERCALIBRATORTOOLIMPL_ICC
7#include <PixelReadoutGeometry/PixelModuleDesign.h>
8#include <onnxruntime_cxx_api.h>
18#include "Acts/Utilities/Zip.hpp"
19#include "ActsGeometry/ActsDetectorElement.h"
20#include "AthenaBaseComps/AthCheckMacros.h"
21#include "Identifier/Identifier.h"
22#include "InDetMeasurementUtilities/Helpers.h"
23#include "InDetReadoutGeometry/SiDetectorElement.h"
24#include "NNPixelClusterCalibratorHelpers.h"
25#include "PixelReadoutGeometry/PixelModuleDesign.h"
26#include "ReadoutGeometryBase/SiCellId.h"
27#include "src/detail/TrackStateFlagHelper.h"
28#include "xAODMeasurementBase/MeasurementDefs.h"
29namespace ActsTrk::detail {
31template <typename calib_data_t, typename traj_t>
32StatusCode NNClusterCalibratorToolImpl<calib_data_t, traj_t>::initialize() {
33 ATH_CHECK(m_readKeyONNX.initialize());
34 ATH_MSG_DEBUG("Initializing ...");
36 ATH_CHECK(BASE::initialize());
37 ATH_MSG_INFO(" successfully initialized, NNClusterCalibrator implementation");
38 return StatusCode::SUCCESS;
41template <typename calib_data_t, typename traj_t>
42NNClusterCalibratorOptions<calib_data_t, traj_t>
43NNClusterCalibratorToolImpl<calib_data_t, traj_t>::createOptions(
44 const EventContext& ctx) const {
45 NNClusterCalibratorOptions<calib_data_t, traj_t> opt;
46 opt.m_minClusterSizeForNN = m_minClusterSizeForNN;
47 opt.m_minClusterChargeForNN = m_minClusterChargeForNN;
48 SG::ReadCondHandle<OnnxNNCollection> onnxCollection(m_readKeyONNX, ctx);
49 if (!onnxCollection.isValid()) {
50 ATH_MSG_FATAL("Failed to get ONNX network collection with key "
51 << m_readKeyONNX.key());
52 return opt; // this options will be invalid
54 ATH_MSG_DEBUG("Obtained NN models for this event");
57 opt.m_models = onnxCollection.cptr();
59 auto barePointer = BASE::createOnTrackCalibrator(ctx).release();
61 dynamic_cast<AnalogueClusteringCalibrator<calib_data_t, traj_t>*>(
64 std::unique_ptr<AnalogueClusteringCalibrator<calib_data_t, traj_t>>(
69template <typename calib_data_t, typename traj_t>
70std::tuple<typename NNClusterCalibrator<calib_data_t, traj_t>::BASE::Pos,
71 typename NNClusterCalibrator<calib_data_t, traj_t>::BASE::Cov,
73NNClusterCalibrator<calib_data_t, traj_t>::calibrate(
74 const EventContext& ctx, const Acts::GeometryContext& gctx,
75 const Acts::CalibrationContext& cctx, const xAOD::PixelCluster& cluster,
76 const InDetDD::SiDetectorElement& detElement,
77 const std::pair<float, float>& tan_incidence_angles,
78 const Acts::Vector2& predicted_local_position) const {
79 double totalCharge = cluster.totalCharge();
80 if (totalCharge < m_options.m_minClusterChargeForNN || cluster.chargeList().size() <
81 m_options.m_minClusterSizeForNN) {
82 return m_options.m_fallback->calibrate(ctx, gctx, cctx, cluster,
83 detElement, tan_incidence_angles,
84 predicted_local_position);
87 ActsTrk::NNinput nnInput;
88 fillClusterData(nnInput, cluster, detElement);
90 nnInput.set(NNinput::Index::phi, std::atan(tan_incidence_angles.first));
91 nnInput.set(NNinput::Index::theta, std::atan(tan_incidence_angles.second));
94 std::array<float, 3> probabilities = predictNumberOfClusters(nnInput);
95 // pick max probability
96 const int maxProbNumberIndex = NumberNNoutput::maxProbIndex(probabilities);
97 const int maxProbNumberOfClusters = maxProbNumberIndex + 1;
99 PositionNNoutput positions =
100 predictPositions(nnInput, maxProbNumberOfClusters);
102 // local module coordinates of the i-th sub-cluster
103 auto subClusterPosition = [&nnInput, &positions](NNinput::index_t i) {
104 return Acts::Vector2(
105 nnInput.indexCoordToRealCoord(positions.x(i), NNinput::Index::pitchXOffset),
106 nnInput.indexCoordToRealCoord(positions.y(i), NNinput::Index::pitchYOffset));
109 // The cluster was created by one or several particles: use the position of the
110 // particle closest to the track
111 assert(positions.numberOfSubClusters() > 0);
112 const NNinput::index_t chosen = *std::ranges::min_element(
113 std::views::iota(NNinput::index_t{0}, positions.numberOfSubClusters()),
115 [&subClusterPosition, &predicted_local_position](NNinput::index_t i) {
116 return (subClusterPosition(i) - predicted_local_position).squaredNorm();
118 typename NNClusterCalibrator<calib_data_t, traj_t>::BASE::Pos pos =
119 subClusterPosition(chosen).template cast<float>();
121 // the NN precision is converted to the RMS (in mm), the covariance is its square
122 const float rmsX = nnInput.precisionToRealCoord(positions.xprec(maxProbNumberIndex),
123 NNinput::Index::pitchXOffset);
124 const float rmsY = nnInput.precisionToRealCoord(positions.yprec(maxProbNumberIndex),
125 NNinput::Index::pitchYOffset);
126 typename NNClusterCalibrator<calib_data_t, traj_t>::BASE::Cov cov;
127 cov(1, 0) = cov(0, 1) = 0.0f;
128 cov(0, 0) = rmsX * rmsX;
129 cov(1, 1) = rmsY * rmsY;
131 unsigned int splitFlag =
132 positions.numberOfSubClusters() > 1
133 ? detail::setTrackStateFlag(Acts::TrackStateFlag::IsSplitHit)
135 return {pos, cov, splitFlag};
138template <typename calib_data_t, typename traj_t>
139std::tuple<int, int, int, int>
140NNClusterCalibrator<calib_data_t, traj_t>::clusterIndexRanges(
141 const xAOD::PixelCluster& cluster) const {
142 int xMin = std::numeric_limits<int>::max();
143 int xMax = std::numeric_limits<int>::min();
144 int yMin = std::numeric_limits<int>::max();
145 int yMax = std::numeric_limits<int>::min();
146 for (const auto& rdoID : cluster.rdoList()) {
147 const Identifier singlePixelID(rdoID);
148 const int x = static_cast<int>(this->pixelID().phi_index(singlePixelID));
149 const int y = static_cast<int>(this->pixelID().eta_index(singlePixelID));
150 xMin = std::min(x, xMin);
151 xMax = std::max(x, xMax);
152 yMin = std::min(y, yMin);
153 yMax = std::max(y, yMax);
155 return {xMin, xMax, yMin, yMax};
158template <typename calib_data_t, typename traj_t>
160NNClusterCalibrator<calib_data_t, traj_t>::weightedClusterCenter(
161 const xAOD::PixelCluster& cluster,
162 const InDetDD::PixelModuleDesign* design) const {
164 // obtain weighted position
166 InDetDD::SiLocalPosition position(0, 0);
167 auto idvals = Acts::zip(cluster.rdoList(), cluster.chargeList());
168 for (const auto [rdoID, charge] : idvals) {
169 const Identifier singlePixelID(rdoID);
170 const int x = static_cast<int>(this->pixelID().phi_index(singlePixelID));
171 const int y = static_cast<int>(this->pixelID().eta_index(singlePixelID));
173 InDetDD::SiLocalPosition siLocalPosition(
174 design->positionFromColumnRow(y, x));
175 position += siLocalPosition * charge;
180 return design->cellIdOfPosition(position);
183template <typename calib_data_t, typename traj_t>
184void NNClusterCalibrator<calib_data_t, traj_t>::fillClusterData(
185 NNinput& nn, const xAOD::PixelCluster& cluster,
186 const InDetDD::SiDetectorElement& detElement) const {
188 const Identifier moduleID(
189 static_cast<Identifier::value_type>(cluster.identifier()));
190 nn.set(NNinput::Index::layer, (float)this->pixelID().layer_disk(moduleID));
191 nn.set(NNinput::Index::bec, (float)this->pixelID().barrel_ec(moduleID));
193 // need to decide how to center the cluster in 7 x 7 matrix
194 // this implementation will center it w/o taking into account the charge
195 // (will be revisited)
196 // reminder: row <-> phi <-> x, column <-> eta <-> y
197 auto [xMin, xMax, yMin, yMax] = clusterIndexRanges(cluster);
199 // this is a particular way of truncating cluster to 7x7
200 // TODO test if picking the center would work better for large clusters
201 xMax = std::clamp(xMax, xMin,
202 xMin + static_cast<int>(NNinput::Index::windowSize));
203 yMax = std::clamp(yMax, yMin,
204 yMin + static_cast<int>(NNinput::Index::windowSize));
206 const InDetDD::PixelModuleDesign* design(
207 static_cast<const InDetDD::PixelModuleDesign*>(&detElement.design()));
209 nn.centerCell = weightedClusterCenter(cluster, design);
210 nn.centerPosition = design->localPositionOfCell(nn.centerCell);
211 const int xCenter = nn.centerCell.phiIndex();
212 const int yCenter = nn.centerCell.etaIndex();
214 auto idvals = Acts::zip(cluster.rdoList(), cluster.chargeList());
215 for (const auto [rdoID, charge] : idvals) {
216 const Identifier singlePixelID(rdoID);
217 const int x = static_cast<int>(this->pixelID().phi_index(singlePixelID));
218 const int y = static_cast<int>(this->pixelID().eta_index(singlePixelID));
220 const int xNN = NNinput::toNNinputIndex(x, xCenter);
221 const int yNN = NNinput::toNNinputIndex(y, yCenter);
223 if (NNinput::inWindow(xNN, yNN)) {
224 nn.setPixelCharge(xNN, yNN, charge);
226 // this happens, we need to understand frequency and impact
230 for (int i = 0; i < static_cast<int>(NNinput::Index::windowSize); ++i) {
231 nn.setPixelXPitch(i, 0.1);
232 nn.setPixelYPitch(i, 0.1);
233 const int x = NNinput::toModuleIndex(i, xCenter);
234 const int y = NNinput::toModuleIndex(i, yCenter);
235 const InDetDD::SiCellId cell(x, y);
237 nn.setPixelXPitch(i, design->parameters(cell).width().xPhi());
241template <typename calib_data_t, typename traj_t>
243NNClusterCalibrator<calib_data_t, traj_t>::predictNumberOfClusters(
245 Ort::Session& session = *m_options.m_models->numberNetwork;
246 auto inputTypeInfo = session.GetInputTypeInfo(0);
247 auto tensorInfo = inputTypeInfo.GetTensorTypeAndShapeInfo();
248 const int64_t expectedDim = tensorInfo.GetShape()[1];
250 // TODO investigate streamlining this
251 // this is copy paste from NnClusterizationFactory
252 // seems to be a lot of preps to be repeated for each invocation
253 // maybe it can be spared?
254 Ort::MemoryInfo memInfo =
255 Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
257 std::vector<int64_t> inputShape = {1, expectedDim};
258 Ort::Value inputTensor = Ort::Value::CreateTensor<float>(
259 memInfo, nn.payload().data(), nn.payload().size(), inputShape.data(),
262 Ort::AllocatorWithDefaultOptions allocator;
263 auto inputName = session.GetInputNameAllocated(0, allocator);
264 auto outputName = session.GetOutputNameAllocated(0, allocator);
265 std::array<const char*, 1> inputNames({inputName.get()});
266 std::array<const char*, 1> outputNames({outputName.get()});
269 auto outputTensors = session.Run(Ort::RunOptions{nullptr}, inputNames.data(),
270 &inputTensor, 1, outputNames.data(), 1);
273 const float* outputData = outputTensors[0].GetTensorData<float>();
274 return std::array<float, 3>({*outputData, *(outputData+1), *(outputData+2)});
277template <typename calib_data_t, typename traj_t>
278Ort::Session& NNClusterCalibrator<calib_data_t, traj_t>::selectPositionNetwork(
281 return *m_options.m_models->positionNetwork1;
282 } else if (number == 2) {
283 return *m_options.m_models->positionNetwork2;
285 return *m_options.m_models->positionNetwork3;
288template <typename calib_data_t, typename traj_t>
289PositionNNoutput NNClusterCalibrator<calib_data_t, traj_t>::predictPositions(
290 NNinput& nn, int number) const {
292 Ort::Session& session = selectPositionNetwork(number);
293 auto inputTypeInfo = session.GetInputTypeInfo(0);
294 auto tensorInfo = inputTypeInfo.GetTensorTypeAndShapeInfo();
295 const int64_t expectedDim = tensorInfo.GetShape()[1];
297 // TODO investigate streamlining this
298 // at least part of preparation can be shared between Number and Posion (this)
299 // this is copy paste from NnClusterizationFactory
300 // seems to be a lot of preps to be repeated for each invocation
301 // maybe it can be spared?
302 Ort::MemoryInfo memInfo =
303 Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
305 std::vector<int64_t> inputShape = {1, expectedDim};
306 Ort::Value inputTensor = Ort::Value::CreateTensor<float>(
307 memInfo, nn.payload().data(), nn.payload().size(), inputShape.data(),
310 Ort::AllocatorWithDefaultOptions allocator;
311 auto inputName = session.GetInputNameAllocated(0, allocator);
312 auto outputName = session.GetOutputNameAllocated(0, allocator);
313 std::array<const char*, 1> inputNames({inputName.get()});
314 std::array<const char*, 1> outputNames({outputName.get()});
316 Ort::TypeInfo typeInfo = session.GetOutputTypeInfo(0);
317 auto tensorInfoOutput = typeInfo.GetTensorTypeAndShapeInfo();
318 std::vector<int64_t> shape = tensorInfoOutput.GetShape();
320 // this shape is supposed to always be: [number*5]
321 assert(shape[1] == 5 * number);
324 auto outputTensors = session.Run(Ort::RunOptions{nullptr}, inputNames.data(),
325 &inputTensor, 1, outputNames.data(), 1);
328 const float* outputData = outputTensors[0].GetTensorData<float>();
330 // NN outputs 5 numbers
331 return PositionNNoutput(outputData, outputData + (number * 5));
335} // namespace ActsTrk::detail
337#endif // NNCLUSTERINGTOOLIMPL_ICC