Back to home page

EIC code displayed by LXR

 
 

    


Warning, file /acts/Core/src/Detector/detail/CylindricalDetectorHelper.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

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