Back to home page

sPhenix code displayed by LXR

 
 

    


File indexing completed on 2025-08-06 08:10:22

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