Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-12 07:36:54

0001 // This file is part of the ACTS project.
0002 //
0003 // Copyright (C) 2016 CERN for the benefit of the ACTS project
0004 //
0005 // This Source Code Form is subject to the terms of the Mozilla Public
0006 // License, v. 2.0. If a copy of the MPL was not distributed with this
0007 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
0008 
0009 #include "ActsExamples/Io/Arrow/ColliderMLRelease1InputConverter.hpp"
0010 
0011 #include "Acts/Definitions/TrackParametrization.hpp"
0012 #include "Acts/Geometry/TrackingGeometry.hpp"
0013 #include "Acts/Surfaces/Surface.hpp"
0014 #include "ActsExamples/Digitization/MeasurementCreation.hpp"
0015 #include "ActsExamples/EventData/Index.hpp"
0016 #include "ActsExamples/EventData/TruthMatching.hpp"
0017 #include "ActsExamples/Framework/AlgorithmContext.hpp"
0018 #include "ActsExamples/Io/Csv/CsvInputOutput.hpp"
0019 #include "ActsPlugins/Arrow/ArrowUtil.hpp"
0020 
0021 #include <cmath>
0022 #include <cstdint>
0023 #include <limits>
0024 #include <memory>
0025 #include <numeric>
0026 #include <stdexcept>
0027 #include <string>
0028 #include <unordered_set>
0029 #include <utility>
0030 #include <vector>
0031 
0032 #include <arrow/api.h>
0033 
0034 namespace ActsExamples {
0035 
0036 namespace {
0037 
0038 // Return the row-0 (offset, length) for a list column in the table.
0039 std::pair<std::int64_t, std::int64_t> rowBounds(const arrow::Table& table,
0040                                                 const std::string& name) {
0041   auto col = table.GetColumnByName(name);
0042   if (!col) {
0043     throw std::runtime_error(
0044         "ColliderMLRelease1InputConverter: missing column '" + name + "'");
0045   }
0046   auto listArr = std::dynamic_pointer_cast<arrow::ListArray>(col->chunk(0));
0047   if (!listArr) {
0048     throw std::runtime_error("ColliderMLRelease1InputConverter: column '" +
0049                              name +
0050                              "' is not a list array (expected nested layout)");
0051   }
0052   return {listArr->value_offset(0), listArr->value_length(0)};
0053 }
0054 
0055 // Return the typed flat-values array for a list column.
0056 template <typename ArrowArrayType>
0057 std::shared_ptr<ArrowArrayType> colValues(const arrow::Table& table,
0058                                           const std::string& name) {
0059   auto col = table.GetColumnByName(name);
0060   if (!col) {
0061     throw std::runtime_error(
0062         "ColliderMLRelease1InputConverter: missing column '" + name + "'");
0063   }
0064   auto listArr = std::dynamic_pointer_cast<arrow::ListArray>(col->chunk(0));
0065   if (!listArr) {
0066     throw std::runtime_error("ColliderMLRelease1InputConverter: column '" +
0067                              name +
0068                              "' is not a list array (expected nested layout)");
0069   }
0070   auto values = std::dynamic_pointer_cast<ArrowArrayType>(listArr->values());
0071   if (!values) {
0072     throw std::runtime_error("ColliderMLRelease1InputConverter: column '" +
0073                              name + "' has unexpected value type");
0074   }
0075   return values;
0076 }
0077 
0078 std::unordered_map<Acts::GeometryIdentifier, Acts::GeometryIdentifier>
0079 loadGeoIdMapFromCsv(const std::filesystem::path& path,
0080                     const std::string& srcPrefix,
0081                     const std::string& tgtPrefix) {
0082   CsvReader reader(path.string());
0083   std::vector<std::string> columns;
0084 
0085   if (!reader.read(columns)) {
0086     throw std::runtime_error("loadGeoIdMapFromCsv: empty file '" +
0087                              path.string() + "'");
0088   }
0089 
0090   auto findCol = [&](const std::string& name) -> std::size_t {
0091     for (std::size_t i = 0; i < columns.size(); ++i) {
0092       if (columns[i] == name) {
0093         return i;
0094       }
0095     }
0096     throw std::runtime_error("loadGeoIdMapFromCsv: missing column '" + name +
0097                              "' in '" + path.string() + "'");
0098   };
0099 
0100   const std::size_t iExtra = findCol(srcPrefix + "_extra");
0101   const std::size_t iVolume = findCol(srcPrefix + "_volume");
0102   const std::size_t iLayer = findCol(srcPrefix + "_layer");
0103   const std::size_t iSensitive = findCol(srcPrefix + "_sensitive");
0104   const std::size_t iTarget = findCol(tgtPrefix + "_packed");
0105 
0106   std::unordered_map<Acts::GeometryIdentifier, Acts::GeometryIdentifier> map;
0107   while (reader.read(columns)) {
0108     auto key =
0109         Acts::GeometryIdentifier()
0110             .withExtra(static_cast<std::uint32_t>(std::stoul(columns[iExtra])))
0111             .withVolume(
0112                 static_cast<std::uint32_t>(std::stoul(columns[iVolume])))
0113             .withLayer(static_cast<std::uint32_t>(std::stoul(columns[iLayer])))
0114             .withSensitive(
0115                 static_cast<std::uint32_t>(std::stoul(columns[iSensitive])));
0116     map.emplace(key, Acts::GeometryIdentifier(std::stoull(columns[iTarget])));
0117   }
0118   return map;
0119 }
0120 
0121 }  // namespace
0122 
0123 // ---------------------------------------------------------------------------
0124 // Schemas
0125 // ---------------------------------------------------------------------------
0126 
0127 ActsPlugins::ArrowUtil::ArrowSchemaHandle
0128 ColliderMLRelease1InputConverter::particleSchema() {
0129   return ActsPlugins::ArrowUtil::ArrowSchemaHandle{arrow::schema({
0130       arrow::field("particle_id", arrow::list(arrow::uint64()), false),
0131       arrow::field("pdg_id", arrow::list(arrow::int64()), false),
0132       arrow::field("mass", arrow::list(arrow::float32()), false),
0133       arrow::field("charge", arrow::list(arrow::float32()), false),
0134       arrow::field("vx", arrow::list(arrow::float32()), false),
0135       arrow::field("vy", arrow::list(arrow::float32()), false),
0136       arrow::field("vz", arrow::list(arrow::float32()), false),
0137       arrow::field("time", arrow::list(arrow::float32()), false),
0138       arrow::field("px", arrow::list(arrow::float32()), false),
0139       arrow::field("py", arrow::list(arrow::float32()), false),
0140       arrow::field("pz", arrow::list(arrow::float32()), false),
0141       arrow::field("vertex_primary", arrow::list(arrow::uint16()), false),
0142       arrow::field("primary", arrow::list(arrow::boolean()), false),
0143   })};
0144 }
0145 
0146 ActsPlugins::ArrowUtil::ArrowSchemaHandle
0147 ColliderMLRelease1InputConverter::hitSchema() {
0148   return ActsPlugins::ArrowUtil::ArrowSchemaHandle{arrow::schema({
0149       arrow::field("x", arrow::list(arrow::float32()), false),
0150       arrow::field("y", arrow::list(arrow::float32()), false),
0151       arrow::field("z", arrow::list(arrow::float32()), false),
0152       arrow::field("true_x", arrow::list(arrow::float32()), false),
0153       arrow::field("true_y", arrow::list(arrow::float32()), false),
0154       arrow::field("true_z", arrow::list(arrow::float32()), false),
0155       arrow::field("time", arrow::list(arrow::float32()), false),
0156       arrow::field("particle_id", arrow::list(arrow::uint64()), false),
0157       arrow::field("detector", arrow::list(arrow::uint8()), false),
0158       arrow::field("volume_id", arrow::list(arrow::uint8()), false),
0159       arrow::field("layer_id", arrow::list(arrow::uint16()), false),
0160       arrow::field("surface_id", arrow::list(arrow::uint32()), false),
0161   })};
0162 }
0163 
0164 // ---------------------------------------------------------------------------
0165 // ColliderMLRelease1InputConverter
0166 // ---------------------------------------------------------------------------
0167 
0168 ColliderMLRelease1InputConverter::ColliderMLRelease1InputConverter(
0169     const Config& cfg, std::unique_ptr<const Acts::Logger> _logger)
0170     : IAlgorithm("ColliderMLRelease1InputConverter", std::move(_logger)),
0171       m_cfg(cfg) {
0172   if (m_cfg.inputParticlesTable.empty()) {
0173     throw std::invalid_argument("inputParticlesTable must be set");
0174   }
0175   if (m_cfg.inputHitsTable.empty()) {
0176     throw std::invalid_argument("inputHitsTable must be set");
0177   }
0178   if (m_cfg.outputParticles.empty() && m_cfg.outputSimHits.empty() &&
0179       m_cfg.outputMeasurements.empty()) {
0180     throw std::invalid_argument(
0181         "at least one output (outputParticles, outputSimHits, "
0182         "outputMeasurements) must be set");
0183   }
0184 
0185   if (!m_cfg.outputMeasurements.empty()) {
0186     if (m_cfg.trackingGeometry == nullptr) {
0187       throw std::invalid_argument(
0188           "trackingGeometry is required for outputMeasurements");
0189     }
0190   }
0191 
0192   // ColliderML Release 1 volume_id → (BoundIndex, pitch in mm) pairs.
0193   // σ = pitch / √12 (binary readout, uniform distribution).
0194   auto sigmaFromPitch = [](double pitch) { return pitch / std::sqrt(12.0); };
0195   struct PitchEntry {
0196     std::uint8_t volumeId;
0197     std::vector<std::pair<Acts::BoundIndices, double>> pitches;
0198   };
0199   // clang-format off
0200   static const std::vector<PitchEntry> kPitchData = {
0201       // Pixel: 50×50 µm
0202       {16, {{Acts::eBoundLoc0, 0.050}, {Acts::eBoundLoc1, 0.050}}},
0203       {17, {{Acts::eBoundLoc0, 0.050}, {Acts::eBoundLoc1, 0.050}}},
0204       {18, {{Acts::eBoundLoc0, 0.050}, {Acts::eBoundLoc1, 0.050}}},
0205       // Short strip: 80×500 µm
0206       {23, {{Acts::eBoundLoc0, 0.080}, {Acts::eBoundLoc1, 0.500}}},
0207       {24, {{Acts::eBoundLoc0, 0.080}, {Acts::eBoundLoc1, 0.500}}},
0208       {25, {{Acts::eBoundLoc0, 0.080}, {Acts::eBoundLoc1, 0.500}}},
0209       // Long strip: 125 µm (vol 28, 30), 100 µm (vol 29)
0210       {28, {{Acts::eBoundLoc0, 0.125}}},
0211       {29, {{Acts::eBoundLoc0, 0.100}}},
0212       {30, {{Acts::eBoundLoc0, 0.125}}},
0213   };
0214   // clang-format on
0215   for (const auto& entry : kPitchData) {
0216     std::vector<std::pair<Acts::BoundIndices, double>> sigmas;
0217     sigmas.reserve(entry.pitches.size());
0218     for (const auto& [idx, pitch] : entry.pitches) {
0219       sigmas.emplace_back(idx, sigmaFromPitch(pitch));
0220     }
0221     m_subsystemSigmas.emplace(entry.volumeId, std::move(sigmas));
0222   }
0223 
0224   m_inputParticles.initialize(m_cfg.inputParticlesTable);
0225   m_inputHits.initialize(m_cfg.inputHitsTable);
0226 
0227   m_outputParticles.maybeInitialize(m_cfg.outputParticles);
0228   m_outputSimHits.maybeInitialize(m_cfg.outputSimHits);
0229   m_outputMeasurements.maybeInitialize(m_cfg.outputMeasurements);
0230   m_outputClusters.maybeInitialize(m_cfg.outputClusters);
0231   m_outputMeasurementSubset.maybeInitialize(m_cfg.outputMeasurementSubset);
0232   m_outputMeasSimHitsMap.maybeInitialize(m_cfg.outputMeasSimHitsMap);
0233   m_outputMeasParticlesMap.maybeInitialize(m_cfg.outputMeasParticlesMap);
0234   m_outputParticleMeasurementsMap.maybeInitialize(
0235       m_cfg.outputParticleMeasurementsMap);
0236 
0237   if (!m_cfg.geoIdMapPath.empty()) {
0238     m_geoIdMap =
0239         loadGeoIdMapFromCsv(m_cfg.geoIdMapPath, m_cfg.geoIdMapSourcePrefix,
0240                             m_cfg.geoIdMapTargetPrefix);
0241     ACTS_INFO("Loaded geo-ID map with " << m_geoIdMap.size() << " entries from "
0242                                         << m_cfg.geoIdMapPath);
0243   } else if (m_cfg.trackingGeometry != nullptr) {
0244     for (const auto& [gid, surface] :
0245          m_cfg.trackingGeometry->geoIdSurfaceMap()) {
0246       if (gid.sensitive() == 0) {
0247         continue;
0248       }
0249       Acts::GeometryIdentifier key = Acts::GeometryIdentifier()
0250                                          .withVolume(gid.volume())
0251                                          .withLayer(gid.layer())
0252                                          .withSensitive(gid.sensitive());
0253       m_geoIdMap[key] = gid;
0254     }
0255     ACTS_INFO(
0256         "No geoIdMap CSV provided — geometry IDs resolved by matching "
0257         "(volume, layer, sensitive) from the tracking geometry.");
0258     ACTS_DEBUG("Built (vol, lay, sen) fallback map with " << m_geoIdMap.size()
0259                                                           << " entries.");
0260   }
0261 }
0262 
0263 ColliderMLRelease1InputConverter::ColliderMLRelease1InputConverter(
0264     const Config& cfg, Acts::Logging::Level level)
0265     : ColliderMLRelease1InputConverter(
0266           cfg,
0267           Acts::getDefaultLogger("ColliderMLRelease1InputConverter", level)) {}
0268 
0269 ColliderMLRelease1InputConverter::~ColliderMLRelease1InputConverter() = default;
0270 
0271 ProcessCode ColliderMLRelease1InputConverter::execute(
0272     const AlgorithmContext& ctx) const {
0273   const arrow::Table& particleTable = *m_inputParticles(ctx).table();
0274   const arrow::Table& hitsTable = *m_inputHits(ctx).table();
0275 
0276   // ------------------------------------------------------------------
0277   // 0. Build set of particle IDs that have at least one hit (optional).
0278   //    The particle_id in the hits table is the row index in the particles
0279   //    table. Pre-scanning here lets the particle loop skip unreferenced
0280   //    particles without a second pass.
0281   // ------------------------------------------------------------------
0282   std::unordered_set<std::uint64_t> particleIdsWithHits;
0283   if (!m_cfg.keepParticlesWithoutHits) {
0284     auto [hpOff, nHitsForFilter] = rowBounds(hitsTable, "particle_id");
0285     auto hpidFilterArr =
0286         colValues<arrow::UInt64Array>(hitsTable, "particle_id");
0287     for (std::int64_t i = 0; i < nHitsForFilter; ++i) {
0288       particleIdsWithHits.insert(hpidFilterArr->Value(hpOff + i));
0289     }
0290   }
0291 
0292   // ------------------------------------------------------------------
0293   // 1. Parse particles table → SimParticleContainer + barcode index
0294   // ------------------------------------------------------------------
0295   auto [pOff, nParticles] = rowBounds(particleTable, "particle_id");
0296   auto pidArr = colValues<arrow::UInt64Array>(particleTable, "particle_id");
0297   auto pdgArr = colValues<arrow::Int64Array>(particleTable, "pdg_id");
0298   auto massArr = colValues<arrow::FloatArray>(particleTable, "mass");
0299   auto chargeArr = colValues<arrow::FloatArray>(particleTable, "charge");
0300   auto vxArr = colValues<arrow::FloatArray>(particleTable, "vx");
0301   auto vyArr = colValues<arrow::FloatArray>(particleTable, "vy");
0302   auto vzArr = colValues<arrow::FloatArray>(particleTable, "vz");
0303   auto vtArr = colValues<arrow::FloatArray>(particleTable, "time");
0304   auto pxArr = colValues<arrow::FloatArray>(particleTable, "px");
0305   auto pyArr = colValues<arrow::FloatArray>(particleTable, "py");
0306   auto pzArr = colValues<arrow::FloatArray>(particleTable, "pz");
0307   auto vprimArr =
0308       colValues<arrow::UInt16Array>(particleTable, "vertex_primary");
0309   auto primaryArr = colValues<arrow::BooleanArray>(particleTable, "primary");
0310 
0311   // barcode vector indexed by ColliderML particle row-index
0312   std::vector<SimBarcode> barcodes(static_cast<std::size_t>(nParticles));
0313 
0314   SimParticleContainer::sequence_type particleSeq;
0315   particleSeq.reserve(static_cast<std::size_t>(nParticles));
0316 
0317   for (std::int64_t i = 0; i < nParticles; ++i) {
0318     const std::uint16_t vp = vprimArr->Value(pOff + i);
0319     const bool isPrimary = primaryArr->Value(pOff + i);
0320 
0321     SimBarcode bc = SimBarcode()
0322                         .withVertexPrimary(vp)
0323                         .withParticle(static_cast<std::uint64_t>(i))
0324                         .withGeneration(isPrimary ? 0u : 1u);
0325     barcodes[static_cast<std::size_t>(i)] = bc;
0326 
0327     if (!m_cfg.keepParticlesWithoutHits &&
0328         particleIdsWithHits.count(static_cast<std::uint64_t>(i)) == 0) {
0329       continue;
0330     }
0331 
0332     const double mass = static_cast<double>(massArr->Value(pOff + i));
0333     const double charge = static_cast<double>(chargeArr->Value(pOff + i));
0334     const auto pdg =
0335         Acts::PdgParticle{static_cast<int>(pdgArr->Value(pOff + i))};
0336 
0337     SimParticleState state(bc, pdg, charge, mass);
0338     state.setPosition4(vxArr->Value(pOff + i), vyArr->Value(pOff + i),
0339                        vzArr->Value(pOff + i), vtArr->Value(pOff + i));
0340     const double px = pxArr->Value(pOff + i);
0341     const double py = pyArr->Value(pOff + i);
0342     const double pz = pzArr->Value(pOff + i);
0343     state.setDirection(px, py, pz);
0344     state.setAbsoluteMomentum(std::hypot(px, py, pz));
0345 
0346     particleSeq.emplace_back(state, state);
0347   }
0348 
0349   SimParticleContainer particles;
0350   particles.insert(particleSeq.begin(), particleSeq.end());
0351 
0352   if (m_outputParticles.isInitialized()) {
0353     m_outputParticles(ctx, SimParticleContainer(particles));
0354   }
0355 
0356   // ------------------------------------------------------------------
0357   // 2. Parse hits table
0358   // ------------------------------------------------------------------
0359   const bool needSimHits = m_outputSimHits.isInitialized();
0360   const bool needMeasurements = m_outputMeasurements.isInitialized();
0361 
0362   if (!needSimHits && !needMeasurements) {
0363     return ProcessCode::SUCCESS;
0364   }
0365 
0366   auto [hOff, nHits] = rowBounds(hitsTable, "x");
0367   auto hxArr = colValues<arrow::FloatArray>(hitsTable, "x");
0368   auto hyArr = colValues<arrow::FloatArray>(hitsTable, "y");
0369   auto hzArr = colValues<arrow::FloatArray>(hitsTable, "z");
0370   auto txArr = colValues<arrow::FloatArray>(hitsTable, "true_x");
0371   auto tyArr = colValues<arrow::FloatArray>(hitsTable, "true_y");
0372   auto tzArr = colValues<arrow::FloatArray>(hitsTable, "true_z");
0373   auto htArr = colValues<arrow::FloatArray>(hitsTable, "time");
0374   auto hpidArr = colValues<arrow::UInt64Array>(hitsTable, "particle_id");
0375   auto detArr = colValues<arrow::UInt8Array>(hitsTable, "detector");
0376   auto volArr = colValues<arrow::UInt8Array>(hitsTable, "volume_id");
0377   auto layerArr = colValues<arrow::UInt16Array>(hitsTable, "layer_id");
0378   auto surfArr = colValues<arrow::UInt32Array>(hitsTable, "surface_id");
0379 
0380   SimHitContainer simHits;
0381   MeasurementContainer measurements;
0382   ClusterContainer clusters;
0383   // Maps loop index i → measurement index; used to cross-reference simhits
0384   // and measurements after the container sorts by geoId.
0385   std::unordered_map<std::int32_t, Index> hitIndexToMeas;
0386   MeasurementParticlesMap measParticlesMap;
0387 
0388   if (needMeasurements) {
0389     measurements.reserve(static_cast<std::size_t>(nHits));
0390     clusters.reserve(static_cast<std::size_t>(nHits));
0391   }
0392 
0393   for (std::int64_t i = 0; i < nHits; ++i) {
0394     const std::uint8_t cmlDet = detArr->Value(hOff + i);
0395     const std::uint8_t cmlVol = volArr->Value(hOff + i);
0396     const std::uint16_t cmlLay = layerArr->Value(hOff + i);
0397     const std::uint32_t cmlSurf = surfArr->Value(hOff + i);
0398     const auto cmlKey = Acts::GeometryIdentifier()
0399                             .withExtra(cmlDet)
0400                             .withVolume(cmlVol)
0401                             .withLayer(cmlLay)
0402                             .withSensitive(cmlSurf);
0403 
0404     const auto lookupKey = m_cfg.geoIdMapPath.empty()
0405                                ? Acts::GeometryIdentifier()
0406                                      .withVolume(cmlVol)
0407                                      .withLayer(cmlLay)
0408                                      .withSensitive(cmlSurf)
0409                                : cmlKey;
0410     auto geoIt = m_geoIdMap.find(lookupKey);
0411     if (geoIt == m_geoIdMap.end()) {
0412       ACTS_ERROR("Hit " << i << " (det=" << +cmlDet << " vol=" << +cmlVol
0413                         << " lay=" << cmlLay << " surf=" << cmlSurf
0414                         << ") not found in geo-ID map");
0415       return ProcessCode::ABORT;
0416     }
0417     const Acts::GeometryIdentifier geoId = geoIt->second;
0418 
0419     const std::uint64_t cmlPid = hpidArr->Value(hOff + i);
0420     SimBarcode barcode{};
0421     if (cmlPid < static_cast<std::uint64_t>(barcodes.size())) {
0422       barcode = barcodes[static_cast<std::size_t>(cmlPid)];
0423     }
0424 
0425     const double tx = txArr->Value(hOff + i);
0426     const double ty = tyArr->Value(hOff + i);
0427     const double tz = tzArr->Value(hOff + i);
0428     const double tt = htArr->Value(hOff + i);
0429 
0430     if (needSimHits) {
0431       Acts::Vector4 pos4{tx, ty, tz, tt};
0432       Acts::Vector4 zero4 = Acts::Vector4::Zero();
0433       simHits.emplace_hint(simHits.end(), geoId, barcode, pos4, zero4, zero4,
0434                            static_cast<std::int32_t>(i));
0435     }
0436 
0437     if (needMeasurements) {
0438       auto sigmaIt = m_subsystemSigmas.find(cmlVol);
0439       if (sigmaIt == m_subsystemSigmas.end()) {
0440         ACTS_ERROR("Hit " << i << " ColliderML volume_id " << +cmlVol
0441                           << " is not a known tracker subsystem");
0442         return ProcessCode::ABORT;
0443       }
0444 
0445       const Acts::Surface* surface = m_cfg.trackingGeometry->findSurface(geoId);
0446       if (surface == nullptr) {
0447         ACTS_ERROR("Hit " << i << " geoId " << geoId
0448                           << " not found in tracking geometry");
0449         return ProcessCode::ABORT;
0450       }
0451 
0452       const auto* regSurface =
0453           dynamic_cast<const Acts::RegularSurface*>(surface);
0454       if (regSurface == nullptr) {
0455         ACTS_ERROR("Hit " << i << " geoId " << geoId
0456                           << " surface is not a RegularSurface — unsupported");
0457         return ProcessCode::ABORT;
0458       }
0459 
0460       Acts::Vector3 globalPos{static_cast<double>(hxArr->Value(hOff + i)),
0461                               static_cast<double>(hyArr->Value(hOff + i)),
0462                               static_cast<double>(hzArr->Value(hOff + i))};
0463 
0464       auto localResult = regSurface->globalToLocal(
0465           ctx.geoContext, globalPos, std::numeric_limits<double>::max());
0466       if (!localResult.ok() ||
0467           !surface->bounds().inside(localResult.value(),
0468                                     Acts::BoundaryTolerance::AbsoluteEuclidean(
0469                                         m_cfg.hitBoundsTolerance))) {
0470         ACTS_ERROR(
0471             "Hit " << i << " geoId " << geoId
0472                    << " projected local position outside sensor bounds (tol="
0473                    << m_cfg.hitBoundsTolerance << " mm)");
0474         return ProcessCode::ABORT;
0475       }
0476       const Acts::Vector2& lp = localResult.value();
0477 
0478       DigitizedParameters dParams;
0479       dParams.cluster.globalPosition = globalPos;
0480       for (const auto& [idx, sigma] : sigmaIt->second) {
0481         dParams.indices.push_back(idx);
0482         dParams.values.push_back(lp[static_cast<int>(idx)]);
0483         dParams.variances.push_back(sigma * sigma);
0484       }
0485 
0486       clusters.push_back(std::move(dParams.cluster));
0487       auto meas = createMeasurement(measurements, geoId, dParams);
0488       const Index measIdx = meas.index();
0489 
0490       hitIndexToMeas.emplace(static_cast<std::int32_t>(i), measIdx);
0491       if (barcode != SimBarcode{}) {
0492         measParticlesMap.emplace(measIdx, barcode);
0493       }
0494     }
0495   }
0496 
0497   // Build measSimHitsMap from the sorted SimHitContainer, using the temporary
0498   // index() as cross-reference, then reset index() to -1 (undefined).
0499   MeasurementSimHitsMap measSimHitsMap;
0500   if (needMeasurements && needSimHits && !hitIndexToMeas.empty()) {
0501     SimHitIndex sortedPos = 0;
0502     for (auto& hit : simHits) {
0503       auto it = hitIndexToMeas.find(hit.index());
0504       if (it != hitIndexToMeas.end()) {
0505         measSimHitsMap.emplace(it->second, sortedPos);
0506       }
0507       hit = SimHit(hit.geometryId(), hit.particleId(), hit.fourPosition(),
0508                    hit.momentum4Before(), hit.momentum4After(), -1);
0509       ++sortedPos;
0510     }
0511   }
0512 
0513   if (needSimHits) {
0514     m_outputSimHits(ctx, std::move(simHits));
0515   }
0516 
0517   if (needMeasurements) {
0518     const auto& storedMeasurements =
0519         m_outputMeasurements(ctx, std::move(measurements));
0520     if (m_outputClusters.isInitialized()) {
0521       m_outputClusters(ctx, std::move(clusters));
0522     }
0523 
0524     if (m_outputMeasurementSubset.isInitialized()) {
0525       std::vector<MeasurementContainer::Index> allIndices(
0526           storedMeasurements.size());
0527       std::iota(allIndices.begin(), allIndices.end(), Index{0});
0528       m_outputMeasurementSubset(
0529           ctx, MeasurementSubset(storedMeasurements, std::move(allIndices)));
0530     }
0531     if (m_outputMeasSimHitsMap.isInitialized()) {
0532       m_outputMeasSimHitsMap(ctx, std::move(measSimHitsMap));
0533     }
0534     if (m_outputParticleMeasurementsMap.isInitialized()) {
0535       m_outputParticleMeasurementsMap(ctx,
0536                                       invertIndexMultimap(measParticlesMap));
0537     }
0538     if (m_outputMeasParticlesMap.isInitialized()) {
0539       m_outputMeasParticlesMap(ctx, std::move(measParticlesMap));
0540     }
0541   }
0542 
0543   return ProcessCode::SUCCESS;
0544 }
0545 
0546 }  // namespace ActsExamples