Back to home page

EIC code displayed by LXR

 
 

    


Warning, file /acts/Core/src/Seeding/GbtsGraphBuilder.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

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 "Acts/Seeding/GbtsGraphBuilder.hpp"
0010 
0011 #include "Acts/SpacePointFormation/detail/StripSpacePointCalibrationImpl.hpp"
0012 #include "Acts/Utilities/MathHelpers.hpp"
0013 
0014 #include <algorithm>
0015 #include <array>
0016 #include <cmath>
0017 #include <cstdint>
0018 #include <numbers>
0019 #include <span>
0020 #include <stdexcept>
0021 #include <utility>
0022 #include <vector>
0023 
0024 namespace Acts::Experimental {
0025 
0026 namespace {
0027 
0028 /// Sliding window in phi used to define range used for edge creation.
0029 ///
0030 /// Covers one non-empty source eta bin, whose phi-ordered node list it holds
0031 /// directly so that the innermost loop does not reach through the bin.
0032 struct SlidingWindow {
0033   /// phi-ordered nodes of the bin, including the wrap-around duplicates
0034   const std::pair<float, SpacePointIndex>* phiNodes{};
0035   /// number of entries in @c phiNodes
0036   std::uint32_t numPhiNodes{};
0037   /// sliding window position
0038   std::uint32_t firstIt{};
0039   /// window half-width;
0040   float deltaPhi{};
0041   /// Inside-out pixel barrel ordinal of the bin's layer, -1 for the rest.
0042   std::int32_t barrelOrder{-1};
0043   /// Type of the bin's layer.
0044   GbtsLayerType type{};
0045   /// Technology of the bin's layer.
0046   GbtsLayerTechnology technology{};
0047 };
0048 
0049 /// Check a triplet against the pT and d0 cuts.
0050 /// @param cfg Graph builder configuration
0051 /// @param nodeView View of the node positions and layers
0052 /// @param candidateTriplet The three graph nodes
0053 /// @param tripletMinPt Minimum transverse momentum
0054 /// @param tauRatio Tau ratio of the triplet
0055 /// @param tauRatioCut Tau ratio cut threshold
0056 /// @param bFieldInZ Magnetic field in z, in GeV/(e*mm)
0057 /// @return Whether the triplet is accepted
0058 bool validateTriplet(const GbtsGraphBuilder::Config& cfg,
0059                      const detail::GbtsNodeView& nodeView,
0060                      const std::array<SpacePointIndex, 3>& candidateTriplet,
0061                      const float tripletMinPt, const float tauRatio,
0062                      const float tauRatioCut, const float bFieldInZ) {
0063   // conformal mapping with the center at the middle spacepoint
0064 
0065   std::array<float, 2> u{};
0066   std::array<float, 2> v{};
0067 
0068   const detail::GbtsNodeProxy n0 = nodeView[candidateTriplet[1]];
0069 
0070   const float x0 = n0.x();
0071   const float y0 = n0.y();
0072 
0073   const float r0 = n0.r();
0074 
0075   const float cosA = x0 / r0;
0076 
0077   const float sinA = y0 / r0;
0078 
0079   for (std::uint32_t k = 0; k < 2; k++) {
0080     const std::uint32_t spIdx = (k == 1) ? 2 : k;
0081 
0082     const detail::GbtsNodeProxy nk = nodeView[candidateTriplet[spIdx]];
0083 
0084     const float dx = nk.x() - x0;
0085 
0086     const float dy = nk.y() - y0;
0087 
0088     const float r2Inv = 1.0f / (dx * dx + dy * dy);
0089 
0090     const float xn = dx * cosA + dy * sinA;
0091 
0092     const float yn = -dx * sinA + dy * cosA;
0093 
0094     u[k] = xn * r2Inv;
0095     v[k] = yn * r2Inv;
0096   }
0097 
0098   const float du = u[0] - u[1];
0099 
0100   if (du == 0.0) {
0101     return false;
0102   }
0103 
0104   const float A = (v[0] - v[1]) / du;
0105 
0106   const float B = v[1] - A * u[1];
0107 
0108   const float d0 = r0 * (B * r0 - A);
0109 
0110   if (std::abs(d0) > cfg.d0Max) {
0111     return false;
0112   }
0113 
0114   if (B != 0.0) {  // straight-line track is OK
0115 
0116     const float R = std::sqrt(1 + A * A) / B;
0117 
0118     // 1T magnetic field used
0119     const float pT = std::abs(bFieldInZ * R / 2);
0120 
0121     if (pT < tripletMinPt) {
0122       return false;
0123     }
0124 
0125     if (pT > 5 * tripletMinPt) {  // relatively high-pT track
0126 
0127       if (tauRatio > 0.9f * tauRatioCut) {
0128         return false;
0129       }
0130     }
0131   }
0132 
0133   return true;
0134 }
0135 
0136 /// Check to see if z0 of segment is within the expected z range of the
0137 /// beamspot
0138 /// @param cfg Graph builder configuration
0139 /// @param z0BitMask Sets allowed bins of allowed z value
0140 /// @param z0 Estimated z0 of segments z value at beamspot
0141 /// @param z0HistoCoeff Scalfactor that converts z coodindate into bin index
0142 /// @return Whether segment is within beamspot range
0143 bool checkZ0BitMask(const GbtsGraphBuilder::Config& cfg,
0144                     const std::uint16_t z0BitMask, const float z0,
0145                     const float z0HistoCoeff) {
0146   if (z0BitMask == 0) {
0147     return true;
0148   }
0149 
0150   // z0 is not yet range checked here -- doubletFilterRZ runs after this -- so
0151   // a bin outside the histogram is reachable and must not reach the shift.
0152   const auto isSet = [z0BitMask](const std::int32_t bin) {
0153     return bin >= 0 && bin < detail::kGbtsZ0HistogramBins &&
0154            ((z0BitMask >> bin) & 1) != 0;
0155   };
0156 
0157   const float dz = z0 - cfg.minZ0;
0158   const std::int32_t z0BinIndex = static_cast<std::int32_t>(z0HistoCoeff * dz);
0159 
0160   if (isSet(z0BinIndex)) {
0161     return true;
0162   }
0163 
0164   // check adjacent bins as well
0165 
0166   const float dzm = dz - cfg.z0Resolution;
0167 
0168   std::int32_t nextBin = static_cast<std::int32_t>(z0HistoCoeff * dzm);
0169 
0170   if (nextBin != z0BinIndex && isSet(nextBin)) {
0171     return true;
0172   }
0173 
0174   const float dzp = dz + cfg.z0Resolution;
0175 
0176   nextBin = static_cast<std::int32_t>(z0HistoCoeff * dzp);
0177 
0178   if (nextBin != z0BinIndex && isSet(nextBin)) {
0179     return true;
0180   }
0181 
0182   return false;
0183 }
0184 
0185 }  // namespace
0186 
0187 GbtsGraphBuilder::GbtsGraphBuilder(const Config& config,
0188                                    std::shared_ptr<const GbtsGeometry> geometry,
0189                                    std::unique_ptr<const Acts::Logger> logger)
0190     : m_cfg(config),
0191       m_geometry(std::move(geometry)),
0192       m_logger(std::move(logger)) {
0193   // buildTheGraph pre-computes the loosest tau ratio threshold it can apply,
0194   // which assumes the corrections only ever widen the cut.
0195   if (m_cfg.tauRatioCorr < 0) {
0196     throw std::invalid_argument(
0197         "GbtsGraphBuilder: tauRatioCorr must not be negative");
0198   }
0199 
0200   if (m_cfg.tauRatioCorrStrip < 0) {
0201     throw std::invalid_argument(
0202         "GbtsGraphBuilder: tauRatioCorrStrip must not be negative");
0203   }
0204 }
0205 
0206 GbtsGraph GbtsGraphBuilder::buildTheGraph(const GbtsRoiDescriptor& roi,
0207                                           GbtsNodeStorage& nodeStorage,
0208                                           const float bFieldInZ) const {
0209   GbtsGraph graph;
0210   std::vector<detail::GbtsEdge>& edgeStorage = graph.edgeStorage;
0211 
0212   // used to calculate Z cut on doublets
0213   const float cutZMinU =
0214       m_cfg.minZ0 + m_cfg.maxOuterRadius * static_cast<float>(roi.dzdrMin());
0215   const float cutZMaxU =
0216       m_cfg.maxZ0 + m_cfg.maxOuterRadius * static_cast<float>(roi.dzdrMax());
0217 
0218   const float tripletPtMin = m_cfg.tripletPtFraction * m_cfg.minPt;
0219 
0220   const float ptScale = m_cfg.tuningPt / m_cfg.minPt;
0221 
0222   const float curvatureCutHighEta = m_cfg.maxCurvatureHighEta * ptScale;
0223   const float curvatureCutLowEta = m_cfg.maxCurvatureLowEta * ptScale;
0224 
0225   // the loosest tau ratio threshold the triplet matching can apply
0226   const float maxTauRatioCut =
0227       m_cfg.tauRatioCut + (m_cfg.useAdaptiveCuts ? m_cfg.tauRatioCorr : 0.0f) +
0228       (nodeStorage.hasStrips() ? m_cfg.tauRatioCorrStrip : 0.0f);
0229 
0230   // the default sliding window along phi. Taken from the node storage so that
0231   // the windows and the phi indexing they slide over cannot disagree.
0232   const float deltaPhi0 = 0.5f * nodeStorage.config().phiSliceWidth;
0233 
0234   std::uint32_t nConnections = 0;
0235 
0236   // Scales the maximum number of edges by the number of space points using the
0237   // factor maxEdgesPerSP
0238   const auto maxEdges = static_cast<std::size_t>(
0239       m_cfg.maxEdgesPerSP * static_cast<float>(nodeStorage.numberOfNodes()));
0240 
0241   edgeStorage.reserve(maxEdges);
0242 
0243   // scale factor to get indexes of binned beamspot
0244   const float z0HistoCoeff =
0245       detail::kGbtsZ0HistogramBins / (m_cfg.maxZ0 - m_cfg.minZ0 + 1e-6f);
0246 
0247   const detail::GbtsNodeView nodeView = nodeStorage.nodeView();
0248   const std::span<const detail::GbtsNodeParams> params =
0249       nodeStorage.nodeParams();
0250   const std::span<detail::GbtsNodeEdgeInfo> edgeInfo =
0251       nodeStorage.nodeEdgeInfo();
0252 
0253   // reused across bin groups so that the windows are allocated once
0254   std::vector<SlidingWindow> phiSlidingWindow;
0255 
0256   const bool calibrate = m_cfg.calibrateStrips && nodeStorage.hasStrips();
0257 
0258   // Put a strip node back where a direction says it crossed. Both ends, since
0259   // the nominal position sits on the inner strip and the calibrated one on the
0260   // outer. A direction that misses either strip is no crossing at all.
0261   const auto calibrateNode = [&](const SpacePointIndex node,
0262                                  const std::array<float, 3>& direction,
0263                                  float& r, float& z) {
0264     std::array<float, 3> point{};
0265     if (!Acts::detail::calibrateOuterStripSpacePoint(
0266             direction, nodeStorage.strip(node), point,
0267             m_cfg.maxStripLengthFraction)) {
0268       return false;
0269     }
0270     r = fastHypot(point[0], point[1]);
0271     z = point[2];
0272     return true;
0273   };
0274 
0275   // loop over bin groups
0276   for (const auto& bg : m_geometry->binGroups()) {
0277     const detail::GbtsEtaBinInfo& B1 = nodeStorage.etaBin(bg.bin);
0278 
0279     if (B1.empty()) {
0280       continue;
0281     }
0282 
0283     const float rb1 = B1.minRadius;
0284 
0285     const std::int32_t barrelOrder1 = B1.barrelOrder;
0286 
0287     const bool isPixel1 = B1.technology == GbtsLayerTechnology::Pixel;
0288     const bool isPixelBarrel1 = barrelOrder1 >= 0;
0289 
0290     const bool useZ0Histogram =
0291         barrelOrder1 >= 0 && barrelOrder1 <= m_cfg.z0HistogramMaxBarrelOrder;
0292     const bool useMatchBeforeCreate =
0293         m_cfg.matchBeforeCreate && barrelOrder1 >= 0 &&
0294         barrelOrder1 <= m_cfg.matchBeforeCreateMaxBarrelOrder;
0295 
0296     // prepare a sliding window for each non-empty bin2 in the group
0297 
0298     phiSlidingWindow.clear();
0299 
0300     // loop over n2 eta-bins in L2 layers
0301     for (const std::uint32_t b2Idx : bg.links) {
0302       const detail::GbtsEtaBinInfo& B2 = nodeStorage.etaBin(b2Idx);
0303 
0304       if (B2.empty()) {
0305         continue;
0306       }
0307 
0308       const float rb2 = B2.maxRadius;
0309 
0310       float deltaPhi = deltaPhi0;  // the default
0311 
0312       // override the default window width
0313       if (m_cfg.useEtaBinning) {
0314         const float absDr = std::fabs(rb2 - rb1);
0315         if (absDr < m_cfg.phiWindowSplitDeltaRadius) {
0316           deltaPhi = m_cfg.phiWindowNearOffset +
0317                      m_cfg.phiWindowNearSlope * ptScale * absDr;
0318         } else {
0319           deltaPhi = m_cfg.phiWindowFarOffset +
0320                      m_cfg.phiWindowFarSlope * ptScale * absDr;
0321         }
0322       }
0323 
0324       SlidingWindow& window = phiSlidingWindow.emplace_back();
0325       window.phiNodes = B2.phiNodes.data();
0326       window.numPhiNodes = static_cast<std::uint32_t>(B2.phiNodes.size());
0327       window.deltaPhi = deltaPhi;
0328       window.barrelOrder = B2.barrelOrder;
0329       window.type = B2.type;
0330       window.technology = B2.technology;
0331     }
0332 
0333     // in GBTSv3 the outer loop goes over n1 nodes in the Layer 1 bin
0334     for (SpacePointIndex n1Idx = B1.nodes.first; n1Idx < B1.nodes.second;
0335          ++n1Idx) {
0336       // initialization using the top watermark of the edge storage
0337       edgeInfo[n1Idx].firstEdge =
0338           static_cast<std::uint32_t>(edgeStorage.size());
0339 
0340       // the counter for the incoming graph edges created for n1
0341       std::uint16_t numCreatedEdges = 0;
0342 
0343       bool isConnected = false;
0344 
0345       std::array<std::uint8_t, detail::kGbtsZ0HistogramBins> z0Histo = {};
0346 
0347       const detail::GbtsNodeParams& n1pars = params[n1Idx];
0348 
0349       const float phi1 = n1pars.phi;
0350       const float r1 = n1pars.r;
0351       const float z1 = n1pars.z;
0352 
0353       // the chord of a pair, which only the strip path below reads
0354       float x1 = 0.f;
0355       float y1 = 0.f;
0356       if (calibrate) {
0357         const std::array<float, 4>& position = nodeView.positions[n1Idx];
0358         x1 = position[0];
0359         y1 = position[1];
0360       }
0361 
0362       // the intermediate loop over sliding windows
0363       for (auto& slw : phiSlidingWindow) {
0364         const std::int32_t barrelOrder2 = slw.barrelOrder;
0365 
0366         const bool isPixel2 = slw.technology == GbtsLayerTechnology::Pixel;
0367         const bool isPixelBarrel2 = barrelOrder2 >= 0;
0368 
0369         const bool stripPair = calibrate && (!isPixel1 || !isPixel2);
0370 
0371         const float deltaPhi = slw.deltaPhi;
0372 
0373         // sliding window phi1 +/- deltaPhi
0374 
0375         const float minPhi = phi1 - deltaPhi;
0376         const float maxPhi = phi1 + deltaPhi;
0377 
0378         // the inner loop over n2 nodes using sliding window
0379         for (std::uint32_t n2PhiIdx = slw.firstIt; n2PhiIdx < slw.numPhiNodes;
0380              ++n2PhiIdx) {
0381           const float phi2 = slw.phiNodes[n2PhiIdx].first;
0382 
0383           if (phi2 < minPhi) {
0384             // update the window position
0385             slw.firstIt = n2PhiIdx;
0386             continue;
0387           }
0388           if (phi2 > maxPhi) {
0389             // break and go to the next window
0390             break;
0391           }
0392 
0393           const SpacePointIndex n2Idx = slw.phiNodes[n2PhiIdx].second;
0394 
0395           const detail::GbtsNodeEdgeInfo& n2Info = edgeInfo[n2Idx];
0396 
0397           const std::uint16_t nodeInfo = n2Info.isConnected;
0398 
0399           // skip isolated nodes as their incoming edges lead to nowhere
0400           if (useZ0Histogram && (nodeInfo == 0)) {
0401             continue;
0402           }
0403 
0404           const std::uint32_t n2FirstEdge = n2Info.firstEdge;
0405           const std::uint16_t n2NumEdges = n2Info.numEdges;
0406           const std::uint32_t n2LastEdge = n2FirstEdge + n2NumEdges;
0407 
0408           const detail::GbtsNodeParams& n2pars = params[n2Idx];
0409 
0410           const float r2 = n2pars.r;
0411 
0412           float dr = r2 - r1;
0413 
0414           // On the nominal radii, so an endcap pair the slide would have
0415           // opened up is lost here. The cheap reject is worth more.
0416           if (dr < m_cfg.minDeltaRadius) {
0417             continue;
0418           }
0419 
0420           const float z2 = n2pars.z;
0421 
0422           // the ends as the pair puts them: nominal, or slid along the strip
0423           // when resolved. Azimuth is kept as it was -- exactly right in the
0424           // barrel, and to the stereo angle in the endcap, where the strip
0425           // slid along is a hair off radial.
0426           float r1c = r1;
0427           float z1c = z1;
0428           float r2c = r2;
0429           float z2c = z2;
0430 
0431           if (stripPair) {
0432             // A circle's chord bisects the tangent directions, so turning the
0433             // chord by the pair's own azimuth difference gives each tangent.
0434             const std::array<float, 4>& position = nodeView.positions[n2Idx];
0435             const float dx = position[0] - x1;
0436             const float dy = position[1] - y1;
0437             const float dzc = z2 - z1;
0438             const float turn = phi2 - phi1;
0439 
0440             bool resolved = true;
0441             if (!isPixel1) {
0442               resolved = calibrateNode(
0443                   n1Idx, {dx + dy * turn, dy - dx * turn, dzc}, r1c, z1c);
0444             }
0445             if (resolved && !isPixel2) {
0446               resolved = calibrateNode(
0447                   n2Idx, {dx - dy * turn, dy + dx * turn, dzc}, r2c, z2c);
0448             }
0449             if (!resolved) {
0450               continue;
0451             }
0452 
0453             dr = r2c - r1c;
0454             if (dr < m_cfg.minDeltaRadius) {
0455               continue;
0456             }
0457           }
0458 
0459           const float dz = z2c - z1c;
0460           const float tau = dz / dr;
0461           const float ftau = std::fabs(tau);
0462           if (ftau > m_cfg.maxAbsTau) {
0463             continue;
0464           }
0465 
0466           if (ftau < n1pars.minTau) {
0467             continue;
0468           }
0469           if (ftau > n1pars.maxTau) {
0470             continue;
0471           }
0472 
0473           if (ftau < n2pars.minTau) {
0474             continue;
0475           }
0476           if (ftau > n2pars.maxTau) {
0477             continue;
0478           }
0479 
0480           const float z0 = z1c - r1c * tau;
0481 
0482           // check against a non-empty z0 histogram
0483           if (useZ0Histogram &&
0484               !checkZ0BitMask(m_cfg, nodeInfo, z0, z0HistoCoeff)) {
0485             continue;
0486           }
0487 
0488           if (m_cfg.doubletFilterRZ) {
0489             if (z0 < m_cfg.minZ0 || z0 > m_cfg.maxZ0) {
0490               continue;
0491             }
0492 
0493             const float zOuter = z0 + m_cfg.maxOuterRadius * tau;
0494 
0495             if (zOuter < cutZMinU || zOuter > cutZMaxU) {
0496               continue;
0497             }
0498           }
0499 
0500           const float curv = (phi2 - phi1) / dr;
0501 
0502           if (std::abs(curv) > (ftau < m_cfg.curvatureSplitAbsTau
0503                                     ? curvatureCutLowEta
0504                                     : curvatureCutHighEta)) {
0505             continue;
0506           }
0507 
0508           const float hypotTau = fastHypot(1, tau);
0509           const float expEta = hypotTau - tau;
0510           // 1 / expEta, since (hypotTau - tau) * (hypotTau + tau) == 1. The
0511           // sum is also the better conditioned form for large tau.
0512           const float invExpEta = hypotTau + tau;
0513 
0514           // match edge candidate against edges incoming to n2
0515           if (useMatchBeforeCreate) {
0516             // we must have enough incoming edges to decide
0517             bool isGood = n2NumEdges <= m_cfg.matchBeforeCreateMaxEdges;
0518 
0519             if (!isGood) {
0520               const float uat1 = invExpEta;
0521 
0522               for (std::uint32_t n2InIdx = n2FirstEdge; n2InIdx < n2LastEdge;
0523                    ++n2InIdx) {
0524                 const float tau2 = edgeStorage[n2InIdx].p[0];
0525                 const float tauRatio = tau2 * uat1 - 1.0f;
0526 
0527                 if (std::abs(tauRatio) > m_cfg.tauRatioPrecut) {  // bad match
0528                   continue;
0529                 }
0530                 isGood = true;  // good match found
0531                 break;
0532               }
0533             }
0534 
0535             if (!isGood) {  // no match found, skip creating [n1 <- n2] edge
0536               continue;
0537             }
0538           }
0539 
0540           const float dPhi2 = curv * r2c;
0541           const float dPhi1 = curv * r1c;
0542 
0543           if (edgeStorage.size() < maxEdges) {
0544             const auto outEdgeIdx =
0545                 static_cast<std::uint32_t>(edgeStorage.size());
0546 
0547             edgeStorage.emplace_back(n1Idx, n2Idx, barrelOrder2, expEta, curv,
0548                                      phi1 + dPhi1);
0549 
0550             ++numCreatedEdges;
0551 
0552             const float uat2 = invExpEta;
0553             const float phi2u = phi2 + dPhi2;
0554             const float curv2 = curv;
0555 
0556             // looking for neighbours of the new edge
0557             for (std::uint32_t inEdgeIdx = n2FirstEdge; inEdgeIdx < n2LastEdge;
0558                  ++inEdgeIdx) {
0559               detail::GbtsEdge* pS = &edgeStorage[inEdgeIdx];
0560 
0561               const float absTauRatio = std::abs(pS->p[0] * uat2 - 1.0f);
0562 
0563               // rejects most candidates before the layer bookkeeping below
0564               if (absTauRatio > maxTauRatioCut) {
0565                 continue;
0566               }
0567 
0568               if (pS->nNei >= detail::kGbtsMaxEdgeNeighbours) {
0569                 continue;
0570               }
0571 
0572               const std::int32_t barrelOrder3 = pS->n2BarrelOrder;
0573 
0574               const bool isPixelBarrel3 = barrelOrder3 >= 0;
0575 
0576               float addTauRatioCorr = 0;
0577 
0578               if (m_cfg.useAdaptiveCuts) {
0579                 if (isPixelBarrel1 && isPixelBarrel2 && isPixelBarrel3) {
0580                   // three radially consecutive layers, none skipped
0581                   const bool noGap = (barrelOrder2 - barrelOrder1) == 1 &&
0582                                      (barrelOrder3 - barrelOrder2) == 1;
0583 
0584                   // assume more scattering due to the layer in between
0585                   if (!noGap) {
0586                     addTauRatioCorr = m_cfg.tauRatioCorr;
0587                   }
0588                 } else {
0589                   bool mixedTriplet =
0590                       isPixelBarrel1 && isPixelBarrel2 && !isPixelBarrel3;
0591                   if (mixedTriplet) {
0592                     addTauRatioCorr = m_cfg.tauRatioCorr;
0593                   }
0594                 }
0595               }
0596               // The two doublets sharing a strip node resolved it separately,
0597               // so a triplet through a strip may disagree on tau by more. Any
0598               // of the three: the outer two carry their end's error into tau.
0599               if (m_cfg.tauRatioCorrStrip > 0.f &&
0600                   (!isPixel1 || !isPixel2 ||
0601                    nodeView.strip(pS->n2) != nullptr)) {
0602                 addTauRatioCorr += m_cfg.tauRatioCorrStrip;
0603               }
0604 
0605               // bad match
0606               if (absTauRatio > m_cfg.tauRatioCut + addTauRatioCorr) {
0607                 continue;
0608               }
0609 
0610               float dPhi = phi2u - pS->p[2];
0611 
0612               if (dPhi < -std::numbers::pi_v<float>) {
0613                 dPhi += 2 * std::numbers::pi_v<float>;
0614               } else if (dPhi > std::numbers::pi_v<float>) {
0615                 dPhi -= 2 * std::numbers::pi_v<float>;
0616               }
0617 
0618               if (std::abs(dPhi) > m_cfg.cutDPhiMax) {
0619                 continue;
0620               }
0621 
0622               const float dcurv = curv2 - pS->p[1];
0623 
0624               if (dcurv < -m_cfg.cutDCurvMax || dcurv > m_cfg.cutDCurvMax) {
0625                 continue;
0626               }
0627 
0628               // final check: cuts on pT and d0
0629               if (m_cfg.validateTriplets && isPixelBarrel1 && isPixelBarrel2 &&
0630                   isPixelBarrel3) {
0631                 const std::array<SpacePointIndex, 3> candidateTriplet = {
0632                     n1Idx, n2Idx, pS->n2};
0633 
0634                 if (!validateTriplet(m_cfg, nodeView, candidateTriplet,
0635                                      tripletPtMin, absTauRatio,
0636                                      m_cfg.tauRatioCut, bFieldInZ)) {
0637                   continue;
0638                 }
0639               }
0640 
0641               pS->vNei[pS->nNei] = outEdgeIdx;
0642               ++pS->nNei;
0643 
0644               isConnected = true;  // there is at least one good match
0645 
0646               // edge confirmed - update z0 histogram. Only doubletFilterRZ
0647               // holds z0 to the histogram range, and it is optional.
0648               if (const auto z0BinIndex = static_cast<std::int32_t>(
0649                       z0HistoCoeff * (z0 - m_cfg.minZ0));
0650                   z0BinIndex >= 0 &&
0651                   z0BinIndex < detail::kGbtsZ0HistogramBins) {
0652                 ++z0Histo[static_cast<std::size_t>(z0BinIndex)];
0653               }
0654 
0655               nConnections++;
0656             }
0657           }
0658         }  // loop over n2 (outer) nodes inside a sliding window on n2 bin
0659       }  // loop over sliding windows associated with n2 bins
0660 
0661       // updating the n1 node attributes
0662 
0663       edgeInfo[n1Idx].numEdges = numCreatedEdges;
0664       if (isConnected) {
0665         std::uint16_t z0BitMask = 0x0;
0666 
0667         for (std::int32_t bIdx = 0; bIdx < detail::kGbtsZ0HistogramBins;
0668              ++bIdx) {
0669           if (z0Histo[static_cast<std::size_t>(bIdx)] == 0) {
0670             continue;
0671           }
0672 
0673           z0BitMask |= (1 << bIdx);
0674         }
0675 
0676         // non-zero mask indicates that there is at least one connected edge
0677         edgeInfo[n1Idx].isConnected = z0BitMask;
0678       }
0679 
0680     }  // loop over n1 (inner) nodes
0681   }  // loop over bin groups: a single n1 bin and multiple n2 bins
0682 
0683   if (edgeStorage.size() >= maxEdges) {
0684     ACTS_WARNING(
0685         "Maximum number of graph edges exceeded - possible efficiency loss "
0686         << edgeStorage.size());
0687   }
0688   graph.nConnections = nConnections;
0689   return graph;
0690 }
0691 
0692 std::uint32_t GbtsGraph::runCCA(const std::uint32_t maxIterations) {
0693   const auto nEdges = static_cast<std::uint32_t>(edgeStorage.size());
0694 
0695   std::uint32_t maxLevel = 0;
0696 
0697   std::uint32_t iter = 0;
0698 
0699   std::vector<detail::GbtsEdge*> vOld;
0700 
0701   for (std::uint32_t edgeIndex = 0; edgeIndex < nEdges; ++edgeIndex) {
0702     detail::GbtsEdge* pS = &(edgeStorage[edgeIndex]);
0703     if (pS->nNei == 0) {
0704       continue;
0705     }
0706 
0707     // TODO: increment level for segments as they already have at least one
0708     // neighbour
0709     vOld.push_back(pS);
0710   }
0711 
0712   std::vector<detail::GbtsEdge*> vNew;
0713   vNew.reserve(vOld.size());
0714 
0715   // generate proposals
0716   for (; iter < maxIterations; iter++) {
0717     vNew.clear();
0718 
0719     for (detail::GbtsEdge* pS : vOld) {
0720       std::int32_t nextLevel = pS->level;
0721 
0722       for (std::uint32_t nIdx = 0; nIdx < pS->nNei; ++nIdx) {
0723         const std::uint32_t nextEdgeIdx = pS->vNei[nIdx];
0724 
0725         const detail::GbtsEdge* pN = &(edgeStorage[nextEdgeIdx]);
0726 
0727         if (pS->level == pN->level) {
0728           nextLevel = pS->level + 1;
0729           vNew.push_back(pS);
0730           break;
0731         }
0732       }
0733 
0734       // proposal
0735       pS->next = static_cast<std::int8_t>(nextLevel);
0736     }
0737 
0738     // update
0739 
0740     std::uint32_t nChanges = 0;
0741 
0742     for (auto pS : vNew) {
0743       if (pS->next != pS->level) {
0744         nChanges++;
0745         pS->level = pS->next;
0746         // levels only grow from zero here, so the cast is safe
0747         maxLevel = std::max(maxLevel, static_cast<std::uint32_t>(pS->level));
0748       }
0749     }
0750 
0751     if (nChanges == 0) {
0752       break;
0753     }
0754 
0755     vOld.swap(vNew);
0756     vNew.clear();
0757   }
0758 
0759   return maxLevel;
0760 }
0761 
0762 std::vector<detail::GbtsEdge*> GbtsGraph::extractChainHeads(
0763     const std::uint8_t minSeedLevel, const bool addTriplets,
0764     const float maxAbsEtaAddTriplets) {
0765   const auto nEdges = static_cast<std::uint32_t>(edgeStorage.size());
0766 
0767   // `addTriplets` accepts a chain one level short. Signed: an uncollected
0768   // edge sits at level -1 and `minSeedLevel` may be configured to 0.
0769   const std::int8_t minLevelAddTriplets =
0770       static_cast<std::int8_t>(minSeedLevel - 1);
0771   std::vector<detail::GbtsEdge*> vChainHeads;
0772 
0773   vChainHeads.reserve(nEdges / 2);
0774 
0775   for (std::uint32_t edgeIndex = 0; edgeIndex < nEdges; ++edgeIndex) {
0776     detail::GbtsEdge* pS = &edgeStorage[edgeIndex];
0777 
0778     if (!addTriplets) {
0779       if (pS->level < minSeedLevel) {
0780         continue;
0781       }
0782     } else {  // eta-dependent cut
0783       const float edgeAbsEta = std::abs(-std::log(pS->p[0]));
0784 
0785       if (edgeAbsEta > maxAbsEtaAddTriplets) {
0786         if (pS->level < minSeedLevel) {
0787           continue;
0788         }
0789       } else {
0790         if (pS->level < minLevelAddTriplets) {
0791           continue;
0792         }
0793       }
0794     }
0795 
0796     vChainHeads.push_back(pS);
0797   }
0798 
0799   std::ranges::sort(vChainHeads, std::ranges::greater{},
0800                     [](const detail::GbtsEdge* e) { return e->level; });
0801 
0802   return vChainHeads;
0803 }
0804 
0805 }  // namespace Acts::Experimental