Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-05 10:53:45

0001 
0002 /// @file SphereImplementation.h
0003 /// @brief Navigation kernels for spherical shells with optional phi and theta cuts.
0004 /// @author Raman Sehgal (raman.sehgal@cern.ch)
0005 
0006 #ifndef VECGEOM_VOLUMES_KERNEL_SPHEREIMPLEMENTATION_H_
0007 #define VECGEOM_VOLUMES_KERNEL_SPHEREIMPLEMENTATION_H_
0008 
0009 #include "VecGeom/base/Vector3D.h"
0010 #include "VecGeom/volumes/SphereStruct.h"
0011 #include "VecGeom/volumes/SurfaceHitView.h"
0012 #include "VecGeom/volumes/kernel/GenericKernels.h"
0013 #include <VecCore/VecCore>
0014 #include "VecGeom/volumes/kernel/OrbImplementation.h"
0015 #include "VecGeom/volumes/SphereUtilities.h"
0016 
0017 #include <cstdio>
0018 
0019 namespace vecgeom {
0020 
0021 VECGEOM_DEVICE_FORWARD_DECLARE(struct SphereImplementation;);
0022 VECGEOM_DEVICE_DECLARE_CONV(struct, SphereImplementation);
0023 
0024 inline namespace VECGEOM_IMPL_NAMESPACE {
0025 
0026 class PlacedSphere;
0027 template <typename T>
0028 struct SphereStruct;
0029 class UnplacedSphere;
0030 
0031 /// @brief Implementation for full, hollow, and angular-cut spheres.
0032 ///
0033 /// @details A sphere is modeled as a radial interval clipped by optional phi
0034 /// and theta boundaries. Full variants skip the corresponding angular checks.
0035 struct SphereImplementation {
0036 
0037   using PlacedShape_t    = PlacedSphere;
0038   using UnplacedStruct_t = SphereStruct<Precision>;
0039   using UnplacedVolume_t = UnplacedSphere;
0040 
0041   /// Boundary selected by the existing DistanceToOut minimization.
0042   enum class ExitSurface : unsigned char { kNone, kOuter, kInner, kPhiStart, kPhiEnd, kThetaStart, kThetaEnd };
0043 
0044   /// @brief Compute one phi-plane exit, retaining the wedge's surface and motion tolerances.
0045   /// @return Forward distance, zero for an outward surface start, or `kInfLength` for no exit.
0046   template <typename Real_v, bool Start>
0047   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static Real_v PhiDistanceToOut(UnplacedStruct_t const &sphere,
0048                                                                               Vector3D<Real_v> const &point,
0049                                                                               Vector3D<Real_v> const &direction)
0050   {
0051     Vector3D<Precision> const normal = Start ? sphere.fPhiWedge.GetNormal1() : sphere.fPhiWedge.GetNormal2();
0052     const Real_v motion              = direction.x() * normal.x() + direction.y() * normal.y();
0053     if (motion >= Real_v(-1.e-12)) return Real_v(kInfLength);
0054 
0055     const Real_v projection         = point.x() * normal.x() + point.y() * normal.y();
0056     Vector3D<Precision> const along = Start ? sphere.fPhiWedge.GetAlong1() : sphere.fPhiWedge.GetAlong2();
0057     if (Abs(projection) < Real_v(kTolerance) && point.x() * along.x() + point.y() * along.y() >= Real_v(0.))
0058       return Real_v(0.);
0059 
0060     Real_v distance = -projection / motion;
0061     if (distance > Real_v(-kTolerance) && distance < Real_v(0.)) distance = Real_v(0.);
0062     return distance >= Real_v(0.) ? distance : Real_v(kInfLength);
0063   }
0064 
0065   /// @brief Compute both phi-plane exit candidates; inactive candidates remain `kInfLength`.
0066   template <typename Real_v>
0067   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void PhiDistancesToOut(UnplacedStruct_t const &sphere,
0068                                                                              Vector3D<Real_v> const &point,
0069                                                                              Vector3D<Real_v> const &direction,
0070                                                                              Real_v &startDistance, Real_v &endDistance)
0071   {
0072     startDistance = PhiDistanceToOut<Real_v, true>(sphere, point, direction);
0073     endDistance   = PhiDistanceToOut<Real_v, false>(sphere, point, direction);
0074   }
0075 
0076   /// @brief Compute the exit-directed root of one theta boundary.
0077   /// @details Sine/cosine scaling keeps the quadratic finite near the equator.
0078   /// The caller shares the transverse point, direction, and mixed products between both boundaries.
0079   /// `intersects` excludes the opposite cone nappe and tangencies without an apex crossing.
0080   template <typename Real_v, bool Start>
0081   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void ThetaDistanceToOut(
0082       UnplacedStruct_t const &sphere, Vector3D<Real_v> const &point, Vector3D<Real_v> const &direction,
0083       Real_v const &rho2, Real_v const &pDotV2d, Real_v const &dirRho2, Real_v &distance, bool &intersects)
0084   {
0085     const Precision theta = Start ? sphere.fSTheta : sphere.eTheta;
0086     distance              = Real_v(kInfLength);
0087     intersects            = false;
0088     if ((Start && theta == 0.) || (!Start && theta == kPi)) return;
0089 
0090     if (Abs(theta - kHalfPi) <= 0.5 * sphere.kAngTolerance) {
0091       const bool movingOut = Start ? direction.z() > Real_v(0.) : direction.z() < Real_v(0.);
0092       if (!movingOut) return;
0093       distance = -point.z() / direction.z();
0094       if (distance > Real_v(-kTolerance) && distance < Real_v(0.)) distance = Real_v(0.);
0095       intersects = distance >= Real_v(0.);
0096       return;
0097     }
0098 
0099     const Real_v sinTheta = Start ? Real_v(sphere.sinSTheta) : Real_v(sphere.sinETheta);
0100     const Real_v cosTheta = Start ? Real_v(sphere.cosSTheta) : Real_v(sphere.cosETheta);
0101     const Real_v sin2     = sinTheta * sinTheta;
0102     const Real_v cos2     = cosTheta * cosTheta;
0103     const Real_v a        = cos2 * dirRho2 - sin2 * direction.z() * direction.z();
0104     const Real_v b        = cos2 * pDotV2d - sin2 * point.z() * direction.z();
0105     const Real_v c        = cos2 * rho2 - sin2 * point.z() * point.z();
0106     Real_v discriminant   = b * b - a * c;
0107     if (discriminant < Real_v(0.) && Abs(discriminant) < Real_v(kHalfTolerance)) discriminant = Real_v(0.);
0108     if (discriminant < Real_v(0.)) return;
0109 
0110     const Real_v root = Sqrt(discriminant);
0111     // The derivative at the two roots is +/-2*sqrt(discriminant); its exit sign selects a single root.
0112     const bool negativeRoot = (Start && theta < kHalfPi) || (!Start && theta > kHalfPi);
0113     if (negativeRoot) {
0114       if (b >= Real_v(0.) && a != Real_v(0.))
0115         distance = (-b - root) / a;
0116       else if (b < Real_v(0.))
0117         distance = c / NonZero(-b + root);
0118     } else {
0119       if (b > Real_v(0.))
0120         distance = c / NonZero(-b - root);
0121       else if (a != Real_v(0.))
0122         distance = (-b + root) / a;
0123     }
0124 
0125     if (distance > Real_v(-kTolerance) && distance < Real_v(0.)) distance = Real_v(0.);
0126     if (distance < Real_v(0.) || distance >= Real_v(kInfLength)) {
0127       distance = Real_v(kInfLength);
0128       return;
0129     }
0130     const Real_v hitZ    = point.z() + distance * direction.z();
0131     bool crossesBoundary = discriminant > Real_v(0.);
0132     if (!crossesBoundary) {
0133       const Real_v hitX   = point.x() + distance * direction.x();
0134       const Real_v hitY   = point.y() + distance * direction.y();
0135       const bool hitsApex = hitX * hitX + hitY * hitY + hitZ * hitZ <= Real_v(kTolerance * kTolerance);
0136       if (hitsApex)
0137         crossesBoundary = Start ? direction.z() > Real_v(sphere.cosSTheta) : direction.z() < Real_v(sphere.cosETheta);
0138     }
0139     intersects = crossesBoundary && (theta < kHalfPi ? hitZ > Real_v(-kHalfTolerance) : hitZ < Real_v(kHalfTolerance));
0140   }
0141 
0142   /// @brief Compute both theta exits, sharing the transverse quadratic coefficients.
0143   template <typename Real_v>
0144   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void ThetaDistancesToOut(
0145       UnplacedStruct_t const &sphere, Vector3D<Real_v> const &point, Vector3D<Real_v> const &direction,
0146       Real_v &startDistance, Real_v &endDistance, bool &startIntersects, bool &endIntersects)
0147   {
0148     const Real_v rho2    = point.Perp2();
0149     const Real_v pDotV2d = point.x() * direction.x() + point.y() * direction.y();
0150     const Real_v dirRho2 = direction.Perp2();
0151     ThetaDistanceToOut<Real_v, true>(sphere, point, direction, rho2, pDotV2d, dirRho2, startDistance, startIntersects);
0152     ThetaDistanceToOut<Real_v, false>(sphere, point, direction, rho2, pDotV2d, dirRho2, endDistance, endIntersects);
0153   }
0154 
0155   /// Return the boundary responsible for an immediate surface exit.
0156   /// @param sphere Sphere parameters and cached angular state.
0157   /// @param point Local point classified on the surface.
0158   /// @param direction Normalized local exit direction.
0159   template <typename Real_v>
0160   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static ExitSurface SelectSurfaceExit(UnplacedStruct_t const &sphere,
0161                                                                                     Vector3D<Real_v> const &point,
0162                                                                                     Vector3D<Real_v> const &direction)
0163   {
0164     const Real_v rad2 = point.Mag2();
0165     if (SphereUtilities::IsPointOnRadialSurfaceAndMovingOut<Real_v, false, true>(sphere, point, direction) &&
0166         sphere.fPhiWedge.Contains<Real_v>(point) && sphere.fThetaCone.Contains<Real_v>(point))
0167       return ExitSurface::kOuter;
0168     if (sphere.fRmin &&
0169         SphereUtilities::IsPointOnRadialSurfaceAndMovingOut<Real_v, true, true>(sphere, point, direction) &&
0170         sphere.fPhiWedge.Contains<Real_v>(point) && sphere.fThetaCone.Contains<Real_v>(point))
0171       return ExitSurface::kInner;
0172     if (!sphere.fFullThetaSphere && rad2 >= sphere.fRmin * sphere.fRmin && rad2 <= sphere.fRmax * sphere.fRmax &&
0173         sphere.fPhiWedge.Contains<Real_v>(point)) {
0174       if (SphereUtilities::IsPointOnThetaSurfaceAndMovingOut<Real_v, true, true>(sphere, point, direction))
0175         return ExitSurface::kThetaStart;
0176       if (SphereUtilities::IsPointOnThetaSurfaceAndMovingOut<Real_v, false, true>(sphere, point, direction))
0177         return ExitSurface::kThetaEnd;
0178     }
0179     if (!sphere.fFullPhiSphere && rad2 >= sphere.fRmin * sphere.fRmin && rad2 <= sphere.fRmax * sphere.fRmax &&
0180         sphere.fThetaCone.Contains<Real_v>(point)) {
0181       if (sphere.fPhiWedge.IsPointOnSurfaceAndMovingOut<Real_v, true, true>(point, direction))
0182         return ExitSurface::kPhiStart;
0183       if (sphere.fPhiWedge.IsPointOnSurfaceAndMovingOut<Real_v, false, true>(point, direction))
0184         return ExitSurface::kPhiEnd;
0185     }
0186     return ExitSurface::kNone;
0187   }
0188 
0189   /// Convert the recorded winning boundary into the optional exit-hit outputs.
0190   /// @details The outward normal of the selected boundary is always returned;
0191   /// `leftBehind` tells whether the whole solid lies behind its tangent plane.
0192   /// @param sphere Sphere parameters and cached angular state.
0193   /// @param point Local starting point.
0194   /// @param direction Normalized local direction.
0195   /// @param distance Computed exit distance.
0196   /// @param surface Boundary selected by the distance minimization.
0197   /// @param hitInfo Optional-output view to fill.
0198   template <typename Real_v>
0199   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void SetExitHit(UnplacedStruct_t const &sphere,
0200                                                                       Vector3D<Real_v> const &point,
0201                                                                       Vector3D<Real_v> const &direction,
0202                                                                       Real_v const &distance, ExitSurface surface,
0203                                                                       SurfaceHitView<Real_v> *hitInfo)
0204   {
0205     bool leftBehind = false;
0206     switch (surface) {
0207     case ExitSurface::kOuter:
0208       leftBehind = true;
0209       if (hitInfo->WantsNormal()) {
0210         Vector3D<Real_v> normal = point + distance * direction;
0211         normal *= distance == Real_v(0.) ? Real_v(1.) / Sqrt(normal.Mag2()) : Real_v(1.) / sphere.fRmax;
0212         hitInfo->SetNormal(normal);
0213       }
0214       break;
0215     case ExitSurface::kPhiStart:
0216     case ExitSurface::kPhiEnd:
0217       leftBehind = sphere.fDPhi <= kPi;
0218       if (hitInfo->WantsNormal()) {
0219         const Vector3D<Precision> normal =
0220             surface == ExitSurface::kPhiStart ? sphere.fPhiWedge.GetNormal1() : sphere.fPhiWedge.GetNormal2();
0221         hitInfo->SetNormal(Vector3D<Real_v>(-normal.x(), -normal.y(), Real_v(0.)));
0222       }
0223       break;
0224     case ExitSurface::kThetaStart:
0225     case ExitSurface::kThetaEnd: {
0226       const bool start = surface == ExitSurface::kThetaStart;
0227       leftBehind       = start ? sphere.fSTheta >= kHalfPi : sphere.eTheta <= kHalfPi;
0228       if (hitInfo->WantsNormal()) {
0229         const Precision theta = start ? sphere.fSTheta : sphere.eTheta;
0230         Vector3D<Real_v> normal;
0231         if (Abs(theta - kHalfPi) <= kTolerance) {
0232           normal.Set(Real_v(0.), Real_v(0.), start ? Real_v(1.) : Real_v(-1.));
0233         } else {
0234           Vector3D<Real_v> reference = point + distance * direction;
0235           Real_v transverse2         = reference.Perp2();
0236           // Every angular boundary meets at the origin. Use the incident radial direction there because the apex
0237           // itself has no azimuth from which to construct a cone normal.
0238           if (transverse2 <= Real_v(kTolerance * kTolerance)) {
0239             reference   = point.Perp2() > Real_v(kTolerance * kTolerance) ? point : -direction;
0240             transverse2 = reference.Perp2();
0241           }
0242           const Real_v invTransverse = transverse2 > Real_v(0.) ? Real_v(1.) / Sqrt(transverse2) : Real_v(0.);
0243           const Real_v sinTheta      = start ? sphere.sinSTheta : sphere.sinETheta;
0244           const Real_v cosTheta      = start ? sphere.cosSTheta : sphere.cosETheta;
0245           normal.Set(cosTheta * reference.x() * invTransverse, cosTheta * reference.y() * invTransverse, -sinTheta);
0246           if (transverse2 <= Real_v(0.)) normal.x() = cosTheta;
0247           if (start) normal = -normal;
0248         }
0249         hitInfo->SetNormal(normal);
0250       }
0251       break;
0252     }
0253     case ExitSurface::kInner:
0254       // The inner sphere never supports the solid; its outward normal points to the center.
0255       if (hitInfo->WantsNormal()) {
0256         Vector3D<Real_v> normal = point + distance * direction;
0257         normal *= distance == Real_v(0.) ? Real_v(-1.) / Sqrt(normal.Mag2()) : Real_v(-1.) / sphere.fRmin;
0258         hitInfo->SetNormal(normal);
0259       }
0260       break;
0261     case ExitSurface::kNone:
0262       break;
0263     }
0264     hitInfo->SetLeftBehind(leftBehind);
0265   }
0266 
0267   /// @brief Return whether a point is inside or on the sphere.
0268   /// @param sphere Sphere parameters and cached angular state.
0269   /// @param point Local point to test.
0270   /// @param inside Set to true for inside or surface points.
0271   template <typename Real_v>
0272   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void Contains(UnplacedStruct_t const &sphere,
0273                                                                     Vector3D<Real_v> const &point, bool &inside)
0274   {
0275     bool unused = false, outside = false;
0276     GenericKernelForContainsAndInside<Real_v, false>(sphere, point, unused, outside);
0277     inside = !outside;
0278   }
0279 
0280   /// @brief Classify a point as inside, outside, or on the surface.
0281   /// @param sphere Sphere parameters and cached angular state.
0282   /// @param point Local point to classify.
0283   /// @param inside Set to the corresponding `EInside` value.
0284   template <typename Real_v>
0285   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void Inside(UnplacedStruct_t const &sphere,
0286                                                                   Vector3D<Real_v> const &point, Inside_t &inside)
0287   {
0288     bool completelyinside = false, completelyoutside = false;
0289     GenericKernelForContainsAndInside<Real_v, true, true>(sphere, point, completelyinside, completelyoutside);
0290     inside = EInside::kSurface;
0291     if (completelyoutside) inside = Inside_t(EInside::kOutside);
0292     if (completelyinside) inside = Inside_t(EInside::kInside);
0293   }
0294 
0295   /// @brief Evaluate complete-inside and complete-outside predicates.
0296   /// @details Combines the radial interval with active phi and theta clips.
0297   /// Points in tolerance regions can leave both outputs false.
0298   /// @tparam ForInside Enables complete-inside updates used by `Inside`.
0299   /// @tparam EarlyReturn Skips angular work after definite rejection in standalone classification.
0300   /// @param sphere Sphere parameters and cached angular state.
0301   /// @param localPoint Local point to classify.
0302   /// @param completelyinside Set when all active predicates are safely inside.
0303   /// @param completelyoutside Set when any active predicate is safely outside.
0304   template <typename Real_v, bool ForInside, bool EarlyReturn = false>
0305   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void GenericKernelForContainsAndInside(
0306       UnplacedStruct_t const &sphere, Vector3D<Real_v> const &localPoint, bool &completelyinside,
0307       bool &completelyoutside)
0308   {
0309     Real_v rad2 = localPoint.Mag2();
0310 
0311     // Check radial surfaces
0312     // Radial check for GenericKernel Start
0313     if (sphere.fRmin)
0314       completelyinside =
0315           rad2 <= MakeMinusTolerantSquare<true>(sphere.fRmax) && rad2 >= MakePlusTolerantSquare<true>(sphere.fRmin);
0316     else
0317       completelyinside = rad2 <= MakeMinusTolerantSquare<true>(sphere.fRmax);
0318 
0319     if (sphere.fRmin)
0320       completelyoutside =
0321           rad2 >= MakePlusTolerantSquare<true>(sphere.fRmax) || rad2 <= MakeMinusTolerantSquare<true>(sphere.fRmin);
0322     else
0323       completelyoutside = rad2 >= MakePlusTolerantSquare<true>(sphere.fRmax);
0324 
0325     // Phi boundaries  : Do not check if it has no phi boundary!
0326     if (!sphere.fFullPhiSphere) {
0327       // Only Inside opts in; overlapping rounded tolerance bounds still need angular checks.
0328       if constexpr (EarlyReturn) {
0329         if (completelyoutside && !completelyinside) return;
0330       }
0331 
0332       bool completelyoutsidephi = false;
0333       bool completelyinsidephi  = false;
0334       sphere.fPhiWedge.GenericKernelForContainsAndInside<Real_v, ForInside>(localPoint, completelyinsidephi,
0335                                                                             completelyoutsidephi);
0336       completelyoutside |= completelyoutsidephi;
0337 
0338       if (ForInside) completelyinside &= completelyinsidephi;
0339     }
0340     // Phi Check for GenericKernel Over
0341 
0342     // Theta bondaries
0343     if (!sphere.fFullThetaSphere) {
0344       if constexpr (EarlyReturn) {
0345         if (completelyoutside && !completelyinside) return;
0346       }
0347 
0348       bool completelyoutsidetheta = false;
0349       bool completelyinsidetheta  = false;
0350       sphere.fThetaCone.GenericKernelForContainsAndInside<Real_v, ForInside>(localPoint, completelyinsidetheta,
0351                                                                              completelyoutsidetheta);
0352       completelyoutside |= completelyoutsidetheta;
0353 
0354       if (ForInside) completelyinside &= completelyinsidetheta;
0355     }
0356     return;
0357   }
0358 
0359   /// @brief Compute the first valid entry distance.
0360   /// @details Radial shell roots, phi-plane roots, and theta-cone roots are
0361   /// filtered against the active cuts. Theta roots must cross into material;
0362   /// grazing roots are ignored except for the explicit apex-entry case.
0363   /// @param sphere Sphere parameters and cached angular state.
0364   /// @param point Local starting point.
0365   /// @param direction Normalized local direction.
0366   /// @param distance Set to the entry distance, `-1`, `0`, or `kInfLength`.
0367   template <typename Real_v>
0368   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void DistanceToIn(UnplacedStruct_t const &sphere,
0369                                                                         Vector3D<Real_v> const &point,
0370                                                                         Vector3D<Real_v> const &direction,
0371                                                                         Real_v const & /* stepMax */, Real_v &distance)
0372   {
0373     distance  = kInfLength;
0374     bool done = false;
0375 
0376     bool fullPhiSphere   = sphere.fFullPhiSphere;
0377     bool fullThetaSphere = sphere.fFullThetaSphere;
0378 
0379     // General Precalcs
0380     Real_v rad2    = point.Mag2();
0381     Real_v pDotV3d = point.Dot(direction);
0382 
0383     Real_v c = rad2 - sphere.fRmax * sphere.fRmax;
0384 
0385     // Beyond the outer radial tolerance, neither an inside nor a surface start is possible.
0386     if (rad2 <= MakePlusTolerantSquare<true>(sphere.fRmax)) {
0387       if (SphereUtilities::IsCompletelyInside<Real_v>(sphere, point)) {
0388         distance = Real_v(-1.0);
0389         return;
0390       }
0391 
0392       if (SphereUtilities::IsPointOnSurfaceAndMovingOut<Real_v, false>(sphere, point, direction)) {
0393         distance = Real_v(0.0);
0394         return;
0395       }
0396     }
0397 
0398     Real_v sd1(kInfLength);
0399     Real_v sd2(kInfLength);
0400     Real_v d2 = (pDotV3d * pDotV3d - c);
0401     if (d2 < Real_v(0.) || ((c > Real_v(0.)) && (pDotV3d > Real_v(0.)))) return;
0402 
0403     sd1 = -pDotV3d - Sqrt(d2);
0404 
0405     Real_v outerDist(kInfLength);
0406     Real_v innerDist(kInfLength);
0407 
0408     if (sphere.fFullSphere) {
0409       if (sd1 > Real_v(kTolerance)) outerDist = sd1;
0410     } else {
0411       if (sd1 > Real_v(kTolerance) && sd1 < Real_v(kInfLength)) {
0412         Vector3D<Real_v> tmpPt = point + sd1 * direction;
0413         // Only active angular cuts can reject a radial entry candidate.
0414         if ((fullPhiSphere || sphere.fPhiWedge.Inside<Real_v>(tmpPt) != EInside::kOutside) &&
0415             (fullThetaSphere || sphere.fThetaCone.Inside<Real_v>(tmpPt) != EInside::kOutside))
0416           outerDist = sd1;
0417       }
0418     }
0419 
0420     if (sphere.fRmin) {
0421       c  = rad2 - sphere.fRmin * sphere.fRmin;
0422       d2 = pDotV3d * pDotV3d - c;
0423       if (d2 >= Real_v(0.)) sd2 = -pDotV3d + Sqrt(d2);
0424       const bool innerTangentContinuation = (c <= Real_v(2. * kTolerance) * sphere.fRmin) &&
0425                                             (c >= -Real_v(2. * kTolerance) * sphere.fRmin) &&
0426                                             (pDotV3d >= -kToleranceDist<Real_v>);
0427 
0428       if (sphere.fFullSphere) {
0429         // Exact tangents on a concave inner sphere enter material at second
0430         // order; DistanceToOut owns the complementary hollow-crossing side.
0431         if (sd2 > Real_v(kTolerance) || (innerTangentContinuation && sd2 >= -Real_v(kTolerance))) {
0432           innerDist = innerTangentContinuation ? Real_v(0.) : sd2;
0433         }
0434       } else {
0435         if (innerTangentContinuation || (sd2 > Real_v(kTolerance) && sd2 < Real_v(kInfLength))) {
0436           const Real_v innerCandidate = innerTangentContinuation ? Real_v(0.) : sd2;
0437           Vector3D<Real_v> tmpPt      = point + innerCandidate * direction;
0438           if ((fullPhiSphere || sphere.fPhiWedge.Inside<Real_v>(tmpPt) != EInside::kOutside) &&
0439               (fullThetaSphere || sphere.fThetaCone.Inside<Real_v>(tmpPt) != EInside::kOutside))
0440             innerDist = innerCandidate;
0441         }
0442       }
0443     }
0444 
0445     distance = Min(outerDist, innerDist);
0446 
0447     if (!fullPhiSphere) {
0448       GetMinDistFromPhi<Real_v, true>(sphere, point, direction, done, distance);
0449     }
0450 
0451     Real_v distThetaMin(kInfLength);
0452 
0453     if (!fullThetaSphere) {
0454       bool intsect1 = false;
0455       bool intsect2 = false;
0456       Real_v distTheta1(kInfLength), distCone1(kInfLength);
0457       Real_v distTheta2(kInfLength), distCone2(kInfLength);
0458       Real_v rmin2 = sphere.fRmin * sphere.fRmin;
0459       Real_v rmax2 = sphere.fRmax * sphere.fRmax;
0460       Vector3D<Real_v> coneIntSecPt1;
0461       Vector3D<Real_v> coneIntSecPt2;
0462 
0463       sphere.fThetaCone.DistanceToIn<Real_v>(point, direction, distTheta1, distTheta2, intsect1,
0464                                              intsect2); //,cone1IntSecPt, cone2IntSecPt);
0465       bool cone1MovesIn    = false;
0466       bool cone1EntersApex = false;
0467       bool cone1ZeroRoot   = intsect1 && (Abs(distTheta1) <= Real_v(kTolerance));
0468       bool candidateCone1  = intsect1 && ((distTheta1 > Real_v(kTolerance)) || cone1ZeroRoot);
0469       Real_v pDotV2d       = point.x() * direction.x() + point.y() * direction.y();
0470       Real_v dirRho2       = direction.Perp2();
0471       Real_v pointZDir     = point.z() * direction.z();
0472       Real_v dirZ2         = direction.z() * direction.z();
0473 
0474       if (candidateCone1) {
0475         distCone1 = rad2 + distTheta1 * (Real_v(2.) * pDotV3d + distTheta1);
0476         candidateCone1 &= distCone1 >= rmin2 && distCone1 <= rmax2;
0477         if (candidateCone1) {
0478           Real_v motion1 = -direction.z();
0479           Real_v a       = Real_v(0.);
0480           if (Abs(sphere.fSTheta - kHalfPi) > kTolerance) {
0481             Real_v tanTheta2 = Real_v(sphere.fThetaCone.GetTanSTheta2());
0482             a                = dirRho2 - dirZ2 * tanTheta2;
0483             Real_v b         = pDotV2d - pointZDir * tanTheta2;
0484             motion1          = b + distTheta1 * a;
0485           }
0486           cone1MovesIn = SphereUtilities::IsThetaConeMotion<Real_v, true, false>(sphere, motion1);
0487           // A zero theta-root with unresolved first-order motion can still
0488           // enter material through the cone curvature.
0489           if (cone1ZeroRoot && !cone1MovesIn && Abs(motion1) <= kToleranceDist<Real_v>) {
0490             cone1MovesIn = (sphere.fSTheta <= kHalfPi) ? (a > Real_v(0.)) : (a < Real_v(0.));
0491           }
0492           cone1EntersApex = (sphere.fRmin == 0.) && (distCone1 <= Real_v(kTolerance * kTolerance)) &&
0493                             sphere.fThetaCone.Contains<Real_v>(direction);
0494           if (!fullPhiSphere) cone1EntersApex &= sphere.fPhiWedge.Contains<Real_v>(direction);
0495         }
0496       }
0497 
0498       bool cone2MovesIn    = false;
0499       bool cone2EntersApex = false;
0500       bool cone2ZeroRoot   = intsect2 && (Abs(distTheta2) <= Real_v(kTolerance));
0501       bool candidateCone2  = intsect2 && ((distTheta2 > Real_v(kTolerance)) || cone2ZeroRoot);
0502       if (candidateCone2) {
0503         distCone2 = rad2 + distTheta2 * (Real_v(2.) * pDotV3d + distTheta2);
0504         candidateCone2 &= distCone2 >= rmin2 && distCone2 <= rmax2;
0505         if (candidateCone2) {
0506           Real_v motion2 = -direction.z();
0507           Real_v a       = Real_v(0.);
0508           if (Abs(sphere.eTheta - kHalfPi) > kTolerance) {
0509             Real_v tanTheta2 = Real_v(sphere.fThetaCone.GetTanETheta2());
0510             a                = dirRho2 - dirZ2 * tanTheta2;
0511             Real_v b         = pDotV2d - pointZDir * tanTheta2;
0512             motion2          = b + distTheta2 * a;
0513           }
0514           cone2MovesIn = SphereUtilities::IsThetaConeMotion<Real_v, false, false>(sphere, motion2);
0515           // Same zero-root rule for the end-theta cone.
0516           if (cone2ZeroRoot && !cone2MovesIn && Abs(motion2) <= kToleranceDist<Real_v>) {
0517             cone2MovesIn = (sphere.eTheta <= kHalfPi) ? (a < Real_v(0.)) : (a > Real_v(0.));
0518           }
0519           cone2EntersApex = (sphere.fRmin == 0.) && (distCone2 <= Real_v(kTolerance * kTolerance)) &&
0520                             sphere.fThetaCone.Contains<Real_v>(direction);
0521           if (!fullPhiSphere) cone2EntersApex &= sphere.fPhiWedge.Contains<Real_v>(direction);
0522         }
0523       }
0524 
0525       bool isValidCone1 = candidateCone1 && (cone1MovesIn || cone1EntersApex);
0526       bool isValidCone2 = candidateCone2 && (cone2MovesIn || cone2EntersApex);
0527 
0528       if (!fullPhiSphere && isValidCone1 && !cone1EntersApex) {
0529         coneIntSecPt1 = point + distTheta1 * direction;
0530         isValidCone1  = sphere.fPhiWedge.Contains<Real_v>(coneIntSecPt1);
0531       }
0532       if (!fullPhiSphere && isValidCone2 && !cone2EntersApex) {
0533         coneIntSecPt2 = point + distTheta2 * direction;
0534         isValidCone2  = sphere.fPhiWedge.Contains<Real_v>(coneIntSecPt2);
0535       }
0536       if (isValidCone2 && !isValidCone1) distThetaMin = distTheta2;
0537       if (isValidCone1 && !isValidCone2) distThetaMin = distTheta1;
0538       if (isValidCone1 && isValidCone2) distThetaMin = Min(distTheta1, distTheta2);
0539 
0540       bool isApexEntry = (sphere.fRmin == 0.) && (rad2 <= Real_v(kTolerance * kTolerance)) &&
0541                          sphere.fThetaCone.Contains<Real_v>(direction);
0542       if (!fullPhiSphere) isApexEntry &= sphere.fPhiWedge.Contains<Real_v>(direction);
0543       if (isApexEntry) distThetaMin = Real_v(0.);
0544     }
0545 
0546     distance = Min(distThetaMin, distance);
0547   }
0548 
0549   /// @brief Compute the first valid exit distance.
0550   /// @details Considers radial shell roots, theta-cone roots, and phi-plane
0551   /// roots, returning the nearest accepted boundary crossing.
0552   /// @tparam RecordHitInfo Whether to populate the optional exit information.
0553   /// @param sphere Sphere parameters and cached angular state.
0554   /// @param point Local starting point.
0555   /// @param direction Normalized local direction.
0556   /// @param[out] distance Set to the exit distance, `-1`, `0`, or `kInfLength`.
0557   /// @param hitInfo Optional-output view; required when `RecordHitInfo` is true.
0558   template <typename Real_v, bool RecordHitInfo>
0559   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void DistanceToOutImpl(
0560       UnplacedStruct_t const &sphere, Vector3D<Real_v> const &point, Vector3D<Real_v> const &direction,
0561       Real_v const & /* stepMax */, Real_v &distance, SurfaceHitView<Real_v> *hitInfo)
0562   {
0563 
0564     distance                                = kInfLength;
0565     [[maybe_unused]] ExitSurface hitSurface = ExitSurface::kNone;
0566     if constexpr (RecordHitInfo) hitInfo->Clear();
0567 
0568     Real_v snxt(kInfLength);
0569 
0570     Real_v d2(0.);
0571 
0572     Real_v pDotV3d = point.Dot(direction);
0573 
0574     Real_v rad2 = point.Mag2();
0575     Real_v c    = rad2 - sphere.fRmax * sphere.fRmax;
0576 
0577     Real_v sd1(kInfLength);
0578     Real_v sd2(kInfLength);
0579 
0580     const Real_v outerLimit = sphere.fRmax + Real_v(kTolerance);
0581     bool completelyOutside  = rad2 > outerLimit * outerLimit;
0582     if (sphere.fRmin > kTolerance) {
0583       const Real_v innerLimit = sphere.fRmin - Real_v(kTolerance);
0584       completelyOutside |= rad2 < innerLimit * innerLimit;
0585     }
0586     if (!sphere.fFullPhiSphere) {
0587       bool unused = false, outsidePhi = false;
0588       sphere.fPhiWedge.GenericKernelForContainsAndInside<Real_v, false>(point, unused, outsidePhi);
0589       if (outsidePhi) {
0590         bool insidePhi = false;
0591         sphere.fPhiWedge.GenericKernelForContainsAndInside<Real_v, true>(point, insidePhi, outsidePhi);
0592       }
0593       completelyOutside |= outsidePhi;
0594     }
0595     bool thetaNeedsSurfaceCheck = false;
0596     if (!sphere.fFullThetaSphere) {
0597       const Real_v radius = Sqrt(rad2);
0598       // Match ThetaCone's 2*10*kAngTolerance broad band before invoking its exact surface classifier.
0599       const Real_v margin = Real_v(20. * sphere.kAngTolerance);
0600       const bool clearlyInsideTheta =
0601           point.z() < radius * sphere.cosSTheta - margin && point.z() > radius * sphere.cosETheta + margin;
0602       thetaNeedsSurfaceCheck = !clearlyInsideTheta;
0603       if (thetaNeedsSurfaceCheck) completelyOutside |= sphere.fThetaCone.IsCompletelyOutside<Real_v>(point);
0604     }
0605     if (completelyOutside) {
0606       distance = Real_v(-1.0);
0607       return;
0608     }
0609 
0610     // Preserve zero exits throughout the radial surface band; angular containment is needed only there.
0611     const bool outerSurfaceExit =
0612         rad2 >= MakeMinusTolerantSquare<true>(sphere.fRmax) && pDotV3d > Real_v(-kToleranceDist<Real_v>);
0613     const bool innerSurfaceExit =
0614         sphere.fRmin && rad2 <= MakePlusTolerantSquare<true>(sphere.fRmin) && pDotV3d < Real_v(-kToleranceDist<Real_v>);
0615     if ((outerSurfaceExit || innerSurfaceExit) && (sphere.fFullPhiSphere || sphere.fPhiWedge.Contains<Real_v>(point)) &&
0616         (sphere.fFullThetaSphere || sphere.fThetaCone.Contains<Real_v>(point))) {
0617       distance = Real_v(0.);
0618       if constexpr (RecordHitInfo)
0619         SetExitHit(sphere, point, direction, distance, outerSurfaceExit ? ExitSurface::kOuter : ExitSurface::kInner,
0620                    hitInfo);
0621       return;
0622     }
0623 
0624     d2 = (pDotV3d * pDotV3d - c);
0625     if (d2 >= Real_v(0.)) sd1 = -pDotV3d + Sqrt(d2);
0626 
0627     if (sphere.fRmin) {
0628       c  = rad2 - sphere.fRmin * sphere.fRmin;
0629       d2 = (pDotV3d * pDotV3d - c);
0630       if (d2 >= Real_v(0.) && pDotV3d < -kToleranceDist<Real_v>) sd2 = -pDotV3d - Sqrt(d2);
0631     }
0632 
0633     if constexpr (RecordHitInfo) {
0634       if (sd1 < sd2) {
0635         snxt       = sd1;
0636         hitSurface = ExitSurface::kOuter;
0637       } else {
0638         snxt       = sd2;
0639         hitSurface = sd2 < Real_v(kInfLength) ? ExitSurface::kInner : ExitSurface::kNone;
0640       }
0641     } else {
0642       snxt = Min(sd1, sd2);
0643     }
0644 
0645     const bool upperHemisphere = sphere.fSTheta == 0. && sphere.eTheta == kHalfPi;
0646     const bool lowerHemisphere = sphere.fSTheta == kHalfPi && sphere.eTheta == kPi;
0647     if (sphere.fFullPhiSphere && (upperHemisphere || lowerHemisphere)) {
0648       distance              = snxt;
0649       const bool exitsPlane = upperHemisphere ? direction.z() < Real_v(0.) : direction.z() > Real_v(0.);
0650       if (exitsPlane) {
0651         Real_v planeDistance = -point.z() / direction.z();
0652         if (Abs(point.z()) < Real_v(kTolerance)) planeDistance = Real_v(0.);
0653         if (planeDistance < distance) {
0654           distance = planeDistance;
0655           if constexpr (RecordHitInfo) hitSurface = upperHemisphere ? ExitSurface::kThetaEnd : ExitSurface::kThetaStart;
0656         }
0657       }
0658       if constexpr (RecordHitInfo) SetExitHit(sphere, point, direction, distance, hitSurface, hitInfo);
0659       return;
0660     }
0661 
0662     Real_v distThetaMin(kInfLength);
0663     Real_v distPhiMin(kInfLength);
0664     [[maybe_unused]] ExitSurface thetaSurface = ExitSurface::kNone;
0665 
0666     if (!sphere.fFullThetaSphere) {
0667       bool intsect1 = false;
0668       bool intsect2 = false;
0669       Real_v distTheta1(kInfLength);
0670       Real_v distTheta2(kInfLength);
0671       ThetaDistancesToOut(sphere, point, direction, distTheta1, distTheta2, intsect1, intsect2);
0672       if (thetaNeedsSurfaceCheck) {
0673         if (SphereUtilities::IsPointOnThetaSurfaceAndMovingOut<Real_v, true, true>(sphere, point, direction)) {
0674           distTheta1 = Real_v(0.);
0675           intsect1   = true;
0676         }
0677         if (SphereUtilities::IsPointOnThetaSurfaceAndMovingOut<Real_v, false, true>(sphere, point, direction)) {
0678           distTheta2 = Real_v(0.);
0679           intsect2   = true;
0680         }
0681       }
0682       // The cone helper can expose a backward root; only tolerated surface roots belong to this exit query.
0683       intsect1 &= distTheta1 >= -Real_v(kTolerance);
0684       intsect2 &= distTheta2 >= -Real_v(kTolerance);
0685       if (intsect1 && Abs(distTheta1) <= Real_v(kTolerance)) {
0686         const bool apexExit = rad2 <= Real_v(kTolerance * kTolerance) && direction.z() > Real_v(sphere.cosSTheta);
0687         Real_v motion1      = -direction.z();
0688         Real_v a            = Real_v(0.);
0689         if (Abs(sphere.fSTheta - kHalfPi) > kTolerance) {
0690           Real_v tanTheta2 = Real_v(sphere.fThetaCone.GetTanSTheta2());
0691           a                = direction.Perp2() - direction.z() * direction.z() * tanTheta2;
0692           motion1 = point.x() * direction.x() + point.y() * direction.y() - point.z() * direction.z() * tanTheta2;
0693         }
0694         // From a theta surface, an inward/tangential material continuation
0695         // must exit at the next boundary, not at the current zero root.
0696         bool materialContinuation = SphereUtilities::IsThetaConeMotion<Real_v, true, false>(sphere, motion1);
0697         if (!materialContinuation && Abs(motion1) <= kToleranceDist<Real_v>) {
0698           materialContinuation = (sphere.fSTheta <= kHalfPi) ? (a > Real_v(0.)) : (a < Real_v(0.));
0699         }
0700         if (materialContinuation && !apexExit) intsect1 = false;
0701       }
0702       if (intsect2 && Abs(distTheta2) <= Real_v(kTolerance)) {
0703         const bool apexExit = rad2 <= Real_v(kTolerance * kTolerance) && direction.z() < Real_v(sphere.cosETheta);
0704         Real_v motion2      = -direction.z();
0705         Real_v a            = Real_v(0.);
0706         if (Abs(sphere.eTheta - kHalfPi) > kTolerance) {
0707           Real_v tanTheta2 = Real_v(sphere.fThetaCone.GetTanETheta2());
0708           a                = direction.Perp2() - direction.z() * direction.z() * tanTheta2;
0709           motion2 = point.x() * direction.x() + point.y() * direction.y() - point.z() * direction.z() * tanTheta2;
0710         }
0711         // Same zero-root suppression for end-theta material continuation.
0712         bool materialContinuation = SphereUtilities::IsThetaConeMotion<Real_v, false, false>(sphere, motion2);
0713         if (!materialContinuation && Abs(motion2) <= kToleranceDist<Real_v>) {
0714           materialContinuation = (sphere.eTheta <= kHalfPi) ? (a < Real_v(0.)) : (a > Real_v(0.));
0715         }
0716         if (materialContinuation && !apexExit) intsect2 = false;
0717       }
0718       if (intsect2 && !intsect1) {
0719         distThetaMin = distTheta2;
0720         if constexpr (RecordHitInfo) thetaSurface = ExitSurface::kThetaEnd;
0721       }
0722       if (!intsect2 && intsect1) {
0723         distThetaMin = distTheta1;
0724         if constexpr (RecordHitInfo) thetaSurface = ExitSurface::kThetaStart;
0725       }
0726       if (intsect2 && intsect1) {
0727         distThetaMin = Min(distTheta1, distTheta2);
0728         if constexpr (RecordHitInfo)
0729           thetaSurface = distTheta1 < distTheta2 ? ExitSurface::kThetaStart : ExitSurface::kThetaEnd;
0730       }
0731     }
0732 
0733     if constexpr (RecordHitInfo) {
0734       distance = snxt;
0735       if (distThetaMin < distance) {
0736         distance   = distThetaMin;
0737         hitSurface = thetaSurface;
0738       }
0739     } else {
0740       distance = Min(distThetaMin, snxt);
0741     }
0742 
0743     if (!sphere.fFullPhiSphere) {
0744       if (sphere.fDPhi <= kPi) {
0745         Real_v distPhi1;
0746         Real_v distPhi2;
0747         PhiDistancesToOut(sphere, point, direction, distPhi1, distPhi2);
0748         distPhiMin = Min(distPhi1, distPhi2);
0749         if constexpr (RecordHitInfo) {
0750           if (distPhiMin < distance) {
0751             distance   = distPhiMin;
0752             hitSurface = distPhi1 < distPhi2 ? ExitSurface::kPhiStart : ExitSurface::kPhiEnd;
0753           }
0754         } else {
0755           distance = Min(distPhiMin, distance);
0756         }
0757       } else {
0758         GetMinDistToOutFromPhi<Real_v, RecordHitInfo>(sphere, point, direction, distance, &hitSurface);
0759       }
0760     }
0761     // A tolerance-surface ray can have no quadratic root when its direction is nearly tangent. Keep the complete
0762     // surface classification as a cold fallback rather than paying for it before every regular distance query.
0763     if (distance >= Real_v(kInfLength) &&
0764         SphereUtilities::IsPointOnSurfaceAndMovingOut<Real_v, true>(sphere, point, direction)) {
0765       distance = Real_v(0.);
0766       if constexpr (RecordHitInfo) hitSurface = SelectSurfaceExit(sphere, point, direction);
0767     }
0768     if constexpr (RecordHitInfo) SetExitHit(sphere, point, direction, distance, hitSurface, hitInfo);
0769   }
0770 
0771   template <typename Real_v>
0772   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void DistanceToOutWithHit(UnplacedStruct_t const &sphere,
0773                                                                                 Vector3D<Real_v> const &point,
0774                                                                                 Vector3D<Real_v> const &direction,
0775                                                                                 Real_v const &stepMax, Real_v &distance,
0776                                                                                 SurfaceHitView<Real_v> *hitInfo)
0777   {
0778     DistanceToOutImpl<Real_v, true>(sphere, point, direction, stepMax, distance, hitInfo);
0779   }
0780 
0781   /// @brief Compute the first valid exit distance and optionally return a supporting normal.
0782   /// @param sphere Sphere parameters and cached angular state.
0783   /// @param point Local starting point.
0784   /// @param direction Normalized local direction.
0785   /// @param stepMax Currently unused distance limit.
0786   /// @param distance Set to the exit distance, `-1`, `0`, or `kInfLength`.
0787   /// @param hitInfo Optional view receiving `leftBehind` and, when requested and
0788   /// valid, the outward unit normal. Passing null preserves the ordinary query.
0789   template <typename Real_v>
0790   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void DistanceToOut(UnplacedStruct_t const &sphere,
0791                                                                          Vector3D<Real_v> const &point,
0792                                                                          Vector3D<Real_v> const &direction,
0793                                                                          Real_v const &stepMax, Real_v &distance,
0794                                                                          SurfaceHitView<Real_v> *hitInfo = nullptr)
0795   {
0796     if (hitInfo)
0797       DistanceToOutWithHit(sphere, point, direction, stepMax, distance, hitInfo);
0798     else
0799       DistanceToOutImpl<Real_v, false>(sphere, point, direction, stepMax, distance, nullptr);
0800   }
0801 
0802   /// Test the two half-planes of a wide phi cut, stopping once a candidate cannot beat another exit.
0803   /// The current distance is already the nearest radial/theta exit. A phi candidate preceding it therefore remains
0804   /// inside those extents and needs only the selected half-plane check.
0805   template <typename Real_v, bool RecordHitInfo>
0806   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void GetMinDistToOutFromPhi(UnplacedStruct_t const &sphere,
0807                                                                                   Vector3D<Real_v> const &localPoint,
0808                                                                                   Vector3D<Real_v> const &localDir,
0809                                                                                   Real_v &distance,
0810                                                                                   ExitSurface *hitSurface)
0811   {
0812     Real_v distPhi1(kInfLength);
0813     Real_v distPhi2(kInfLength);
0814     PhiDistancesToOut(sphere, localPoint, localDir, distPhi1, distPhi2);
0815 
0816     const Real_v nearest  = Min(distPhi1, distPhi2);
0817     const Real_v farthest = Max(distPhi1, distPhi2);
0818     if (nearest < distance) {
0819       const Vector3D<Real_v> hit = localPoint + nearest * localDir;
0820       const bool onFace          = (nearest == distPhi1) ? sphere.fPhiWedge.IsOnSurfaceGeneric<Real_v, true>(hit)
0821                                                          : sphere.fPhiWedge.IsOnSurfaceGeneric<Real_v, false>(hit);
0822       if (onFace) {
0823         // At the shared axis, leaving one half-space need not leave the wide wedge.
0824         // Motion into the other half-space excludes this and any later phi exit.
0825         if (hit.Perp2() < Real_v(kTolerance * kTolerance)) {
0826           const auto other = nearest == distPhi1 ? sphere.fPhiWedge.GetNormal2() : sphere.fPhiWedge.GetNormal1();
0827           if (localDir.x() * other.x() + localDir.y() * other.y() > Real_v(1.e-12)) return;
0828         }
0829         distance = nearest;
0830         if constexpr (RecordHitInfo) *hitSurface = distPhi1 < distPhi2 ? ExitSurface::kPhiStart : ExitSurface::kPhiEnd;
0831         return;
0832       }
0833     }
0834     if (farthest < distance) {
0835       const Vector3D<Real_v> hit = localPoint + farthest * localDir;
0836       const bool onFace          = (farthest == distPhi1) ? sphere.fPhiWedge.IsOnSurfaceGeneric<Real_v, true>(hit)
0837                                                           : sphere.fPhiWedge.IsOnSurfaceGeneric<Real_v, false>(hit);
0838       if (onFace) {
0839         distance = farthest;
0840         if constexpr (RecordHitInfo) *hitSurface = distPhi1 > distPhi2 ? ExitSurface::kPhiStart : ExitSurface::kPhiEnd;
0841       }
0842     }
0843   }
0844 
0845   /// @brief Test phi-plane crossings and update the best distance.
0846   /// @details Candidate phi intersections are accepted only when the hit point
0847   /// is on the selected phi surface and inside the radial/theta extents.
0848   /// @tparam DistToIn Selects entry or exit phi-plane queries.
0849   /// @param sphere Sphere parameters and cached angular state.
0850   /// @param localPoint Local starting point.
0851   /// @param localDir Normalized local direction.
0852   /// @param done Suppresses updates when an earlier phase completed the query.
0853   /// @param distance Current best distance, updated if a closer phi hit exists.
0854   /// @param hitSurface Winning phi boundary, updated only when hit recording is enabled.
0855   template <typename Real_v, bool DistToIn, bool RecordHitInfo = false>
0856   VECCORE_ATT_HOST_DEVICE static void GetMinDistFromPhi(UnplacedStruct_t const &sphere,
0857                                                         Vector3D<Real_v> const &localPoint,
0858                                                         Vector3D<Real_v> const &localDir, bool done, Real_v &distance,
0859                                                         ExitSurface *hitSurface = nullptr)
0860   {
0861     Real_v distPhi1(kInfLength);
0862     Real_v distPhi2(kInfLength);
0863     Real_v dist(kInfLength);
0864 
0865     if (DistToIn)
0866       sphere.fPhiWedge.DistanceToIn<Real_v>(localPoint, localDir, distPhi1, distPhi2);
0867     else
0868       sphere.fPhiWedge.DistanceToOut<Real_v>(localPoint, localDir, distPhi1, distPhi2);
0869 
0870     bool containsCond1 = false, containsCond2 = false;
0871     bool tempCond = false;
0872     // Min Face
0873     dist = Min(distPhi1, distPhi2);
0874     if (dist < kInfLength) {
0875       Vector3D<Real_v> tmpPt = localPoint + dist * localDir;
0876       Real_v rad2            = tmpPt.Mag2();
0877 
0878       tempCond = ((dist == distPhi1) && sphere.fPhiWedge.IsOnSurfaceGeneric<Real_v, true>(tmpPt)) ||
0879                  ((dist == distPhi2) && sphere.fPhiWedge.IsOnSurfaceGeneric<Real_v, false>(tmpPt));
0880 
0881       containsCond1 = tempCond && (rad2 > sphere.fRmin * sphere.fRmin) && (rad2 < sphere.fRmax * sphere.fRmax) &&
0882                       sphere.fThetaCone.Contains<Real_v>(tmpPt);
0883     }
0884 
0885     if (!done && containsCond1) {
0886       if constexpr (RecordHitInfo) {
0887         if (dist < distance) {
0888           distance    = dist;
0889           *hitSurface = distPhi1 < distPhi2 ? ExitSurface::kPhiStart : ExitSurface::kPhiEnd;
0890         }
0891       } else {
0892         distance = Min(dist, distance);
0893       }
0894     }
0895 
0896     // Max Face
0897     dist = Max(distPhi1, distPhi2);
0898     if (dist < kInfLength) {
0899       Vector3D<Real_v> tmpPt = localPoint + dist * localDir;
0900 
0901       Real_v rad2 = tmpPt.Mag2();
0902       tempCond    = ((dist == distPhi1) && sphere.fPhiWedge.IsOnSurfaceGeneric<Real_v, true>(tmpPt)) ||
0903                     ((dist == distPhi2) && sphere.fPhiWedge.IsOnSurfaceGeneric<Real_v, false>(tmpPt));
0904 
0905       containsCond2 = tempCond && (rad2 > sphere.fRmin * sphere.fRmin) && (rad2 < sphere.fRmax * sphere.fRmax) &&
0906                       sphere.fThetaCone.Contains<Real_v>(tmpPt);
0907     }
0908     if (!done && !containsCond1 && containsCond2) {
0909       if constexpr (RecordHitInfo) {
0910         if (dist < distance) {
0911           distance    = dist;
0912           *hitSurface = distPhi1 > distPhi2 ? ExitSurface::kPhiStart : ExitSurface::kPhiEnd;
0913         }
0914       } else {
0915         distance = Min(dist, distance);
0916       }
0917     }
0918   }
0919 
0920   /// @brief Compute signed safety for entering the sphere.
0921   /// @details Full-theta spheres use radial safety, optionally combined with
0922   /// phi safety, to decide outside, surface, and inside states directly.
0923   /// @param sphere Sphere parameters and cached angular state.
0924   /// @param point Local point to test.
0925   /// @param safety Set to positive distance, `0`, or `-1`.
0926   template <typename Real_v>
0927   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void SafetyToIn(UnplacedStruct_t const &sphere,
0928                                                                       Vector3D<Real_v> const &point, Real_v &safety)
0929   {
0930     // General Precalcs
0931     Real_v rad = point.Mag();
0932 
0933     Real_v safeRMin(0.);
0934     Real_v safeRMax(0.);
0935 
0936     if (sphere.fFullThetaSphere) {
0937       // With no theta cuts, signed radial/phi safety fully determines inside,
0938       // surface, and outside state, so avoid a separate classification pass.
0939       if (sphere.fRmin) {
0940         safeRMin = sphere.fRmin - rad;
0941         safeRMax = rad - sphere.fRmax;
0942         safety   = safeRMin > safeRMax ? safeRMin : safeRMax;
0943       } else {
0944         safety = rad - sphere.fRmax;
0945       }
0946 
0947       if (!sphere.fFullPhiSphere) safety = Max(sphere.fPhiWedge.SafetyToIn<Real_v>(point), safety);
0948       if (safety > Real_v(kTolerance)) return;
0949       safety = safety < Real_v(-kTolerance) ? Real_v(-1.) : Real_v(0.);
0950       return;
0951     }
0952 
0953     bool completelyinside  = false;
0954     bool completelyoutside = false;
0955     GenericKernelForContainsAndInside<Real_v, true>(sphere, point, completelyinside, completelyoutside);
0956 
0957     if (completelyinside) {
0958       safety = Real_v(-1.0);
0959       return;
0960     }
0961 
0962     if (!completelyoutside) {
0963       safety = Real_v(0.0);
0964       return;
0965     }
0966 
0967     if (sphere.fRmin) {
0968       safeRMin = sphere.fRmin - rad;
0969       safeRMax = rad - sphere.fRmax;
0970       safety   = safeRMin > safeRMax ? safeRMin : safeRMax;
0971     } else {
0972       safety = rad - sphere.fRmax;
0973     }
0974     // Distance to r shells over
0975 
0976     // Distance to phi extent
0977     if (!sphere.fFullPhiSphere) {
0978       Real_v safetyPhi = sphere.fPhiWedge.SafetyToIn<Real_v>(point);
0979       safety           = Max(safetyPhi, safety);
0980     }
0981 
0982     // Distance to Theta extent
0983     if (!sphere.fFullThetaSphere) {
0984       Real_v safetyTheta = sphere.fThetaCone.SafetyToIn<Real_v>(point);
0985       safety             = Max(safetyTheta, safety);
0986     }
0987   }
0988 
0989   /// @brief Compute signed safety for leaving the sphere.
0990   /// @details Uses the nearest active radial, phi, or theta boundary for points
0991   /// classified safely inside.
0992   /// @param sphere Sphere parameters and cached angular state.
0993   /// @param point Local point to test.
0994   /// @param safety Set to positive distance, `0`, or `-1`.
0995   template <typename Real_v>
0996   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static void SafetyToOut(UnplacedStruct_t const &sphere,
0997                                                                        Vector3D<Real_v> const &point, Real_v &safety)
0998   {
0999 
1000     Real_v rad = point.Mag();
1001 
1002     bool completelyinside  = false;
1003     bool completelyoutside = false;
1004     GenericKernelForContainsAndInside<Real_v, true>(sphere, point, completelyinside, completelyoutside);
1005     if (completelyoutside) {
1006       safety = Real_v(-1.0);
1007       return;
1008     }
1009 
1010     if (!completelyinside) {
1011       safety = Real_v(0.0);
1012       return;
1013     }
1014 
1015     // Distance to r shells
1016     if (sphere.fRmin) {
1017       Real_v safeRMin = (rad - sphere.fRmin);
1018       Real_v safeRMax = (sphere.fRmax - rad);
1019       safety          = safeRMin < safeRMax ? safeRMin : safeRMax;
1020     } else {
1021       safety = sphere.fRmax - rad;
1022     }
1023 
1024     // Distance to phi extent
1025     if (!sphere.fFullPhiSphere) {
1026       Real_v safetyPhi = sphere.fPhiWedge.SafetyToOut<Real_v>(point);
1027       safety           = Min(safetyPhi, safety);
1028     }
1029 
1030     // Distance to Theta extent
1031     if (!sphere.fFullThetaSphere) {
1032       Real_v safeTheta = sphere.fThetaCone.SafetyToOut<Real_v>(point);
1033       safety           = Min(safeTheta, safety);
1034     }
1035   }
1036 
1037   /// @brief Approximate the normal from the closest sphere boundary.
1038   /// @details Used by `Normal` when the point is not on a recognized surface.
1039   /// @param sphere Sphere parameters and cached angular state.
1040   /// @param point Local point to evaluate.
1041   /// @return Normal of the closest radial, phi, or theta boundary.
1042   template <typename Real_v>
1043   VECCORE_ATT_HOST_DEVICE static Vector3D<Real_v> ApproxSurfaceNormalKernel(UnplacedStruct_t const &sphere,
1044                                                                             Vector3D<Real_v> const &point)
1045   {
1046     Vector3D<Real_v> norm(0., 0., 0.);
1047     Real_v radius     = point.Mag();
1048     Real_v distRMax   = Abs(radius - sphere.fRmax);
1049     Real_v distRMin   = InfinityLength<Real_v>();
1050     Real_v distPhi1   = InfinityLength<Real_v>();
1051     Real_v distPhi2   = InfinityLength<Real_v>();
1052     Real_v distTheta1 = InfinityLength<Real_v>();
1053     Real_v distTheta2 = InfinityLength<Real_v>();
1054     Real_v distMin    = distRMax;
1055 
1056     if (sphere.fRmin > 0.) {
1057       distRMin = Abs(sphere.fRmin - radius);
1058       distMin  = Min(distRMin, distRMax);
1059     }
1060 
1061     if (!sphere.fFullPhiSphere) {
1062       distPhi1 = Abs(point.x() * sphere.fPhiWedge.GetNormal1().x() + point.y() * sphere.fPhiWedge.GetNormal1().y());
1063       distPhi2 = Abs(point.x() * sphere.fPhiWedge.GetNormal2().x() + point.y() * sphere.fPhiWedge.GetNormal2().y());
1064       distMin  = Min(distMin, distPhi1, distPhi2);
1065     }
1066 
1067     if (!sphere.fFullThetaSphere) {
1068       Real_v rho = point.Perp();
1069       distTheta1 = sphere.fThetaCone.DistanceToLine<Real_v>(sphere.fThetaCone.GetSlope1(), rho, point.z());
1070       distTheta2 = sphere.fThetaCone.DistanceToLine<Real_v>(sphere.fThetaCone.GetSlope2(), rho, point.z());
1071       distMin    = Min(distMin, distTheta1, distTheta2);
1072     }
1073 
1074     if (distMin == distRMax) norm = point.Unit();
1075     if (distMin == distRMin) norm = -point.Unit();
1076 
1077     Vector3D<Real_v> normal1 = sphere.fPhiWedge.GetNormal1();
1078     Vector3D<Real_v> normal2 = sphere.fPhiWedge.GetNormal2();
1079     if (distMin == distPhi1) norm = -normal1;
1080     if (distMin == distPhi2) norm = -normal2;
1081 
1082     if (distMin == distTheta1) norm += sphere.fThetaCone.GetNormal1<Real_v>(point);
1083     if (distMin == distTheta2) norm += sphere.fThetaCone.GetNormal2<Real_v>(point);
1084 
1085     return norm;
1086   }
1087 
1088   /// @brief Return the outward normal and whether a surface was identified.
1089   /// @details Surface normals are accumulated from all matching active
1090   /// boundaries; off-surface points use the closest-boundary fallback.
1091   /// @param sphere Sphere parameters and cached angular state.
1092   /// @param point Local surface point.
1093   /// @param valid Set when at least one surface was identified.
1094   /// @return Outward normal direction.
1095   template <typename Real_v>
1096   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static Vector3D<Real_v> Normal(UnplacedStruct_t const &sphere,
1097                                                                               Vector3D<Real_v> const &point,
1098                                                                               bool &valid)
1099   {
1100     Vector3D<Real_v> normal(0., 0., 0.);
1101     normal.Set(1e-30);
1102 
1103     /* Assumption : This function assumes that the point is on the surface.
1104      *
1105      * Algorithm :
1106      * Detect all those surfaces on which the point is at, and count the
1107      * numOfSurfaces. if(numOfSurfaces == 1) then normal corresponds to the
1108      * normal for that particular case.
1109      *
1110      * if(numOfSurfaces > 1 ), then add the normals corresponds to different
1111      * cases, and finally normalize it and return.
1112      *
1113      * We need following function
1114      * IsPointOnInnerRadius()
1115      * IsPointOnOuterRadius()
1116      * IsPointOnStartPhi()
1117      * IsPointOnEndPhi()
1118      * IsPointOnStartTheta()
1119      * IsPointOnEndTheta()
1120      *
1121      * set valid=true if numOfSurface > 0
1122      *
1123      * if above mentioned assumption not followed , ie.
1124      * In case the given point is outside, then find the closest boundary,
1125      * the required normal will be the normal to that boundary.
1126      * This logic is implemented in "ApproxSurfaceNormalKernel" function
1127      */
1128 
1129     bool isPointOutside = false;
1130 
1131     bool isPointInside = false;
1132     GenericKernelForContainsAndInside<Real_v, true>(sphere, point, isPointInside, isPointOutside);
1133     if (isPointInside || isPointOutside) normal = ApproxSurfaceNormalKernel<Real_v>(sphere, point);
1134     bool needsNormalization = isPointInside || isPointOutside;
1135 
1136     valid = false;
1137 
1138     int noSurfaces            = 0;
1139     bool isPointOnOuterRadius = SphereUtilities::IsPointOnOuterRadius<Real_v>(sphere, point);
1140 
1141     if (isPointOnOuterRadius) ++noSurfaces;
1142     if (!isPointOutside && isPointOnOuterRadius) normal += point.Unit();
1143 
1144     if (sphere.fRmin) {
1145       bool isPointOnInnerRadius = SphereUtilities::IsPointOnInnerRadius<Real_v>(sphere, point);
1146       if (isPointOnInnerRadius) ++noSurfaces;
1147       if (!isPointOutside && isPointOnInnerRadius) normal -= point.Unit();
1148     }
1149 
1150     if (!sphere.fFullPhiSphere) {
1151       bool isPointOnStartPhi = SphereUtilities::IsPointOnStartPhi<Real_v>(sphere, point);
1152       bool isPointOnEndPhi   = SphereUtilities::IsPointOnEndPhi<Real_v>(sphere, point);
1153       if (isPointOnStartPhi) ++noSurfaces;
1154       if (isPointOnEndPhi) ++noSurfaces;
1155       if (!isPointOutside && isPointOnStartPhi) normal -= sphere.fPhiWedge.GetNormal1();
1156       if (!isPointOutside && isPointOnEndPhi) normal -= sphere.fPhiWedge.GetNormal2();
1157     }
1158 
1159     if (!sphere.fFullThetaSphere) {
1160       bool isPointOnStartTheta = SphereUtilities::IsPointOnStartTheta<Real_v>(sphere, point);
1161       bool isPointOnEndTheta   = SphereUtilities::IsPointOnEndTheta<Real_v>(sphere, point);
1162       needsNormalization |= isPointOnStartTheta || isPointOnEndTheta;
1163 
1164       if (isPointOnStartTheta) ++noSurfaces;
1165       if (!isPointOutside && isPointOnStartTheta) normal += sphere.fThetaCone.GetNormal1<Real_v>(point);
1166 
1167       if (isPointOnEndTheta) ++noSurfaces;
1168       if (!isPointOutside && isPointOnEndTheta) normal += sphere.fThetaCone.GetNormal2<Real_v>(point);
1169 
1170       Vector3D<Real_v> tempNormal(0., 0., -1.);
1171       if (!isPointOutside && isPointOnStartTheta && isPointOnEndTheta && (sphere.eTheta <= kPi / 2.))
1172         normal = tempNormal;
1173       Vector3D<Real_v> tempNormal2(0., 0., 1.);
1174       if (!isPointOutside && isPointOnStartTheta && isPointOnEndTheta && (sphere.fSTheta >= kPi / 2.))
1175         normal = tempNormal2;
1176     }
1177 
1178     // Inside() uses wider theta-cone bands than the per-surface tests: a point
1179     // it classifies on the surface still gets its closest boundary normal.
1180     if (noSurfaces == 0 && !isPointInside && !isPointOutside) {
1181       normal             = ApproxSurfaceNormalKernel<Real_v>(sphere, point);
1182       noSurfaces         = 1;
1183       needsNormalization = true;
1184     }
1185 
1186     // A unique radial or phi contribution is already unit length.
1187     if (needsNormalization || noSurfaces != 1) normal.Normalize();
1188 
1189     valid = (noSurfaces > 0);
1190 
1191     return normal;
1192   }
1193 };
1194 } // namespace VECGEOM_IMPL_NAMESPACE
1195 } // namespace vecgeom
1196 
1197 #endif // VECGEOM_VOLUMES_KERNEL_sphereIMPLEMENTATION_H_