Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-07-13 07:50:39

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/Seeding2/BroadTripletSeedFinder.hpp"
0010 
0011 #include "Acts/EventData/SpacePointContainer2.hpp"
0012 #include "Acts/Seeding2/BroadTripletSeedFilter.hpp"
0013 #include "Acts/Seeding2/DoubletSeedFinder.hpp"
0014 #include "Acts/Utilities/MathHelpers.hpp"
0015 
0016 #include <numeric>
0017 
0018 #include <Eigen/src/Core/Matrix.h>
0019 
0020 using namespace Acts::UnitLiterals;
0021 
0022 namespace Acts::Experimental {
0023 
0024 namespace {
0025 
0026 /// Check the compatibility of strip space point coordinates in xyz assuming
0027 /// the Bottom-Middle direction with the strip measurement details
0028 bool stripCoordinateCheck(float tolerance, const ConstSpacePointProxy2& sp,
0029                           const Eigen::Vector3f& spacePointPosition,
0030                           Eigen::Vector3f& outputCoordinates) {
0031   const Eigen::Vector3f& topStripVector = sp.topStripVector();
0032   const Eigen::Vector3f& bottomStripVector = sp.bottomStripVector();
0033   const Eigen::Vector3f& stripCenterDistance = sp.stripCenterDistance();
0034 
0035   // cross product between top strip vector and spacepointPosition
0036   Eigen::Vector3f d1 = topStripVector.cross(spacePointPosition);
0037 
0038   // scalar product between bottom strip vector and d1
0039   float bd1 = bottomStripVector.dot(d1);
0040 
0041   // compatibility check using distance between strips to evaluate if
0042   // spacepointPosition is inside the bottom detector element
0043   float s1 = stripCenterDistance.dot(d1);
0044   if (std::abs(s1) > std::abs(bd1) * tolerance) {
0045     return false;
0046   }
0047 
0048   // cross product between bottom strip vector and spacepointPosition
0049   Eigen::Vector3f d0 = bottomStripVector.cross(spacePointPosition);
0050 
0051   // compatibility check using distance between strips to evaluate if
0052   // spacepointPosition is inside the top detector element
0053   float s0 = stripCenterDistance.dot(d0);
0054   if (std::abs(s0) > std::abs(bd1) * tolerance) {
0055     return false;
0056   }
0057 
0058   // if arrive here spacepointPosition is compatible with strip directions and
0059   // detector elements
0060 
0061   const Eigen::Vector3f& topStripCenter = sp.topStripCenter();
0062 
0063   // spacepointPosition corrected with respect to the top strip position and
0064   // direction and the distance between the strips
0065   s0 = s0 / bd1;
0066   outputCoordinates = topStripCenter + topStripVector * s0;
0067   return true;
0068 }
0069 
0070 }  // namespace
0071 
0072 BroadTripletSeedFinder::DerivedTripletCuts::DerivedTripletCuts(
0073     const TripletCuts& cuts, float bFieldInZ_)
0074     : TripletCuts(cuts), bFieldInZ(bFieldInZ_) {
0075   // similar to `theta0Highland` in `Core/src/Material/Interactions.cpp`
0076   {
0077     const double xOverX0 = radLengthPerSeed;
0078     const double q2OverBeta2 = 1;  // q^2=1, beta^2~1
0079     // RPP2018 eq. 33.15 (treats beta and q² consistently)
0080     const double t = std::sqrt(xOverX0 * q2OverBeta2);
0081     // log((x/X0) * (q²/beta²)) = log((sqrt(x/X0) * (q/beta))²)
0082     //                          = 2 * log(sqrt(x/X0) * (q/beta))
0083     highland =
0084         static_cast<float>(13.6_MeV * t * (1.0 + 0.038 * 2 * std::log(t)));
0085   }
0086 
0087   const float maxScatteringAngle = highland / minPt;
0088   const float maxScatteringAngle2 = maxScatteringAngle * maxScatteringAngle;
0089 
0090   // bFieldInZ is in (pT/radius) natively, no need for conversion
0091   pTPerHelixRadius = bFieldInZ;
0092   minHelixDiameter2 = square(minPt * 2 / pTPerHelixRadius) * helixCutTolerance;
0093   const float pT2perRadius = square(highland / pTPerHelixRadius);
0094   sigmapT2perRadius = pT2perRadius * square(2 * sigmaScattering);
0095   multipleScattering2 = maxScatteringAngle2 * square(sigmaScattering);
0096 }
0097 
0098 BroadTripletSeedFinder::BroadTripletSeedFinder(
0099     std::unique_ptr<const Logger> logger_)
0100     : m_logger(std::move(logger_)) {}
0101 
0102 void BroadTripletSeedFinder::createSeedsFromGroup(
0103     const Options& options, State& state, Cache& cache,
0104     const DoubletSeedFinder& bottomFinder, const DoubletSeedFinder& topFinder,
0105     const DerivedTripletCuts& tripletCuts, const BroadTripletSeedFilter& filter,
0106     const SpacePointContainer2& spacePoints,
0107     std::span<const SpacePointIndex2> bottomSps, SpacePointIndex2 middleSp,
0108     std::span<const SpacePointIndex2> topSps,
0109     SeedContainer2& outputSeeds) const {
0110   cache.candidatesCollector.setMaxElements(
0111       filter.config().maxSeedsPerSpMConf,
0112       filter.config().maxQualitySeedsPerSpMConf);
0113 
0114   auto spM = spacePoints[middleSp];
0115 
0116   DoubletSeedFinder::MiddleSpInfo middleSpInfo =
0117       DoubletSeedFinder::computeMiddleSpInfo(spM);
0118 
0119   // create middle-top doublets
0120   cache.topDoublets.clear();
0121   topFinder.createDoublets(spacePoints, spM, middleSpInfo, topSps,
0122                            cache.topDoublets);
0123 
0124   // no top SP found -> cannot form any triplet
0125   if (cache.topDoublets.empty()) {
0126     ACTS_VERBOSE("No compatible Tops, returning");
0127     return;
0128   }
0129 
0130   // apply cut on the number of top SP if seedConfirmation is true
0131   float rMaxSeedConf = 0;
0132   if (filter.config().seedConfirmation) {
0133     // check if middle SP is in the central or forward region
0134     const bool isForwardRegion =
0135         spM.z() > filter.config().centralSeedConfirmationRange.zMaxSeedConf ||
0136         spM.z() < filter.config().centralSeedConfirmationRange.zMinSeedConf;
0137     SeedConfirmationRangeConfig seedConfRange =
0138         isForwardRegion ? filter.config().forwardSeedConfirmationRange
0139                         : filter.config().centralSeedConfirmationRange;
0140     // set the minimum number of top SP depending on whether the middle SP is
0141     // in the central or forward region
0142     std::size_t nTopSeedConf = spM.r() > seedConfRange.rMaxSeedConf
0143                                    ? seedConfRange.nTopForLargeR
0144                                    : seedConfRange.nTopForSmallR;
0145     // set max bottom radius for seed confirmation
0146     rMaxSeedConf = seedConfRange.rMaxSeedConf;
0147     // continue if number of top SPs is smaller than minimum
0148     if (cache.topDoublets.size() < nTopSeedConf) {
0149       ACTS_VERBOSE("Number of top SPs is "
0150                    << cache.topDoublets.size()
0151                    << " and is smaller than minimum, returning");
0152       return;
0153     }
0154   }
0155 
0156   // create middle-bottom doublets
0157   cache.bottomDoublets.clear();
0158   bottomFinder.createDoublets(spacePoints, spM, middleSpInfo, bottomSps,
0159                               cache.bottomDoublets);
0160 
0161   // no bottom SP found -> cannot form any triplet
0162   if (cache.bottomDoublets.empty()) {
0163     ACTS_VERBOSE("No compatible Bottoms, returning");
0164     return;
0165   }
0166 
0167   ACTS_VERBOSE("Candidates: " << cache.bottomDoublets.size() << " bottoms and "
0168                               << cache.topDoublets.size()
0169                               << " tops for middle candidate indexed "
0170                               << spM.index());
0171 
0172   // combine doublets to triplets
0173   cache.candidatesCollector.clear();
0174   if (options.useStripMeasurementInfo) {
0175     createStripTriplets(tripletCuts, rMaxSeedConf, filter, state.filter,
0176                         cache.filter, spacePoints, spM, cache.bottomDoublets,
0177                         cache.topDoublets, cache.tripletTopCandidates,
0178                         cache.candidatesCollector);
0179   } else {
0180     createTriplets(cache.tripletCache, tripletCuts, rMaxSeedConf, filter,
0181                    state.filter, cache.filter, spacePoints, spM,
0182                    cache.bottomDoublets, cache.topDoublets,
0183                    cache.tripletTopCandidates, cache.candidatesCollector);
0184   }
0185 
0186   // retrieve all candidates
0187   // this collection is already sorted, higher weights first
0188   const std::size_t numQualitySeeds =
0189       cache.candidatesCollector.nHighQualityCandidates();
0190   cache.candidatesCollector.toSortedCandidates(spacePoints,
0191                                                cache.sortedCandidates);
0192   filter.filter1SpFixed(state.filter, spacePoints, cache.sortedCandidates,
0193                         numQualitySeeds, outputSeeds);
0194 }
0195 
0196 void BroadTripletSeedFinder::createSeedsFromSortedGroups(
0197     const Options& options, State& state, Cache& cache,
0198     const DoubletSeedFinder& bottomFinder, const DoubletSeedFinder& topFinder,
0199     const DerivedTripletCuts& tripletCuts, const BroadTripletSeedFilter& filter,
0200     const SpacePointContainer2& spacePoints,
0201     const std::vector<std::span<const SpacePointIndex2>>& bottomSpGroups,
0202     std::span<const SpacePointIndex2> middleSps,
0203     const std::vector<std::span<const SpacePointIndex2>>& topSpGroups,
0204     const std::pair<float, float>& radiusRangeForMiddle,
0205     SeedContainer2& outputSeeds) const {
0206   if (middleSps.empty()) {
0207     return;
0208   }
0209 
0210   // initialize cache
0211   cache.candidatesCollector.setMaxElements(
0212       filter.config().maxSeedsPerSpMConf,
0213       filter.config().maxQualitySeedsPerSpMConf);
0214 
0215   cache.bottomSpOffsets.clear();
0216   cache.topSpOffsets.clear();
0217 
0218   // Initialize initial offsets for bottom and top space points with binary
0219   // search. This requires at least one middle space point to be present which
0220   // is already checked above.
0221   auto firstMiddleSp = spacePoints[middleSps.front()];
0222   float firstMiddleSpR = firstMiddleSp.r();
0223 
0224   std::ranges::transform(
0225       bottomSpGroups, std::back_inserter(cache.bottomSpOffsets),
0226       [&](const std::span<const SpacePointIndex2>& bottomSps) {
0227         auto low = std::ranges::lower_bound(
0228             bottomSps, firstMiddleSpR - bottomFinder.config().deltaRMax, {},
0229             [&](const SpacePointIndex2& spIndex) {
0230               auto sp = spacePoints[spIndex];
0231               return sp.r();
0232             });
0233         return low - bottomSps.begin();
0234       });
0235   std::ranges::transform(topSpGroups, std::back_inserter(cache.topSpOffsets),
0236                          [&](const std::span<const SpacePointIndex2>& topSps) {
0237                            auto low = std::ranges::lower_bound(
0238                                topSps,
0239                                firstMiddleSpR + topFinder.config().deltaRMin,
0240                                {}, [&](const SpacePointIndex2& spIndex) {
0241                                  auto sp = spacePoints[spIndex];
0242                                  return sp.r();
0243                                });
0244                            return low - topSps.begin();
0245                          });
0246 
0247   for (SpacePointIndex2 middleSp : middleSps) {
0248     auto spM = spacePoints[middleSp];
0249     const float rM = spM.r();
0250 
0251     // check if spM is outside our radial region of interest
0252     if (rM < radiusRangeForMiddle.first) {
0253       continue;
0254     }
0255     if (rM > radiusRangeForMiddle.second) {
0256       // break because SPs are sorted in r
0257       break;
0258     }
0259 
0260     DoubletSeedFinder::MiddleSpInfo middleSpInfo =
0261         DoubletSeedFinder::computeMiddleSpInfo(spM);
0262 
0263     // create middle-top doublets
0264     cache.topDoublets.clear();
0265     for (std::size_t i = 0; i < topSpGroups.size(); ++i) {
0266       topFinder.createSortedDoublets(spacePoints, spM, middleSpInfo,
0267                                      topSpGroups[i], cache.topSpOffsets[i],
0268                                      cache.topDoublets);
0269     }
0270 
0271     // no top SP found -> try next spM
0272     if (cache.topDoublets.empty()) {
0273       ACTS_VERBOSE("No compatible Tops, moving to next middle candidate");
0274       continue;
0275     }
0276 
0277     // apply cut on the number of top SP if seedConfirmation is true
0278     float rMaxSeedConf = 0;
0279     if (filter.config().seedConfirmation) {
0280       // check if middle SP is in the central or forward region
0281       const bool isForwardRegion =
0282           spM.z() > filter.config().centralSeedConfirmationRange.zMaxSeedConf ||
0283           spM.z() < filter.config().centralSeedConfirmationRange.zMinSeedConf;
0284       SeedConfirmationRangeConfig seedConfRange =
0285           isForwardRegion ? filter.config().forwardSeedConfirmationRange
0286                           : filter.config().centralSeedConfirmationRange;
0287       // set the minimum number of top SP depending on whether the middle SP is
0288       // in the central or forward region
0289       std::size_t nTopSeedConf = spM.r() > seedConfRange.rMaxSeedConf
0290                                      ? seedConfRange.nTopForLargeR
0291                                      : seedConfRange.nTopForSmallR;
0292       // set max bottom radius for seed confirmation
0293       rMaxSeedConf = seedConfRange.rMaxSeedConf;
0294       // continue if number of top SPs is smaller than minimum
0295       if (cache.topDoublets.size() < nTopSeedConf) {
0296         ACTS_VERBOSE(
0297             "Number of top SPs is "
0298             << cache.topDoublets.size()
0299             << " and is smaller than minimum, moving to next middle candidate");
0300         continue;
0301       }
0302     }
0303 
0304     // create middle-bottom doublets
0305     cache.bottomDoublets.clear();
0306     for (std::size_t i = 0; i < bottomSpGroups.size(); ++i) {
0307       bottomFinder.createSortedDoublets(
0308           spacePoints, spM, middleSpInfo, bottomSpGroups[i],
0309           cache.bottomSpOffsets[i], cache.bottomDoublets);
0310     }
0311 
0312     // no bottom SP found -> try next spM
0313     if (cache.bottomDoublets.empty()) {
0314       ACTS_VERBOSE("No compatible Bottoms, moving to next middle candidate");
0315       continue;
0316     }
0317 
0318     ACTS_VERBOSE("Candidates: " << cache.bottomDoublets.size()
0319                                 << " bottoms and " << cache.topDoublets.size()
0320                                 << " tops for middle candidate indexed "
0321                                 << spM.index());
0322 
0323     // combine doublets to triplets
0324     cache.candidatesCollector.clear();
0325     if (options.useStripMeasurementInfo) {
0326       createStripTriplets(tripletCuts, rMaxSeedConf, filter, state.filter,
0327                           cache.filter, spacePoints, spM, cache.bottomDoublets,
0328                           cache.topDoublets, cache.tripletTopCandidates,
0329                           cache.candidatesCollector);
0330     } else {
0331       createTriplets(cache.tripletCache, tripletCuts, rMaxSeedConf, filter,
0332                      state.filter, cache.filter, spacePoints, spM,
0333                      cache.bottomDoublets, cache.topDoublets,
0334                      cache.tripletTopCandidates, cache.candidatesCollector);
0335     }
0336 
0337     // retrieve all candidates
0338     // this collection is already sorted, higher weights first
0339     const std::size_t numQualitySeeds =
0340         cache.candidatesCollector.nHighQualityCandidates();
0341     cache.candidatesCollector.toSortedCandidates(spacePoints,
0342                                                  cache.sortedCandidates);
0343     filter.filter1SpFixed(state.filter, spacePoints, cache.sortedCandidates,
0344                           numQualitySeeds, outputSeeds);
0345   }
0346 }
0347 
0348 void BroadTripletSeedFinder::createTriplets(
0349     TripletCache& cache, const DerivedTripletCuts& cuts, float rMaxSeedConf,
0350     const BroadTripletSeedFilter& filter,
0351     BroadTripletSeedFilter::State& filterState,
0352     BroadTripletSeedFilter::Cache& filterCache,
0353     const SpacePointContainer2& spacePoints, const ConstSpacePointProxy2& spM,
0354     const DoubletSeedFinder::DoubletsForMiddleSp& bottomDoublets,
0355     const DoubletSeedFinder::DoubletsForMiddleSp& topDoublets,
0356     TripletTopCandidates& tripletTopCandidates,
0357     CandidatesForMiddleSp2& candidatesCollector) {
0358   const float rM = spM.r();
0359   const float varianceRM = spM.varianceR();
0360   const float varianceZM = spM.varianceZ();
0361 
0362   // make index vectors for sorting
0363   cache.sortedBottoms.resize(bottomDoublets.size());
0364   std::iota(cache.sortedBottoms.begin(), cache.sortedBottoms.end(), 0);
0365   std::ranges::sort(cache.sortedBottoms, {},
0366                     [&bottomDoublets](const std::size_t s) {
0367                       return bottomDoublets.cotTheta[s];
0368                     });
0369 
0370   cache.sortedTops.resize(topDoublets.size());
0371   std::iota(cache.sortedTops.begin(), cache.sortedTops.end(), 0);
0372   std::ranges::sort(cache.sortedTops, {}, [&topDoublets](const std::size_t s) {
0373     return topDoublets.cotTheta[s];
0374   });
0375 
0376   // Reserve enough space, in case current capacity is too little
0377   tripletTopCandidates.resize(topDoublets.size());
0378 
0379   std::size_t t0 = 0;
0380 
0381   for (const std::size_t b : cache.sortedBottoms) {
0382     // break if we reached the last top SP
0383     if (t0 >= topDoublets.size()) {
0384       break;
0385     }
0386 
0387     auto spB = spacePoints[bottomDoublets.spacePoints[b]];
0388     const auto& lb = bottomDoublets.linCircles[b];
0389 
0390     float cotThetaB = lb.cotTheta;
0391     float Vb = lb.V;
0392     float Ub = lb.U;
0393     float ErB = lb.Er;
0394     float iDeltaRB = lb.iDeltaR;
0395 
0396     // 1+(cot^2(theta)) = 1/sin^2(theta)
0397     float iSinTheta2 = 1 + cotThetaB * cotThetaB;
0398     // calculate max scattering for min momentum at the seed's theta angle
0399     // scaling scatteringAngle^2 by sin^2(theta) to convert pT^2 to p^2
0400     // accurate would be taking 1/atan(thetaBottom)-1/atan(thetaTop) <
0401     // scattering
0402     // but to avoid trig functions we approximate cot by scaling by
0403     // 1/sin^4(theta)
0404     // resolving with pT to p scaling --> only divide by sin^2(theta)
0405     // max approximation error for allowed scattering angles of 0.04 rad at
0406     // eta=infinity: ~8.5%
0407     float scatteringInRegion2 = cuts.multipleScattering2 * iSinTheta2;
0408 
0409     // clear all vectors used in each inner for loop
0410     tripletTopCandidates.clear();
0411 
0412     // minimum number of compatible top SPs to trigger the filter for a certain
0413     // middle bottom pair if seedConfirmation is false we always ask for at
0414     // least one compatible top to trigger the filter
0415     std::size_t minCompatibleTopSPs = 2;
0416     if (!filter.config().seedConfirmation || spB.r() > rMaxSeedConf) {
0417       minCompatibleTopSPs = 1;
0418     }
0419     if (filter.config().seedConfirmation &&
0420         candidatesCollector.nHighQualityCandidates() > 0) {
0421       minCompatibleTopSPs++;
0422     }
0423 
0424     for (std::size_t indexSortedTop = t0; indexSortedTop < topDoublets.size();
0425          ++indexSortedTop) {
0426       const std::size_t t = cache.sortedTops[indexSortedTop];
0427       auto spT = spacePoints[topDoublets.spacePoints[t]];
0428       const auto& lt = topDoublets.linCircles[t];
0429       float cotThetaT = lt.cotTheta;
0430 
0431       // use geometric average
0432       float cotThetaAvg2 = cotThetaB * cotThetaT;
0433       if (cotThetaAvg2 <= 0) {
0434         continue;
0435       }
0436 
0437       // add errors of spB-spM and spM-spT pairs and add the correlation term
0438       // for errors on spM
0439       float error2 =
0440           lt.Er + ErB +
0441           2 * (cotThetaAvg2 * varianceRM + varianceZM) * iDeltaRB * lt.iDeltaR;
0442 
0443       float deltaCotTheta = cotThetaB - cotThetaT;
0444       float deltaCotTheta2 = deltaCotTheta * deltaCotTheta;
0445 
0446       // Apply a cut on the compatibility between the r-z slope of the two
0447       // seed segments. This is done by comparing the squared difference
0448       // between slopes, and comparing to the squared uncertainty in this
0449       // difference - we keep a seed if the difference is compatible within
0450       // the assumed uncertainties. The uncertainties get contribution from
0451       // the  space-point-related squared error (error2) and a scattering term
0452       // calculated assuming the minimum pt we expect to reconstruct
0453       // (scatteringInRegion2). This assumes gaussian error propagation which
0454       // allows just adding the two errors if they are uncorrelated (which is
0455       // fair for scattering and measurement uncertainties)
0456       if (deltaCotTheta2 > error2 + scatteringInRegion2) {
0457         // skip top SPs based on cotTheta sorting when producing triplets
0458         // break if cotTheta from bottom SP < cotTheta from top SP because
0459         // the SP are sorted by cotTheta
0460         if (cotThetaB < cotThetaT) {
0461           break;
0462         }
0463         t0 = indexSortedTop + 1;
0464         continue;
0465       }
0466 
0467       float dU = lt.U - Ub;
0468       // protects against division by 0
0469       if (dU == 0.) {
0470         continue;
0471       }
0472       // A and B are evaluated as a function of the circumference parameters
0473       // x_0 and y_0
0474       float A = (lt.V - Vb) / dU;
0475       float S2 = 1 + A * A;
0476       float B = Vb - A * Ub;
0477       float B2 = B * B;
0478 
0479       // sqrt(S2)/B = 2 * helixradius
0480       // calculated radius must not be smaller than minimum radius
0481       if (S2 < B2 * cuts.minHelixDiameter2) {
0482         continue;
0483       }
0484 
0485       // refinement of the cut on the compatibility between the r-z slope of
0486       // the two seed segments using a scattering term scaled by the actual
0487       // measured pT (p2scatterSigma)
0488       float iHelixDiameter2 = B2 / S2;
0489       float sigmaSquaredPtDependent = iSinTheta2 * cuts.sigmapT2perRadius;
0490       // convert p(T) to p scaling by sin^2(theta) AND scale by 1/sin^4(theta)
0491       // from rad to deltaCotTheta
0492       float p2scatterSigma = iHelixDiameter2 * sigmaSquaredPtDependent;
0493       if (!std::isinf(cuts.maxPtScattering)) {
0494         // if pT > maxPtScattering, calculate allowed scattering angle using
0495         // maxPtScattering instead of pt.
0496         // To avoid 0-divison the pT check is skipped in case of B2==0, and
0497         // p2scatterSigma is calculated directly from maxPtScattering
0498         if (B2 == 0 || cuts.pTPerHelixRadius * std::sqrt(S2 / B2) >
0499                            2. * cuts.maxPtScattering) {
0500           float pTscatterSigma =
0501               (cuts.highland / cuts.maxPtScattering) * cuts.sigmaScattering;
0502           p2scatterSigma = pTscatterSigma * pTscatterSigma * iSinTheta2;
0503         }
0504       }
0505 
0506       // if deltaTheta larger than allowed scattering for calculated pT, skip
0507       if (deltaCotTheta2 > error2 + p2scatterSigma) {
0508         if (cotThetaB < cotThetaT) {
0509           break;
0510         }
0511         t0 = indexSortedTop;
0512         continue;
0513       }
0514 
0515       // A and B allow calculation of impact params in U/V plane with linear
0516       // function
0517       // (in contrast to having to solve a quadratic function in x/y plane)
0518       float Im = std::abs((A - B * rM) * rM);
0519       if (Im > cuts.impactMax) {
0520         continue;
0521       }
0522 
0523       // inverse diameter is signed depending on if the curvature is
0524       // positive/negative in phi
0525       tripletTopCandidates.emplace_back(spT.index(), B / std::sqrt(S2), Im);
0526     }  // loop on tops
0527 
0528     // continue if number of top SPs is smaller than minimum required for filter
0529     if (tripletTopCandidates.size() < minCompatibleTopSPs) {
0530       continue;
0531     }
0532 
0533     float zOrigin = spM.z() - rM * lb.cotTheta;
0534     filter.filter2SpFixed(
0535         filterState, filterCache, spacePoints, spB.index(), spM.index(),
0536         tripletTopCandidates.topSpacePoints, tripletTopCandidates.curvatures,
0537         tripletTopCandidates.impactParameters, zOrigin, candidatesCollector);
0538   }  // loop on bottoms
0539 }
0540 
0541 void BroadTripletSeedFinder::createStripTriplets(
0542     const DerivedTripletCuts& cuts, float rMaxSeedConf,
0543     const BroadTripletSeedFilter& filter,
0544     BroadTripletSeedFilter::State& filterState,
0545     BroadTripletSeedFilter::Cache& filterCache,
0546     const SpacePointContainer2& spacePoints, const ConstSpacePointProxy2& spM,
0547     const DoubletSeedFinder::DoubletsForMiddleSp& bottomDoublets,
0548     const DoubletSeedFinder::DoubletsForMiddleSp& topDoublets,
0549     TripletTopCandidates& tripletTopCandidates,
0550     CandidatesForMiddleSp2& candidatesCollector) {
0551   const float rM = spM.r();
0552   const float cosPhiM = spM.x() / rM;
0553   const float sinPhiM = spM.y() / rM;
0554   const float varianceRM = spM.varianceR();
0555   const float varianceZM = spM.varianceZ();
0556 
0557   // Reserve enough space, in case current capacity is too little
0558   tripletTopCandidates.resize(topDoublets.size());
0559 
0560   for (std::size_t b = 0; b < bottomDoublets.size(); ++b) {
0561     auto spB = spacePoints[bottomDoublets.spacePoints[b]];
0562     const auto& lb = bottomDoublets.linCircles[b];
0563 
0564     float cotThetaB = lb.cotTheta;
0565     float Vb = lb.V;
0566     float Ub = lb.U;
0567     float ErB = lb.Er;
0568     float iDeltaRB = lb.iDeltaR;
0569 
0570     // 1+(cot^2(theta)) = 1/sin^2(theta)
0571     float iSinTheta2 = 1 + cotThetaB * cotThetaB;
0572     // calculate max scattering for min momentum at the seed's theta angle
0573     // scaling scatteringAngle^2 by sin^2(theta) to convert pT^2 to p^2
0574     // accurate would be taking 1/atan(thetaBottom)-1/atan(thetaTop) <
0575     // scattering
0576     // but to avoid trig functions we approximate cot by scaling by
0577     // 1/sin^4(theta)
0578     // resolving with pT to p scaling --> only divide by sin^2(theta)
0579     // max approximation error for allowed scattering angles of 0.04 rad at
0580     // eta=infinity: ~8.5%
0581     float scatteringInRegion2 = cuts.multipleScattering2 * iSinTheta2;
0582 
0583     float sinTheta = 1 / std::sqrt(iSinTheta2);
0584     float cosTheta = cotThetaB * sinTheta;
0585 
0586     // clear all vectors used in each inner for loop
0587     tripletTopCandidates.clear();
0588 
0589     // coordinate transformation and checks for middle spacepoint
0590     // x and y terms for the rotation from UV to XY plane
0591     Eigen::Vector2f rotationTermsUVtoXY = {cosPhiM * sinTheta,
0592                                            sinPhiM * sinTheta};
0593 
0594     // minimum number of compatible top SPs to trigger the filter for a certain
0595     // middle bottom pair if seedConfirmation is false we always ask for at
0596     // least one compatible top to trigger the filter
0597     std::size_t minCompatibleTopSPs = 2;
0598     if (!filter.config().seedConfirmation || spB.r() > rMaxSeedConf) {
0599       minCompatibleTopSPs = 1;
0600     }
0601     if (filter.config().seedConfirmation &&
0602         candidatesCollector.nHighQualityCandidates() > 0) {
0603       minCompatibleTopSPs++;
0604     }
0605 
0606     for (std::size_t t = 0; t < topDoublets.size(); ++t) {
0607       auto spT = spacePoints[topDoublets.spacePoints[t]];
0608       const auto& lt = topDoublets.linCircles[t];
0609 
0610       // protects against division by 0
0611       float dU = lt.U - Ub;
0612       if (dU == 0.) {
0613         continue;
0614       }
0615       // A and B are evaluated as a function of the circumference parameters
0616       // x_0 and y_0
0617       float A0 = (lt.V - Vb) / dU;
0618 
0619       float zPositionMiddle = cosTheta * std::sqrt(1 + A0 * A0);
0620 
0621       // position of Middle SP converted from UV to XY assuming cotTheta
0622       // evaluated from the Bottom and Middle SPs double
0623       Eigen::Vector3f positionMiddle = {
0624           rotationTermsUVtoXY[0] - rotationTermsUVtoXY[1] * A0,
0625           rotationTermsUVtoXY[0] * A0 + rotationTermsUVtoXY[1],
0626           zPositionMiddle};
0627 
0628       Eigen::Vector3f rMTransf;
0629       if (!stripCoordinateCheck(cuts.toleranceParam, spM, positionMiddle,
0630                                 rMTransf)) {
0631         continue;
0632       }
0633 
0634       // coordinate transformation and checks for bottom spacepoint
0635       float B0 = 2 * (Vb - A0 * Ub);
0636       float Cb = 1 - B0 * lb.y;
0637       float Sb = A0 + B0 * lb.x;
0638       Eigen::Vector3f positionBottom = {
0639           rotationTermsUVtoXY[0] * Cb - rotationTermsUVtoXY[1] * Sb,
0640           rotationTermsUVtoXY[0] * Sb + rotationTermsUVtoXY[1] * Cb,
0641           zPositionMiddle};
0642 
0643       Eigen::Vector3f rBTransf;
0644       if (!stripCoordinateCheck(cuts.toleranceParam, spB, positionBottom,
0645                                 rBTransf)) {
0646         continue;
0647       }
0648 
0649       // coordinate transformation and checks for top spacepoint
0650       float Ct = 1 - B0 * lt.y;
0651       float St = A0 + B0 * lt.x;
0652       Eigen::Vector3f positionTop = {
0653           rotationTermsUVtoXY[0] * Ct - rotationTermsUVtoXY[1] * St,
0654           rotationTermsUVtoXY[0] * St + rotationTermsUVtoXY[1] * Ct,
0655           zPositionMiddle};
0656 
0657       Eigen::Vector3f rTTransf;
0658       if (!stripCoordinateCheck(cuts.toleranceParam, spT, positionTop,
0659                                 rTTransf)) {
0660         continue;
0661       }
0662 
0663       // bottom and top coordinates in the spM reference frame
0664       float xB = rBTransf[0] - rMTransf[0];
0665       float yB = rBTransf[1] - rMTransf[1];
0666       float zB = rBTransf[2] - rMTransf[2];
0667       float xT = rTTransf[0] - rMTransf[0];
0668       float yT = rTTransf[1] - rMTransf[1];
0669       float zT = rTTransf[2] - rMTransf[2];
0670 
0671       float iDeltaRB2 = 1 / (xB * xB + yB * yB);
0672       float iDeltaRT2 = 1 / (xT * xT + yT * yT);
0673 
0674       cotThetaB = -zB * std::sqrt(iDeltaRB2);
0675       float cotThetaT = zT * std::sqrt(iDeltaRT2);
0676 
0677       // use arithmetic average
0678       float averageCotTheta = 0.5f * (cotThetaB + cotThetaT);
0679       float cotThetaAvg2 = averageCotTheta * averageCotTheta;
0680 
0681       // add errors of spB-spM and spM-spT pairs and add the correlation term
0682       // for errors on spM
0683       float error2 =
0684           lt.Er + ErB +
0685           2 * (cotThetaAvg2 * varianceRM + varianceZM) * iDeltaRB * lt.iDeltaR;
0686 
0687       float deltaCotTheta = cotThetaB - cotThetaT;
0688       float deltaCotTheta2 = deltaCotTheta * deltaCotTheta;
0689 
0690       // Apply a cut on the compatibility between the r-z slope of the two
0691       // seed segments. This is done by comparing the squared difference
0692       // between slopes, and comparing to the squared uncertainty in this
0693       // difference - we keep a seed if the difference is compatible within
0694       // the assumed uncertainties. The uncertainties get contribution from
0695       // the  space-point-related squared error (error2) and a scattering term
0696       // calculated assuming the minimum pt we expect to reconstruct
0697       // (scatteringInRegion2). This assumes gaussian error propagation which
0698       // allows just adding the two errors if they are uncorrelated (which is
0699       // fair for scattering and measurement uncertainties)
0700       if (deltaCotTheta2 > error2 + scatteringInRegion2) {
0701         // skip top SPs based on cotTheta sorting when producing triplets
0702         continue;
0703       }
0704 
0705       float rMxy =
0706           std::sqrt(rMTransf[0] * rMTransf[0] + rMTransf[1] * rMTransf[1]);
0707       float irMxy = 1 / rMxy;
0708       float Ax = rMTransf[0] * irMxy;
0709       float Ay = rMTransf[1] * irMxy;
0710 
0711       float ub = (xB * Ax + yB * Ay) * iDeltaRB2;
0712       float vb = (yB * Ax - xB * Ay) * iDeltaRB2;
0713       float ut = (xT * Ax + yT * Ay) * iDeltaRT2;
0714       float vt = (yT * Ax - xT * Ay) * iDeltaRT2;
0715 
0716       dU = ut - ub;
0717       // protects against division by 0
0718       if (dU == 0.) {
0719         continue;
0720       }
0721       float A = (vt - vb) / dU;
0722       float S2 = 1 + A * A;
0723       float B = vb - A * ub;
0724       float B2 = B * B;
0725 
0726       // sqrt(S2)/B = 2 * helixradius
0727       // calculated radius must not be smaller than minimum radius
0728       if (S2 < B2 * cuts.minHelixDiameter2) {
0729         continue;
0730       }
0731 
0732       // refinement of the cut on the compatibility between the r-z slope of
0733       // the two seed segments using a scattering term scaled by the actual
0734       // measured pT (p2scatterSigma)
0735       float iHelixDiameter2 = B2 / S2;
0736       float sigmaSquaredPtDependent = iSinTheta2 * cuts.sigmapT2perRadius;
0737       // convert p(T) to p scaling by sin^2(theta) AND scale by 1/sin^4(theta)
0738       // from rad to deltaCotTheta
0739       float p2scatterSigma = iHelixDiameter2 * sigmaSquaredPtDependent;
0740       if (!std::isinf(cuts.maxPtScattering)) {
0741         // if pT > maxPtScattering, calculate allowed scattering angle using
0742         // maxPtScattering instead of pt.
0743         // To avoid 0-divison the pT check is skipped in case of B2==0, and
0744         // p2scatterSigma is calculated directly from maxPtScattering
0745         if (B2 == 0 || cuts.pTPerHelixRadius * std::sqrt(S2 / B2) >
0746                            2. * cuts.maxPtScattering) {
0747           float pTscatterSigma =
0748               (cuts.highland / cuts.maxPtScattering) * cuts.sigmaScattering;
0749           p2scatterSigma = pTscatterSigma * pTscatterSigma * iSinTheta2;
0750         }
0751       }
0752 
0753       // if deltaTheta larger than allowed scattering for calculated pT, skip
0754       if (deltaCotTheta2 > error2 + p2scatterSigma) {
0755         continue;
0756       }
0757 
0758       // A and B allow calculation of impact params in U/V plane with linear
0759       // function
0760       // (in contrast to having to solve a quadratic function in x/y plane)
0761       float Im = std::abs((A - B * rMxy) * rMxy);
0762       if (Im > cuts.impactMax) {
0763         continue;
0764       }
0765 
0766       // inverse diameter is signed depending on if the curvature is
0767       // positive/negative in phi
0768       tripletTopCandidates.emplace_back(spT.index(), B / std::sqrt(S2), Im);
0769     }  // loop on tops
0770 
0771     // continue if number of top SPs is smaller than minimum required for filter
0772     if (tripletTopCandidates.size() < minCompatibleTopSPs) {
0773       continue;
0774     }
0775 
0776     float zOrigin = spM.z() - rM * lb.cotTheta;
0777     filter.filter2SpFixed(
0778         filterState, filterCache, spacePoints, spB.index(), spM.index(),
0779         tripletTopCandidates.topSpacePoints, tripletTopCandidates.curvatures,
0780         tripletTopCandidates.impactParameters, zOrigin, candidatesCollector);
0781   }  // loop on bottoms
0782 }
0783 
0784 }  // namespace Acts::Experimental