Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-07-11 07:50:16

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/Surfaces/CylinderSurface.hpp"
0010 
0011 #include "Acts/Definitions/Algebra.hpp"
0012 #include "Acts/Definitions/Tolerance.hpp"
0013 #include "Acts/Definitions/Units.hpp"
0014 #include "Acts/Geometry/GeometryObject.hpp"
0015 #include "Acts/Surfaces/CylinderBounds.hpp"
0016 #include "Acts/Surfaces/SurfaceError.hpp"
0017 #include "Acts/Surfaces/SurfaceMergingException.hpp"
0018 #include "Acts/Surfaces/detail/AlignmentHelper.hpp"
0019 #include "Acts/Surfaces/detail/FacesHelper.hpp"
0020 #include "Acts/Surfaces/detail/MergeHelper.hpp"
0021 #include "Acts/Utilities/Intersection.hpp"
0022 #include "Acts/Utilities/ThrowAssert.hpp"
0023 #include "Acts/Utilities/detail/periodic.hpp"
0024 
0025 #include <algorithm>
0026 #include <cassert>
0027 #include <cmath>
0028 #include <iostream>
0029 #include <memory>
0030 #include <stdexcept>
0031 #include <utility>
0032 #include <vector>
0033 
0034 namespace Acts {
0035 
0036 using VectorHelpers::perp;
0037 using VectorHelpers::phi;
0038 
0039 CylinderSurface::CylinderSurface(const CylinderSurface& other)
0040     : GeometryObject(), RegularSurface(other), m_bounds(other.m_bounds) {}
0041 
0042 CylinderSurface::CylinderSurface(const GeometryContext& gctx,
0043                                  const CylinderSurface& other,
0044                                  const Transform3& shift)
0045     : GeometryObject(),
0046       RegularSurface(gctx, other, shift),
0047       m_bounds(other.m_bounds) {}
0048 
0049 CylinderSurface::CylinderSurface(const Transform3& transform, double radius,
0050                                  double halfz, double halfphi, double avphi,
0051                                  double bevelMinZ, double bevelMaxZ)
0052     : RegularSurface(transform),
0053       m_bounds(std::make_shared<const CylinderBounds>(
0054           radius, halfz, halfphi, avphi, bevelMinZ, bevelMaxZ)) {}
0055 
0056 CylinderSurface::CylinderSurface(std::shared_ptr<const CylinderBounds> cbounds,
0057                                  const DetectorElementBase& detelement)
0058     : RegularSurface(detelement), m_bounds(std::move(cbounds)) {
0059   // surfaces representing a detector element must have bounds
0060   throw_assert(m_bounds, "CylinderBounds must not be nullptr");
0061 }
0062 
0063 CylinderSurface::CylinderSurface(const Transform3& transform,
0064                                  std::shared_ptr<const CylinderBounds> cbounds)
0065     : RegularSurface(transform), m_bounds(std::move(cbounds)) {
0066   throw_assert(m_bounds, "CylinderBounds must not be nullptr");
0067 }
0068 
0069 CylinderSurface& CylinderSurface::operator=(const CylinderSurface& other) {
0070   if (this != &other) {
0071     Surface::operator=(other);
0072     m_bounds = other.m_bounds;
0073   }
0074   return *this;
0075 }
0076 
0077 // return the binning position for ordering in the BinnedArray
0078 Vector3 CylinderSurface::referencePosition(const GeometryContext& gctx,
0079                                            AxisDirection aDir) const {
0080   // special binning type for R-type methods
0081   if (aDir == AxisDirection::AxisR || aDir == AxisDirection::AxisRPhi) {
0082     double R = bounds().get(CylinderBounds::eR);
0083     double phi = bounds().get(CylinderBounds::eAveragePhi);
0084     return localToGlobal(gctx, Vector2{phi * R, 0}, Vector3{});
0085   }
0086   // give the center as default for all of these binning types
0087   // AxisDirection::AxisX, AxisDirection::AxisY, AxisDirection::AxisZ,
0088   // AxisDirection::AxisR, AxisDirection::AxisPhi, AxisDirection::AxisRPhi,
0089   // AxisDirection::AxisTheta, AxisDirection::AxisEta
0090   return center(gctx);
0091 }
0092 
0093 // return the measurement frame: it's the tangential plane
0094 RotationMatrix3 CylinderSurface::referenceFrame(
0095     const GeometryContext& gctx, const Vector3& position,
0096     const Vector3& /*direction*/) const {
0097   RotationMatrix3 mFrame;
0098   // construct the measurement frame
0099   // measured Y is the z axis
0100   Vector3 measY = rotSymmetryAxis(gctx);
0101   // measured z is the position normalized transverse (in local)
0102   Vector3 measDepth = normal(gctx, position);
0103   // measured X is what comoes out of it
0104   Vector3 measX(measY.cross(measDepth).normalized());
0105   // assign the columnes
0106   mFrame.col(0) = measX;
0107   mFrame.col(1) = measY;
0108   mFrame.col(2) = measDepth;
0109   // return the rotation matrix
0110   return mFrame;
0111 }
0112 
0113 Surface::SurfaceType CylinderSurface::type() const {
0114   return Surface::Cylinder;
0115 }
0116 
0117 Vector3 CylinderSurface::localToGlobal(const GeometryContext& gctx,
0118                                        const Vector2& lposition) const {
0119   // create the position in the local 3d frame
0120   double r = bounds().get(CylinderBounds::eR);
0121   double phi = lposition[0] / r;
0122   Vector3 position(r * cos(phi), r * sin(phi), lposition[1]);
0123   return transform(gctx) * position;
0124 }
0125 
0126 Result<Vector2> CylinderSurface::globalToLocal(const GeometryContext& gctx,
0127                                                const Vector3& position,
0128                                                double tolerance) const {
0129   double inttol = tolerance;
0130   if (tolerance == s_onSurfaceTolerance) {
0131     // transform default value!
0132     // @TODO: check if s_onSurfaceTolerance would do here
0133     inttol = bounds().get(CylinderBounds::eR) * 0.0001;
0134   }
0135   if (inttol < 0.01) {
0136     inttol = 0.01;
0137   }
0138   const Transform3& sfTransform = transform(gctx);
0139   Transform3 inverseTrans(sfTransform.inverse());
0140   Vector3 loc3Dframe(inverseTrans * position);
0141   if (std::abs(perp(loc3Dframe) - bounds().get(CylinderBounds::eR)) > inttol) {
0142     return Result<Vector2>::failure(SurfaceError::GlobalPositionNotOnSurface);
0143   }
0144   return Result<Vector2>::success(
0145       {bounds().get(CylinderBounds::eR) * phi(loc3Dframe), loc3Dframe.z()});
0146 }
0147 
0148 std::string CylinderSurface::name() const {
0149   return "Acts::CylinderSurface";
0150 }
0151 
0152 Vector3 CylinderSurface::normal(const GeometryContext& gctx,
0153                                 const Vector2& lposition) const {
0154   double phi = lposition[0] / m_bounds->get(CylinderBounds::eR);
0155   Vector3 localNormal(cos(phi), sin(phi), 0.);
0156   return transform(gctx).linear() * localNormal;
0157 }
0158 
0159 Vector3 CylinderSurface::normal(const GeometryContext& gctx,
0160                                 const Vector3& position) const {
0161   const Transform3& sfTransform = transform(gctx);
0162   // get it into the cylinder frame
0163   Vector3 pos3D = sfTransform.inverse() * position;
0164   // set the z coordinate to 0
0165   pos3D.z() = 0.;
0166   // normalize and rotate back into global
0167   return sfTransform.linear() * pos3D.normalized();
0168 }
0169 
0170 double CylinderSurface::pathCorrection(const GeometryContext& gctx,
0171                                        const Vector3& position,
0172                                        const Vector3& direction) const {
0173   Vector3 normalT = normal(gctx, position);
0174   double cosAlpha = normalT.dot(direction);
0175   return std::abs(1. / cosAlpha);
0176 }
0177 
0178 const CylinderBounds& CylinderSurface::bounds() const {
0179   return *m_bounds;
0180 }
0181 
0182 Polyhedron CylinderSurface::polyhedronRepresentation(
0183     const GeometryContext& gctx, unsigned int quarterSegments) const {
0184   auto ctrans = transform(gctx);
0185 
0186   // Prepare vertices and faces
0187   std::vector<Vector3> vertices =
0188       bounds().circleVertices(ctrans, quarterSegments);
0189   auto [faces, triangularMesh] =
0190       detail::FacesHelper::cylindricalFaceMesh(vertices);
0191   return Polyhedron(vertices, faces, triangularMesh, false);
0192 }
0193 
0194 Vector3 CylinderSurface::rotSymmetryAxis(const GeometryContext& gctx) const {
0195   // fast access via transform matrix (and not rotation())
0196   return transform(gctx).matrix().block<3, 1>(0, 2);
0197 }
0198 
0199 detail::RealQuadraticEquation CylinderSurface::intersectionSolver(
0200     const Transform3& transform, const Vector3& position,
0201     const Vector3& direction) const {
0202   // Solve for radius R
0203   double R = bounds().get(CylinderBounds::eR);
0204 
0205   // Get the transformation matrtix
0206   const auto& tMatrix = transform.matrix();
0207   Vector3 caxis = tMatrix.block<3, 1>(0, 2).transpose();
0208   Vector3 ccenter = tMatrix.block<3, 1>(0, 3).transpose();
0209 
0210   // Check documentation for explanation
0211   Vector3 pc = position - ccenter;
0212   Vector3 pcXcd = pc.cross(caxis);
0213   Vector3 ldXcd = direction.cross(caxis);
0214   double a = ldXcd.dot(ldXcd);
0215   double b = 2. * (ldXcd.dot(pcXcd));
0216   double c = pcXcd.dot(pcXcd) - (R * R);
0217   // And solve the qaudratic equation
0218   return detail::RealQuadraticEquation(a, b, c);
0219 }
0220 
0221 SurfaceMultiIntersection CylinderSurface::intersect(
0222     const GeometryContext& gctx, const Vector3& position,
0223     const Vector3& direction, const BoundaryTolerance& boundaryTolerance,
0224     double tolerance) const {
0225   const auto& gctxTransform = transform(gctx);
0226 
0227   // Solve the quadratic equation
0228   auto qe = intersectionSolver(gctxTransform, position, direction);
0229 
0230   // If no valid solution return a non-valid surfaceIntersection
0231   if (qe.solutions == 0) {
0232     return {{Intersection3D::invalid(), Intersection3D::invalid()},
0233             this,
0234             boundaryTolerance};
0235   }
0236 
0237   // Check the validity of the first solution
0238   Vector3 solution1 = position + qe.first * direction;
0239   IntersectionStatus status1 = std::abs(qe.first) < std::abs(tolerance)
0240                                    ? IntersectionStatus::onSurface
0241                                    : IntersectionStatus::reachable;
0242 
0243   // Helper method for boundary check
0244   auto boundaryCheck = [&](const Vector3& solution,
0245                            IntersectionStatus status) -> IntersectionStatus {
0246     // No check to be done, return current status
0247     if (boundaryTolerance.isInfinite()) {
0248       return status;
0249     }
0250     const auto& cBounds = bounds();
0251     if (auto absoluteBound = boundaryTolerance.asAbsoluteBoundOpt();
0252         absoluteBound.has_value() && cBounds.coversFullAzimuth()) {
0253       // Project out the current Z value via local z axis
0254       // Built-in local to global for speed reasons
0255       const auto& tMatrix = gctxTransform.matrix();
0256       // Create the reference vector in local
0257       const Vector3 vecLocal(solution - tMatrix.block<3, 1>(0, 3));
0258       double cZ = vecLocal.dot(tMatrix.block<3, 1>(0, 2));
0259       double modifiedTolerance = tolerance + absoluteBound->tolerance1;
0260       double hZ = cBounds.get(CylinderBounds::eHalfLengthZ) + modifiedTolerance;
0261       return std::abs(cZ) < std::abs(hZ) ? status
0262                                          : IntersectionStatus::unreachable;
0263     }
0264     return isOnSurface(gctx, solution, direction, boundaryTolerance)
0265                ? status
0266                : IntersectionStatus::unreachable;
0267   };
0268   // Check first solution for boundary compatibility
0269   status1 = boundaryCheck(solution1, status1);
0270   // Set the intersection
0271   Intersection3D first(solution1, qe.first, status1);
0272   if (qe.solutions == 1) {
0273     return {{first, first}, this, boundaryTolerance};
0274   }
0275   // Check the validity of the second solution
0276   Vector3 solution2 = position + qe.second * direction;
0277   IntersectionStatus status2 = std::abs(qe.second) < std::abs(tolerance)
0278                                    ? IntersectionStatus::onSurface
0279                                    : IntersectionStatus::reachable;
0280   // Check first solution for boundary compatibility
0281   status2 = boundaryCheck(solution2, status2);
0282   Intersection3D second(solution2, qe.second, status2);
0283   // Order based on path length
0284   if (first.pathLength() <= second.pathLength()) {
0285     return {{first, second}, this, boundaryTolerance};
0286   }
0287   return {{second, first}, this, boundaryTolerance};
0288 }
0289 
0290 AlignmentToPathMatrix CylinderSurface::alignmentToPathDerivative(
0291     const GeometryContext& gctx, const Vector3& position,
0292     const Vector3& direction) const {
0293   assert(isOnSurface(gctx, position, direction, BoundaryTolerance::Infinite()));
0294 
0295   // The vector between position and center
0296   const auto pcRowVec = (position - center(gctx)).transpose().eval();
0297   // The rotation
0298   const auto& rotation = transform(gctx).rotation();
0299   // The local frame x/y/z axis
0300   const auto& localXAxis = rotation.col(0);
0301   const auto& localYAxis = rotation.col(1);
0302   const auto& localZAxis = rotation.col(2);
0303   // The local coordinates
0304   const auto localPos = (rotation.transpose() * position).eval();
0305   const auto dx = direction.dot(localXAxis);
0306   const auto dy = direction.dot(localYAxis);
0307   const auto dz = direction.dot(localZAxis);
0308   // The normalization factor
0309   const auto norm = 1 / (1 - dz * dz);
0310   // The direction transpose
0311   const auto& dirRowVec = direction.transpose();
0312   // The derivative of path w.r.t. the local axes
0313   // @note The following calculations assume that the intersection of the track
0314   // with the cylinder always satisfy: perp(localPos) = R
0315   const auto localXAxisToPath =
0316       (-2 * norm * (dx * pcRowVec + localPos.x() * dirRowVec)).eval();
0317   const auto localYAxisToPath =
0318       (-2 * norm * (dy * pcRowVec + localPos.y() * dirRowVec)).eval();
0319   const auto localZAxisToPath =
0320       (-4 * norm * norm * (dx * localPos.x() + dy * localPos.y()) * dz *
0321        dirRowVec)
0322           .eval();
0323   // Calculate the derivative of local frame axes w.r.t its rotation
0324   const auto [rotToLocalXAxis, rotToLocalYAxis, rotToLocalZAxis] =
0325       detail::rotationToLocalAxesDerivative(rotation);
0326   // Initialize the derivative of propagation path w.r.t. local frame
0327   // translation (origin) and rotation
0328   AlignmentToPathMatrix alignToPath = AlignmentToPathMatrix::Zero();
0329   alignToPath.segment<3>(eAlignmentCenter0) =
0330       2 * norm * (dx * localXAxis.transpose() + dy * localYAxis.transpose());
0331   alignToPath.segment<3>(eAlignmentRotation0) =
0332       localXAxisToPath * rotToLocalXAxis + localYAxisToPath * rotToLocalYAxis +
0333       localZAxisToPath * rotToLocalZAxis;
0334 
0335   return alignToPath;
0336 }
0337 
0338 ActsMatrix<2, 3> CylinderSurface::localCartesianToBoundLocalDerivative(
0339     const GeometryContext& gctx, const Vector3& position) const {
0340   using VectorHelpers::perp;
0341   using VectorHelpers::phi;
0342   // The local frame transform
0343   const auto& sTransform = transform(gctx);
0344   // calculate the transformation to local coordinates
0345   const Vector3 localPos = sTransform.inverse() * position;
0346   const double lr = perp(localPos);
0347   const double lphi = phi(localPos);
0348   const double lcphi = std::cos(lphi);
0349   const double lsphi = std::sin(lphi);
0350   // Solve for radius R
0351   double R = bounds().get(CylinderBounds::eR);
0352   ActsMatrix<2, 3> loc3DToLocBound = ActsMatrix<2, 3>::Zero();
0353   loc3DToLocBound << -R * lsphi / lr, R * lcphi / lr, 0, 0, 0, 1;
0354 
0355   return loc3DToLocBound;
0356 }
0357 
0358 std::pair<std::shared_ptr<CylinderSurface>, bool> CylinderSurface::mergedWith(
0359     const CylinderSurface& other, AxisDirection direction,
0360     bool externalRotation, const Logger& logger) const {
0361   using namespace UnitLiterals;
0362 
0363   ACTS_VERBOSE("Merging cylinder surfaces in " << axisDirectionName(direction)
0364                                                << " direction");
0365 
0366   if (m_associatedDetElement != nullptr ||
0367       other.m_associatedDetElement != nullptr) {
0368     throw SurfaceMergingException(getSharedPtr(), other.getSharedPtr(),
0369                                   "CylinderSurface::merge: surfaces are "
0370                                   "associated with a detector element");
0371   }
0372 
0373   assert(m_transform != nullptr && other.m_transform != nullptr);
0374 
0375   Transform3 otherLocal = m_transform->inverse() * *other.m_transform;
0376 
0377   constexpr auto tolerance = s_onSurfaceTolerance;
0378 
0379   // surface cannot have any relative rotation
0380 
0381   if (std::abs(otherLocal.linear().col(eX)[eZ]) >= tolerance ||
0382       std::abs(otherLocal.linear().col(eY)[eZ]) >= tolerance) {
0383     ACTS_ERROR("CylinderSurface::merge: surfaces have relative rotation");
0384     throw SurfaceMergingException(
0385         getSharedPtr(), other.getSharedPtr(),
0386         "CylinderSurface::merge: surfaces have relative rotation");
0387   }
0388 
0389   auto checkNoBevel = [this, &logger, &other](const auto& bounds) {
0390     if (bounds.get(CylinderBounds::eBevelMinZ) != 0.0) {
0391       ACTS_ERROR(
0392           "CylinderVolumeStack requires all volumes to have a bevel angle of "
0393           "0");
0394       throw SurfaceMergingException(
0395           getSharedPtr(), other.getSharedPtr(),
0396           "CylinderVolumeStack requires all volumes to have a bevel angle of "
0397           "0");
0398     }
0399 
0400     if (bounds.get(CylinderBounds::eBevelMaxZ) != 0.0) {
0401       ACTS_ERROR(
0402           "CylinderVolumeStack requires all volumes to have a bevel angle of "
0403           "0");
0404       throw SurfaceMergingException(
0405           getSharedPtr(), other.getSharedPtr(),
0406           "CylinderVolumeStack requires all volumes to have a bevel angle of "
0407           "0");
0408     }
0409   };
0410 
0411   checkNoBevel(bounds());
0412   checkNoBevel(other.bounds());
0413 
0414   // radii need to be identical
0415   if (std::abs(bounds().get(CylinderBounds::eR) -
0416                other.bounds().get(CylinderBounds::eR)) > tolerance) {
0417     ACTS_ERROR("CylinderSurface::merge: surfaces have different radii");
0418     throw SurfaceMergingException(
0419         getSharedPtr(), other.getSharedPtr(),
0420         "CylinderSurface::merge: surfaces have different radii");
0421   }
0422 
0423   double r = bounds().get(CylinderBounds::eR);
0424 
0425   // no translation in x/z is allowed
0426   Vector3 translation = otherLocal.translation();
0427 
0428   if (std::abs(translation[0]) > tolerance ||
0429       std::abs(translation[1]) > tolerance) {
0430     ACTS_ERROR(
0431         "CylinderSurface::merge: surfaces have relative translation in x/y");
0432     throw SurfaceMergingException(
0433         getSharedPtr(), other.getSharedPtr(),
0434         "CylinderSurface::merge: surfaces have relative translation in x/y");
0435   }
0436 
0437   double hlZ = bounds().get(CylinderBounds::eHalfLengthZ);
0438   double minZ = -hlZ;
0439   double maxZ = hlZ;
0440 
0441   double zShift = translation[2];
0442   double otherHlZ = other.bounds().get(CylinderBounds::eHalfLengthZ);
0443   double otherMinZ = -otherHlZ + zShift;
0444   double otherMaxZ = otherHlZ + zShift;
0445 
0446   double hlPhi = bounds().get(CylinderBounds::eHalfPhiSector);
0447   double avgPhi = bounds().get(CylinderBounds::eAveragePhi);
0448 
0449   double otherHlPhi = other.bounds().get(CylinderBounds::eHalfPhiSector);
0450   double otherAvgPhi = other.bounds().get(CylinderBounds::eAveragePhi);
0451 
0452   if (direction == AxisDirection::AxisZ) {
0453     // z shift must match the bounds
0454 
0455     if (std::abs(otherLocal.linear().col(eY)[eX]) >= tolerance &&
0456         (!bounds().coversFullAzimuth() ||
0457          !other.bounds().coversFullAzimuth())) {
0458       throw SurfaceMergingException(getSharedPtr(), other.getSharedPtr(),
0459                                     "CylinderSurface::merge: surfaces have "
0460                                     "relative rotation in z and phi sector");
0461     }
0462 
0463     ACTS_VERBOSE("this: [" << minZ << ", " << maxZ << "] ~> "
0464                            << (minZ + maxZ) / 2.0 << " +- " << hlZ);
0465     ACTS_VERBOSE("zShift: " << zShift);
0466 
0467     ACTS_VERBOSE("other: [" << otherMinZ << ", " << otherMaxZ << "] ~> "
0468                             << (otherMinZ + otherMaxZ) / 2.0 << " +- "
0469                             << otherHlZ);
0470     if (std::abs(maxZ - otherMinZ) > tolerance &&
0471         std::abs(minZ - otherMaxZ) > tolerance) {
0472       ACTS_ERROR("CylinderSurface::merge: surfaces have incompatible z bounds");
0473       throw SurfaceMergingException(
0474           getSharedPtr(), other.getSharedPtr(),
0475           "CylinderSurface::merge: surfaces have incompatible z bounds");
0476     }
0477 
0478     if (hlPhi != otherHlPhi || avgPhi != otherAvgPhi) {
0479       throw SurfaceMergingException(getSharedPtr(), other.getSharedPtr(),
0480                                     "CylinderSurface::merge: surfaces have "
0481                                     "different phi sectors");
0482     }
0483 
0484     double newMaxZ = std::max(maxZ, otherMaxZ);
0485     double newMinZ = std::min(minZ, otherMinZ);
0486     double newHlZ = (newMaxZ - newMinZ) / 2.0;
0487     double newMidZ = (newMaxZ + newMinZ) / 2.0;
0488     ACTS_VERBOSE("merged: [" << newMinZ << ", " << newMaxZ << "] ~> " << newMidZ
0489                              << " +- " << newHlZ);
0490 
0491     auto newBounds = std::make_shared<CylinderBounds>(r, newHlZ, hlPhi, avgPhi);
0492 
0493     Transform3 newTransform =
0494         *m_transform * Translation3{Vector3::UnitZ() * newMidZ};
0495 
0496     return {Surface::makeShared<CylinderSurface>(newTransform, newBounds),
0497             zShift < 0};
0498 
0499   } else if (direction == AxisDirection::AxisRPhi) {
0500     // no z shift is allowed
0501     if (std::abs(translation[2]) > tolerance) {
0502       ACTS_ERROR(
0503           "CylinderSurface::merge: surfaces have relative translation in z for "
0504           "rPhi merging");
0505       throw SurfaceMergingException(
0506           getSharedPtr(), other.getSharedPtr(),
0507           "CylinderSurface::merge: surfaces have relative translation in z for "
0508           "rPhi merging");
0509     }
0510 
0511     if (hlZ != otherHlZ) {
0512       throw SurfaceMergingException(getSharedPtr(), other.getSharedPtr(),
0513                                     "CylinderSurface::merge: surfaces have "
0514                                     "different z bounds");
0515     }
0516 
0517     // Figure out signed relative rotation
0518     Vector2 rotatedX = otherLocal.linear().col(eX).head<2>();
0519     double zrotation = std::atan2(rotatedX[1], rotatedX[0]);
0520 
0521     ACTS_VERBOSE("this:  [" << avgPhi / 1_degree << " +- " << hlPhi / 1_degree
0522                             << "]");
0523     ACTS_VERBOSE("other: [" << otherAvgPhi / 1_degree << " +- "
0524                             << otherHlPhi / 1_degree << "]");
0525 
0526     ACTS_VERBOSE("Relative rotation around local z: " << zrotation / 1_degree);
0527 
0528     double prevOtherAvgPhi = otherAvgPhi;
0529     otherAvgPhi = detail::radian_sym(otherAvgPhi + zrotation);
0530     ACTS_VERBOSE("~> local other average phi: "
0531                  << otherAvgPhi / 1_degree
0532                  << " (was: " << prevOtherAvgPhi / 1_degree << ")");
0533 
0534     try {
0535       auto [newHlPhi, newAvgPhi, reversed] = detail::mergedPhiSector(
0536           hlPhi, avgPhi, otherHlPhi, otherAvgPhi, logger, tolerance);
0537 
0538       Transform3 newTransform = *m_transform;
0539 
0540       if (externalRotation) {
0541         ACTS_VERBOSE("Modifying transform for external rotation of "
0542                      << newAvgPhi / 1_degree);
0543         newTransform = newTransform * AngleAxis3(newAvgPhi, Vector3::UnitZ());
0544         newAvgPhi = 0.;
0545       }
0546 
0547       auto newBounds = std::make_shared<CylinderBounds>(
0548           r, bounds().get(CylinderBounds::eHalfLengthZ), newHlPhi, newAvgPhi);
0549 
0550       return {Surface::makeShared<CylinderSurface>(newTransform, newBounds),
0551               reversed};
0552     } catch (const std::invalid_argument& e) {
0553       throw SurfaceMergingException(getSharedPtr(), other.getSharedPtr(),
0554                                     e.what());
0555     }
0556   } else {
0557     throw SurfaceMergingException(getSharedPtr(), other.getSharedPtr(),
0558                                   "CylinderSurface::merge: invalid direction " +
0559                                       axisDirectionName(direction));
0560   }
0561 }
0562 
0563 }  // namespace Acts