Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-26 08:19:00

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/TripletSeedFinder.hpp"
0010 
0011 #include "Acts/EventData/SpacePointContainer.hpp"
0012 #include "Acts/EventData/StripSpacePointCalibrationDetails.hpp"
0013 #include "Acts/SpacePointFormation/detail/StripSpacePointCalibrationImpl.hpp"
0014 #include "Acts/Utilities/MathHelpers.hpp"
0015 #include "Acts/Utilities/Zip.hpp"
0016 
0017 #include <ranges>
0018 
0019 #include <Eigen/Dense>
0020 #include <boost/mp11.hpp>
0021 #include <boost/mp11/algorithm.hpp>
0022 
0023 namespace Acts {
0024 
0025 namespace {
0026 
0027 template <bool useStripInfo, bool sortedByCotTheta>
0028 class Impl final : public TripletSeedFinder {
0029  public:
0030   explicit Impl(const DerivedConfig& config) : m_cfg(config) {}
0031 
0032   const DerivedConfig& config() const override { return m_cfg; }
0033 
0034   template <typename TopDoublets>
0035   void createPixelTripletTopCandidates(
0036       const ConstSpacePointProxy& spM,
0037       const DoubletsForMiddleSp::Proxy& bottomDoublet, TopDoublets& topDoublets,
0038       TripletTopCandidates& tripletTopCandidates) const {
0039     const float rM = spM.zr()[1];
0040     const float varianceZM = spM.varianceZ();
0041     const float varianceRM = spM.varianceR();
0042 
0043     // Reserve enough space, in case current capacity is too little
0044     tripletTopCandidates.reserve(tripletTopCandidates.size() +
0045                                  topDoublets.size());
0046 
0047     const float cotThetaB = bottomDoublet.cotTheta();
0048     const float erB = bottomDoublet.er();
0049     const float iDeltaRB = bottomDoublet.iDeltaR();
0050     const float Ub = bottomDoublet.u();
0051     const float Vb = bottomDoublet.v();
0052 
0053     // 1+(cot^2(theta)) = 1/sin^2(theta)
0054     const float iSinTheta2 = 1 + cotThetaB * cotThetaB;
0055     const float sigmaSquaredPtDependent = iSinTheta2 * m_cfg.sigmapT2perRadius;
0056     // calculate max scattering for min momentum at the seed's theta angle
0057     // scaling scatteringAngle^2 by sin^2(theta) to convert pT^2 to p^2
0058     // accurate would be taking 1/atan(thetaBottom)-1/atan(thetaTop) <
0059     // scattering
0060     // but to avoid trig functions we approximate cot by scaling by
0061     // 1/sin^4(theta)
0062     // resolving with pT to p scaling --> only divide by sin^2(theta)
0063     // max approximation error for allowed scattering angles of 0.04 rad at
0064     // eta=infinity: ~8.5%
0065     const float scatteringInRegion2 = m_cfg.multipleScattering2 * iSinTheta2;
0066 
0067     std::size_t topDoubletOffset = 0;
0068     for (auto [topDoublet, topDoubletIndex] :
0069          zip(topDoublets, std::ranges::iota_view<std::size_t, std::size_t>(
0070                               0, topDoublets.size()))) {
0071       const SpacePointIndex spT = topDoublet.spacePointIndex();
0072       const float cotThetaT = topDoublet.cotTheta();
0073 
0074       // use geometric average
0075       const float cotThetaAvg2 = cotThetaB * cotThetaT;
0076 
0077       // add errors of spB-spM and spM-spT pairs and add the correlation term
0078       // for errors on spM
0079       const float error2 = topDoublet.er() + erB +
0080                            2 * (cotThetaAvg2 * varianceRM + varianceZM) *
0081                                iDeltaRB * topDoublet.iDeltaR();
0082 
0083       const float deltaCotTheta = cotThetaB - cotThetaT;
0084       const float deltaCotTheta2 = deltaCotTheta * deltaCotTheta;
0085 
0086       // Apply a cut on the compatibility between the r-z slope of the two
0087       // seed segments. This is done by comparing the squared difference
0088       // between slopes, and comparing to the squared uncertainty in this
0089       // difference - we keep a seed if the difference is compatible within
0090       // the assumed uncertainties. The uncertainties get contribution from
0091       // the  space-point-related squared error (error2) and a scattering term
0092       // calculated assuming the minimum pt we expect to reconstruct
0093       // (scatteringInRegion2). This assumes gaussian error propagation which
0094       // allows just adding the two errors if they are uncorrelated (which is
0095       // fair for scattering and measurement uncertainties)
0096       if (deltaCotTheta2 > error2 + scatteringInRegion2) {
0097         if constexpr (sortedByCotTheta) {
0098           // skip top SPs based on cotTheta sorting when producing triplets
0099           // break if cotTheta from bottom SP < cotTheta from top SP because
0100           // the SP are sorted by cotTheta
0101           if (cotThetaB < cotThetaT) {
0102             break;
0103           }
0104           topDoubletOffset = topDoubletIndex + 1;
0105         }
0106         continue;
0107       }
0108 
0109       const float dU = topDoublet.u() - Ub;
0110       // protects against division by 0
0111       if (dU == 0) {
0112         continue;
0113       }
0114       // A and B are evaluated as a function of the circumference parameters
0115       // x_0 and y_0
0116       const float A = (topDoublet.v() - Vb) / dU;
0117       const float S2 = 1 + A * A;
0118       const float B = Vb - A * Ub;
0119       const float B2 = B * B;
0120 
0121       // sqrt(S2)/B = 2 * helixradius
0122       // calculated radius must not be smaller than minimum radius
0123       if (S2 < B2 * m_cfg.minHelixDiameter2) {
0124         continue;
0125       }
0126 
0127       // refinement of the cut on the compatibility between the r-z slope of
0128       // the two seed segments using a scattering term scaled by the actual
0129       // measured pT (p2scatterSigma)
0130       const float iHelixDiameter2 = B2 / S2;
0131       // convert p(T) to p scaling by sin^2(theta) AND scale by 1/sin^4(theta)
0132       // from rad to deltaCotTheta
0133       const float p2scatterSigma = iHelixDiameter2 * sigmaSquaredPtDependent;
0134       // if deltaTheta larger than allowed scattering for calculated pT, skip
0135       if (deltaCotTheta2 > error2 + p2scatterSigma) {
0136         if constexpr (sortedByCotTheta) {
0137           if (cotThetaB < cotThetaT) {
0138             break;
0139           }
0140           topDoubletOffset = topDoubletIndex;
0141         }
0142         continue;
0143       }
0144 
0145       // A and B allow calculation of impact params in U/V plane with linear
0146       // function
0147       // (in contrast to having to solve a quadratic function in x/y plane)
0148       const float im = std::abs((A - B * rM) * rM);
0149       if (im > m_cfg.impactMax) {
0150         continue;
0151       }
0152 
0153       // inverse diameter is signed depending on if the curvature is
0154       // positive/negative in phi
0155       tripletTopCandidates.emplace_back(spT, B / std::sqrt(S2), im);
0156     }  // loop on tops
0157 
0158     if constexpr (sortedByCotTheta) {
0159       // remove the top doublets that were skipped due to cotTheta sorting
0160       topDoublets = topDoublets.subrange(topDoubletOffset);
0161     }
0162   }
0163 
0164   template <typename TopDoublets>
0165   void createStripTripletTopCandidates(
0166       const SpacePointContainer& spacePoints, const ConstSpacePointProxy& spM,
0167       const DoubletsForMiddleSp::Proxy& bottomDoublet, TopDoublets& topDoublets,
0168       TripletTopCandidates& tripletTopCandidates) const {
0169     const float rM = spM.zr()[1];
0170     const float cosPhiM = spM.xy()[0] / rM;
0171     const float sinPhiM = spM.xy()[1] / rM;
0172     const float varianceZM = spM.varianceZ();
0173     const float varianceRM = spM.varianceR();
0174 
0175     // Reserve enough space, in case current capacity is too little
0176     tripletTopCandidates.reserve(tripletTopCandidates.size() +
0177                                  topDoublets.size());
0178 
0179     const float cotThetaB0 = bottomDoublet.cotTheta();
0180     const float erB = bottomDoublet.er();
0181     const float iDeltaRB = bottomDoublet.iDeltaR();
0182     const float Ub0 = bottomDoublet.u();
0183     const float Vb0 = bottomDoublet.v();
0184 
0185     // 1+(cot^2(theta)) = 1/sin^2(theta)
0186     const float iSinTheta2 = 1 + cotThetaB0 * cotThetaB0;
0187     const float sigmaSquaredPtDependent = iSinTheta2 * m_cfg.sigmapT2perRadius;
0188     // calculate max scattering for min momentum at the seed's theta angle
0189     // scaling scatteringAngle^2 by sin^2(theta) to convert pT^2 to p^2
0190     // accurate would be taking 1/atan(thetaBottom)-1/atan(thetaTop) <
0191     // scattering
0192     // but to avoid trig functions we approximate cot by scaling by
0193     // 1/sin^4(theta)
0194     // resolving with pT to p scaling --> only divide by sin^2(theta)
0195     // max approximation error for allowed scattering angles of 0.04 rad at
0196     // eta=infinity: ~8.5%
0197     const float scatteringInRegion2 = m_cfg.multipleScattering2 * iSinTheta2;
0198 
0199     const float sinTheta = 1 / std::sqrt(iSinTheta2);
0200     const float cosTheta = cotThetaB0 * sinTheta;
0201 
0202     // coordinate transformation and checks for middle space point
0203     // x and y terms for the rotation from UV to XY plane
0204     const std::array<float, 2> rotationTermsUVtoXY = {cosPhiM * sinTheta,
0205                                                       sinPhiM * sinTheta};
0206 
0207     // Pre-cache strip data for the loop-invariant middle and bottom SPs
0208     const OuterStripSpacePointCalibrationDetailsDerived calM =
0209         detail::deriveOuterStripSpacePointCalibrationDetails(
0210             spM.outerStripCalibrationDetails());
0211     const ConstSpacePointProxy spB =
0212         spacePoints[bottomDoublet.spacePointIndex()];
0213     const OuterStripSpacePointCalibrationDetailsDerived calB =
0214         detail::deriveOuterStripSpacePointCalibrationDetails(
0215             spB.outerStripCalibrationDetails());
0216 
0217     std::size_t topDoubletOffset = 0;
0218     for (auto [topDoublet, topDoubletIndex] :
0219          zip(topDoublets, std::ranges::iota_view<std::size_t, std::size_t>(
0220                               0, topDoublets.size()))) {
0221       // Pre-filter on the doublet stage cot(theta) difference before the
0222       // expensive strip coordinate transformation. The doublet cot(theta)
0223       // values are computed from SP centers and are approximate, so the
0224       // cut is very loose.
0225       //
0226       // this pre-filter is only applied when `sortedByCotTheta` is enabled,
0227       // mirroring the pixel path. Top doublets are sorted in ascending
0228       // approximate cotTheta, which lets us kill looping over doublets once
0229       // cotThetaT exceeds cotThetaB by more than the tolerance, and advance
0230       // `topDoubletOffset` to discard tops that can never satisfy the cut
0231       // for any subsequent (larger) bottom cotTheta.
0232       if constexpr (sortedByCotTheta) {
0233         const float cotThetaT = topDoublet.cotTheta();
0234         const float deltaCotTheta = cotThetaB0 - cotThetaT;
0235         const float cotThetaDiffMax2 =
0236             m_cfg.cotThetaDiffMax * m_cfg.cotThetaDiffMax;
0237         if (deltaCotTheta * deltaCotTheta > cotThetaDiffMax2) {
0238           if (cotThetaB0 < cotThetaT) {
0239             break;
0240           }
0241           topDoubletOffset = topDoubletIndex + 1;
0242           continue;
0243         }
0244       }
0245 
0246       // protects against division by 0
0247       const float dU0 = topDoublet.u() - Ub0;
0248       if (dU0 == 0) {
0249         continue;
0250       }
0251       // A and B are evaluated as a function of the circumference parameters
0252       // x_0 and y_0
0253       const float A0 = (topDoublet.v() - Vb0) / dU0;
0254 
0255       // The middle strip check is scale-invariant (ratios s1/bd1 and s0/bd1
0256       // are unaffected by uniform scaling of direction), so we use cosTheta as
0257       // the z-component instead of cosTheta * sqrt(1 + A0^2), deferring the
0258       // sqrt.
0259       const std::array<float, 3> directionMiddle = {
0260           rotationTermsUVtoXY[0] - rotationTermsUVtoXY[1] * A0,
0261           rotationTermsUVtoXY[0] * A0 + rotationTermsUVtoXY[1], cosTheta};
0262 
0263       std::array<float, 3> rMTransf{};
0264       if (!detail::calibrateOuterStripSpacePoint(
0265               directionMiddle, calM, rMTransf, m_cfg.toleranceParam)) {
0266         continue;
0267       }
0268 
0269       // sqrt only computed on the less-common path where middle check passed
0270       const float zDirectionMiddle = cosTheta * std::sqrt(1 + A0 * A0);
0271 
0272       // coordinate transformation and checks for bottom space point
0273       const float B0 = 2 * (Vb0 - A0 * Ub0);
0274       const float Cb = 1 - B0 * bottomDoublet.y();
0275       const float Sb = A0 + B0 * bottomDoublet.x();
0276       const std::array<float, 3> directionBottom = {
0277           rotationTermsUVtoXY[0] * Cb - rotationTermsUVtoXY[1] * Sb,
0278           rotationTermsUVtoXY[0] * Sb + rotationTermsUVtoXY[1] * Cb,
0279           zDirectionMiddle};
0280 
0281       std::array<float, 3> rBTransf{};
0282       if (!detail::calibrateOuterStripSpacePoint(
0283               directionBottom, calB, rBTransf, m_cfg.toleranceParam)) {
0284         continue;
0285       }
0286 
0287       // coordinate transformation and checks for top space point
0288       const float Ct = 1 - B0 * topDoublet.y();
0289       const float St = A0 + B0 * topDoublet.x();
0290       const std::array<float, 3> directionTop = {
0291           rotationTermsUVtoXY[0] * Ct - rotationTermsUVtoXY[1] * St,
0292           rotationTermsUVtoXY[0] * St + rotationTermsUVtoXY[1] * Ct,
0293           zDirectionMiddle};
0294 
0295       const ConstSpacePointProxy spT =
0296           spacePoints[topDoublet.spacePointIndex()];
0297       const OuterStripSpacePointCalibrationDetailsDerived calT =
0298           detail::deriveOuterStripSpacePointCalibrationDetails(
0299               spT.outerStripCalibrationDetails());
0300       std::array<float, 3> rTTransf{};
0301       if (!detail::calibrateOuterStripSpacePoint(directionTop, calT, rTTransf,
0302                                                  m_cfg.toleranceParam)) {
0303         continue;
0304       }
0305 
0306       // bottom and top coordinates in the spM reference frame
0307       const float xB = rBTransf[0] - rMTransf[0];
0308       const float yB = rBTransf[1] - rMTransf[1];
0309       const float zB = rBTransf[2] - rMTransf[2];
0310       const float xT = rTTransf[0] - rMTransf[0];
0311       const float yT = rTTransf[1] - rMTransf[1];
0312       const float zT = rTTransf[2] - rMTransf[2];
0313 
0314       const float iDeltaRB2 = 1 / (xB * xB + yB * yB);
0315       const float iDeltaRT2 = 1 / (xT * xT + yT * yT);
0316 
0317       const float cotThetaB = -zB * std::sqrt(iDeltaRB2);
0318       const float cotThetaT = zT * std::sqrt(iDeltaRT2);
0319 
0320       // use arithmetic average
0321       const float averageCotTheta = 0.5f * (cotThetaB + cotThetaT);
0322       const float cotThetaAvg2 = averageCotTheta * averageCotTheta;
0323 
0324       // add errors of spB-spM and spM-spT pairs and add the correlation term
0325       // for errors on spM
0326       const float error2 = topDoublet.er() + erB +
0327                            2 * (cotThetaAvg2 * varianceRM + varianceZM) *
0328                                iDeltaRB * topDoublet.iDeltaR();
0329 
0330       const float deltaCotTheta = cotThetaB - cotThetaT;
0331       const float deltaCotTheta2 = deltaCotTheta * deltaCotTheta;
0332 
0333       // Apply a cut on the compatibility between the r-z slope of the two
0334       // seed segments. This is done by comparing the squared difference
0335       // between slopes, and comparing to the squared uncertainty in this
0336       // difference - we keep a seed if the difference is compatible within
0337       // the assumed uncertainties. The uncertainties get contribution from
0338       // the  space-point-related squared error (error2) and a scattering term
0339       // calculated assuming the minimum pt we expect to reconstruct
0340       // (scatteringInRegion2). This assumes gaussian error propagation which
0341       // allows just adding the two errors if they are uncorrelated (which is
0342       // fair for scattering and measurement uncertainties)
0343       if (deltaCotTheta2 > error2 + scatteringInRegion2) {
0344         // skip top SPs based on cotTheta sorting when producing triplets
0345         continue;
0346       }
0347 
0348       const float rMxy =
0349           std::sqrt(rMTransf[0] * rMTransf[0] + rMTransf[1] * rMTransf[1]);
0350       const float irMxy = 1 / rMxy;
0351       const float Ax = rMTransf[0] * irMxy;
0352       const float Ay = rMTransf[1] * irMxy;
0353 
0354       const float Ub = (xB * Ax + yB * Ay) * iDeltaRB2;
0355       const float Vb = (yB * Ax - xB * Ay) * iDeltaRB2;
0356       const float Ut = (xT * Ax + yT * Ay) * iDeltaRT2;
0357       const float Vt = (yT * Ax - xT * Ay) * iDeltaRT2;
0358 
0359       const float dU = Ut - Ub;
0360       // protects against division by 0
0361       if (dU == 0) {
0362         continue;
0363       }
0364       const float A = (Vt - Vb) / dU;
0365       const float S2 = 1 + A * A;
0366       const float B = Vb - A * Ub;
0367       const float B2 = B * B;
0368 
0369       // sqrt(S2)/B = 2 * helixradius
0370       // calculated radius must not be smaller than minimum radius
0371       if (S2 < B2 * m_cfg.minHelixDiameter2) {
0372         continue;
0373       }
0374 
0375       // refinement of the cut on the compatibility between the r-z slope of
0376       // the two seed segments using a scattering term scaled by the actual
0377       // measured pT (p2scatterSigma)
0378       const float iHelixDiameter2 = B2 / S2;
0379       // convert p(T) to p scaling by sin^2(theta) AND scale by 1/sin^4(theta)
0380       // from rad to deltaCotTheta
0381       const float p2scatterSigma = iHelixDiameter2 * sigmaSquaredPtDependent;
0382       // if deltaTheta larger than allowed scattering for calculated pT, skip
0383       if (deltaCotTheta2 > error2 + p2scatterSigma) {
0384         continue;
0385       }
0386 
0387       // A and B allow calculation of impact params in U/V plane with linear
0388       // function
0389       // (in contrast to having to solve a quadratic function in x/y plane)
0390       const float im = std::abs((A - B * rMxy) * rMxy);
0391       if (im > m_cfg.impactMax) {
0392         continue;
0393       }
0394 
0395       // inverse diameter is signed depending on if the curvature is
0396       // positive/negative in phi
0397       tripletTopCandidates.emplace_back(topDoublet.spacePointIndex(),
0398                                         B / std::sqrt(S2), im);
0399     }  // loop on tops
0400 
0401     if constexpr (sortedByCotTheta) {
0402       // remove the top doublets that can no longer be compatible with any
0403       // subsequent bottom doublet (which has a larger approximate cotTheta)
0404       topDoublets = topDoublets.subrange(topDoubletOffset);
0405     }
0406   }
0407 
0408   void createTripletTopCandidates(
0409       const SpacePointContainer& spacePoints, const ConstSpacePointProxy& spM,
0410       const DoubletsForMiddleSp::Proxy& bottomDoublet,
0411       DoubletsForMiddleSp::Range& topDoublets,
0412       TripletTopCandidates& tripletTopCandidates) const override {
0413     if constexpr (useStripInfo) {
0414       createStripTripletTopCandidates(spacePoints, spM, bottomDoublet,
0415                                       topDoublets, tripletTopCandidates);
0416     } else {
0417       createPixelTripletTopCandidates(spM, bottomDoublet, topDoublets,
0418                                       tripletTopCandidates);
0419     }
0420   }
0421 
0422   void createTripletTopCandidates(
0423       const SpacePointContainer& spacePoints, const ConstSpacePointProxy& spM,
0424       const DoubletsForMiddleSp::Proxy& bottomDoublet,
0425       DoubletsForMiddleSp::Subset& topDoublets,
0426       TripletTopCandidates& tripletTopCandidates) const override {
0427     if constexpr (useStripInfo) {
0428       createStripTripletTopCandidates(spacePoints, spM, bottomDoublet,
0429                                       topDoublets, tripletTopCandidates);
0430     } else {
0431       createPixelTripletTopCandidates(spM, bottomDoublet, topDoublets,
0432                                       tripletTopCandidates);
0433     }
0434   }
0435 
0436   void createTripletTopCandidates(
0437       const SpacePointContainer& spacePoints, const ConstSpacePointProxy& spM,
0438       const DoubletsForMiddleSp::Proxy& bottomDoublet,
0439       DoubletsForMiddleSp::Subset2& topDoublets,
0440       TripletTopCandidates& tripletTopCandidates) const override {
0441     if constexpr (useStripInfo) {
0442       createStripTripletTopCandidates(spacePoints, spM, bottomDoublet,
0443                                       topDoublets, tripletTopCandidates);
0444     } else {
0445       createPixelTripletTopCandidates(spM, bottomDoublet, topDoublets,
0446                                       tripletTopCandidates);
0447     }
0448   }
0449 
0450  private:
0451   DerivedConfig m_cfg;
0452 };
0453 
0454 }  // namespace
0455 
0456 TripletSeedFinder::DerivedConfig::DerivedConfig(const Config& config,
0457                                                 float bFieldInZ_)
0458     : Config(config), bFieldInZ(bFieldInZ_) {
0459   // similar to `theta0Highland` in `Core/src/Material/Interactions.cpp`
0460   {
0461     using namespace Acts::UnitLiterals;
0462     const double xOverX0 = radLengthPerSeed;
0463     const double q2OverBeta2 = 1;  // q^2=1, beta^2~1
0464     // RPP2018 eq. 33.15 (treats beta and q² consistently)
0465     const double t = std::sqrt(xOverX0 * q2OverBeta2);
0466     // log((x/X0) * (q²/beta²)) = log((sqrt(x/X0) * (q/beta))²)
0467     //                          = 2 * log(sqrt(x/X0) * (q/beta))
0468     highland =
0469         static_cast<float>(13.6_MeV * t * (1.0 + 0.038 * 2 * std::log(t)));
0470   }
0471 
0472   const float maxScatteringAngle = highland / minPt;
0473   const float maxScatteringAngle2 = maxScatteringAngle * maxScatteringAngle;
0474 
0475   // bFieldInZ is in (pT/radius) natively, no need for conversion
0476   pTPerHelixRadius = bFieldInZ;
0477   minHelixDiameter2 = square(minPt * 2 / pTPerHelixRadius) * helixCutTolerance;
0478   const float pT2perRadius = square(highland / pTPerHelixRadius);
0479   sigmapT2perRadius = pT2perRadius * square(2 * sigmaScattering);
0480   multipleScattering2 = maxScatteringAngle2 * square(sigmaScattering);
0481 }
0482 
0483 std::unique_ptr<TripletSeedFinder> TripletSeedFinder::create(
0484     const DerivedConfig& config) {
0485   using BooleanOptions =
0486       boost::mp11::mp_list<std::bool_constant<false>, std::bool_constant<true>>;
0487 
0488   using UseStripInfoOptions = BooleanOptions;
0489   using SortedByCotThetaOptions = BooleanOptions;
0490 
0491   using TripletOptions =
0492       boost::mp11::mp_product<boost::mp11::mp_list, UseStripInfoOptions,
0493                               SortedByCotThetaOptions>;
0494 
0495   std::unique_ptr<TripletSeedFinder> result;
0496   boost::mp11::mp_for_each<TripletOptions>([&](auto option) {
0497     using OptionType = decltype(option);
0498 
0499     using UseStripInfo = boost::mp11::mp_at_c<OptionType, 0>;
0500     using SortedByCotTheta = boost::mp11::mp_at_c<OptionType, 1>;
0501 
0502     if (config.useStripInfo != UseStripInfo::value ||
0503         config.sortedByCotTheta != SortedByCotTheta::value) {
0504       return;  // skip if the configuration does not match
0505     }
0506 
0507     // check if we already have an implementation for this configuration
0508     if (result != nullptr) {
0509       throw std::runtime_error(
0510           "TripletSeedFinder: Multiple implementations found for one "
0511           "configuration");
0512     }
0513 
0514     // create the implementation for the given configuration
0515     result =
0516         std::make_unique<Impl<UseStripInfo::value, SortedByCotTheta::value>>(
0517             config);
0518   });
0519   if (result == nullptr) {
0520     throw std::runtime_error(
0521         "TripletSeedFinder: No implementation found for the given "
0522         "configuration");
0523   }
0524   return result;
0525 }
0526 
0527 }  // namespace Acts