Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-07-03 07:52:20

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/Detector/detail/CylindricalDetectorHelper.hpp"
0010 
0011 #include "Acts/Definitions/Direction.hpp"
0012 #include "Acts/Definitions/Tolerance.hpp"
0013 #include "Acts/Detector/DetectorVolume.hpp"
0014 #include "Acts/Detector/Portal.hpp"
0015 #include "Acts/Detector/detail/DetectorVolumeConsistency.hpp"
0016 #include "Acts/Detector/detail/PortalHelper.hpp"
0017 #include "Acts/Geometry/CutoutCylinderVolumeBounds.hpp"
0018 #include "Acts/Surfaces/CylinderBounds.hpp"
0019 #include "Acts/Surfaces/CylinderSurface.hpp"
0020 #include "Acts/Surfaces/DiscSurface.hpp"
0021 #include "Acts/Surfaces/PlanarBounds.hpp"
0022 #include "Acts/Surfaces/PlaneSurface.hpp"
0023 #include "Acts/Surfaces/RadialBounds.hpp"
0024 #include "Acts/Surfaces/RectangleBounds.hpp"
0025 #include "Acts/Surfaces/Surface.hpp"
0026 #include "Acts/Surfaces/SurfaceBounds.hpp"
0027 #include "Acts/Utilities/Axis.hpp"
0028 #include "Acts/Utilities/BinningType.hpp"
0029 #include "Acts/Utilities/Enumerate.hpp"
0030 #include "Acts/Utilities/Grid.hpp"
0031 #include "Acts/Utilities/Helpers.hpp"
0032 #include "Acts/Utilities/StringHelpers.hpp"
0033 
0034 #include <algorithm>
0035 #include <cmath>
0036 #include <cstddef>
0037 #include <iterator>
0038 #include <map>
0039 #include <numbers>
0040 #include <ostream>
0041 #include <ranges>
0042 #include <stdexcept>
0043 #include <string>
0044 #include <tuple>
0045 #include <utility>
0046 
0047 // Indexing of the portals follows the generation order of portals in the
0048 // CylinderVolumeBounds and BevelledCylinderVolumeBounds (latter for wrapping)
0049 //
0050 // In short:
0051 //
0052 // 0: index of the low-z disc bound
0053 // 1: index of the high-z disc bound
0054 // 2: index of the outer cylinder bound
0055 //
0056 // If the volume doesn't extend inward to inner radius=0:
0057 // 3: index of the inner cylinder bound
0058 //
0059 // Cylindrical sectors have up to 6 portals, enumerated as follows:
0060 // 0: index of the low-z disc sector bound
0061 // 1: index of the high-z disc sector bound
0062 // 2: index of the outer cylinder sector bound
0063 //
0064 // If no inner cylinder sector bound is present:
0065 // 3: index of the low-phi sector bound
0066 // 4: index of the high-phi sector bound
0067 // If an inner cylinder sector bound exists, it takes index 3 and the phi sector
0068 // bounds are shifted by one index: 3: index of the inner cylinder sector bound
0069 // 4: index of the low-phi sector bound
0070 // 5: index of the high-phi sector bound
0071 //
0072 
0073 namespace {
0074 
0075 /// @brief Helper function to create a disc portal replacement
0076 ///
0077 /// @param transform The transform of the newly created portal
0078 /// @param rBoundaries the vector of binning boundaries in r
0079 /// @param phiBoundaries an eventual phi sector value
0080 /// @param dir the  direction to be set
0081 ///
0082 /// @return a new portal replacement object
0083 Acts::Experimental::PortalReplacement createDiscReplacement(
0084     const Acts::Transform3& transform, const std::vector<double>& rBoundaries,
0085     const std::vector<double>& phiBoundaries, unsigned int index,
0086     Acts::Direction dir) {
0087   // Autodetector stitch value
0088   Acts::AxisDirection stitchValue = phiBoundaries.size() == 2u
0089                                         ? Acts::AxisDirection::AxisR
0090                                         : Acts::AxisDirection::AxisPhi;
0091   // Estimate ranges
0092   auto [minRit, maxRit] = std::ranges::minmax_element(rBoundaries);
0093   auto [sectorPhi, avgPhi] = Acts::range_medium(phiBoundaries);
0094 
0095   // Transform and bounds
0096   auto bounds = std::make_unique<Acts::RadialBounds>(*minRit, *maxRit,
0097                                                      0.5 * sectorPhi, avgPhi);
0098   // A new surface on the negative side over the full range
0099   auto surface = Acts::Surface::makeShared<Acts::DiscSurface>(
0100       transform, std::move(bounds));
0101   // Make a portal and indicate the new link direction
0102   const auto& stitchBoundaries =
0103       (stitchValue == Acts::AxisDirection::AxisR) ? rBoundaries : phiBoundaries;
0104   return Acts::Experimental::PortalReplacement(
0105       std::make_shared<Acts::Experimental::Portal>(surface), index, dir,
0106       stitchBoundaries, stitchValue);
0107 }
0108 
0109 /// @brief Helper function to create a cylinder portal replacement
0110 ///
0111 /// @param transform The transform of the newly created portal
0112 /// @param r is the radius of the cylinder
0113 /// @param zBoundaries the vector of binning boundaries
0114 /// @param phiBoundaries the vector of binning boundaries
0115 /// @param index the index of this portal to be set
0116 /// @param dir the navigation direction to be set
0117 ///
0118 /// @return a new portal replacement object
0119 Acts::Experimental::PortalReplacement createCylinderReplacement(
0120     const Acts::Transform3& transform, double r,
0121     const std::vector<double>& zBoundaries,
0122     const std::vector<double>& phiBoundaries, unsigned int index,
0123     Acts::Direction dir) {
0124   // Autodetector stitch value
0125   Acts::AxisDirection stitchValue = phiBoundaries.size() == 2u
0126                                         ? Acts::AxisDirection::AxisZ
0127                                         : Acts::AxisDirection::AxisPhi;
0128   auto [lengthZ, medZ] = Acts::range_medium(zBoundaries);
0129   auto [sectorPhi, avgPhi] = Acts::range_medium(phiBoundaries);
0130 
0131   // New bounds, with current length and sector values
0132   auto bounds = std::make_unique<Acts::CylinderBounds>(r, 0.5 * lengthZ,
0133                                                        0.5 * sectorPhi, avgPhi);
0134   // A new surface on the negative side over the full range
0135   auto surface = Acts::Surface::makeShared<Acts::CylinderSurface>(
0136       transform, std::move(bounds));
0137 
0138   // A make a portal and indicate the new link direction
0139   const auto& stitchBoundaries =
0140       (stitchValue == Acts::AxisDirection::AxisZ) ? zBoundaries : phiBoundaries;
0141   return Acts::Experimental::PortalReplacement(
0142       std::make_shared<Acts::Experimental::Portal>(surface), index, dir,
0143       stitchBoundaries, stitchValue);
0144 }
0145 
0146 /// @brief Helper function to create a disc portal replacement
0147 /// @param gcxt the geometry context of this call
0148 /// @param volumeCenter a reference center of the volume (combined)
0149 /// @param refSurface the reference surface (old portal)
0150 /// @param boundaries the vector of binning boundaries in r
0151 /// @param binning the binning of the sector (inR, inZ)
0152 /// @param index the index of this portal to be set
0153 /// @param dir the navigation direction to be set
0154 ///
0155 /// @return a new portal replacement object
0156 Acts::Experimental::PortalReplacement createSectorReplacement(
0157     const Acts::GeometryContext& gctx, const Acts::Vector3& volumeCenter,
0158     const Acts::Surface& refSurface, const std::vector<double>& boundaries,
0159     Acts::AxisDirection binning, unsigned int index, Acts::Direction dir) {
0160   // Get a reference transform
0161   const auto& refTransform = refSurface.transform(gctx);
0162   auto refRotation = refTransform.rotation();
0163   // Bounds handling
0164   const auto& boundValues = refSurface.bounds().values();
0165   std::unique_ptr<Acts::PlanarBounds> bounds = nullptr;
0166 
0167   // Create a new transform
0168   Acts::Transform3 transform = Acts::Transform3::Identity();
0169   if (binning == Acts::AxisDirection::AxisR) {
0170     // Range and center-r calculation
0171     auto [range, medium] = Acts::range_medium(boundaries);
0172     // New joint center:
0173     // - you start from the center of the volume, and then head the distence
0174     //   of medium-r along the y-axis of the former (and new) portal
0175     Acts::Vector3 pCenter = volumeCenter + medium * refRotation.col(1u);
0176     transform.pretranslate(pCenter);
0177     // Create the halflength
0178     double halfX =
0179         0.5 * (boundValues[Acts::RectangleBounds::BoundValues::eMaxX] -
0180                boundValues[Acts::RectangleBounds::BoundValues::eMinX]);
0181     // New joint bounds
0182     bounds = std::make_unique<Acts::RectangleBounds>(halfX, 0.5 * range);
0183   } else if (binning == Acts::AxisDirection::AxisZ) {
0184     // Range and medium z alculation
0185     auto [range, medium] = Acts::range_medium(boundaries);
0186     // Center R calculation, using projection onto vector
0187     const auto& surfaceCenter = refSurface.center(gctx);
0188     Acts::Vector3 centerDiffs = (surfaceCenter - volumeCenter);
0189     double centerR = centerDiffs.dot(refRotation.col(2));
0190     // New joint center
0191     Acts::Vector3 pCenter = volumeCenter + centerR * refRotation.col(2);
0192     transform.pretranslate(pCenter);
0193     // New joint bounds
0194     double halfY =
0195         0.5 * (boundValues[Acts::RectangleBounds::BoundValues::eMaxY] -
0196                boundValues[Acts::RectangleBounds::BoundValues::eMinY]);
0197     bounds = std::make_unique<Acts::RectangleBounds>(0.5 * range, halfY);
0198   }
0199   // Set the rotation
0200   transform.prerotate(refRotation);
0201   // A new surface on the negative side over the full range
0202   auto surface = Acts::Surface::makeShared<Acts::PlaneSurface>(
0203       transform, std::move(bounds));
0204   // A make a portal and indicate the new link direction
0205   Acts::Experimental::PortalReplacement pRep = {
0206       std::make_shared<Acts::Experimental::Portal>(surface), index, dir,
0207       boundaries, binning};
0208   return pRep;
0209 }
0210 
0211 /// @brief Helper method to check the volumes in general and throw and exception if fails
0212 ///
0213 /// @param gctx the geometry context
0214 /// @param volumes the input volumes to be checked
0215 ///
0216 /// @note throws exception if any of checks fails
0217 void checkVolumes(
0218     const Acts::GeometryContext& gctx,
0219     const std::vector<std::shared_ptr<Acts::Experimental::DetectorVolume>>&
0220         volumes) {
0221   // A minimal set of checks - exceptions are thrown
0222   // - not enough volumes given
0223   std::string message = "CylindricalDetectorHelper: ";
0224   if (volumes.size() < 2u) {
0225     message += std::string("not enough volume given (") +
0226                std::to_string(volumes.size());
0227     message += std::string(" ), when required >=2.");
0228     throw std::invalid_argument(message.c_str());
0229   }
0230   // - null pointer detected or non-cylindrical volume detected
0231   for (auto [iv, v] : Acts::enumerate(volumes)) {
0232     // Check for nullptr
0233     if (v == nullptr) {
0234       message += "nullptr detector instead of volume " + std::to_string(iv);
0235       throw std::invalid_argument(message.c_str());
0236     }
0237     // Check for cylindrical volume type
0238     if (v->volumeBounds().type() != Acts::VolumeBounds::BoundsType::eCylinder) {
0239       message += "non-cylindrical volume bounds detected for volume " +
0240                  std::to_string(iv);
0241       throw std::invalid_argument(message.c_str());
0242     }
0243   }
0244   // Check the alignment of the volumes
0245   Acts::Experimental::detail::DetectorVolumeConsistency::checkRotationAlignment(
0246       gctx, volumes);
0247 }
0248 
0249 /// @brief Helper method to check the volume bounds
0250 ///
0251 /// @param gctx the geometry context
0252 /// @param volumes the input volumes to be checked
0253 /// @param refCur reference versus current check
0254 ///
0255 /// @note throws exception if any of checks fails
0256 void checkBounds(
0257     [[maybe_unused]] const Acts::GeometryContext& gctx,
0258     const std::vector<std::shared_ptr<Acts::Experimental::DetectorVolume>>&
0259         volumes,
0260     const std::vector<std::array<unsigned int, 2u>>& refCur) {
0261   // Reference values
0262   auto refValues = volumes[0u]->volumeBounds().values();
0263   for (auto [iv, v] : Acts::enumerate(volumes)) {
0264     if (iv > 0u) {
0265       auto curValues = v->volumeBounds().values();
0266       for (auto [r, c] : refCur) {
0267         if (std::abs(refValues[r] - curValues[c]) >
0268             Acts::s_onSurfaceTolerance) {
0269           std::string message = "CylindricalDetectorHelper: '";
0270           message += volumes[iv - 1]->name();
0271           if (r != c) {
0272             message += "' does not attach to '";
0273           } else {
0274             message += "' does not match with '";
0275           }
0276           message += volumes[iv]->name();
0277           message += "'\n";
0278           message += " - at bound values ";
0279           message += std::to_string(refValues[r]);
0280           message += " / " + std::to_string(curValues[c]);
0281           throw std::runtime_error(message.c_str());
0282         }
0283       }
0284       refValues = curValues;
0285     }
0286   }
0287 }
0288 
0289 }  // namespace
0290 
0291 Acts::Experimental::DetectorComponent::PortalContainer
0292 Acts::Experimental::detail::CylindricalDetectorHelper::connectInR(
0293     const GeometryContext& gctx,
0294     std::vector<std::shared_ptr<Experimental::DetectorVolume>>& volumes,
0295     const std::vector<unsigned int>& selectedOnly,
0296     Acts::Logging::Level logLevel) {
0297   // Basic checks for eligability of the volumes
0298   checkVolumes(gctx, volumes);
0299   // Check for bounds values of volumes (i) and (i+1) succesively for
0300   // compatibility:
0301   // - check outer (1u) of volume (i) vs inner radius (0u) of volume (i+1)
0302   // - phi sector (3u) and average phi (4u) between volumes (i), (i+1)
0303   std::vector<std::array<unsigned int, 2u>> checks = {
0304       {1u, 0u}, {3u, 3u}, {4u, 4u}};
0305   // - And we check the half length if it is not a selective attachment
0306   if (selectedOnly.empty()) {
0307     checks.push_back({2u, 2u});
0308   }
0309   checkBounds(gctx, volumes, checks);
0310 
0311   // The local logger
0312   ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
0313 
0314   ACTS_DEBUG("Connect " << volumes.size() << " detector volumes in R.");
0315 
0316   // Return proto container
0317   DetectorComponent::PortalContainer dShell;
0318 
0319   // Innermost volume boundaries
0320   std::vector<double> rBoundaries = {};
0321   auto refValues = volumes[0u]->volumeBounds().values();
0322 
0323   // Reference boundary values
0324   rBoundaries.push_back(refValues[CylinderVolumeBounds::BoundValues::eMinR]);
0325   rBoundaries.push_back(refValues[CylinderVolumeBounds::BoundValues::eMaxR]);
0326 
0327   // Connect in R ? (2u is the index of the outer cylinder)
0328   bool connectR = selectedOnly.empty() || rangeContainsValue(selectedOnly, 2u);
0329 
0330   // Get phi sector and average phi
0331   double phiSector =
0332       refValues[CylinderVolumeBounds::BoundValues::eHalfPhiSector];
0333   double avgPhi = refValues[CylinderVolumeBounds::BoundValues::eAveragePhi];
0334 
0335   // Fuse the cylinders
0336   for (unsigned int iv = 1; iv < volumes.size(); ++iv) {
0337     refValues = volumes[iv]->volumeBounds().values();
0338     // Keep on collecting the outside maximum r for the overall r boundaries
0339     rBoundaries.push_back(refValues[CylinderVolumeBounds::BoundValues::eMaxR]);
0340     // Only connect if configured to do so
0341     if (connectR) {
0342       ACTS_VERBOSE("Connect volume '" << volumes[iv - 1]->name() << "' to "
0343                                       << volumes[iv]->name() << "'.");
0344 
0345       // Fusing cylinders from inner and outer volume
0346       auto innerCylinder = volumes[iv - 1]->portalPtrs()[2u];
0347       auto outerCylinder = volumes[iv]->portalPtrs()[3u];
0348       auto fusedCylinder = Portal::fuse(innerCylinder, outerCylinder);
0349       volumes[iv - 1]->updatePortal(fusedCylinder, 2u);
0350       volumes[iv]->updatePortal(fusedCylinder, 3u);
0351     }
0352   }
0353 
0354   // Proto container setting
0355   if (connectR) {
0356     // The number of portals indicate again if the inner is present or not,
0357     if (volumes[0u]->portals().size() == 4u ||
0358         volumes[0u]->portals().size() == 6u) {
0359       dShell[3u] = volumes[0u]->portalPtrs()[3u];
0360     }
0361     dShell[2u] = volumes[volumes.size() - 1u]->portalPtrs()[2u];
0362   }
0363 
0364   // Check if sectors are present by the number of portals, check is done on the
0365   // outermost volume as this is required to have an inner cylinder, and hence
0366   // no offset needs to be respected
0367   bool sectorsPresent = volumes[volumes.size() - 1u]->portals().size() > 4u;
0368 
0369   // A portal replacement, it comprises of the portal, the index, the
0370   // direction, the binning and bins
0371   std::vector<PortalReplacement> pReplacements = {};
0372 
0373   // Disc assignments are forward for negative disc, backward for positive
0374   std::vector<Acts::Direction> discDirs = {Acts::Direction::Forward(),
0375                                            Acts::Direction::Backward()};
0376   for (const auto [iu, idir] : enumerate(discDirs)) {
0377     if (selectedOnly.empty() || rangeContainsValue(selectedOnly, iu)) {
0378       const Surface& refSurface = volumes[0u]->portals()[iu]->surface();
0379       const Transform3& refTransform = refSurface.transform(gctx);
0380       pReplacements.push_back(createDiscReplacement(
0381           refTransform, rBoundaries, {avgPhi - phiSector, avgPhi + phiSector},
0382           iu, idir));
0383     }
0384   }
0385 
0386   // If volume sectors are present, these have to be respected
0387   if (sectorsPresent) {
0388     ACTS_VERBOSE("Sector planes are present, they need replacement.");
0389     // Sector assignments are forward backward
0390     std::vector<Acts::Direction> sectorDirs = {Acts::Direction::Forward(),
0391                                                Acts::Direction::Backward()};
0392     Acts::Vector3 vCenter = volumes[0u]->transform(gctx).translation();
0393     for (const auto [iu, idir] : enumerate(sectorDirs)) {
0394       // (iu + 4u) corresponds to the indices of the phi-low and phi-high sector
0395       // planes.
0396       if (selectedOnly.empty() || rangeContainsValue(selectedOnly, iu + 4u)) {
0397         // As it is r-wrapping, the inner tube is guaranteed
0398         const Surface& refSurface =
0399             volumes[volumes.size() - 1u]->portals()[iu + 4u]->surface();
0400         pReplacements.push_back(createSectorReplacement(
0401             gctx, vCenter, refSurface, rBoundaries, Acts::AxisDirection::AxisR,
0402             iu + 4ul, idir));
0403       }
0404     }
0405   } else {
0406     ACTS_VERBOSE(
0407         "No sector planes present, full 2 * std::numbers::pi cylindrical "
0408         "geometry.");
0409   }
0410 
0411   // Attach the new volume multi links
0412   PortalHelper::attachExternalNavigationDelegates(gctx, volumes, pReplacements);
0413 
0414   // Exchange the portals of the volumes
0415   ACTS_VERBOSE("Portals of " << volumes.size() << " volumes need updating.");
0416   // Exchange the portals of the volumes
0417   for (auto& iv : volumes) {
0418     ACTS_VERBOSE("- update portals of volume '" << iv->name() << "'.");
0419     for (auto& [p, i, dir, boundaries, binning] : pReplacements) {
0420       // Fill the map
0421       dShell[i] = p;
0422 
0423       // Potential offset for tube vs/ cylinder
0424       // - if the volume doesn't have an inner portal, indices need to
0425       //   be shifted by -1 to update the correct index, that's the case for
0426       //   size 3 and 5 for portals
0427       std::size_t nPortals = iv->portals().size();
0428       bool innerPresent = (nPortals == 3u || nPortals == 5u);
0429       int iOffset = (innerPresent && i > 2u) ? -1 : 0;
0430       ACTS_VERBOSE("-- update portal with index "
0431                    << i + iOffset << " (including offset " << iOffset << ")");
0432       iv->updatePortal(p, static_cast<unsigned int>(i + iOffset));
0433     }
0434   }
0435   // Done.
0436   return dShell;
0437 }
0438 
0439 Acts::Experimental::DetectorComponent::PortalContainer
0440 Acts::Experimental::detail::CylindricalDetectorHelper::connectInZ(
0441     const Acts::GeometryContext& gctx,
0442     std::vector<std::shared_ptr<Acts::Experimental::DetectorVolume>>& volumes,
0443     const std::vector<unsigned int>& selectedOnly,
0444     Acts::Logging::Level logLevel) {
0445   // Basic checks for eligability of the volumes
0446   checkVolumes(gctx, volumes);
0447   // Check for bounds compatibility
0448   // We check phi sector (3u) and average phi (4u)
0449   std::vector<std::array<unsigned int, 2u>> checks = {{3u, 3u}, {4u, 4u}};
0450   // And we check the inner radius [0u], outer radius[1u] if it is not a
0451   // selective attachment
0452   if (selectedOnly.empty()) {
0453     checks.push_back({0u, 0u});
0454     checks.push_back({1u, 1u});
0455   }
0456   checkBounds(gctx, volumes, checks);
0457 
0458   // The local logger
0459   ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
0460 
0461   ACTS_DEBUG("Connect " << volumes.size() << " detector volumes in Z.");
0462 
0463   // Return proto container
0464   DetectorComponent::PortalContainer dShell;
0465 
0466   // Connect in Z ?
0467   // - 1u corresponds to the index of the high-z disc portal for the reference
0468   // volume.
0469   const bool connectZ =
0470       selectedOnly.empty() || rangeContainsValue(selectedOnly, 1u);
0471   // Reference z axis
0472   const auto rotation = volumes[0u]->transform(gctx).rotation();
0473 
0474   std::vector<Vector3> zBoundaries3D = {};
0475 
0476   /// @brief  Add the z boundary
0477   /// @param gctx the geometry context
0478   /// @param volume the volume
0479   /// @param side side value
0480   auto addZboundary3D = [&](const Experimental::DetectorVolume& volume,
0481                             int side) -> void {
0482     const auto boundValues = volume.volumeBounds().values();
0483     double halflengthZ =
0484         boundValues[CylinderVolumeBounds::BoundValues::eHalfLengthZ];
0485     zBoundaries3D.push_back(volume.transform(gctx).translation() +
0486                             side * halflengthZ * rotation.col(2));
0487   };
0488 
0489   // Fuse the discs - portals can be reused
0490   addZboundary3D(*volumes[0u].get(), -1);
0491   addZboundary3D(*volumes[0u].get(), 1);
0492   for (unsigned int iv = 1; iv < volumes.size(); ++iv) {
0493     // Add the z boundary
0494     addZboundary3D(*volumes[iv].get(), 1u);
0495     // Do the connection
0496     if (connectZ) {
0497       ACTS_VERBOSE("Connect volume '" << volumes[iv - 1]->name() << "' to "
0498                                       << volumes[iv]->name() << "'.");
0499       // Fusing the discs: positive at lower z, negative at hgiher z
0500       auto& pDisc = volumes[iv - 1]->portalPtrs()[1u];
0501       auto& nDisc = volumes[iv]->portalPtrs()[0u];
0502       // Throw an exception if the discs are not at the same position
0503       Vector3 pPosition = pDisc->surface().center(gctx);
0504       Vector3 nPosition = nDisc->surface().center(gctx);
0505       if (!pPosition.isApprox(nPosition)) {
0506         std::string message = "CylindricalDetectorHelper: '";
0507         message += volumes[iv - 1]->name();
0508         message += "' does not attach to '";
0509         message += volumes[iv]->name();
0510         message += "'\n";
0511         message += " - along z with values ";
0512         message += Acts::toString(pPosition);
0513         message += " / " + Acts::toString(nPosition);
0514         throw std::runtime_error(message.c_str());
0515       }
0516       auto fusedDisc = Portal::fuse(pDisc, nDisc);
0517       volumes[iv - 1]->updatePortal(fusedDisc, 1u);
0518       volumes[iv]->updatePortal(fusedDisc, 0u);
0519     }
0520   }
0521 
0522   // Register what we have from the container
0523   if (connectZ) {
0524     dShell[0u] = volumes[0u]->portalPtrs()[0u];
0525     dShell[1u] = volumes[volumes.size() - 1u]->portalPtrs()[1u];
0526   }
0527 
0528   // Centre of gravity
0529   Vector3 combinedCenter =
0530       0.5 * (zBoundaries3D[zBoundaries3D.size() - 1u] + zBoundaries3D[0u]);
0531 
0532   ACTS_VERBOSE("New combined center calculated at "
0533                << toString(combinedCenter));
0534 
0535   // Evaluate the series of z boundaries
0536   std::vector<double> zBoundaries = {};
0537   for (const auto& zb3D : zBoundaries3D) {
0538     auto proj3D = (zb3D - combinedCenter).dot(rotation.col(2));
0539     double zBoundary = std::copysign((zb3D - combinedCenter).norm(), proj3D);
0540     zBoundaries.push_back(zBoundary);
0541   }
0542 
0543   Transform3 combinedTransform = Transform3::Identity();
0544   combinedTransform.pretranslate(combinedCenter);
0545   combinedTransform.rotate(rotation);
0546 
0547   // Get phi sector and average phi
0548   const auto& refVolume = volumes[0u];
0549   const auto refValues = refVolume->volumeBounds().values();
0550 
0551   // Get phi sector and average phi
0552   double minR = refValues[CylinderVolumeBounds::BoundValues::eMinR];
0553   double maxR = refValues[CylinderVolumeBounds::BoundValues::eMaxR];
0554   double phiSector =
0555       refValues[CylinderVolumeBounds::BoundValues::eHalfPhiSector];
0556   double avgPhi = refValues[CylinderVolumeBounds::BoundValues::eAveragePhi];
0557 
0558   // Check if inner cylinder and sectors are present by the number of portals
0559   std::size_t nPortals = volumes[volumes.size() - 1u]->portals().size();
0560   bool innerPresent = (nPortals != 3u && nPortals != 5u);
0561   bool sectorsPresent = nPortals > 4u;
0562 
0563   // A portal replacement, it comprises of the portal, the index, the
0564   // direction, the binning and bins
0565   std::vector<PortalReplacement> pReplacements = {};
0566 
0567   // Disc assignments are forward for negative disc, backward for positive
0568   std::vector<Acts::Direction> cylinderDirs = {Acts::Direction::Backward()};
0569   // Cylinder radii
0570   std::vector<double> cylinderR = {maxR};
0571   if (innerPresent) {
0572     ACTS_VERBOSE("Inner surface present, tube geometry detected.");
0573     cylinderDirs.push_back(Direction::Forward());
0574     cylinderR.push_back(minR);
0575   } else {
0576     ACTS_VERBOSE("No inner surface present, solid cylinder geometry detected.");
0577   }
0578   // Tube/cylinder offset
0579   unsigned int iSecOffset = innerPresent ? 4u : 3u;
0580   // Prepare the cylinder replacements
0581   for (const auto [iu, idir] : enumerate(cylinderDirs)) {
0582     if (selectedOnly.empty() || rangeContainsValue(selectedOnly, iu + 2u)) {
0583       pReplacements.push_back(createCylinderReplacement(
0584           combinedTransform, cylinderR[iu], zBoundaries,
0585           {avgPhi - phiSector, avgPhi + phiSector}, iu + 2u, idir));
0586     }
0587   }
0588 
0589   // Prepare the sector side replacements
0590   if (sectorsPresent) {
0591     ACTS_VERBOSE("Sector planes are present, they need replacement.");
0592     // Sector assignmenta are forward backward
0593     std::vector<Acts::Direction> sectorDirs = {Acts::Direction::Forward(),
0594                                                Acts::Direction::Backward()};
0595     for (const auto [iu, idir] : enumerate(sectorDirs)) {
0596       // Access with 3u or 4u but always write 4u (to be caught later)
0597       if (selectedOnly.empty() || rangeContainsValue(selectedOnly, iu + 4u)) {
0598         const Surface& refSurface =
0599             volumes[0u]->portals()[iu + iSecOffset]->surface();
0600         pReplacements.push_back(createSectorReplacement(
0601             gctx, combinedCenter, refSurface, zBoundaries,
0602             Acts::AxisDirection::AxisZ, iu + 4ul, idir));
0603       }
0604     }
0605   } else {
0606     ACTS_VERBOSE(
0607         "No sector planes present, full 2 * std::numbers::pi cylindrical "
0608         "geometry.");
0609   }
0610 
0611   // Attach the new volume multi links
0612   PortalHelper::attachExternalNavigationDelegates(gctx, volumes, pReplacements);
0613 
0614   // Exchange the portals of the volumes
0615   ACTS_VERBOSE("Portals of " << volumes.size() << " volumes need updating.");
0616   for (auto& iv : volumes) {
0617     ACTS_VERBOSE("- update portals of volume '" << iv->name() << "'.");
0618     for (auto& [p, i, dir, boundaries, binning] : pReplacements) {
0619       // Potential offset for tube vs/ cylinder
0620       // if the volume doesn't have an inner portal, indices need to be shifted
0621       // by -1 to update the correct index.
0622       int iOffset = (i > 2u && !innerPresent) ? -1 : 0;
0623       ACTS_VERBOSE("-- update portal with index " << i);
0624       iv->updatePortal(p, static_cast<unsigned int>(i + iOffset));
0625       // Fill the map
0626       dShell[i] = p;
0627     }
0628   }
0629   // Done.
0630   return dShell;
0631 }
0632 
0633 Acts::Experimental::DetectorComponent::PortalContainer
0634 Acts::Experimental::detail::CylindricalDetectorHelper::connectInPhi(
0635     const Acts::GeometryContext& gctx,
0636     std::vector<std::shared_ptr<Acts::Experimental::DetectorVolume>>& volumes,
0637     const std::vector<unsigned int>& /*selectedOnly*/,
0638     Acts::Logging::Level logLevel) {
0639   // Basic checks for eligability of the volumes
0640   checkVolumes(gctx, volumes);
0641 
0642   // The local logger
0643   ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
0644 
0645   ACTS_DEBUG("Connect " << volumes.size() << " detector volumes in phi.");
0646 
0647   // Return proto container
0648   DetectorComponent::PortalContainer dShell;
0649 
0650   // Check if inner cylinder and sectors are present by the number of portals
0651   std::size_t nPortals = volumes[volumes.size() - 1u]->portals().size();
0652   bool innerPresent = (nPortals != 3u && nPortals != 5u);
0653 
0654   Transform3 refTransform = volumes[0u]->transform(gctx);
0655 
0656   // Sector offset
0657   unsigned int iSecOffset = innerPresent ? 4u : 3u;
0658   std::vector<double> phiBoundaries = {};
0659   auto refValues = volumes[0u]->volumeBounds().values();
0660   phiBoundaries.push_back(
0661       refValues[CylinderVolumeBounds::BoundValues::eAveragePhi] -
0662       refValues[CylinderVolumeBounds::BoundValues::eHalfPhiSector]);
0663   phiBoundaries.push_back(
0664       refValues[CylinderVolumeBounds::BoundValues::eAveragePhi] +
0665       refValues[CylinderVolumeBounds::BoundValues::eHalfPhiSector]);
0666   // Fuse the sectors
0667   for (unsigned int iv = 1; iv < volumes.size(); ++iv) {
0668     ACTS_VERBOSE("Connect volume '" << volumes[iv - 1]->name() << "' to "
0669                                     << volumes[iv]->name() << "'.");
0670 
0671     // Fuse sector surfaces r handed at lower index, l handed at higher index
0672     auto& rSector = volumes[iv - 1]->portalPtrs()[iSecOffset + 1u];
0673     auto& lSector = volumes[iv]->portalPtrs()[iSecOffset];
0674     auto fusedSector = Portal::fuse(rSector, lSector);
0675     volumes[iv - 1]->updatePortal(fusedSector, iSecOffset + 1u);
0676     volumes[iv]->updatePortal(fusedSector, iSecOffset);
0677     // The current values
0678     auto curValues = volumes[iv]->volumeBounds().values();
0679     // Bail out if they do not match
0680     double lowPhi =
0681         curValues[CylinderVolumeBounds::BoundValues::eAveragePhi] -
0682         curValues[CylinderVolumeBounds::BoundValues::eHalfPhiSector];
0683     double highPhi =
0684         curValues[CylinderVolumeBounds::BoundValues::eAveragePhi] +
0685         curValues[CylinderVolumeBounds::BoundValues::eHalfPhiSector];
0686     // Check phi attachment
0687     if (std::abs(phiBoundaries[phiBoundaries.size() - 1u] - lowPhi) >
0688         Acts::s_onSurfaceTolerance) {
0689       std::string message = "CylindricalDetectorHelper: '";
0690       message += volumes[iv - 1]->name();
0691       message += "' does not attach to '";
0692       message += volumes[iv]->name();
0693       message += "'\n";
0694       message += " - within phi sectors ";
0695       message += std::to_string(lowPhi);
0696       message +=
0697           " / " + std::to_string(phiBoundaries[phiBoundaries.size() - 1u]);
0698       throw std::runtime_error(message.c_str());
0699     }
0700     // Check radial and longitudinal compatibility - @TODO
0701     phiBoundaries.push_back(highPhi);
0702     // Recursive setting of the values
0703     refValues = curValues;
0704   }
0705 
0706   // A portal replacement, it comprises of the portal, the index, the
0707   // direction, the binning and bins
0708   std::vector<PortalReplacement> pReplacements = {};
0709   // Negative disc
0710   pReplacements.push_back(createDiscReplacement(
0711       refTransform,
0712       {refValues[CylinderVolumeBounds::BoundValues::eMinR],
0713        refValues[CylinderVolumeBounds::BoundValues::eMaxR]},
0714       phiBoundaries, 0u, Acts::Direction::Forward()));
0715 
0716   // Positive disc
0717   pReplacements.push_back(createDiscReplacement(
0718       refTransform,
0719       {refValues[CylinderVolumeBounds::BoundValues::eMinR],
0720        refValues[CylinderVolumeBounds::BoundValues::eMaxR]},
0721       phiBoundaries, 1u, Acts::Direction::Backward()));
0722 
0723   // Outside cylinder
0724   pReplacements.push_back(createCylinderReplacement(
0725       refTransform, refValues[CylinderVolumeBounds::BoundValues::eMaxR],
0726       {-refValues[CylinderVolumeBounds::BoundValues::eHalfLengthZ],
0727        refValues[CylinderVolumeBounds::BoundValues::eHalfLengthZ]},
0728       phiBoundaries, 2u, Acts::Direction::Backward()));
0729 
0730   // If the volume has a different inner radius than 0, it MUST have
0731   // an inner cylinder
0732   if (refValues[CylinderVolumeBounds::BoundValues::eMinR] > 0.) {
0733     // Inner cylinder
0734     pReplacements.push_back(createCylinderReplacement(
0735         refTransform, refValues[CylinderVolumeBounds::BoundValues::eMinR],
0736         {-refValues[CylinderVolumeBounds::BoundValues::eHalfLengthZ],
0737          refValues[CylinderVolumeBounds::BoundValues::eHalfLengthZ]},
0738         phiBoundaries, 3u, Acts::Direction::Forward()));
0739   }
0740 
0741   // Attach the new volume multi links
0742   PortalHelper::attachExternalNavigationDelegates(gctx, volumes, pReplacements);
0743   // Exchange the portals of the volumes
0744   ACTS_VERBOSE("Portals of " << volumes.size() << " volumes need updating.");
0745   for (auto& iv : volumes) {
0746     ACTS_VERBOSE("- update portals of volume '" << iv->name() << "'.");
0747     for (auto& [p, i, dir, boundaries, binning] : pReplacements) {
0748       ACTS_VERBOSE("-- update portal with index " << i);
0749       iv->updatePortal(p, static_cast<unsigned int>(i));
0750       // Fill the map
0751       dShell[i] = p;
0752     }
0753   }
0754 
0755   // Done.
0756   return dShell;
0757 }
0758 
0759 Acts::Experimental::DetectorComponent::PortalContainer
0760 Acts::Experimental::detail::CylindricalDetectorHelper::wrapInZR(
0761     const Acts::GeometryContext& gctx,
0762     std::vector<std::shared_ptr<Acts::Experimental::DetectorVolume>>& volumes,
0763     Acts::Logging::Level logLevel) {
0764   // The local logger
0765   ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
0766 
0767   ACTS_DEBUG("Wrapping volumes in Z-R.");
0768 
0769   // Minimal set of checks
0770   if (volumes.size() != 2u) {
0771     throw std::invalid_argument(
0772         "Wrapping the detector volume requires exactly 2 volumes.");
0773   }
0774 
0775   // Return the new container
0776   DetectorComponent::PortalContainer dShell;
0777 
0778   // Keep the outer shells
0779   dShell[0u] = volumes[1u]->portalPtrs()[0u];
0780   dShell[1u] = volumes[1u]->portalPtrs()[1u];
0781   dShell[2u] = volumes[1u]->portalPtrs()[2u];
0782 
0783   // Fuse outer cover of first with inner cylinder of wrapping volume
0784   auto& outerCover = volumes[0u]->portalPtrs()[2u];
0785   auto& innerCover = volumes[1u]->portalPtrs()[3u];
0786   auto fusedCover = Portal::fuse(outerCover, innerCover);
0787   volumes[0u]->updatePortal(fusedCover, 2u);
0788   volumes[1u]->updatePortal(fusedCover, 3u);
0789 
0790   // Stitch sides - negative
0791   auto& firstDiscN = volumes[1u]->portalPtrs()[4u];
0792   auto& secondDiscN = volumes[0u]->portalPtrs()[0u];
0793   auto fusedDiscN = Portal::fuse(firstDiscN, secondDiscN);
0794   volumes[1u]->updatePortal(fusedDiscN, 4u);
0795   volumes[0u]->updatePortal(fusedDiscN, 0u);
0796 
0797   // Stich sides - positive
0798   auto& firstDiscP = volumes[0u]->portalPtrs()[1u];
0799   auto& secondDiscP = volumes[1u]->portalPtrs()[5u];
0800   auto fusedDiscP = Portal::fuse(firstDiscP, secondDiscP);
0801   volumes[0u]->updatePortal(fusedDiscP, 1u);
0802   volumes[1u]->updatePortal(fusedDiscP, 5u);
0803 
0804   // If needed, insert new cylinder
0805   if (volumes[0u]->portalPtrs().size() == 4u &&
0806       volumes[1u]->portalPtrs().size() == 8u) {
0807     const auto* cylVolBounds =
0808         dynamic_cast<const CylinderVolumeBounds*>(&volumes[0u]->volumeBounds());
0809     const auto* ccylVolBounds = dynamic_cast<const CutoutCylinderVolumeBounds*>(
0810         &volumes[1u]->volumeBounds());
0811     if (cylVolBounds == nullptr || ccylVolBounds == nullptr) {
0812       throw std::invalid_argument(
0813           "Wrapping the detector volume requires a cylinder and a cutout "
0814           "cylinder volume.");
0815     }
0816     // We need a new cylinder spanning over the entire inner tube
0817     double hlZ = cylVolBounds->get(
0818         Acts::CylinderVolumeBounds::BoundValues::eHalfLengthZ);
0819     double HlZ = ccylVolBounds->get(
0820         Acts::CutoutCylinderVolumeBounds::BoundValues::eHalfLengthZ);
0821     double innerR = cylVolBounds->get(CylinderVolumeBounds::BoundValues::eMinR);
0822     // Create the inner replacement
0823     std::vector<PortalReplacement> pReplacements;
0824     pReplacements.push_back(createCylinderReplacement(
0825         volumes[0u]->transform(gctx), innerR, {-HlZ, -hlZ, hlZ, HlZ},
0826         {-std::numbers::pi, std::numbers::pi}, 3u, Direction::Forward()));
0827     std::vector<std::shared_ptr<DetectorVolume>> zVolumes = {
0828         volumes[1u], volumes[0u], volumes[1u]};
0829     // Attach the new volume multi links
0830     PortalHelper::attachExternalNavigationDelegates(gctx, zVolumes,
0831                                                     pReplacements);
0832     auto& [p, i, dir, boundaries, binning] = pReplacements[0u];
0833     // Update the portals
0834     volumes[1u]->updatePortal(p, 6u);
0835     volumes[0u]->updatePortal(p, 3u);
0836     volumes[1u]->updatePortal(p, 7u);
0837     // Inner skin
0838     dShell[3u] = p;
0839   }
0840   // Done.
0841   return dShell;
0842 }
0843 
0844 Acts::Experimental::DetectorComponent::PortalContainer
0845 Acts::Experimental::detail::CylindricalDetectorHelper::connectInR(
0846     const GeometryContext& gctx,
0847     const std::vector<DetectorComponent::PortalContainer>& containers,
0848     const std::vector<unsigned int>& selectedOnly,
0849     Acts::Logging::Level logLevel) noexcept(false) {
0850   // The local logger
0851   ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
0852 
0853   ACTS_DEBUG("Connect " << containers.size() << " proto containers in R.");
0854 
0855   // Return the new container
0856   DetectorComponent::PortalContainer dShell;
0857 
0858   // Fuse the cylinders - portals can be reused for this operation
0859   for (unsigned int ic = 1; ic < containers.size(); ++ic) {
0860     auto& formerContainer = containers[ic - 1];
0861     auto& currentContainer = containers[ic];
0862     // Check and throw exception
0863     if (!formerContainer.contains(2u)) {
0864       throw std::invalid_argument(
0865           "CylindricalDetectorHelper: proto container has no outer cover, can "
0866           "not be connected in R");
0867     }
0868     if (!currentContainer.contains(3u)) {
0869       throw std::invalid_argument(
0870           "CylindricalDetectorHelper: proto container has no inner cover, can "
0871           "not be connected in R");
0872     }
0873 
0874     // Fuse containers, and update the attached volumes
0875     std::shared_ptr<Portal> innerCylinder = containers[ic - 1].find(2u)->second;
0876     // Direction is explicitly addressed with a direction index
0877     auto innerAttachedVolumes =
0878         innerCylinder->attachedDetectorVolumes()[Direction::Backward().index()];
0879     std::shared_ptr<Portal> outerCylinder = containers[ic].find(3u)->second;
0880     auto outerAttachedVolume =
0881         outerCylinder->attachedDetectorVolumes()[Direction::Forward().index()];
0882     auto fusedCylinder = Portal::fuse(innerCylinder, outerCylinder);
0883 
0884     // Update the attached volumes with the new portal
0885     std::ranges::for_each(innerAttachedVolumes,
0886                           [&](std::shared_ptr<DetectorVolume>& av) {
0887                             av->updatePortal(fusedCylinder, 2u);
0888                           });
0889     std::ranges::for_each(outerAttachedVolume,
0890                           [&](std::shared_ptr<DetectorVolume>& av) {
0891                             av->updatePortal(fusedCylinder, 3u);
0892                           });
0893   }
0894 
0895   // Proto container refurbishment
0896   if (containers[0u].contains(3u)) {
0897     dShell[3u] = containers[0u].find(3u)->second;
0898   }
0899   dShell[2u] = containers[containers.size() - 1u].find(2u)->second;
0900 
0901   auto sideVolumes = PortalHelper::stripSideVolumes(
0902       containers, {0u, 1u, 4u, 5u}, selectedOnly, logLevel);
0903 
0904   for (auto [s, volumes] : sideVolumes) {
0905     auto pR = connectInR(gctx, volumes, {s});
0906     if (pR.contains(s)) {
0907       dShell[s] = pR.find(s)->second;
0908     }
0909   }
0910 
0911   // Done.
0912   return dShell;
0913 }
0914 
0915 Acts::Experimental::DetectorComponent::PortalContainer
0916 Acts::Experimental::detail::CylindricalDetectorHelper::connectInZ(
0917     const GeometryContext& gctx,
0918     const std::vector<DetectorComponent::PortalContainer>& containers,
0919     const std::vector<unsigned int>& selectedOnly,
0920     Acts::Logging::Level logLevel) noexcept(false) {
0921   // The local logger
0922   ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
0923 
0924   ACTS_DEBUG("Connect " << containers.size() << " proto containers in Z.");
0925 
0926   // Return the new container
0927   DetectorComponent::PortalContainer dShell;
0928 
0929   for (unsigned int ic = 1; ic < containers.size(); ++ic) {
0930     auto& formerContainer = containers[ic - 1];
0931     auto& currentContainer = containers[ic];
0932     // Check and throw exception
0933     if (!formerContainer.contains(1u)) {
0934       throw std::invalid_argument(
0935           "CylindricalDetectorHelper: proto container has no negative disc, "
0936           "can not be connected in Z");
0937     }
0938     if (!currentContainer.contains(0u)) {
0939       throw std::invalid_argument(
0940           "CylindricalDetectorHelper: proto container has no positive disc, "
0941           "can not be connected in Z");
0942     }
0943     // Container attachment positive Disc of lower, negative Disc at higher
0944     std::shared_ptr<Portal> pDisc = formerContainer.find(1u)->second;
0945     auto pAttachedVolumes =
0946         pDisc->attachedDetectorVolumes()[Direction::Backward().index()];
0947 
0948     std::shared_ptr<Portal> nDisc = currentContainer.find(0u)->second;
0949     auto nAttachedVolumes =
0950         nDisc->attachedDetectorVolumes()[Direction::Forward().index()];
0951 
0952     auto fusedDisc = Portal::fuse(pDisc, nDisc);
0953 
0954     std::ranges::for_each(pAttachedVolumes,
0955                           [&](std::shared_ptr<DetectorVolume>& av) {
0956                             av->updatePortal(fusedDisc, 1u);
0957                           });
0958     std::ranges::for_each(nAttachedVolumes,
0959                           [&](std::shared_ptr<DetectorVolume>& av) {
0960                             av->updatePortal(fusedDisc, 0u);
0961                           });
0962   }
0963 
0964   // Proto container refurbishment
0965   dShell[0u] = containers[0u].find(0u)->second;
0966   dShell[1u] = containers[containers.size() - 1u].find(1u)->second;
0967 
0968   // Check if this is a tube or a cylinder container (check done on 1st)
0969   std::vector<unsigned int> nominalSides = {2u, 4u, 5u};
0970   if (containers[0u].contains(3u)) {
0971     nominalSides.push_back(3u);
0972   }
0973 
0974   // Strip the side volumes
0975   auto sideVolumes = PortalHelper::stripSideVolumes(containers, nominalSides,
0976                                                     selectedOnly, logLevel);
0977 
0978   ACTS_VERBOSE("There remain " << sideVolumes.size()
0979                                << " side volume packs to be connected");
0980   for (auto [s, volumes] : sideVolumes) {
0981     ACTS_VERBOSE(" - connect " << volumes.size() << " at selected side " << s);
0982     auto pR = connectInZ(gctx, volumes, {s}, logLevel);
0983     if (pR.contains(s)) {
0984       dShell[s] = pR.find(s)->second;
0985     }
0986   }
0987 
0988   // Done.
0989   return dShell;
0990 }
0991 
0992 Acts::Experimental::DetectorComponent::PortalContainer
0993 Acts::Experimental::detail::CylindricalDetectorHelper::connectInPhi(
0994     [[maybe_unused]] const GeometryContext& gctx,
0995     [[maybe_unused]] const std::vector<DetectorComponent::PortalContainer>&
0996         containers,
0997     [[maybe_unused]] const std::vector<unsigned int>& selectedOnly,
0998     [[maybe_unused]] Acts::Logging::Level logLevel) noexcept(false) {
0999   throw std::invalid_argument(
1000       "CylindricalDetectorHelper: container connection in phi not implemented "
1001       "yet.");
1002   DetectorComponent::PortalContainer dShell;
1003   // Done.
1004   return dShell;
1005 }
1006 
1007 Acts::Experimental::DetectorComponent::PortalContainer
1008 Acts::Experimental::detail::CylindricalDetectorHelper::wrapInZR(
1009     [[maybe_unused]] const GeometryContext& gctx,
1010     const std::vector<DetectorComponent::PortalContainer>& containers,
1011     Acts::Logging::Level logLevel) {
1012   if (containers.size() != 2u) {
1013     throw std::invalid_argument(
1014         "CylindricalDetectorHelper: wrapping must take exactly two "
1015         "containers.");
1016   }
1017 
1018   // The inner one is a container
1019   auto innerContainer = containers.front();
1020   // The outer one is a single volume represented as a container
1021   auto outerContainer = containers.back();
1022   std::shared_ptr<DetectorVolume> wrappingVolume = nullptr;
1023   for (const auto& [key, value] : outerContainer) {
1024     auto attachedVolumes = value->attachedDetectorVolumes();
1025     for (const auto& ava : attachedVolumes) {
1026       for (const auto& av : ava) {
1027         if (wrappingVolume == nullptr && av != nullptr) {
1028           wrappingVolume = av;
1029         } else if (wrappingVolume != nullptr && av != wrappingVolume) {
1030           throw std::invalid_argument(
1031               "CylindricalDetectorHelper: wrapping container must represent a "
1032               "single volume.");
1033         }
1034       }
1035     }
1036   }
1037   if (wrappingVolume == nullptr) {
1038     throw std::invalid_argument(
1039         "CylindricalDetectorHelper: wrapping volume could not be "
1040         "determined.");
1041   }
1042 
1043   // The local logger
1044   ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
1045 
1046   ACTS_DEBUG("Wrapping a container with volume `" << wrappingVolume->name()
1047                                                   << "'.");
1048   // Return the new container
1049   DetectorComponent::PortalContainer dShell;
1050 
1051   // Keep the outer shells of the proto container
1052   dShell[0u] = wrappingVolume->portalPtrs()[0u];
1053   dShell[1u] = wrappingVolume->portalPtrs()[1u];
1054   dShell[2u] = wrappingVolume->portalPtrs()[2u];
1055 
1056   // Fuse outer cover of first with inner cylinder of wrapping volume
1057   auto& innerCover = innerContainer[2u];
1058   auto innerAttachedVolumes =
1059       innerCover->attachedDetectorVolumes()[Direction::Backward().index()];
1060   auto& innerTube = wrappingVolume->portalPtrs()[3u];
1061   auto fusedCover = Portal::fuse(innerCover, innerTube);
1062 
1063   std::ranges::for_each(innerAttachedVolumes,
1064                         [&](std::shared_ptr<DetectorVolume>& av) {
1065                           av->updatePortal(fusedCover, 2u);
1066                         });
1067   wrappingVolume->updatePortal(fusedCover, 3u);
1068 
1069   // Stitch sides - negative
1070   // positive disc of lower , negative disc of higher
1071   auto& firstDiscN = innerContainer[0u];
1072 
1073   auto firstNAttachedVolumes =
1074       firstDiscN->attachedDetectorVolumes()[Direction::Forward().index()];
1075 
1076   auto& secondDiscN = wrappingVolume->portalPtrs()[4u];
1077   auto fusedDiscN = Portal::fuse(firstDiscN, secondDiscN);
1078 
1079   std::ranges::for_each(firstNAttachedVolumes,
1080                         [&](std::shared_ptr<DetectorVolume>& av) {
1081                           av->updatePortal(fusedDiscN, 0u);
1082                         });
1083   wrappingVolume->updatePortal(fusedDiscN, 4u);
1084 
1085   // Stich sides - positive
1086   auto& firstDiscP = innerContainer[1u];
1087   auto firstPAttachedVolumes =
1088       firstDiscP->attachedDetectorVolumes()[Direction::Backward().index()];
1089 
1090   auto& secondDiscP = wrappingVolume->portalPtrs()[5u];
1091   auto fusedDiscP = Portal::fuse(firstDiscP, secondDiscP);
1092 
1093   std::ranges::for_each(firstPAttachedVolumes,
1094                         [&](std::shared_ptr<DetectorVolume>& av) {
1095                           av->updatePortal(fusedDiscP, 1u);
1096                         });
1097 
1098   wrappingVolume->updatePortal(fusedDiscP, 5u);
1099 
1100   // If inner stitching is necessary
1101   if (innerContainer.size() == 4u &&
1102       wrappingVolume->portalPtrs().size() == 8u) {
1103     // Inner Container portal
1104     auto& centralSegment = innerContainer[3u];
1105     auto centralValues = centralSegment->surface().bounds().values();
1106     double centralHalfLengthZ =
1107         centralValues[CylinderBounds::BoundValues::eHalfLengthZ];
1108     // The two segments
1109     auto& nSegment = wrappingVolume->portalPtrs()[6u];
1110     auto nValues = nSegment->surface().bounds().values();
1111     double nHalfLengthZ = nValues[CylinderBounds::BoundValues::eHalfLengthZ];
1112     auto& pSegment = wrappingVolume->portalPtrs()[7u];
1113     auto pValues = pSegment->surface().bounds().values();
1114     double pHalfLengthZ = pValues[CylinderBounds::BoundValues::eHalfLengthZ];
1115 
1116     auto sideVolumes =
1117         PortalHelper::stripSideVolumes({innerContainer}, {3u}, {3u}, logLevel);
1118 
1119     // First the left volume sector
1120     std::vector<std::shared_ptr<DetectorVolume>> innerVolumes = {
1121         wrappingVolume->getSharedPtr()};
1122 
1123     std::vector<double> zBoundaries = {-centralHalfLengthZ - 2 * nHalfLengthZ,
1124                                        centralHalfLengthZ};
1125     // Loop over side volume and register the z boundaries
1126     for (auto& svs : sideVolumes) {
1127       for (auto& v : svs.second) {
1128         const auto* cylVolBounds =
1129             dynamic_cast<const CylinderVolumeBounds*>(&v->volumeBounds());
1130         if (cylVolBounds == nullptr) {
1131           throw std::invalid_argument(
1132               "CylindricalDetectorHelper: side volume must be a cylinder.");
1133         }
1134         double hlZ =
1135             cylVolBounds->get(CylinderVolumeBounds::BoundValues::eHalfLengthZ);
1136         zBoundaries.push_back(zBoundaries.back() + 2 * hlZ);
1137         innerVolumes.push_back(v);
1138       }
1139     }
1140     // Last the right volume sector
1141     zBoundaries.push_back(zBoundaries.back() + 2 * pHalfLengthZ);
1142     innerVolumes.push_back(wrappingVolume);
1143   }
1144 
1145   // Done.
1146   return dShell;
1147 }