Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-03-30 07:46:48

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 <boost/test/data/test_case.hpp>
0010 #include <boost/test/detail/log_level.hpp>
0011 #include <boost/test/tools/context.hpp>
0012 #include <boost/test/tools/old/interface.hpp>
0013 #include <boost/test/unit_test.hpp>
0014 #include <boost/test/unit_test_log.hpp>
0015 #include <boost/test/unit_test_parameters.hpp>
0016 #include <boost/test/unit_test_suite.hpp>
0017 
0018 #include "Acts/Definitions/Algebra.hpp"
0019 #include "Acts/Definitions/Units.hpp"
0020 #include "Acts/Geometry/CylinderVolumeBounds.hpp"
0021 #include "Acts/Geometry/CylinderVolumeStack.hpp"
0022 #include "Acts/Geometry/VolumeAttachmentStrategy.hpp"
0023 #include "Acts/Geometry/VolumeResizeStrategy.hpp"
0024 #include "Acts/Utilities/BinningType.hpp"
0025 #include "Acts/Utilities/Logger.hpp"
0026 #include "Acts/Utilities/Zip.hpp"
0027 #include "ActsTests/CommonHelpers/FloatComparisons.hpp"
0028 
0029 #include <numbers>
0030 
0031 using namespace Acts;
0032 using namespace Acts::UnitLiterals;
0033 
0034 namespace ActsTests {
0035 
0036 auto logger = getDefaultLogger("UnitTests", Logging::VERBOSE);
0037 
0038 struct Fixture {
0039   Logging::Level m_level;
0040   Fixture() {
0041     m_level = Logging::getFailureThreshold();
0042     Logging::setFailureThreshold(Logging::FATAL);
0043   }
0044 
0045   ~Fixture() { Logging::setFailureThreshold(m_level); }
0046 };
0047 
0048 BOOST_FIXTURE_TEST_SUITE(GeometrySuite, Fixture)
0049 
0050 static const std::vector<VolumeAttachmentStrategy> strategies = {
0051     VolumeAttachmentStrategy::Gap,
0052     VolumeAttachmentStrategy::First,
0053     VolumeAttachmentStrategy::Second,
0054     VolumeAttachmentStrategy::Midpoint,
0055 };
0056 
0057 static const std::vector<VolumeResizeStrategy> resizeStrategies = {
0058     VolumeResizeStrategy::Expand,
0059     VolumeResizeStrategy::Gap,
0060 };
0061 
0062 BOOST_AUTO_TEST_SUITE(CylinderVolumeStackTest)
0063 BOOST_AUTO_TEST_SUITE(ZDirection)
0064 
0065 BOOST_DATA_TEST_CASE(Baseline,
0066                      (boost::unit_test::data::xrange(-135, 180, 45) *
0067                       boost::unit_test::data::xrange(0, 2, 1) *
0068                       boost::unit_test::data::make(0.8, 1.0, 1.2) *
0069                       boost::unit_test::data::make(Vector3{0_mm, 0_mm, 0_mm},
0070                                                    Vector3{20_mm, 0_mm, 0_mm},
0071                                                    Vector3{0_mm, 20_mm, 0_mm},
0072                                                    Vector3{20_mm, 20_mm, 0_mm},
0073                                                    Vector3{0_mm, 0_mm, 20_mm}) *
0074                       boost::unit_test::data::make(strategies)),
0075                      angle, rotate, shift, offset, strategy) {
0076   double hlZ = 400_mm;
0077 
0078   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
0079 
0080   // Cylinder volumes which already line up, but have different1 radii
0081   auto bounds1 = std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, hlZ);
0082   auto bounds2 = std::make_shared<CylinderVolumeBounds>(200_mm, 600_mm, hlZ);
0083   auto bounds3 = std::make_shared<CylinderVolumeBounds>(300_mm, 500_mm, hlZ);
0084 
0085   Transform3 base =
0086       AngleAxis3(angle * 1_degree, Vector3::UnitX()) * Translation3(offset);
0087 
0088   Transform3 transform1 = base;
0089   transform1.translate(Vector3{0_mm, 0_mm, -2 * hlZ * shift});
0090   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
0091 
0092   Transform3 transform2 = base;
0093   transform2.translate(Vector3{0_mm, 0_mm, 0_mm});
0094   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
0095 
0096   Transform3 transform3 = base;
0097   transform3.translate(Vector3{0_mm, 0_mm, 2 * hlZ * shift});
0098   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
0099 
0100   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
0101   // Rotate to simulate unsorted volumes: all results should be the same!
0102   std::rotate(volumes.begin(), volumes.begin() + rotate, volumes.end());
0103 
0104   auto origVolumes = volumes;
0105 
0106   std::vector<CylinderVolumeBounds> originalBounds;
0107   std::transform(
0108       volumes.begin(), volumes.end(), std::back_inserter(originalBounds),
0109       [](const auto& vol) {
0110         return *dynamic_cast<const CylinderVolumeBounds*>(&vol->volumeBounds());
0111       });
0112 
0113   if (shift < 1.0) {
0114     BOOST_CHECK_THROW(
0115         CylinderVolumeStack(gctx, volumes, AxisDirection::AxisZ, strategy,
0116                             VolumeResizeStrategy::Gap, *logger),
0117         std::invalid_argument);
0118     return;
0119   }
0120   CylinderVolumeStack cylStack(gctx, volumes, AxisDirection::AxisZ, strategy,
0121                                VolumeResizeStrategy::Gap, *logger);
0122 
0123   auto stackBounds =
0124       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0125   BOOST_REQUIRE(stackBounds != nullptr);
0126 
0127   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
0128   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMaxR), 600_mm);
0129   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0130                     hlZ + 2 * hlZ * shift);
0131   CHECK_CLOSE_OR_SMALL(cylStack.localToGlobalTransform(gctx).matrix(),
0132                        base.matrix(), 1e-10, 1e-14);
0133 
0134   // All volumes (including gaps) are cylinders and have the same radial bounds
0135   for (const auto& volume : volumes) {
0136     const auto* cylinderBounds =
0137         dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
0138     BOOST_REQUIRE(cylinderBounds != nullptr);
0139     BOOST_CHECK_EQUAL(cylinderBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
0140     BOOST_CHECK_EQUAL(cylinderBounds->get(CylinderVolumeBounds::eMaxR), 600_mm);
0141   }
0142 
0143   // Volumes are sorted in (local) z
0144   for (std::size_t i = 0; i < volumes.size() - 1; ++i) {
0145     const auto& a = volumes.at(i);
0146     const auto& b = volumes.at(i + 1);
0147 
0148     BOOST_CHECK_LT((base.inverse() * a->center(gctx))[eZ],
0149                    (base.inverse() * b->center(gctx))[eZ]);
0150   }
0151 
0152   if (shift <= 1.0) {
0153     // No gap volumes were added
0154     BOOST_CHECK_EQUAL(volumes.size(), 3);
0155 
0156     // No expansion, original volumes did not move
0157     BOOST_CHECK_EQUAL(vol1->localToGlobalTransform(gctx).matrix(),
0158                       transform1.matrix());
0159     BOOST_CHECK_EQUAL(vol2->localToGlobalTransform(gctx).matrix(),
0160                       transform2.matrix());
0161     BOOST_CHECK_EQUAL(vol3->localToGlobalTransform(gctx).matrix(),
0162                       transform3.matrix());
0163 
0164     for (const auto& [volume, bounds] : zip(origVolumes, originalBounds)) {
0165       const auto* newBounds =
0166           dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
0167       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0168                         bounds.get(CylinderVolumeBounds::eHalfLengthZ));
0169     }
0170   } else {
0171     if (strategy == VolumeAttachmentStrategy::Gap) {
0172       // Gap volumes were added
0173       BOOST_CHECK_EQUAL(volumes.size(), 5);
0174       auto gap1 = volumes.at(1);
0175       auto gap2 = volumes.at(3);
0176 
0177       BOOST_TEST_MESSAGE(
0178           "Gap 1: " << gap1->localToGlobalTransform(gctx).matrix());
0179       BOOST_TEST_MESSAGE(
0180           "Gap 2: " << gap2->localToGlobalTransform(gctx).matrix());
0181 
0182       const auto* gapBounds1 =
0183           dynamic_cast<const CylinderVolumeBounds*>(&gap1->volumeBounds());
0184       const auto* gapBounds2 =
0185           dynamic_cast<const CylinderVolumeBounds*>(&gap2->volumeBounds());
0186 
0187       double gapHlZ = (shift - 1.0) * hlZ;
0188 
0189       BOOST_CHECK(std::abs(gapBounds1->get(CylinderVolumeBounds::eHalfLengthZ) -
0190                            gapHlZ) < 1e-10);
0191       BOOST_CHECK(std::abs(gapBounds2->get(CylinderVolumeBounds::eHalfLengthZ) -
0192                            gapHlZ) < 1e-10);
0193 
0194       double gap1Z = (-2 * hlZ * shift) + hlZ + gapHlZ;
0195       double gap2Z = (2 * hlZ * shift) - hlZ - gapHlZ;
0196 
0197       Transform3 gap1Transform = base * Translation3{0_mm, 0_mm, gap1Z};
0198       Transform3 gap2Transform = base * Translation3{0_mm, 0_mm, gap2Z};
0199 
0200       CHECK_CLOSE_OR_SMALL(gap1->localToGlobalTransform(gctx).matrix(),
0201                            gap1Transform.matrix(), 1e-10, 1e-14);
0202       CHECK_CLOSE_OR_SMALL(gap2->localToGlobalTransform(gctx).matrix(),
0203                            gap2Transform.matrix(), 1e-10, 1e-14);
0204 
0205       // Original volumes did not changes bounds
0206       for (const auto& [volume, bounds] : zip(origVolumes, originalBounds)) {
0207         const auto* newBounds =
0208             dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
0209         BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0210                           bounds.get(CylinderVolumeBounds::eHalfLengthZ));
0211       }
0212 
0213       // No expansion, original volumes did not move
0214       BOOST_CHECK_EQUAL(vol1->localToGlobalTransform(gctx).matrix(),
0215                         transform1.matrix());
0216       BOOST_CHECK_EQUAL(vol2->localToGlobalTransform(gctx).matrix(),
0217                         transform2.matrix());
0218       BOOST_CHECK_EQUAL(vol3->localToGlobalTransform(gctx).matrix(),
0219                         transform3.matrix());
0220 
0221     } else if (strategy == VolumeAttachmentStrategy::First) {
0222       // No gap volumes were added
0223       BOOST_CHECK_EQUAL(volumes.size(), 3);
0224 
0225       double wGap = (shift - 1.0) * hlZ * 2;
0226 
0227       // Volume 1 got bigger and shifted right
0228       auto newBounds1 =
0229           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
0230       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0231                         hlZ + wGap / 2.0);
0232       double pZ1 = -2 * hlZ * shift + wGap / 2.0;
0233       Transform3 expectedTransform1 = base * Translation3{0_mm, 0_mm, pZ1};
0234       CHECK_CLOSE_OR_SMALL(vol1->localToGlobalTransform(gctx).matrix(),
0235                            expectedTransform1.matrix(), 1e-10, 1e-14);
0236 
0237       // Volume 2 got bigger and shifted left
0238       auto newBounds2 =
0239           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
0240       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0241                         hlZ + wGap / 2.0);
0242       double pZ2 = wGap / 2.0;
0243       Transform3 expectedTransform2 = base * Translation3{0_mm, 0_mm, pZ2};
0244       CHECK_CLOSE_OR_SMALL(vol2->localToGlobalTransform(gctx).matrix(),
0245                            expectedTransform2.matrix(), 1e-10, 1e-14);
0246 
0247       // Volume 3 stayed the same
0248       auto newBounds3 =
0249           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
0250       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eHalfLengthZ),
0251                         hlZ);
0252       double pZ3 = 2 * hlZ * shift;
0253       Transform3 expectedTransform3 = base * Translation3{0_mm, 0_mm, pZ3};
0254       CHECK_CLOSE_OR_SMALL(vol3->localToGlobalTransform(gctx).matrix(),
0255                            expectedTransform3.matrix(), 1e-10, 1e-14);
0256     } else if (strategy == VolumeAttachmentStrategy::Second) {
0257       // No gap volumes were added
0258       BOOST_CHECK_EQUAL(volumes.size(), 3);
0259 
0260       double wGap = (shift - 1.0) * hlZ * 2;
0261 
0262       // Volume 1 stayed the same
0263       auto newBounds1 =
0264           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
0265       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0266                         hlZ);
0267       double pZ1 = -2 * hlZ * shift;
0268       Transform3 expectedTransform1 = base * Translation3{0_mm, 0_mm, pZ1};
0269       CHECK_CLOSE_OR_SMALL(vol1->localToGlobalTransform(gctx).matrix(),
0270                            expectedTransform1.matrix(), 1e-10, 1e-14);
0271 
0272       // Volume 2 got bigger and shifted left
0273       auto newBounds2 =
0274           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
0275       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0276                         hlZ + wGap / 2.0);
0277       double pZ2 = -wGap / 2.0;
0278       Transform3 expectedTransform2 = base * Translation3{0_mm, 0_mm, pZ2};
0279       CHECK_CLOSE_OR_SMALL(vol2->localToGlobalTransform(gctx).matrix(),
0280                            expectedTransform2.matrix(), 1e-10, 1e-14);
0281 
0282       // Volume 3 got bigger and shifted left
0283       auto newBounds3 =
0284           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
0285       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eHalfLengthZ),
0286                         hlZ + wGap / 2.0);
0287       double pZ3 = 2 * hlZ * shift - wGap / 2.0;
0288       Transform3 expectedTransform3 = base * Translation3{0_mm, 0_mm, pZ3};
0289       CHECK_CLOSE_OR_SMALL(vol3->localToGlobalTransform(gctx).matrix(),
0290                            expectedTransform3.matrix(), 1e-10, 1e-14);
0291     } else if (strategy == VolumeAttachmentStrategy::Midpoint) {
0292       // No gap volumes were added
0293       BOOST_CHECK_EQUAL(volumes.size(), 3);
0294 
0295       double wGap = (shift - 1.0) * hlZ * 2;
0296 
0297       // Volume 1 got bigger and shifted right
0298       auto newBounds1 =
0299           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
0300       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0301                         hlZ + wGap / 4.0);
0302       double pZ1 = -2 * hlZ * shift + wGap / 4.0;
0303       Transform3 expectedTransform1 = base * Translation3{0_mm, 0_mm, pZ1};
0304       CHECK_CLOSE_OR_SMALL(vol1->localToGlobalTransform(gctx).matrix(),
0305                            expectedTransform1.matrix(), 1e-10, 1e-14);
0306 
0307       // Volume 2 got bigger but didn't move
0308       auto newBounds2 =
0309           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
0310       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0311                         hlZ + wGap / 2.0);
0312       CHECK_CLOSE_OR_SMALL(vol2->localToGlobalTransform(gctx).matrix(),
0313                            base.matrix(), 1e-10, 1e-14);
0314 
0315       // Volume 3 got bigger and shifted left
0316       auto newBounds3 =
0317           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
0318       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eHalfLengthZ),
0319                         hlZ + wGap / 4.0);
0320       double pZ3 = 2 * hlZ * shift - wGap / 4.0;
0321       Transform3 expectedTransform3 = base * Translation3{0_mm, 0_mm, pZ3};
0322       CHECK_CLOSE_OR_SMALL(vol3->localToGlobalTransform(gctx).matrix(),
0323                            expectedTransform3.matrix(), 1e-10, 1e-14);
0324     }
0325   }
0326 }
0327 
0328 BOOST_AUTO_TEST_CASE(Asymmetric) {
0329   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
0330   double hlZ1 = 200_mm;
0331   double pZ1 = -1100_mm;
0332   double hlZ2 = 600_mm;
0333   double pZ2 = -200_mm;
0334   double hlZ3 = 400_mm;
0335   double pZ3 = 850_mm;
0336 
0337   // Cylinder volumes which already line up, but have different1 radii
0338   auto bounds1 = std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, hlZ1);
0339   auto bounds2 = std::make_shared<CylinderVolumeBounds>(200_mm, 600_mm, hlZ2);
0340   auto bounds3 = std::make_shared<CylinderVolumeBounds>(300_mm, 500_mm, hlZ3);
0341 
0342   Transform3 transform1 = Transform3::Identity();
0343   transform1.translate(Vector3{0_mm, 0_mm, pZ1});
0344   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
0345 
0346   Transform3 transform2 = Transform3::Identity();
0347   transform2.translate(Vector3{0_mm, 0_mm, pZ2});
0348   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
0349 
0350   Transform3 transform3 = Transform3::Identity();
0351   transform3.translate(Vector3{0_mm, 0_mm, pZ3});
0352   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
0353 
0354   std::vector<Volume*> volumes = {vol2.get(), vol1.get(), vol3.get()};
0355 
0356   CylinderVolumeStack cylStack(gctx, volumes, AxisDirection::AxisZ,
0357                                VolumeAttachmentStrategy::Gap,
0358                                VolumeResizeStrategy::Gap, *logger);
0359   BOOST_CHECK_EQUAL(volumes.size(), 5);
0360 
0361   auto stackBounds =
0362       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0363   BOOST_REQUIRE(stackBounds != nullptr);
0364 
0365   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
0366   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMaxR), 600_mm);
0367   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0368                     (std::abs(pZ1 - hlZ1) + pZ3 + hlZ3) / 2.0);
0369 
0370   double midZ = (pZ1 - hlZ1 + pZ3 + hlZ3) / 2.0;
0371   Transform3 expectedTransform{Translation3{0_mm, 0_mm, midZ}};
0372   CHECK_CLOSE_OR_SMALL(cylStack.localToGlobalTransform(gctx).matrix(),
0373                        expectedTransform.matrix(), 1e-10, 1e-14);
0374 }
0375 
0376 BOOST_DATA_TEST_CASE(RotationInZ, boost::unit_test::data::make(strategies),
0377                      strategy) {
0378   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
0379   double hlZ = 400_mm;
0380   double gap = 100_mm;
0381   double shift = 300_mm;
0382 
0383   auto bounds1 = std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, hlZ);
0384   auto bounds2 = std::make_shared<CylinderVolumeBounds>(200_mm, 300_mm, hlZ);
0385 
0386   auto vol1 = std::make_shared<Volume>(
0387       Transform3::Identity() *
0388           Translation3{0_mm, 0_mm, -hlZ - gap / 2.0 + shift},
0389       bounds1);
0390 
0391   auto vol2 = std::make_shared<Volume>(
0392       Transform3::Identity() *
0393           Translation3{0_mm, 0_mm, hlZ + gap / 2.0 + shift} *
0394           AngleAxis3{30_degree, Vector3::UnitZ()},
0395       bounds2);
0396 
0397   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
0398 
0399   CylinderVolumeStack cylStack(gctx, volumes, AxisDirection::AxisZ, strategy,
0400                                VolumeResizeStrategy::Gap, *logger);
0401 
0402   auto stackBounds =
0403       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0404   BOOST_REQUIRE(stackBounds != nullptr);
0405   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
0406   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMaxR), 400_mm);
0407   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0408                     2 * hlZ + gap / 2.0);
0409 
0410   auto newBounds1 =
0411       dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
0412   auto newBounds2 =
0413       dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
0414 
0415   for (const auto& bounds : {newBounds1, newBounds2}) {
0416     BOOST_CHECK_EQUAL(bounds->get(CylinderVolumeBounds::eMinR), 100_mm);
0417     BOOST_CHECK_EQUAL(bounds->get(CylinderVolumeBounds::eMaxR), 400_mm);
0418   }
0419 
0420   if (strategy == VolumeAttachmentStrategy::Gap) {
0421     // Volumes stayed at the same position, not resized
0422     BOOST_CHECK_EQUAL(vol1->center(gctx)[eZ], -hlZ - gap / 2.0 + shift);
0423     BOOST_CHECK_EQUAL(vol2->center(gctx)[eZ], hlZ + gap / 2.0 + shift);
0424     BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
0425     BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
0426   } else if (strategy == VolumeAttachmentStrategy::First) {
0427     // Left volume moved, got resized
0428     BOOST_CHECK_EQUAL(vol1->center(gctx)[eZ], -hlZ + shift);
0429     BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0430                       hlZ + gap / 2.0);
0431     // Right volume stayed the same
0432     BOOST_CHECK_EQUAL(vol2->center(gctx)[eZ], hlZ + gap / 2.0 + shift);
0433     BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
0434   } else if (strategy == VolumeAttachmentStrategy::Second) {
0435     // Left volume stayed the same
0436     BOOST_CHECK_EQUAL(vol1->center(gctx)[eZ], -hlZ - gap / 2.0 + shift);
0437     BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
0438     // Right volume moved, got resized
0439     BOOST_CHECK_EQUAL(vol2->center(gctx)[eZ], hlZ + shift);
0440     BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0441                       hlZ + gap / 2.0);
0442   } else if (strategy == VolumeAttachmentStrategy::Midpoint) {
0443     // Left volume moved, got resized
0444     BOOST_CHECK_EQUAL(vol1->center(gctx)[eZ], -hlZ - gap / 4.0 + shift);
0445     BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0446                       hlZ + gap / 4.0);
0447 
0448     // Right volume moved, got resized
0449     BOOST_CHECK_EQUAL(vol2->center(gctx)[eZ], hlZ + gap / 4.0 + shift);
0450     BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0451                       hlZ + gap / 4.0);
0452   }
0453 }
0454 
0455 BOOST_DATA_TEST_CASE(UpdateStack,
0456                      (boost::unit_test::data::xrange(-135, 180, 45) *
0457                       boost::unit_test::data::make(Vector3{0_mm, 0_mm, 0_mm},
0458                                                    Vector3{20_mm, 0_mm, 0_mm},
0459                                                    Vector3{0_mm, 20_mm, 0_mm},
0460                                                    Vector3{20_mm, 20_mm, 0_mm},
0461                                                    Vector3{0_mm, 0_mm, 20_mm}) *
0462                       boost::unit_test::data::make(-100_mm, 0_mm, 100_mm) *
0463                       boost::unit_test::data::make(resizeStrategies)),
0464                      angle, offset, zshift, strategy) {
0465   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
0466   double hlZ = 400_mm;
0467 
0468   // Cylinder volumes which already line up, but have different1 radii
0469   auto bounds1 = std::make_shared<CylinderVolumeBounds>(100_mm, 600_mm, hlZ);
0470   auto bounds2 = std::make_shared<CylinderVolumeBounds>(100_mm, 600_mm, hlZ);
0471   auto bounds3 = std::make_shared<CylinderVolumeBounds>(100_mm, 600_mm, hlZ);
0472 
0473   Transform3 base = AngleAxis3(angle * 1_degree, Vector3::UnitX()) *
0474                     Translation3(offset + Vector3{0_mm, 0_mm, zshift});
0475 
0476   Transform3 transform1 = base;
0477   transform1.translate(Vector3{0_mm, 0_mm, -2 * hlZ});
0478   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
0479 
0480   Transform3 transform2 = base;
0481   transform2.translate(Vector3{0_mm, 0_mm, 0_mm});
0482   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
0483 
0484   Transform3 transform3 = base;
0485   transform3.translate(Vector3{0_mm, 0_mm, 2 * hlZ});
0486   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
0487 
0488   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
0489   std::vector<Volume*> originalVolumes = volumes;
0490 
0491   std::vector<Transform3> originalTransforms = {transform1, transform2,
0492                                                 transform3};
0493 
0494   CylinderVolumeStack cylStack(
0495       gctx, volumes, AxisDirection::AxisZ,
0496       VolumeAttachmentStrategy::Gap,  // should not make a
0497                                       // difference
0498       strategy, *logger);
0499 
0500   const auto* originalBounds =
0501       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0502 
0503   auto assertOriginalBounds = [&]() {
0504     const auto* cylBounds =
0505         dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0506     BOOST_REQUIRE(cylBounds != nullptr);
0507     BOOST_CHECK_EQUAL(cylBounds, originalBounds);
0508     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
0509     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 600_mm);
0510     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0511                       3 * hlZ);
0512   };
0513 
0514   assertOriginalBounds();
0515 
0516   {
0517     // Assign a copy of the identical bounds gives identical bounds
0518     auto bounds = std::make_shared<CylinderVolumeBounds>(
0519         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0520     cylStack.update(gctx, bounds, std::nullopt, *logger);
0521     assertOriginalBounds();
0522   }
0523 
0524   {
0525     // Cannot increase mininmum r
0526     auto bounds = std::make_shared<CylinderVolumeBounds>(
0527         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0528     bounds->set(CylinderVolumeBounds::eMinR, 200_mm);
0529     BOOST_CHECK_THROW(cylStack.update(gctx, bounds, std::nullopt, *logger),
0530                       std::invalid_argument);
0531     assertOriginalBounds();
0532   }
0533 
0534   {
0535     // Cannot decrease maximum r
0536     auto bounds = std::make_shared<CylinderVolumeBounds>(
0537         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0538     bounds->set(CylinderVolumeBounds::eMaxR, 500_mm);
0539     BOOST_CHECK_THROW(cylStack.update(gctx, bounds, std::nullopt, *logger),
0540                       std::invalid_argument);
0541     assertOriginalBounds();
0542   }
0543 
0544   {
0545     // Cannot decrease half length z
0546     auto bounds = std::make_shared<CylinderVolumeBounds>(
0547         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0548     bounds->set(CylinderVolumeBounds::eHalfLengthZ, 2 * hlZ);
0549     BOOST_CHECK_THROW(cylStack.update(gctx, bounds, std::nullopt, *logger),
0550                       std::invalid_argument);
0551     assertOriginalBounds();
0552   }
0553 
0554   {
0555     // Reduce minimum r
0556     auto bounds = std::make_shared<CylinderVolumeBounds>(
0557         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0558     bounds->set(CylinderVolumeBounds::eMinR, 50_mm);
0559     cylStack.update(gctx, bounds, std::nullopt, *logger);
0560     const auto* cylBounds =
0561         dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0562     BOOST_REQUIRE(cylBounds != nullptr);
0563     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
0564     // Rest unchanged
0565     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 600_mm);
0566     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0567                       3 * hlZ);
0568 
0569     // No gap volumes were added
0570     BOOST_CHECK_EQUAL(volumes.size(), 3);
0571 
0572     // All volumes reduces min r to accommodate
0573     for (const auto& [volume, origTransform] :
0574          zip(volumes, originalTransforms)) {
0575       const auto* newBounds =
0576           dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
0577       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
0578       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMaxR), 600_mm);
0579       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0580                         hlZ);
0581 
0582       // Position stayed the same
0583       BOOST_CHECK_EQUAL(volume->localToGlobalTransform(gctx).matrix(),
0584                         origTransform.matrix());
0585     }
0586   }
0587 
0588   {
0589     // Increase maximum r
0590     auto bounds = std::make_shared<CylinderVolumeBounds>(
0591         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0592     bounds->set(CylinderVolumeBounds::eMaxR, 700_mm);
0593     cylStack.update(gctx, bounds, std::nullopt, *logger);
0594     const auto* cylBounds =
0595         dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0596     BOOST_REQUIRE(cylBounds != nullptr);
0597     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 700_mm);
0598     // Rest as before
0599     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
0600     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0601                       3 * hlZ);
0602 
0603     // No gap volumes were added
0604     BOOST_CHECK_EQUAL(volumes.size(), 3);
0605 
0606     // All volumes reduces min r to accommodate
0607     for (const auto& [volume, origTransform] :
0608          zip(volumes, originalTransforms)) {
0609       const auto* newBounds =
0610           dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
0611       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
0612       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMaxR), 700_mm);
0613       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0614                         hlZ);
0615 
0616       // Position stayed the same
0617       BOOST_CHECK_EQUAL(volume->localToGlobalTransform(gctx).matrix(),
0618                         origTransform.matrix());
0619     }
0620   }
0621 
0622   {
0623     // Increase half length z
0624     auto bounds = std::make_shared<CylinderVolumeBounds>(
0625         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0626     bounds->set(CylinderVolumeBounds::eHalfLengthZ, 4 * hlZ);
0627     cylStack.update(gctx, bounds, std::nullopt, *logger);
0628     const auto* cylBounds =
0629         dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0630     BOOST_REQUIRE(cylBounds != nullptr);
0631     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0632                       4 * hlZ);
0633 
0634     // Rest as before
0635     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
0636     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 700_mm);
0637 
0638     if (strategy == VolumeResizeStrategy::Expand) {
0639       // No gap volumes were added
0640       BOOST_CHECK_EQUAL(volumes.size(), 3);
0641 
0642       // Volume 1 got bigger and shifted left
0643       auto newBounds1 =
0644           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
0645       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0646                         hlZ + hlZ / 2.0);
0647       Transform3 expectedTransform1 =
0648           base * Translation3{0_mm, 0_mm, -2 * hlZ - hlZ / 2.0};
0649       BOOST_CHECK_EQUAL(vol1->localToGlobalTransform(gctx).matrix(),
0650                         expectedTransform1.matrix());
0651 
0652       // Volume 2 stayed the same
0653       auto newBounds2 =
0654           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
0655       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0656                         hlZ);
0657       BOOST_CHECK_EQUAL(vol2->localToGlobalTransform(gctx).matrix(),
0658                         transform2.matrix());
0659 
0660       // Volume 3 got bigger and shifted right
0661       auto newBounds3 =
0662           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
0663       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eHalfLengthZ),
0664                         hlZ + hlZ / 2.0);
0665       Transform3 expectedTransform3 =
0666           base * Translation3{0_mm, 0_mm, 2 * hlZ + hlZ / 2.0};
0667       BOOST_CHECK_EQUAL(vol3->localToGlobalTransform(gctx).matrix(),
0668                         expectedTransform3.matrix());
0669     } else if (strategy == VolumeResizeStrategy::Gap) {
0670       // Gap volumes were added
0671       BOOST_CHECK_EQUAL(volumes.size(), 5);
0672 
0673       for (const auto& [volume, origTransform] :
0674            zip(originalVolumes, originalTransforms)) {
0675         const auto* newBounds =
0676             dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
0677         BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
0678         BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMaxR), 700_mm);
0679         BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0680                           hlZ);
0681         // Position stayed the same
0682         BOOST_CHECK_EQUAL(volume->localToGlobalTransform(gctx).matrix(),
0683                           origTransform.matrix());
0684       }
0685 
0686       auto gap1 = volumes.front();
0687       auto gap2 = volumes.back();
0688 
0689       const auto* gapBounds1 =
0690           dynamic_cast<const CylinderVolumeBounds*>(&gap1->volumeBounds());
0691       const auto* gapBounds2 =
0692           dynamic_cast<const CylinderVolumeBounds*>(&gap2->volumeBounds());
0693 
0694       BOOST_CHECK_EQUAL(gapBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0695                         hlZ / 2.0);
0696       BOOST_CHECK_EQUAL(gapBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0697                         hlZ / 2.0);
0698 
0699       Transform3 gap1Transform =
0700           base * Translation3{0_mm, 0_mm, -3 * hlZ - hlZ / 2.0};
0701       Transform3 gap2Transform =
0702           base * Translation3{0_mm, 0_mm, 3 * hlZ + hlZ / 2.0};
0703 
0704       CHECK_CLOSE_OR_SMALL(gap1->localToGlobalTransform(gctx).matrix(),
0705                            gap1Transform.matrix(), 1e-10, 1e-14);
0706       CHECK_CLOSE_OR_SMALL(gap2->localToGlobalTransform(gctx).matrix(),
0707                            gap2Transform.matrix(), 1e-10, 1e-14);
0708     }
0709   }
0710 }
0711 
0712 BOOST_DATA_TEST_CASE(
0713     UpdateStackOneSided,
0714     (boost::unit_test::data::make(-1.0, 1.0) ^
0715      boost::unit_test::data::make(VolumeResizeStrategy::Gap,
0716                                   VolumeResizeStrategy::Expand)),
0717     f, strategy) {
0718   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
0719 
0720   auto trf = Transform3::Identity();
0721 
0722   auto trf1 = trf * Translation3{Vector3{0_mm, 0_mm, -500_mm}};
0723   auto vol1 = std::make_shared<Volume>(
0724       trf1, std::make_shared<CylinderVolumeBounds>(100_mm, 300_mm, 400_mm));
0725 
0726   auto trf2 = trf * Translation3{Vector3{0_mm, 0_mm, 500_mm}};
0727   auto vol2 = std::make_shared<Volume>(
0728       trf2, std::make_shared<CylinderVolumeBounds>(100_mm, 300_mm, 400_mm));
0729 
0730   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
0731 
0732   CylinderVolumeStack cylStack{
0733       gctx,     volumes, AxisDirection::AxisZ, VolumeAttachmentStrategy::Gap,
0734       strategy, *logger};
0735   const auto* originalBounds =
0736       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0737 
0738   // Increase halflength by 50mm
0739   auto newBounds = std::make_shared<CylinderVolumeBounds>(
0740       dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0741   newBounds->set(CylinderVolumeBounds::eHalfLengthZ, 950_mm);
0742   // Shift to +z by 50mm
0743   trf *= Translation3{Vector3{0_mm, 0_mm, f * 50_mm}};
0744   // -> left edge should stay at -400mm, right edge should be at 500mm or the
0745   // other direction
0746 
0747   auto checkUnchanged = [&]() {
0748     const auto* cylBounds =
0749         dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0750     BOOST_REQUIRE(cylBounds != nullptr);
0751     BOOST_CHECK_EQUAL(*cylBounds, *originalBounds);
0752   };
0753 
0754   // Invalid: shift too far in z
0755   BOOST_CHECK_THROW(
0756       cylStack.update(gctx, newBounds,
0757                       trf * Translation3{Vector3{0, 0, f * 20_mm}}, *logger),
0758       std::invalid_argument);
0759   checkUnchanged();
0760 
0761   // Invalid: shift in x
0762   BOOST_CHECK_THROW(
0763       cylStack.update(gctx, newBounds, trf * Translation3{Vector3{10_mm, 0, 0}},
0764                       *logger),
0765       std::invalid_argument);
0766   checkUnchanged();
0767 
0768   // Invalid: shift in y
0769   BOOST_CHECK_THROW(
0770       cylStack.update(gctx, newBounds, trf * Translation3{Vector3{0, 10_mm, 0}},
0771                       *logger),
0772       std::invalid_argument);
0773   checkUnchanged();
0774 
0775   // Invalid: rotation
0776   BOOST_CHECK_THROW(
0777       cylStack.update(gctx, newBounds,
0778                       trf * AngleAxis3{10_degree, Vector3::UnitY()}, *logger),
0779       std::invalid_argument);
0780   checkUnchanged();
0781 
0782   cylStack.update(gctx, newBounds, trf, *logger);
0783 
0784   BOOST_CHECK_EQUAL(cylStack.localToGlobalTransform(gctx).matrix(),
0785                     trf.matrix());
0786   const auto* cylBounds =
0787       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0788   BOOST_REQUIRE(cylBounds != nullptr);
0789   BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ), 950_mm);
0790 
0791   // All volumes including gaps should have same r size
0792   for (const auto* vol : volumes) {
0793     const auto* volBounds =
0794         dynamic_cast<const CylinderVolumeBounds*>(&vol->volumeBounds());
0795     BOOST_REQUIRE(volBounds != nullptr);
0796     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
0797     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eMaxR), 300_mm);
0798   }
0799 
0800   if (strategy == VolumeResizeStrategy::Expand) {
0801     // No gaps were added, there was one gap initially
0802     BOOST_CHECK_EQUAL(volumes.size(), 3);
0803     const Volume* vol = nullptr;
0804     if (f < 0.0) {
0805       // first volume should have gotten bigger
0806       vol = volumes.front();
0807     } else {
0808       // last volume should have gotten bigger
0809       vol = volumes.back();
0810     }
0811 
0812     const auto* volBounds =
0813         dynamic_cast<const CylinderVolumeBounds*>(&vol->volumeBounds());
0814     BOOST_REQUIRE(volBounds != nullptr);
0815     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0816                       450_mm);
0817     BOOST_CHECK_EQUAL(vol->center(gctx)[eZ], f * 550_mm);
0818   } else if (strategy == VolumeResizeStrategy::Gap) {
0819     // One gap volume was added
0820     BOOST_CHECK_EQUAL(volumes.size(), 4);
0821 
0822     const Volume* gap = nullptr;
0823     if (f < 0.0) {
0824       gap = volumes.front();
0825     } else {
0826       gap = volumes.back();
0827     }
0828     const auto* gapBounds =
0829         dynamic_cast<const CylinderVolumeBounds*>(&gap->volumeBounds());
0830     BOOST_REQUIRE(gapBounds != nullptr);
0831 
0832     BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0833                       50_mm);
0834     BOOST_CHECK_EQUAL(gap->center(gctx)[eZ], f * 950_mm);
0835   }
0836 }
0837 
0838 BOOST_AUTO_TEST_CASE(ResizeReproduction1) {
0839   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
0840   Transform3 trf1 =
0841       Transform3::Identity() * Translation3{Vector3::UnitZ() * -2000};
0842   auto bounds1 = std::make_shared<CylinderVolumeBounds>(70, 100, 100.0);
0843   Volume vol1{trf1, bounds1};
0844 
0845   std::vector<Volume*> volumes = {&vol1};
0846   CylinderVolumeStack stack(gctx, volumes, AxisDirection::AxisZ,
0847                             VolumeAttachmentStrategy::Gap,
0848                             VolumeResizeStrategy::Gap, *logger);
0849 
0850   Transform3 trf2 =
0851       Transform3::Identity() * Translation3{Vector3::UnitZ() * -1500};
0852   stack.update(gctx, std::make_shared<CylinderVolumeBounds>(30.0, 100, 600),
0853                trf2, *logger);
0854 
0855   std::cout << stack.volumeBounds() << std::endl;
0856   std::cout << stack.localToGlobalTransform(gctx).matrix() << std::endl;
0857 
0858   Transform3 trf3 =
0859       Transform3::Identity() * Translation3{Vector3::UnitZ() * -1600};
0860   stack.update(gctx, std::make_shared<CylinderVolumeBounds>(30.0, 100, 700),
0861                trf3, *logger);
0862 }
0863 
0864 BOOST_AUTO_TEST_CASE(ResizeReproduction2) {
0865   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
0866   // The numbers are tuned a bit to reproduce the faulty behavior
0867   Transform3 trf1 =
0868       Transform3::Identity() * Translation3{Vector3::UnitZ() * 263};
0869   auto bounds1 = std::make_shared<CylinderVolumeBounds>(30, 100, 4.075);
0870   Volume vol1{trf1, bounds1};
0871 
0872   std::vector<Volume*> volumes = {&vol1};
0873   CylinderVolumeStack stack(gctx, volumes, AxisDirection::AxisZ,
0874                             VolumeAttachmentStrategy::Gap,
0875                             VolumeResizeStrategy::Gap, *logger);
0876 
0877   Transform3 trf2 =
0878       Transform3::Identity() * Translation3{Vector3::UnitZ() * 260.843};
0879   stack.update(gctx, std::make_shared<CylinderVolumeBounds>(30.0, 100, 6.232),
0880                trf2, *logger);
0881 
0882   std::cout << stack.volumeBounds() << std::endl;
0883   std::cout << stack.localToGlobalTransform(gctx).matrix() << std::endl;
0884 
0885   Transform3 trf3 =
0886       Transform3::Identity() * Translation3{Vector3::UnitZ() * 1627.31};
0887   stack.update(gctx,
0888                std::make_shared<CylinderVolumeBounds>(30.0, 100, 1372.699),
0889                trf3, *logger);
0890 }
0891 
0892 //   original size
0893 // <--------------->
0894 // +---------------+
0895 // |               |
0896 // |               |
0897 // |   Volume 1    |
0898 // |               |
0899 // |               |
0900 // +---------------+
0901 //         first resize
0902 // <-------------------------->
0903 // +---------------+----------+
0904 // |               |          |
0905 // |               |          |
0906 // |   Volume 1    |   Gap    |
0907 // |               |          |      Gap is
0908 // |               |          |      reused!--+
0909 // +---------------+----------+               |
0910 //             second resize                  |
0911 // <----------------------------------->      |
0912 // +---------------+-------------------+      |
0913 // |               |                   |      |
0914 // |               |                   |      |
0915 // |   Volume 1    |        Gap        |<-----+
0916 // |               |                   |
0917 // |               |                   |
0918 // +---------------+-------------------+
0919 //
0920 BOOST_AUTO_TEST_CASE(ResizeGapMultiple) {
0921   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
0922   Transform3 trf = Transform3::Identity();
0923   auto bounds = std::make_shared<CylinderVolumeBounds>(70, 100, 100.0);
0924   Volume vol{trf, bounds};
0925 
0926   BOOST_TEST_CONTEXT("Positive") {
0927     std::vector<Volume*> volumes = {&vol};
0928     CylinderVolumeStack stack(gctx, volumes, AxisDirection::AxisZ,
0929                               VolumeAttachmentStrategy::Gap,
0930                               VolumeResizeStrategy::Gap, *logger);
0931 
0932     BOOST_CHECK_EQUAL(volumes.size(), 1);
0933     BOOST_CHECK(stack.gaps().empty());
0934 
0935     stack.update(gctx, std::make_shared<CylinderVolumeBounds>(30.0, 100, 200),
0936                  trf * Translation3{Vector3::UnitZ() * 100}, *logger);
0937     BOOST_CHECK_EQUAL(volumes.size(), 2);
0938     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
0939 
0940     BOOST_CHECK_EQUAL(stack.gaps().front()->center(gctx)[eZ], 200.0);
0941     const auto* cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
0942         &stack.gaps().front()->volumeBounds());
0943     BOOST_REQUIRE_NE(cylBounds, nullptr);
0944     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0945                       100.0);
0946 
0947     stack.update(gctx, std::make_shared<CylinderVolumeBounds>(30.0, 100, 300),
0948                  trf * Translation3{Vector3::UnitZ() * 200}, *logger);
0949 
0950     BOOST_CHECK_EQUAL(volumes.size(), 2);
0951     // No additional gap volume was added!
0952     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
0953 
0954     BOOST_CHECK_EQUAL(stack.gaps().front()->center(gctx)[eZ], 300.0);
0955     cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
0956         &stack.gaps().front()->volumeBounds());
0957     BOOST_REQUIRE_NE(cylBounds, nullptr);
0958     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0959                       200.0);
0960   }
0961 
0962   BOOST_TEST_CONTEXT("Negative") {
0963     std::vector<Volume*> volumes = {&vol};
0964     CylinderVolumeStack stack(gctx, volumes, AxisDirection::AxisZ,
0965                               VolumeAttachmentStrategy::Gap,
0966                               VolumeResizeStrategy::Gap, *logger);
0967 
0968     BOOST_CHECK_EQUAL(volumes.size(), 1);
0969     BOOST_CHECK(stack.gaps().empty());
0970 
0971     stack.update(gctx, std::make_shared<CylinderVolumeBounds>(30.0, 100, 200),
0972                  trf * Translation3{Vector3::UnitZ() * -100}, *logger);
0973     BOOST_CHECK_EQUAL(volumes.size(), 2);
0974     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
0975 
0976     BOOST_CHECK_EQUAL(stack.gaps().front()->center(gctx)[eZ], -200.0);
0977     const auto* cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
0978         &stack.gaps().front()->volumeBounds());
0979     BOOST_REQUIRE_NE(cylBounds, nullptr);
0980     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0981                       100.0);
0982 
0983     stack.update(gctx, std::make_shared<CylinderVolumeBounds>(30.0, 100, 300),
0984                  trf * Translation3{Vector3::UnitZ() * -200}, *logger);
0985 
0986     BOOST_CHECK_EQUAL(volumes.size(), 2);
0987     // No additional gap volume was added!
0988     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
0989 
0990     BOOST_CHECK_EQUAL(stack.gaps().front()->center(gctx)[eZ], -300.0);
0991     cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
0992         &stack.gaps().front()->volumeBounds());
0993     BOOST_REQUIRE_NE(cylBounds, nullptr);
0994     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0995                       200.0);
0996   }
0997 }
0998 
0999 BOOST_AUTO_TEST_SUITE_END()
1000 
1001 BOOST_AUTO_TEST_SUITE(RDirection)
1002 
1003 BOOST_DATA_TEST_CASE(Baseline,
1004                      (boost::unit_test::data::xrange(-135, 180, 45) *
1005                       boost::unit_test::data::xrange(0, 2, 1) *
1006                       boost::unit_test::data::make(-0.1, 0.0, 0.1) *
1007                       boost::unit_test::data::make(Vector3{0_mm, 0_mm, 0_mm},
1008                                                    Vector3{20_mm, 0_mm, 0_mm},
1009                                                    Vector3{0_mm, 20_mm, 0_mm},
1010                                                    Vector3{20_mm, 20_mm, 0_mm},
1011                                                    Vector3{0_mm, 0_mm, 20_mm}) *
1012                       boost::unit_test::data::make(strategies)),
1013                      angle, rotate, f, offset, strategy) {
1014   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
1015   double hlZ = 400_mm;
1016 
1017   double fInner = 1.0 + f;
1018   double fOuter = 1.0 - f;
1019 
1020   // Cylinder volumes which already line up in r but have different z and hl
1021   auto bounds1 = std::make_shared<CylinderVolumeBounds>(fInner * 100_mm,
1022                                                         fOuter * 300_mm, hlZ);
1023   auto bounds2 = std::make_shared<CylinderVolumeBounds>(fInner * 300_mm,
1024                                                         fOuter * 600_mm, hlZ);
1025   auto bounds3 = std::make_shared<CylinderVolumeBounds>(fInner * 600_mm,
1026                                                         fOuter * 900_mm, hlZ);
1027 
1028   Transform3 base =
1029       AngleAxis3(angle * 1_degree, Vector3::UnitX()) * Translation3(offset);
1030 
1031   // volumes are shifted in z
1032 
1033   Transform3 transform1 = base;
1034   transform1.translate(Vector3{0_mm, 0_mm, 20_mm});
1035   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
1036 
1037   Transform3 transform2 = base;
1038   transform2.translate(Vector3{0_mm, 0_mm, -30_mm});
1039   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
1040 
1041   Transform3 transform3 = base;
1042   transform3.translate(Vector3{0_mm, 0_mm, 40_mm});
1043   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
1044 
1045   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
1046   // Rotate to simulate unsorted volumes: all results should be the same!
1047   std::rotate(volumes.begin(), volumes.begin() + rotate, volumes.end());
1048 
1049   std::vector<Volume*> origVolumes = volumes;
1050 
1051   std::vector<CylinderVolumeBounds> originalBounds;
1052   std::transform(
1053       volumes.begin(), volumes.end(), std::back_inserter(originalBounds),
1054       [](const auto& vol) {
1055         return dynamic_cast<const CylinderVolumeBounds&>(vol->volumeBounds());
1056       });
1057 
1058   if (f < 0.0) {
1059     BOOST_CHECK_THROW(
1060         CylinderVolumeStack(gctx, volumes, AxisDirection::AxisR, strategy,
1061                             VolumeResizeStrategy::Gap, *logger),
1062         std::invalid_argument);
1063     return;
1064   }
1065 
1066   CylinderVolumeStack cylStack(gctx, volumes, AxisDirection::AxisR, strategy,
1067                                VolumeResizeStrategy::Gap, *logger);
1068 
1069   auto stackBounds =
1070       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
1071   BOOST_REQUIRE(stackBounds != nullptr);
1072 
1073   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMinR),
1074                     fInner * 100_mm);
1075   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMaxR),
1076                     fOuter * 900_mm);
1077   double expectedHalfLengthZ = (40_mm + 30_mm + 2 * hlZ) / 2.0;
1078   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1079                     expectedHalfLengthZ);
1080 
1081   // After synchronization, all volumes should have the same z position and half
1082   // length
1083   // This includes possible gap volumes!
1084   Transform3 commonTransform = base * Translation3{0_mm, 0_mm, 5_mm};
1085 
1086   CHECK_CLOSE_OR_SMALL(cylStack.localToGlobalTransform(gctx).matrix(),
1087                        commonTransform.matrix(), 1e-10, 1e-14);
1088 
1089   for (const auto& volume : volumes) {
1090     const auto* cylinderBounds =
1091         dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
1092     BOOST_REQUIRE(cylinderBounds != nullptr);
1093     BOOST_CHECK_EQUAL(cylinderBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1094                       expectedHalfLengthZ);
1095   }
1096 
1097   BOOST_CHECK_EQUAL(
1098       dynamic_cast<const CylinderVolumeBounds&>(vol1->volumeBounds())
1099           .get(CylinderVolumeBounds::eMinR),
1100       fInner * 100_mm);
1101 
1102   BOOST_CHECK_EQUAL(
1103       dynamic_cast<const CylinderVolumeBounds&>(vol3->volumeBounds())
1104           .get(CylinderVolumeBounds::eMaxR),
1105       fOuter * 900_mm);
1106 
1107   // Volumes are sorted in r
1108   for (std::size_t i = 0; i < volumes.size() - 1; ++i) {
1109     const auto& a = volumes.at(i);
1110     const auto& b = volumes.at(i + 1);
1111 
1112     const auto* aBounds =
1113         dynamic_cast<const CylinderVolumeBounds*>(&a->volumeBounds());
1114     const auto* bBounds =
1115         dynamic_cast<const CylinderVolumeBounds*>(&b->volumeBounds());
1116 
1117     double aMidR = (aBounds->get(CylinderVolumeBounds::eMinR) +
1118                     aBounds->get(CylinderVolumeBounds::eMaxR)) /
1119                    2.0;
1120 
1121     double bMidR = (bBounds->get(CylinderVolumeBounds::eMinR) +
1122                     bBounds->get(CylinderVolumeBounds::eMaxR)) /
1123                    2.0;
1124 
1125     BOOST_CHECK_LT(aMidR, bMidR);
1126   }
1127 
1128   if (f == 0.0) {
1129     // No gap volumes were added
1130     BOOST_CHECK_EQUAL(volumes.size(), 3);
1131 
1132     // Original volumes did not change r bounds
1133     for (const auto& [volume, origCylBounds] :
1134          zip(origVolumes, originalBounds)) {
1135       const auto* newBounds =
1136           dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
1137       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMinR),
1138                         origCylBounds.get(CylinderVolumeBounds::eMinR));
1139       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMaxR),
1140                         origCylBounds.get(CylinderVolumeBounds::eMaxR));
1141     }
1142   } else {
1143     const auto* newBounds1 =
1144         dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
1145     const auto* newBounds2 =
1146         dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
1147     const auto* newBounds3 =
1148         dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
1149     if (strategy == VolumeAttachmentStrategy::Gap) {
1150       // Two gap volumes were added
1151       BOOST_CHECK_EQUAL(volumes.size(), 5);
1152 
1153       // Original volumes did not change r bounds
1154       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMinR),
1155                         fInner * 100_mm);
1156       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMaxR),
1157                         fOuter * 300_mm);
1158       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMinR),
1159                         fInner * 300_mm);
1160       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMaxR),
1161                         fOuter * 600_mm);
1162       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMinR),
1163                         fInner * 600_mm);
1164       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMaxR),
1165                         fOuter * 900_mm);
1166 
1167       auto gap1 = volumes.at(1);
1168       auto gap2 = volumes.at(3);
1169 
1170       const auto* gapBounds1 =
1171           dynamic_cast<const CylinderVolumeBounds*>(&gap1->volumeBounds());
1172       const auto* gapBounds2 =
1173           dynamic_cast<const CylinderVolumeBounds*>(&gap2->volumeBounds());
1174 
1175       BOOST_CHECK_EQUAL(gapBounds1->get(CylinderVolumeBounds::eMinR),
1176                         fOuter * 300_mm);
1177       BOOST_CHECK_EQUAL(gapBounds1->get(CylinderVolumeBounds::eMaxR),
1178                         fInner * 300_mm);
1179       BOOST_CHECK_EQUAL(gapBounds2->get(CylinderVolumeBounds::eMinR),
1180                         fOuter * 600_mm);
1181       BOOST_CHECK_EQUAL(gapBounds2->get(CylinderVolumeBounds::eMaxR),
1182                         fInner * 600_mm);
1183 
1184     } else if (strategy == VolumeAttachmentStrategy::First) {
1185       // No gap volumes were added
1186       BOOST_CHECK_EQUAL(volumes.size(), 3);
1187 
1188       // Volume 1 got bigger and grew to meet Volume 2
1189       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMinR),
1190                         fInner * 100_mm);
1191       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMaxR),
1192                         fInner * 300_mm);
1193 
1194       // Volume 2 got bigger and grew to meet Volume 3
1195       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMinR),
1196                         fInner * 300_mm);
1197       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMaxR),
1198                         fInner * 600_mm);
1199 
1200       // Volume 3 stayed the same
1201       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMinR),
1202                         fInner * 600_mm);
1203       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMaxR),
1204                         fOuter * 900_mm);
1205 
1206     } else if (strategy == VolumeAttachmentStrategy::Second) {
1207       // No gap volumes were added
1208       BOOST_CHECK_EQUAL(volumes.size(), 3);
1209 
1210       // Volume 1 stayed the same
1211       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMinR),
1212                         fInner * 100_mm);
1213       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMaxR),
1214                         fOuter * 300_mm);
1215 
1216       // Volume 2 got bigger and grew inward to meet Volume 1
1217       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMinR),
1218                         fOuter * 300_mm);
1219       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMaxR),
1220                         fOuter * 600_mm);
1221 
1222       // Volume 3 got bigger and grew inward to meet Volume 2
1223       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMinR),
1224                         fOuter * 600_mm);
1225       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMaxR),
1226                         fOuter * 900_mm);
1227     } else if (strategy == VolumeAttachmentStrategy::Midpoint) {
1228       // No gap volumes were added
1229       BOOST_CHECK_EQUAL(volumes.size(), 3);
1230 
1231       // Volume 1 grew outward to meet Volume 2 half way
1232       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMinR),
1233                         fInner * 100_mm);
1234       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMaxR),
1235                         (fOuter * 300_mm + fInner * 300_mm) / 2.0);
1236 
1237       // Volume 2 grew inward and outward to meet Volume 1 and 3 half way
1238       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMinR),
1239                         (fOuter * 300_mm + fInner * 300_mm) / 2.0);
1240       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMaxR),
1241                         (fOuter * 600_mm + fInner * 600_mm) / 2.0);
1242 
1243       // Volume 3 grew inward to meet Volume 2 half way
1244       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMinR),
1245                         (fOuter * 600_mm + fInner * 600_mm) / 2.0);
1246       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMaxR),
1247                         fOuter * 900_mm);
1248     }
1249   }
1250 }
1251 
1252 BOOST_DATA_TEST_CASE(UpdateStack,
1253                      (boost::unit_test::data::xrange(-135, 180, 45) *
1254                       boost::unit_test::data::make(Vector3{0_mm, 0_mm, 0_mm},
1255                                                    Vector3{20_mm, 0_mm, 0_mm},
1256                                                    Vector3{0_mm, 20_mm, 0_mm},
1257                                                    Vector3{20_mm, 20_mm, 0_mm},
1258                                                    Vector3{0_mm, 0_mm, 20_mm}) *
1259                       boost::unit_test::data::make(-100_mm, 0_mm, 100_mm) *
1260                       boost::unit_test::data::make(resizeStrategies)),
1261                      angle, offset, zshift, strategy) {
1262   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
1263   double hlZ = 400_mm;
1264 
1265   // Cylinder volumes which already line up in r but have different z and hl
1266   auto bounds1 = std::make_shared<CylinderVolumeBounds>(100_mm, 300_mm, hlZ);
1267   auto bounds2 = std::make_shared<CylinderVolumeBounds>(300_mm, 600_mm, hlZ);
1268   auto bounds3 = std::make_shared<CylinderVolumeBounds>(600_mm, 900_mm, hlZ);
1269 
1270   Transform3 base = AngleAxis3(angle * 1_degree, Vector3::UnitX()) *
1271                     Translation3(offset + Vector3{0, 0, zshift});
1272 
1273   // volumes are shifted in z
1274   auto vol1 = std::make_shared<Volume>(base, bounds1);
1275   auto vol2 = std::make_shared<Volume>(base, bounds2);
1276   auto vol3 = std::make_shared<Volume>(base, bounds3);
1277 
1278   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
1279   std::vector<Volume*> originalVolumes = volumes;
1280 
1281   std::vector<CylinderVolumeBounds> originalBounds;
1282 
1283   std::transform(
1284       volumes.begin(), volumes.end(), std::back_inserter(originalBounds),
1285       [](const auto& vol) {
1286         return *dynamic_cast<const CylinderVolumeBounds*>(&vol->volumeBounds());
1287       });
1288 
1289   const CylinderVolumeBounds* originalOuterBounds = nullptr;
1290 
1291   std::unique_ptr<CylinderVolumeStack> cylStack;
1292 
1293   auto resetCylStack = [&]() {
1294     volumes = originalVolumes;
1295 
1296     for (const auto& [volume, origBounds] : zip(volumes, originalBounds)) {
1297       volume->assignVolumeBounds(
1298           std::make_shared<CylinderVolumeBounds>(origBounds));
1299     }
1300 
1301     cylStack = std::make_unique<CylinderVolumeStack>(
1302         gctx, volumes, AxisDirection::AxisR,
1303         VolumeAttachmentStrategy::Gap,  // should not make a
1304                                         // difference
1305         strategy, *logger);
1306 
1307     originalOuterBounds =
1308         dynamic_cast<const CylinderVolumeBounds*>(&cylStack->volumeBounds());
1309   };
1310 
1311   resetCylStack();
1312 
1313   auto assertInitialVolumesUnchanged = [&]() {
1314     for (const auto& [volume, origCylBounds] :
1315          zip(originalVolumes, originalBounds)) {
1316       const auto* newBounds =
1317           dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
1318       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMinR),
1319                         origCylBounds.get(CylinderVolumeBounds::eMinR));
1320       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMaxR),
1321                         origCylBounds.get(CylinderVolumeBounds::eMaxR));
1322       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1323                         origCylBounds.get(CylinderVolumeBounds::eHalfLengthZ));
1324       BOOST_CHECK_EQUAL(volume->localToGlobalTransform(gctx).matrix(),
1325                         base.matrix());
1326     }
1327   };
1328 
1329   auto assertOriginalBounds = [&]() {
1330     const auto* cylBounds =
1331         dynamic_cast<const CylinderVolumeBounds*>(&cylStack->volumeBounds());
1332     BOOST_REQUIRE(cylBounds != nullptr);
1333     BOOST_CHECK_EQUAL(cylBounds, originalOuterBounds);
1334     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
1335     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 900_mm);
1336     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
1337   };
1338 
1339   assertOriginalBounds();
1340 
1341   {
1342     // Assign a copy of the identical bounds gives identical bounds
1343     auto bounds = std::make_shared<CylinderVolumeBounds>(
1344         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1345     cylStack->update(gctx, bounds, std::nullopt, *logger);
1346     assertOriginalBounds();
1347   }
1348 
1349   {
1350     // Cannot increase mininmum r
1351     auto bounds = std::make_shared<CylinderVolumeBounds>(
1352         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1353     bounds->set(CylinderVolumeBounds::eMinR, 200_mm);
1354     BOOST_CHECK_THROW(cylStack->update(gctx, bounds, std::nullopt, *logger),
1355                       std::invalid_argument);
1356     assertOriginalBounds();
1357   }
1358 
1359   {
1360     // Cannot decrease maximum r
1361     auto bounds = std::make_shared<CylinderVolumeBounds>(
1362         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1363     bounds->set(CylinderVolumeBounds::eMaxR, 500_mm);
1364     BOOST_CHECK_THROW(cylStack->update(gctx, bounds, std::nullopt, *logger),
1365                       std::invalid_argument);
1366     assertOriginalBounds();
1367   }
1368 
1369   {
1370     // Cannot decrease half length z
1371     auto bounds = std::make_shared<CylinderVolumeBounds>(
1372         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1373     bounds->set(CylinderVolumeBounds::eHalfLengthZ, 0.5 * hlZ);
1374     BOOST_CHECK_THROW(cylStack->update(gctx, bounds, std::nullopt, *logger),
1375                       std::invalid_argument);
1376     assertOriginalBounds();
1377   }
1378 
1379   {
1380     // Reduce minimum r
1381     auto bounds = std::make_shared<CylinderVolumeBounds>(
1382         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1383     bounds->set(CylinderVolumeBounds::eMinR, 50_mm);
1384     cylStack->update(gctx, bounds, std::nullopt, *logger);
1385     const auto* cylBounds =
1386         dynamic_cast<const CylinderVolumeBounds*>(&cylStack->volumeBounds());
1387     BOOST_REQUIRE(cylBounds != nullptr);
1388     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
1389     // Rest unchanged
1390     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 900_mm);
1391     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
1392 
1393     if (strategy == VolumeResizeStrategy::Expand) {
1394       // No gap volumes were added
1395       BOOST_CHECK_EQUAL(volumes.size(), 3);
1396 
1397       // Innermost volume reduced r size
1398       const auto* newBounds1 =
1399           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
1400       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMinR), 50_mm);
1401       // Position stayed the same
1402       BOOST_CHECK_EQUAL(vol1->localToGlobalTransform(gctx).matrix(),
1403                         base.matrix());
1404 
1405       // Other volumes are unchanged
1406       const auto* newBounds2 =
1407           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
1408       BOOST_CHECK_EQUAL(*newBounds2, originalBounds[1]);
1409       BOOST_CHECK_EQUAL(vol2->localToGlobalTransform(gctx).matrix(),
1410                         base.matrix());
1411 
1412       const auto* newBounds3 =
1413           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
1414       BOOST_CHECK_EQUAL(*newBounds3, originalBounds[2]);
1415       BOOST_CHECK_EQUAL(vol3->localToGlobalTransform(gctx).matrix(),
1416                         base.matrix());
1417 
1418     } else if (strategy == VolumeResizeStrategy::Gap) {
1419       // One gap volume was added
1420       BOOST_CHECK_EQUAL(volumes.size(), 4);
1421 
1422       auto gap = volumes.front();
1423       auto gapBounds =
1424           dynamic_cast<const CylinderVolumeBounds*>(&gap->volumeBounds());
1425       BOOST_REQUIRE(gapBounds != nullptr);
1426       BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
1427       BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), 100_mm);
1428       BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1429                         hlZ);
1430       BOOST_CHECK_EQUAL(gap->localToGlobalTransform(gctx).matrix(),
1431                         base.matrix());
1432 
1433       // Other volumes are unchanged
1434       assertInitialVolumesUnchanged();
1435     }
1436   }
1437 
1438   resetCylStack();
1439 
1440   {
1441     // Increase maximum r
1442     auto bounds = std::make_shared<CylinderVolumeBounds>(
1443         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1444     bounds->set(CylinderVolumeBounds::eMaxR, 1000_mm);
1445     cylStack->update(gctx, bounds, std::nullopt, *logger);
1446     const auto* cylBounds =
1447         dynamic_cast<const CylinderVolumeBounds*>(&cylStack->volumeBounds());
1448     BOOST_REQUIRE(cylBounds != nullptr);
1449     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 1000_mm);
1450     // Rest as before
1451     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
1452     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
1453 
1454     if (strategy == VolumeResizeStrategy::Expand) {
1455       // No gap volumes were added
1456       BOOST_CHECK_EQUAL(volumes.size(), 3);
1457 
1458       // Outermost volume increased r size
1459       const auto* newBounds3 =
1460           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
1461       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMaxR), 1000_mm);
1462       // Position stayed the same
1463       BOOST_CHECK_EQUAL(vol3->localToGlobalTransform(gctx).matrix(),
1464                         base.matrix());
1465 
1466       // Other volumes are unchanged
1467       const auto* newBounds1 =
1468           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
1469       BOOST_CHECK_EQUAL(*newBounds1, originalBounds[0]);
1470       BOOST_CHECK_EQUAL(vol1->localToGlobalTransform(gctx).matrix(),
1471                         base.matrix());
1472 
1473       const auto* newBounds2 =
1474           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
1475       BOOST_CHECK_EQUAL(*newBounds2, originalBounds[1]);
1476       BOOST_CHECK_EQUAL(vol2->localToGlobalTransform(gctx).matrix(),
1477                         base.matrix());
1478 
1479     } else if (strategy == VolumeResizeStrategy::Gap) {
1480       // One gap volume was added
1481       BOOST_CHECK_EQUAL(volumes.size(), 4);
1482 
1483       auto gap = volumes.back();
1484       auto gapBounds =
1485           dynamic_cast<const CylinderVolumeBounds*>(&gap->volumeBounds());
1486       BOOST_REQUIRE(gapBounds != nullptr);
1487       BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), 900_mm);
1488       BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), 1000_mm);
1489       BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1490                         hlZ);
1491       BOOST_CHECK_EQUAL(gap->localToGlobalTransform(gctx).matrix(),
1492                         base.matrix());
1493 
1494       // Other volumes are unchanged
1495       assertInitialVolumesUnchanged();
1496     }
1497   }
1498 
1499   resetCylStack();
1500 
1501   {
1502     // Decrease r min and increase r max
1503     auto bounds = std::make_shared<CylinderVolumeBounds>(
1504         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1505     bounds->set({
1506         {CylinderVolumeBounds::eMinR, 0_mm},
1507         {CylinderVolumeBounds::eMaxR, 1100_mm},
1508     });
1509 
1510     cylStack->update(gctx, bounds, std::nullopt, *logger);
1511     const auto* cylBounds =
1512         dynamic_cast<const CylinderVolumeBounds*>(&cylStack->volumeBounds());
1513     BOOST_REQUIRE(cylBounds != nullptr);
1514     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 1100_mm);
1515     // Rest as before
1516     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 0_mm);
1517     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
1518 
1519     if (strategy == VolumeResizeStrategy::Expand) {
1520       // No gap volumes were added
1521       BOOST_CHECK_EQUAL(volumes.size(), 3);
1522 
1523       // Innermost volume reduced r size
1524       const auto* newBounds1 =
1525           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
1526       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMinR), 0_mm);
1527       // Position stayed the same
1528       BOOST_CHECK_EQUAL(vol1->localToGlobalTransform(gctx).matrix(),
1529                         base.matrix());
1530 
1531       // Middle volume is unchanged
1532       const auto* newBounds2 =
1533           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
1534       BOOST_CHECK_EQUAL(*newBounds2, originalBounds[1]);
1535       BOOST_CHECK_EQUAL(vol2->localToGlobalTransform(gctx).matrix(),
1536                         base.matrix());
1537 
1538       // Outermost volume increased r size
1539       const auto* newBounds3 =
1540           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
1541       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMaxR), 1100_mm);
1542       // Position stayed the same
1543       BOOST_CHECK_EQUAL(vol3->localToGlobalTransform(gctx).matrix(),
1544                         base.matrix());
1545 
1546     } else if (strategy == VolumeResizeStrategy::Gap) {
1547       // One gap volume was added
1548       BOOST_CHECK_EQUAL(volumes.size(), 5);
1549 
1550       auto gap1 = volumes.front();
1551       auto gapBounds1 =
1552           dynamic_cast<const CylinderVolumeBounds*>(&gap1->volumeBounds());
1553       BOOST_REQUIRE(gapBounds1 != nullptr);
1554       BOOST_CHECK_EQUAL(gapBounds1->get(CylinderVolumeBounds::eMinR), 0_mm);
1555       BOOST_CHECK_EQUAL(gapBounds1->get(CylinderVolumeBounds::eMaxR), 100_mm);
1556       BOOST_CHECK_EQUAL(gapBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
1557                         hlZ);
1558       BOOST_CHECK_EQUAL(gap1->localToGlobalTransform(gctx).matrix(),
1559                         base.matrix());
1560 
1561       auto gap2 = volumes.back();
1562       auto gapBounds2 =
1563           dynamic_cast<const CylinderVolumeBounds*>(&gap2->volumeBounds());
1564       BOOST_REQUIRE(gapBounds2 != nullptr);
1565       BOOST_CHECK_EQUAL(gapBounds2->get(CylinderVolumeBounds::eMinR), 900_mm);
1566       BOOST_CHECK_EQUAL(gapBounds2->get(CylinderVolumeBounds::eMaxR), 1100_mm);
1567       BOOST_CHECK_EQUAL(gapBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
1568                         hlZ);
1569 
1570       // Other volumes are unchanged
1571       assertInitialVolumesUnchanged();
1572     }
1573   }
1574 
1575   resetCylStack();
1576 
1577   {
1578     // Increase half length z
1579     auto bounds = std::make_shared<CylinderVolumeBounds>(
1580         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1581     bounds->set(CylinderVolumeBounds::eHalfLengthZ, 2 * hlZ);
1582     cylStack->update(gctx, bounds, std::nullopt, *logger);
1583     const auto* cylBounds =
1584         dynamic_cast<const CylinderVolumeBounds*>(&cylStack->volumeBounds());
1585     BOOST_REQUIRE(cylBounds != nullptr);
1586     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1587                       2 * hlZ);
1588 
1589     // Rest as before
1590     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
1591     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 900_mm);
1592 
1593     // No gap volumes were added
1594     BOOST_CHECK_EQUAL(volumes.size(), 3);
1595 
1596     for (const auto& [volume, origCylBounds] :
1597          zip(originalVolumes, originalBounds)) {
1598       const auto* newBounds =
1599           dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
1600       // Radii are all as before
1601       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMinR),
1602                         origCylBounds.get(CylinderVolumeBounds::eMinR));
1603       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMaxR),
1604                         origCylBounds.get(CylinderVolumeBounds::eMaxR));
1605 
1606       // Half length z is changed on all
1607       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1608                         2 * hlZ);
1609 
1610       // Position stayed the same
1611       BOOST_CHECK_EQUAL(volume->localToGlobalTransform(gctx).matrix(),
1612                         base.matrix());
1613     }
1614   }
1615 }
1616 
1617 BOOST_DATA_TEST_CASE(
1618     UpdateStackOneSided,
1619     (boost::unit_test::data::make(-1.0, 1.0) ^
1620      boost::unit_test::data::make(VolumeResizeStrategy::Gap,
1621                                   VolumeResizeStrategy::Expand)),
1622     f, strategy) {
1623   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
1624   // Strategy should not affect the sizing here at all
1625 
1626   auto trf = Transform3::Identity();
1627 
1628   auto vol1 = std::make_shared<Volume>(
1629       trf, std::make_shared<CylinderVolumeBounds>(100_mm, 300_mm, 400_mm));
1630 
1631   auto vol2 = std::make_shared<Volume>(
1632       trf, std::make_shared<CylinderVolumeBounds>(400_mm, 600_mm, 400_mm));
1633 
1634   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
1635 
1636   CylinderVolumeStack cylStack{
1637       gctx,     volumes, AxisDirection::AxisR, VolumeAttachmentStrategy::Gap,
1638       strategy, *logger};
1639   const auto* originalBounds =
1640       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
1641 
1642   // Increase halflength by 50mm
1643   auto newBounds = std::make_shared<CylinderVolumeBounds>(
1644       dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
1645   newBounds->set(CylinderVolumeBounds::eHalfLengthZ, 450_mm);
1646   // Shift to +z by 50mm
1647   trf *= Translation3{Vector3{0_mm, 0_mm, f * 50_mm}};
1648   // -> left edge should stay at -400mm, right edge should be at 500mm
1649 
1650   auto checkUnchanged = [&]() {
1651     const auto* cylBounds =
1652         dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
1653     BOOST_REQUIRE(cylBounds != nullptr);
1654     BOOST_CHECK_EQUAL(*cylBounds, *originalBounds);
1655   };
1656 
1657   // Invalid: shift too far in z
1658   BOOST_CHECK_THROW(
1659       cylStack.update(gctx, newBounds,
1660                       trf * Translation3{Vector3{0, 0, f * 20_mm}}, *logger),
1661       std::invalid_argument);
1662   checkUnchanged();
1663 
1664   // Invalid: shift in x
1665   BOOST_CHECK_THROW(
1666       cylStack.update(gctx, newBounds, trf * Translation3{Vector3{10_mm, 0, 0}},
1667                       *logger),
1668       std::invalid_argument);
1669   checkUnchanged();
1670 
1671   // Invalid: shift in y
1672   BOOST_CHECK_THROW(
1673       cylStack.update(gctx, newBounds, trf * Translation3{Vector3{0, 10_mm, 0}},
1674                       *logger),
1675       std::invalid_argument);
1676   checkUnchanged();
1677 
1678   // Invalid: rotation
1679   BOOST_CHECK_THROW(
1680       cylStack.update(gctx, newBounds,
1681                       trf * AngleAxis3{10_degree, Vector3::UnitY()}, *logger),
1682       std::invalid_argument);
1683   checkUnchanged();
1684 
1685   cylStack.update(gctx, newBounds, trf, *logger);
1686 
1687   BOOST_CHECK_EQUAL(cylStack.localToGlobalTransform(gctx).matrix(),
1688                     trf.matrix());
1689   const auto* cylBounds =
1690       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
1691   BOOST_REQUIRE(cylBounds != nullptr);
1692   BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
1693   BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 600_mm);
1694 
1695   // All volumes including gaps should have same new position and halflength
1696   for (const auto* vol : volumes) {
1697     const auto* volBounds =
1698         dynamic_cast<const CylinderVolumeBounds*>(&vol->volumeBounds());
1699     BOOST_REQUIRE(volBounds != nullptr);
1700     BOOST_CHECK_EQUAL(vol->localToGlobalTransform(gctx).matrix(), trf.matrix());
1701     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1702                       450_mm);
1703   }
1704 }
1705 
1706 BOOST_AUTO_TEST_CASE(ResizeGapMultiple) {
1707   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
1708   Transform3 trf = Transform3::Identity();
1709   auto bounds = std::make_shared<CylinderVolumeBounds>(100, 200, 100);
1710   Volume vol{trf, bounds};
1711 
1712   BOOST_TEST_CONTEXT("Outer") {
1713     std::vector<Volume*> volumes = {&vol};
1714     CylinderVolumeStack stack(gctx, volumes, AxisDirection::AxisR,
1715                               VolumeAttachmentStrategy::Gap,
1716                               VolumeResizeStrategy::Gap, *logger);
1717 
1718     BOOST_CHECK_EQUAL(volumes.size(), 1);
1719     BOOST_CHECK(stack.gaps().empty());
1720 
1721     stack.update(gctx, std::make_shared<CylinderVolumeBounds>(100, 250, 100),
1722                  trf, *logger);
1723     BOOST_CHECK_EQUAL(volumes.size(), 2);
1724     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
1725 
1726     const auto* cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
1727         &stack.gaps().front()->volumeBounds());
1728     BOOST_REQUIRE_NE(cylBounds, nullptr);
1729     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 200);
1730     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 250);
1731 
1732     stack.update(gctx, std::make_shared<CylinderVolumeBounds>(100, 300, 100),
1733                  trf, *logger);
1734 
1735     BOOST_CHECK_EQUAL(volumes.size(), 2);
1736     // No additional gap volume was added!
1737     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
1738 
1739     cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
1740         &stack.gaps().front()->volumeBounds());
1741     BOOST_REQUIRE_NE(cylBounds, nullptr);
1742     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 200);
1743     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 300);
1744   }
1745 
1746   BOOST_TEST_CONTEXT("Inner") {
1747     std::vector<Volume*> volumes = {&vol};
1748     CylinderVolumeStack stack(gctx, volumes, AxisDirection::AxisR,
1749                               VolumeAttachmentStrategy::Gap,
1750                               VolumeResizeStrategy::Gap, *logger);
1751 
1752     BOOST_CHECK_EQUAL(volumes.size(), 1);
1753     BOOST_CHECK(stack.gaps().empty());
1754 
1755     stack.update(gctx, std::make_shared<CylinderVolumeBounds>(50, 200, 100),
1756                  trf, *logger);
1757     BOOST_CHECK_EQUAL(volumes.size(), 2);
1758     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
1759 
1760     const auto* cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
1761         &stack.gaps().front()->volumeBounds());
1762     BOOST_REQUIRE_NE(cylBounds, nullptr);
1763     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 50);
1764     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 100);
1765 
1766     stack.update(gctx, std::make_shared<CylinderVolumeBounds>(0, 200, 100), trf,
1767                  *logger);
1768 
1769     BOOST_CHECK_EQUAL(volumes.size(), 2);
1770     // No additional gap volume was added!
1771     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
1772 
1773     cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
1774         &stack.gaps().front()->volumeBounds());
1775     BOOST_REQUIRE_NE(cylBounds, nullptr);
1776     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 0);
1777     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 100);
1778   }
1779 }
1780 
1781 BOOST_AUTO_TEST_SUITE_END()
1782 
1783 BOOST_AUTO_TEST_SUITE(Common)
1784 
1785 BOOST_DATA_TEST_CASE(JoinCylinderVolumesInvalidDirection,
1786                      boost::unit_test::data::make(strategies), strategy) {
1787   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
1788   std::vector<Volume*> volumes;
1789   auto vol1 = std::make_shared<Volume>(
1790       Transform3::Identity(),
1791       std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1792   volumes.push_back(vol1.get());
1793 
1794   // Single volume invalid direction still gives an error
1795   BOOST_CHECK_THROW(
1796       CylinderVolumeStack(gctx, volumes, AxisDirection::AxisY, strategy),
1797       std::invalid_argument);
1798 
1799   auto vol2 = std::make_shared<Volume>(
1800       Transform3::Identity(),
1801       std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1802   volumes.push_back(vol2.get());
1803 
1804   BOOST_CHECK_THROW(
1805       CylinderVolumeStack(gctx, volumes, AxisDirection::AxisY, strategy),
1806       std::invalid_argument);
1807 }
1808 
1809 BOOST_DATA_TEST_CASE(JoinCylinderVolumesInvalidInput,
1810                      (boost::unit_test::data::make(strategies) *
1811                       boost::unit_test::data::make(AxisDirection::AxisZ,
1812                                                    AxisDirection::AxisR)),
1813                      strategy, direction) {
1814   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
1815   BOOST_TEST_CONTEXT("Empty Volume") {
1816     std::vector<Volume*> volumes;
1817     BOOST_CHECK_THROW(CylinderVolumeStack(gctx, volumes, direction, strategy),
1818                       std::invalid_argument);
1819   }
1820 
1821   BOOST_TEST_CONTEXT("Volumes rotated relative to each other") {
1822     // At this time, all rotations are considered invalid, even around z
1823     for (const Vector3 axis : {Vector3::UnitX(), Vector3::UnitY()}) {
1824       std::vector<Volume*> volumes;
1825       auto vol1 = std::make_shared<Volume>(
1826           Transform3{Translation3{Vector3{0_mm, 0_mm, -500_mm}}},
1827           std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1828       volumes.push_back(vol1.get());
1829 
1830       BOOST_TEST_MESSAGE("Axis: " << axis);
1831       auto vol2 = std::make_shared<Volume>(
1832           Transform3{Translation3{Vector3{0_mm, 0_mm, 500_mm}} *
1833                      AngleAxis3(1_degree, axis)},
1834           std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1835       volumes.push_back(vol2.get());
1836 
1837       BOOST_CHECK_THROW(CylinderVolumeStack(gctx, volumes, direction, strategy,
1838                                             VolumeResizeStrategy::Gap, *logger),
1839                         std::invalid_argument);
1840     }
1841   }
1842 
1843   BOOST_TEST_CONTEXT("Volumes shifted in the xy plane relative to each other") {
1844     for (const Vector3& shift :
1845          {Vector3{5_mm, 0, 0}, Vector3{0, -5_mm, 0}, Vector3{2_mm, -2_mm, 0}}) {
1846       std::vector<Volume*> volumes;
1847       auto vol1 = std::make_shared<Volume>(
1848           Transform3{Translation3{Vector3{0_mm, 0_mm, -500_mm}}},
1849           std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1850       volumes.push_back(vol1.get());
1851 
1852       auto vol2 = std::make_shared<Volume>(
1853           Transform3{Translation3{Vector3{0_mm, 0_mm, 500_mm} + shift}},
1854           std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1855       volumes.push_back(vol2.get());
1856 
1857       BOOST_CHECK_THROW(CylinderVolumeStack(gctx, volumes, direction, strategy,
1858                                             VolumeResizeStrategy::Gap, *logger),
1859                         std::invalid_argument);
1860     }
1861   }
1862 
1863   BOOST_TEST_CONTEXT("Volume has phi values or bevel values") {
1864     std::vector<std::shared_ptr<CylinderVolumeBounds>> invalidVolumeBounds = {
1865         std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm,
1866                                                0.2 * std::numbers::pi),
1867 
1868         std::make_shared<CylinderVolumeBounds>(
1869             100_mm, 400_mm, 400_mm, std::numbers::pi, 0.3 * std::numbers::pi),
1870 
1871         std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm,
1872                                                std::numbers::pi, 0.,
1873                                                0.3 * std::numbers::pi),
1874         std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm,
1875                                                std::numbers::pi, 0., 0.,
1876                                                0.3 * std::numbers::pi),
1877     };
1878 
1879     for (const auto& invalid : invalidVolumeBounds) {
1880       std::stringstream ss;
1881       ss << "Invalid bounds: " << *invalid;
1882       BOOST_TEST_CONTEXT(ss.str()) {
1883         std::vector<Volume*> volumes;
1884         auto vol1 = std::make_shared<Volume>(
1885             Transform3{Translation3{Vector3{0_mm, 0_mm, -500_mm}}},
1886             std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1887         volumes.push_back(vol1.get());
1888 
1889         {
1890           // have valid stack, try to assign extra
1891           CylinderVolumeStack cylStack(gctx, volumes, direction, strategy,
1892                                        VolumeResizeStrategy::Gap, *logger);
1893           BOOST_CHECK_THROW(
1894               cylStack.update(gctx, invalid, std::nullopt, *logger),
1895               std::invalid_argument);
1896         }
1897 
1898         {
1899           std::shared_ptr<Volume> vol;
1900           if (direction == AxisDirection::AxisZ) {
1901             vol = std::make_shared<Volume>(
1902                 Transform3{Translation3{Vector3{0_mm, 0_mm, 500_mm}}}, invalid);
1903           } else {
1904             invalid->set({
1905                 {CylinderVolumeBounds::eMinR, 400_mm},
1906                 {CylinderVolumeBounds::eMaxR, 600_mm},
1907             });
1908             vol = std::make_shared<Volume>(
1909                 Transform3{Translation3{Vector3{0_mm, 0_mm, 0_mm}}}, invalid);
1910           }
1911           volumes.push_back(vol.get());
1912           BOOST_CHECK_THROW(
1913               CylinderVolumeStack(gctx, volumes, direction, strategy,
1914                                   VolumeResizeStrategy::Gap, *logger),
1915               std::invalid_argument);
1916         }
1917       }
1918     }
1919   }
1920 }
1921 
1922 BOOST_DATA_TEST_CASE(JoinCylinderVolumeSingle,
1923                      (boost::unit_test::data::make(AxisDirection::AxisZ,
1924                                                    AxisDirection::AxisR) *
1925                       boost::unit_test::data::make(strategies)),
1926                      direction, strategy) {
1927   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
1928   auto vol = std::make_shared<Volume>(
1929       Transform3::Identity() * Translation3{14_mm, 24_mm, 0_mm} *
1930           AngleAxis3(73_degree, Vector3::UnitX()),
1931       std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1932 
1933   std::vector<Volume*> volumes{vol.get()};
1934 
1935   CylinderVolumeStack cylStack(gctx, volumes, direction, strategy,
1936                                VolumeResizeStrategy::Gap, *logger);
1937 
1938   // Cylinder stack has the same transform as bounds as the single input
1939   // volume
1940   BOOST_CHECK_EQUAL(volumes.size(), 1);
1941   BOOST_CHECK_EQUAL(volumes.at(0), vol.get());
1942   BOOST_CHECK_EQUAL(vol->localToGlobalTransform(gctx).matrix(),
1943                     cylStack.localToGlobalTransform(gctx).matrix());
1944   BOOST_CHECK_EQUAL(vol->volumeBounds(), cylStack.volumeBounds());
1945 }
1946 
1947 BOOST_AUTO_TEST_SUITE_END()
1948 BOOST_AUTO_TEST_SUITE_END()
1949 BOOST_AUTO_TEST_CASE(AsymmetricResizeZ) {
1950   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
1951   double hlZ = 400_mm;
1952   double rMin = 100_mm;
1953   double rMax = 200_mm;
1954 
1955   // Create three cylinder volumes stacked in z
1956   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
1957   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
1958   auto bounds3 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
1959 
1960   Transform3 transform1 = Transform3::Identity();
1961   transform1.translate(Vector3{0_mm, 0_mm, -2 * hlZ});
1962   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
1963 
1964   Transform3 transform2 = Transform3::Identity();
1965   transform2.translate(Vector3{0_mm, 0_mm, 0_mm});
1966   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
1967 
1968   Transform3 transform3 = Transform3::Identity();
1969   transform3.translate(Vector3{0_mm, 0_mm, 2 * hlZ});
1970   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
1971 
1972   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
1973   // Test with Gap for negative z and Expand for positive z
1974   CylinderVolumeStack cylStack(
1975       gctx, volumes, AxisDirection::AxisZ, VolumeAttachmentStrategy::Gap,
1976       {VolumeResizeStrategy::Gap, VolumeResizeStrategy::Expand}, *logger);
1977   // Initial stack spans [-3*hlZ, 3*hlZ]. Update bounds to test asymmetric
1978   // resize in z only New bounds should span [-4*hlZ, 4*hlZ] to ensure we only
1979   // grow
1980   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, 4 * hlZ);
1981   Transform3 newTransform =
1982       Transform3::Identity() * Translation3{0_mm, 0_mm, 0_mm};
1983 
1984   cylStack.update(gctx, newBounds, newTransform, *logger);
1985 
1986   // Check that we have one gap volume at negative z
1987   BOOST_CHECK_EQUAL(volumes.size(), 4);  // Original 3 + 1 gap volume
1988 
1989   // Check gap volume at negative z
1990   auto gapVol = volumes.front();
1991   auto gapBounds =
1992       dynamic_cast<const CylinderVolumeBounds*>(&gapVol->volumeBounds());
1993   BOOST_REQUIRE(gapBounds != nullptr);
1994   BOOST_CHECK_EQUAL(
1995       gapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1996       hlZ / 2);  // Half the original half-length to fill 1*hlZ gap
1997   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), rMin);
1998   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), rMax);
1999   BOOST_CHECK_CLOSE(gapVol->center(gctx)[eZ], -3.5 * hlZ,
2000                     1e-10);  // Center of [-4*hlZ, -3*hlZ]
2001 
2002   // Check that last volume was expanded in positive z
2003   auto* lastVol = volumes.back();
2004   BOOST_CHECK_EQUAL(lastVol, vol3.get());
2005   auto lastBounds =
2006       dynamic_cast<const CylinderVolumeBounds*>(&lastVol->volumeBounds());
2007   BOOST_REQUIRE(lastBounds != nullptr);
2008   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2009                     1.5 * hlZ);  // Original hlZ plus 0.5*hlZ expansion
2010   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMinR), rMin);
2011   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2012   BOOST_CHECK_CLOSE(lastVol->center(gctx)[eZ], 2.5 * hlZ,
2013                     1e-10);  // Center of [2*hlZ, 3*hlZ]
2014 
2015   // Check middle volumes maintain their size
2016   for (std::size_t i = 1; i < volumes.size() - 1; i++) {
2017     auto volBounds =
2018         dynamic_cast<const CylinderVolumeBounds*>(&volumes[i]->volumeBounds());
2019     BOOST_REQUIRE(volBounds != nullptr);
2020     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2021     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eMinR), rMin);
2022     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2023   }
2024   BOOST_CHECK_CLOSE(volumes[1]->center(gctx)[eZ], -2 * hlZ, 1e-10);
2025   BOOST_CHECK_CLOSE(volumes[2]->center(gctx)[eZ], 0, 1e-10);
2026 }
2027 
2028 BOOST_AUTO_TEST_CASE(AsymmetricResizeZFlipped) {
2029   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2030 
2031   double hlZ = 400_mm;
2032   double rMin = 100_mm;
2033   double rMax = 200_mm;
2034 
2035   // Create three cylinder volumes stacked in z
2036   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2037   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2038   auto bounds3 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2039 
2040   Transform3 transform1 = Transform3::Identity() * Translation3(0, 0, -2 * hlZ);
2041   Transform3 transform2 = Transform3::Identity();
2042   Transform3 transform3 = Transform3::Identity() * Translation3(0, 0, 2 * hlZ);
2043 
2044   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
2045   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
2046   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
2047 
2048   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
2049   // Test with Expand for inner radius and Gap for outer radius
2050   CylinderVolumeStack cylStack(
2051       gctx, volumes, AxisDirection::AxisZ, VolumeAttachmentStrategy::Gap,
2052       {VolumeResizeStrategy::Expand, VolumeResizeStrategy::Gap}, *logger);
2053 
2054   // Update bounds to test asymmetric expansion
2055   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, 4 * hlZ);
2056   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2057   // Check that we have one gap volume at positive z
2058   BOOST_CHECK_EQUAL(volumes.size(), 4);  // Original 3 + 1 gap volume
2059 
2060   // Check gap volume at positive z
2061   auto gapVol = volumes.back();
2062   auto gapBounds =
2063       dynamic_cast<const CylinderVolumeBounds*>(&gapVol->volumeBounds());
2064   BOOST_REQUIRE(gapBounds != nullptr);
2065   BOOST_CHECK_EQUAL(
2066       gapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2067       hlZ / 2);  // Half the original half-length to fill 1*hlZ gap
2068   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), rMin);
2069   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2070   BOOST_CHECK_CLOSE(gapVol->center(gctx)[eZ], 3.5 * hlZ,
2071                     1e-10);  // Center of [3*hlZ, 4*hlZ]
2072 
2073   // Check that first volume was expanded in positive z
2074   auto* firstVol = volumes.front();
2075   BOOST_CHECK_EQUAL(firstVol, vol1.get());
2076   auto firstBounds =
2077       dynamic_cast<const CylinderVolumeBounds*>(&firstVol->volumeBounds());
2078   BOOST_REQUIRE(firstBounds != nullptr);
2079   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2080                     1.5 * hlZ);  // Original hlZ plus 0.5*hlZ expansion
2081   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMinR), rMin);
2082   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2083   BOOST_CHECK_CLOSE(firstVol->center(gctx)[eZ], -2.5 * hlZ,
2084                     1e-10);  // Center of [-3*hlZ, -2*hlZ]
2085 
2086   // Check middle volumes maintain their size
2087   for (std::size_t i = 1; i < volumes.size() - 1; i++) {
2088     auto volBounds =
2089         dynamic_cast<const CylinderVolumeBounds*>(&volumes[i]->volumeBounds());
2090     BOOST_REQUIRE(volBounds != nullptr);
2091     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2092     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eMinR), rMin);
2093     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2094   }
2095   BOOST_CHECK_CLOSE(volumes[1]->center(gctx)[eZ], 0, 1e-10);
2096   BOOST_CHECK_CLOSE(volumes[2]->center(gctx)[eZ], 2 * hlZ, 1e-10);
2097 }
2098 
2099 BOOST_AUTO_TEST_CASE(AsymmetricResizeR) {
2100   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2101   double hlZ = 400_mm;
2102 
2103   // Create three cylinder volumes stacked in r with gaps
2104   auto bounds1 = std::make_shared<CylinderVolumeBounds>(100_mm, 200_mm, hlZ);
2105   auto bounds2 = std::make_shared<CylinderVolumeBounds>(200_mm, 300_mm, hlZ);
2106   auto bounds3 = std::make_shared<CylinderVolumeBounds>(300_mm, 400_mm, hlZ);
2107 
2108   Transform3 transform = Transform3::Identity();
2109   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2110   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2111   auto vol3 = std::make_shared<Volume>(transform, bounds3);
2112 
2113   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
2114   // Test with Gap for inner radius and Expand for outer radius
2115   CylinderVolumeStack cylStack(
2116       gctx, volumes, AxisDirection::AxisR, VolumeAttachmentStrategy::Midpoint,
2117       {VolumeResizeStrategy::Gap, VolumeResizeStrategy::Expand}, *logger);
2118 
2119   // Update bounds to test asymmetric resize in r only
2120   auto newBounds = std::make_shared<CylinderVolumeBounds>(50_mm, 500_mm, hlZ);
2121   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2122   // Check that we have one gap volume at inner radius
2123   BOOST_CHECK_EQUAL(volumes.size(), 4);  // Original 3 + 1 gap volume
2124 
2125   // Check gap volume at inner radius
2126   auto innerGap = volumes.front();
2127   auto innerGapBounds =
2128       dynamic_cast<const CylinderVolumeBounds*>(&innerGap->volumeBounds());
2129   BOOST_REQUIRE(innerGapBounds != nullptr);
2130   BOOST_CHECK_EQUAL(innerGapBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
2131   BOOST_CHECK_EQUAL(innerGapBounds->get(CylinderVolumeBounds::eMaxR), 100_mm);
2132   BOOST_CHECK_EQUAL(innerGapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2133                     hlZ);
2134 
2135   // Check that outer volume was expanded
2136   auto* outerVol = volumes.back();
2137   BOOST_CHECK_EQUAL(outerVol, vol3.get());
2138 
2139   auto outerBounds =
2140       dynamic_cast<const CylinderVolumeBounds*>(&outerVol->volumeBounds());
2141   BOOST_REQUIRE(outerBounds != nullptr);
2142   BOOST_CHECK_EQUAL(outerBounds->get(CylinderVolumeBounds::eMinR), 300_mm);
2143   BOOST_CHECK_EQUAL(outerBounds->get(CylinderVolumeBounds::eMaxR), 500_mm);
2144   BOOST_CHECK_EQUAL(outerBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2145 
2146   // Check middle volumes maintain their size
2147   for (std::size_t i = 1; i < volumes.size() - 1; i++) {
2148     auto volBounds =
2149         dynamic_cast<const CylinderVolumeBounds*>(&volumes[i]->volumeBounds());
2150     BOOST_REQUIRE(volBounds != nullptr);
2151     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2152   }
2153 }
2154 
2155 BOOST_AUTO_TEST_CASE(AsymmetricResizeRFlipped) {
2156   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2157   double hlZ = 400_mm;
2158 
2159   // Create three cylinder volumes stacked in r
2160   auto bounds1 = std::make_shared<CylinderVolumeBounds>(100_mm, 200_mm, hlZ);
2161   auto bounds2 = std::make_shared<CylinderVolumeBounds>(200_mm, 300_mm, hlZ);
2162   auto bounds3 = std::make_shared<CylinderVolumeBounds>(300_mm, 400_mm, hlZ);
2163 
2164   Transform3 transform = Transform3::Identity();
2165   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2166   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2167   auto vol3 = std::make_shared<Volume>(transform, bounds3);
2168 
2169   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
2170   // Test with Expand for inner radius and Gap for outer radius
2171   CylinderVolumeStack cylStack(
2172       gctx, volumes, AxisDirection::AxisR, VolumeAttachmentStrategy::Gap,
2173       {VolumeResizeStrategy::Expand, VolumeResizeStrategy::Gap}, *logger);
2174 
2175   // Update bounds to test asymmetric expansion
2176   auto newBounds = std::make_shared<CylinderVolumeBounds>(50_mm, 500_mm, hlZ);
2177   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2178   // Check that we have one gap volume at outer radius
2179   BOOST_CHECK_EQUAL(volumes.size(), 4);  // Original 3 + 1 gap volume
2180 
2181   // Check gap volume at outer radius
2182   auto outerGap = volumes.back();
2183   auto outerGapBounds =
2184       dynamic_cast<const CylinderVolumeBounds*>(&outerGap->volumeBounds());
2185   BOOST_REQUIRE(outerGapBounds != nullptr);
2186   BOOST_CHECK_EQUAL(outerGapBounds->get(CylinderVolumeBounds::eMinR), 400_mm);
2187   BOOST_CHECK_EQUAL(outerGapBounds->get(CylinderVolumeBounds::eMaxR), 500_mm);
2188   BOOST_CHECK_EQUAL(outerGapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2189                     hlZ);
2190 
2191   // Check that inner volume was expanded
2192   auto* innerVol = volumes.front();
2193   BOOST_CHECK_EQUAL(innerVol, vol1.get());
2194 
2195   auto innerBounds =
2196       dynamic_cast<const CylinderVolumeBounds*>(&innerVol->volumeBounds());
2197   BOOST_REQUIRE(innerBounds != nullptr);
2198   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
2199   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eMaxR), 200_mm);
2200   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2201 
2202   // Check middle volumes maintain their size
2203   for (std::size_t i = 1; i < volumes.size() - 1; i++) {
2204     auto volBounds =
2205         dynamic_cast<const CylinderVolumeBounds*>(&volumes[i]->volumeBounds());
2206     BOOST_REQUIRE(volBounds != nullptr);
2207     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2208   }
2209 }
2210 
2211 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeZ) {
2212   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2213   double hlZ = 400_mm;
2214   double rMin = 100_mm;
2215   double rMax = 200_mm;
2216 
2217   // Create two cylinder volumes stacked in z
2218   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2219   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2220 
2221   Transform3 transform1 = Transform3::Identity();
2222   transform1.translate(Vector3{0_mm, 0_mm, -hlZ});
2223   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
2224 
2225   Transform3 transform2 = Transform3::Identity();
2226   transform2.translate(Vector3{0_mm, 0_mm, hlZ});
2227   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
2228 
2229   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2230 
2231   // Test with Gap for negative z and Expand for positive z
2232   CylinderVolumeStack cylStack(
2233       gctx, volumes, AxisDirection::AxisZ, VolumeAttachmentStrategy::Gap,
2234       {VolumeResizeStrategy::Gap, VolumeResizeStrategy::Expand}, *logger);
2235 
2236   // Update bounds to test only positive z expansion
2237   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, 3 * hlZ);
2238   Transform3 newTransform =
2239       Transform3::Identity() * Translation3{0_mm, 0_mm, hlZ};
2240   cylStack.update(gctx, newBounds, newTransform, *logger);
2241   // Check that first volume maintains its size and position
2242   auto* firstVol = volumes.front();
2243   BOOST_CHECK_EQUAL(firstVol, vol1.get());
2244   auto firstBounds =
2245       dynamic_cast<const CylinderVolumeBounds*>(&firstVol->volumeBounds());
2246   BOOST_REQUIRE(firstBounds != nullptr);
2247   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2248   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMinR), rMin);
2249   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2250   BOOST_CHECK_CLOSE(firstVol->center(gctx)[eZ], -hlZ, 1e-10);
2251 
2252   // Check that second volume was expanded in positive z
2253   auto* lastVol = volumes.back();
2254   BOOST_CHECK_EQUAL(lastVol, vol2.get());
2255   auto lastBounds =
2256       dynamic_cast<const CylinderVolumeBounds*>(&lastVol->volumeBounds());
2257   BOOST_REQUIRE(lastBounds != nullptr);
2258   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2259                     2 * hlZ);
2260   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMinR), rMin);
2261   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2262   BOOST_CHECK_CLOSE(lastVol->center(gctx)[eZ], 2 * hlZ, 1e-10);
2263 
2264   // No gap volumes should be created since only positive z changed
2265   BOOST_CHECK_EQUAL(volumes.size(), 2);
2266 }
2267 
2268 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeZFlipped) {
2269   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2270   double hlZ = 400_mm;
2271   double rMin = 100_mm;
2272   double rMax = 200_mm;
2273 
2274   // Create two cylinder volumes stacked in z
2275   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2276   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2277 
2278   Transform3 transform1 = Transform3::Identity();
2279   transform1.translate(Vector3{0_mm, 0_mm, -hlZ});
2280   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
2281 
2282   Transform3 transform2 = Transform3::Identity();
2283   transform2.translate(Vector3{0_mm, 0_mm, hlZ});
2284   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
2285 
2286   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2287   // Test with Expand for negative z and Gap for positive z
2288   CylinderVolumeStack cylStack(
2289       gctx, volumes, AxisDirection::AxisZ, VolumeAttachmentStrategy::Gap,
2290       {VolumeResizeStrategy::Expand, VolumeResizeStrategy::Gap}, *logger);
2291 
2292   // Update bounds to test only positive z expansion
2293   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, 3 * hlZ);
2294   Transform3 newTransform =
2295       Transform3::Identity() * Translation3{0_mm, 0_mm, hlZ};
2296   cylStack.update(gctx, newBounds, newTransform, *logger);
2297   // Check that first volume maintains its size and position
2298   auto* firstVol = volumes.front();
2299   BOOST_CHECK_EQUAL(firstVol, vol1.get());
2300   auto firstBounds =
2301       dynamic_cast<const CylinderVolumeBounds*>(&firstVol->volumeBounds());
2302   BOOST_REQUIRE(firstBounds != nullptr);
2303   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2304   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMinR), rMin);
2305   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2306   BOOST_CHECK_CLOSE(firstVol->center(gctx)[eZ], -hlZ, 1e-10);
2307 
2308   // Check that second volume stays the same
2309   auto* midVol = volumes[1];
2310   BOOST_CHECK_EQUAL(midVol, vol2.get());
2311   auto midBounds =
2312       dynamic_cast<const CylinderVolumeBounds*>(&midVol->volumeBounds());
2313   BOOST_REQUIRE(midBounds != nullptr);
2314   BOOST_CHECK_EQUAL(midBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2315   BOOST_CHECK_EQUAL(midBounds->get(CylinderVolumeBounds::eMinR), rMin);
2316   BOOST_CHECK_EQUAL(midBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2317   BOOST_CHECK_CLOSE(midVol->center(gctx)[eZ], hlZ, 1e-10);
2318 
2319   // A gap volume should be created at positive z
2320   BOOST_CHECK_EQUAL(volumes.size(), 3);  // 2 volumes + 1 gap volume
2321 
2322   // Check gap volume at positive z
2323   auto* gapVol = volumes.back();
2324   auto gapBounds =
2325       dynamic_cast<const CylinderVolumeBounds*>(&gapVol->volumeBounds());
2326   BOOST_REQUIRE(gapBounds != nullptr);
2327   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2328   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), rMin);
2329   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2330   BOOST_CHECK_CLOSE(gapVol->center(gctx)[eZ], 3 * hlZ, 1e-10);
2331 }
2332 
2333 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeR) {
2334   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2335   double hlZ = 400_mm;
2336   double rMin1 = 100_mm;
2337   double rMax1 = 200_mm;
2338   double rMin2 = 200_mm;
2339   double rMax2 = 300_mm;
2340 
2341   // Create two cylinder volumes stacked in r
2342   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin1, rMax1, hlZ);
2343   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin2, rMax2, hlZ);
2344 
2345   Transform3 transform = Transform3::Identity();
2346   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2347   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2348 
2349   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2350 
2351   // Test with Gap for inner radius and Expand for outer radius
2352   CylinderVolumeStack cylStack(
2353       gctx, volumes, AxisDirection::AxisR, VolumeAttachmentStrategy::Gap,
2354       {VolumeResizeStrategy::Gap, VolumeResizeStrategy::Expand}, *logger);
2355 
2356   // Update bounds to test only outer radius expansion
2357   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin1, 500_mm, hlZ);
2358   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2359 
2360   // Check that inner volume maintains its size
2361   auto* innerVol = volumes.front();
2362   BOOST_CHECK_EQUAL(innerVol, vol1.get());
2363   auto innerBounds =
2364       dynamic_cast<const CylinderVolumeBounds*>(&innerVol->volumeBounds());
2365   BOOST_REQUIRE(innerBounds != nullptr);
2366   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eMinR), rMin1);
2367   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eMaxR), rMax1);
2368   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2369 
2370   // Check that outer volume was expanded only in outer radius
2371   auto* outerVol = volumes.back();
2372   BOOST_CHECK_EQUAL(outerVol, vol2.get());
2373   auto outerBounds =
2374       dynamic_cast<const CylinderVolumeBounds*>(&outerVol->volumeBounds());
2375   BOOST_REQUIRE(outerBounds != nullptr);
2376   BOOST_CHECK_EQUAL(outerBounds->get(CylinderVolumeBounds::eMinR), rMin2);
2377   BOOST_CHECK_EQUAL(outerBounds->get(CylinderVolumeBounds::eMaxR), 500_mm);
2378   BOOST_CHECK_EQUAL(outerBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2379 
2380   // No gap volumes should be created since only outer radius changed
2381   BOOST_CHECK_EQUAL(volumes.size(), 2);
2382 }
2383 
2384 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeRFlipped) {
2385   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2386   double hlZ = 400_mm;
2387   double rMin1 = 100_mm;
2388   double rMax1 = 200_mm;
2389   double rMin2 = 200_mm;
2390   double rMax2 = 300_mm;
2391 
2392   // Create two cylinder volumes stacked in r
2393   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin1, rMax1, hlZ);
2394   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin2, rMax2, hlZ);
2395 
2396   Transform3 transform = Transform3::Identity();
2397   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2398   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2399 
2400   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2401   // Test with Expand for inner radius and Gap for outer radius
2402   CylinderVolumeStack cylStack(
2403       gctx, volumes, AxisDirection::AxisR, VolumeAttachmentStrategy::Gap,
2404       {VolumeResizeStrategy::Expand, VolumeResizeStrategy::Gap}, *logger);
2405 
2406   // Update bounds to test only outer radius expansion
2407   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin1, 500_mm, hlZ);
2408   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2409   // Check that inner volume maintains its size
2410   auto* innerVol = volumes.front();
2411   BOOST_CHECK_EQUAL(innerVol, vol1.get());
2412   auto innerBounds =
2413       dynamic_cast<const CylinderVolumeBounds*>(&innerVol->volumeBounds());
2414   BOOST_REQUIRE(innerBounds != nullptr);
2415   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eMinR), rMin1);
2416   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eMaxR), rMax1);
2417   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2418   BOOST_CHECK_CLOSE(innerVol->center(gctx)[eZ], 0, 1e-10);
2419 
2420   // Check that second volume maintains its size
2421   auto* midVol = volumes[1];
2422   BOOST_CHECK_EQUAL(midVol, vol2.get());
2423   auto midBounds =
2424       dynamic_cast<const CylinderVolumeBounds*>(&midVol->volumeBounds());
2425   BOOST_REQUIRE(midBounds != nullptr);
2426   BOOST_CHECK_EQUAL(midBounds->get(CylinderVolumeBounds::eMinR), rMin2);
2427   BOOST_CHECK_EQUAL(midBounds->get(CylinderVolumeBounds::eMaxR), rMax2);
2428   BOOST_CHECK_EQUAL(midBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2429   BOOST_CHECK_CLOSE(midVol->center(gctx)[eZ], 0, 1e-10);
2430 
2431   // A gap volume should be created at outer radius
2432   BOOST_CHECK_EQUAL(volumes.size(), 3);  // 2 volumes + 1 gap volume
2433 
2434   // Check gap volume at positive z
2435   auto* gapVol = volumes.back();
2436   auto gapBounds =
2437       dynamic_cast<const CylinderVolumeBounds*>(&gapVol->volumeBounds());
2438   BOOST_REQUIRE(gapBounds != nullptr);
2439   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2440   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), rMax2);
2441   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), 500_mm);
2442   BOOST_CHECK_CLOSE(gapVol->center(gctx)[eZ], 0, 1e-10);
2443 }
2444 
2445 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeZNegative) {
2446   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2447   double hlZ = 400_mm;
2448   double rMin = 100_mm;
2449   double rMax = 200_mm;
2450 
2451   // Create two cylinder volumes stacked in z
2452   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2453   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2454 
2455   Transform3 transform1 = Transform3::Identity();
2456   transform1.translate(Vector3{0_mm, 0_mm, -hlZ});
2457   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
2458 
2459   Transform3 transform2 = Transform3::Identity();
2460   transform2.translate(Vector3{0_mm, 0_mm, hlZ});
2461   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
2462 
2463   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2464   // Test with Gap for positive z and Expand for negative z
2465   CylinderVolumeStack cylStack(
2466       gctx, volumes, AxisDirection::AxisZ, VolumeAttachmentStrategy::Gap,
2467       {VolumeResizeStrategy::Expand, VolumeResizeStrategy::Gap}, *logger);
2468 
2469   // Update bounds to test only negative z expansion
2470   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, 3 * hlZ);
2471   Transform3 newTransform =
2472       Transform3::Identity() * Translation3{0_mm, 0_mm, -hlZ};
2473   cylStack.update(gctx, newBounds, newTransform, *logger);
2474   // Check that first volume was expanded in negative z
2475   auto* firstVol = volumes.front();
2476   BOOST_CHECK_EQUAL(firstVol, vol1.get());
2477   auto firstBounds =
2478       dynamic_cast<const CylinderVolumeBounds*>(&firstVol->volumeBounds());
2479   BOOST_REQUIRE(firstBounds != nullptr);
2480   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2481                     2 * hlZ);
2482   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMinR), rMin);
2483   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2484   BOOST_CHECK_CLOSE(firstVol->center(gctx)[eZ], -2 * hlZ, 1e-10);
2485 
2486   // Check that second volume maintains its size and position
2487   auto* lastVol = volumes.back();
2488   BOOST_CHECK_EQUAL(lastVol, vol2.get());
2489   auto lastBounds =
2490       dynamic_cast<const CylinderVolumeBounds*>(&lastVol->volumeBounds());
2491   BOOST_REQUIRE(lastBounds != nullptr);
2492   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2493   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMinR), rMin);
2494   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2495   BOOST_CHECK_CLOSE(lastVol->center(gctx)[eZ], hlZ, 1e-10);
2496 
2497   // No gap volumes should be created since only negative z changed
2498   BOOST_CHECK_EQUAL(volumes.size(), 2);
2499 }
2500 
2501 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeZNegativeFlipped) {
2502   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2503   double hlZ = 400_mm;
2504   double rMin = 100_mm;
2505   double rMax = 200_mm;
2506 
2507   // Create two cylinder volumes stacked in z
2508   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2509   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2510 
2511   Transform3 transform1 = Transform3::Identity();
2512   transform1.translate(Vector3{0_mm, 0_mm, -hlZ});
2513   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
2514 
2515   Transform3 transform2 = Transform3::Identity();
2516   transform2.translate(Vector3{0_mm, 0_mm, hlZ});
2517   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
2518 
2519   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2520   // Test with Gap for negative z and Expand for positive z
2521   CylinderVolumeStack cylStack(
2522       gctx, volumes, AxisDirection::AxisZ, VolumeAttachmentStrategy::Gap,
2523       {VolumeResizeStrategy::Gap, VolumeResizeStrategy::Expand}, *logger);
2524 
2525   // Update bounds to test only negative z expansion
2526   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, 3 * hlZ);
2527   Transform3 newTransform =
2528       Transform3::Identity() * Translation3{0_mm, 0_mm, -hlZ};
2529   cylStack.update(gctx, newBounds, newTransform, *logger);
2530 
2531   // A gap volume should be created at negative z
2532   BOOST_CHECK_EQUAL(volumes.size(), 3);  // 2 volumes + 1 gap volume
2533 
2534   // Check gap volume at negative z
2535   auto* gapVol = volumes[0];
2536   auto gapBounds =
2537       dynamic_cast<const CylinderVolumeBounds*>(&gapVol->volumeBounds());
2538   BOOST_REQUIRE(gapBounds != nullptr);
2539   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2540   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), rMin);
2541   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2542   BOOST_CHECK_CLOSE(gapVol->center(gctx)[eZ], -3 * hlZ, 1e-10);
2543 
2544   // Check that first original volume maintains its size and position
2545   auto* originalFirstVol = volumes[1];
2546   BOOST_CHECK_EQUAL(originalFirstVol, vol1.get());
2547   auto originalFirstBounds = dynamic_cast<const CylinderVolumeBounds*>(
2548       &originalFirstVol->volumeBounds());
2549   BOOST_REQUIRE(originalFirstBounds != nullptr);
2550   BOOST_CHECK_EQUAL(
2551       originalFirstBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2552   BOOST_CHECK_EQUAL(originalFirstBounds->get(CylinderVolumeBounds::eMinR),
2553                     rMin);
2554   BOOST_CHECK_EQUAL(originalFirstBounds->get(CylinderVolumeBounds::eMaxR),
2555                     rMax);
2556   BOOST_CHECK_CLOSE(originalFirstVol->center(gctx)[eZ], -hlZ, 1e-10);
2557 }
2558 
2559 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeRNegative) {
2560   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2561   double hlZ = 400_mm;
2562   double rMin1 = 100_mm;
2563   double rMax1 = 200_mm;
2564   double rMin2 = 200_mm;
2565   double rMax2 = 300_mm;
2566 
2567   // Create two cylinder volumes stacked in r
2568   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin1, rMax1, hlZ);
2569   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin2, rMax2, hlZ);
2570 
2571   Transform3 transform = Transform3::Identity();
2572   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2573   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2574 
2575   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2576   // Test with Gap for outer radius and Expand for inner radius
2577   CylinderVolumeStack cylStack(
2578       gctx, volumes, AxisDirection::AxisR, VolumeAttachmentStrategy::Gap,
2579       {VolumeResizeStrategy::Expand, VolumeResizeStrategy::Gap}, *logger);
2580 
2581   // Update bounds to test only inner radius expansion
2582   auto newBounds = std::make_shared<CylinderVolumeBounds>(50_mm, rMax2, hlZ);
2583   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2584   // Check that first volume was expanded in inner radius
2585   auto* firstVol = volumes.front();
2586   BOOST_CHECK_EQUAL(firstVol, vol1.get());
2587   auto firstBounds =
2588       dynamic_cast<const CylinderVolumeBounds*>(&firstVol->volumeBounds());
2589   BOOST_REQUIRE(firstBounds != nullptr);
2590   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
2591   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMaxR), rMax1);
2592   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2593   BOOST_CHECK_CLOSE(firstVol->center(gctx)[eZ], 0, 1e-10);
2594 
2595   // Check that second volume maintains its size and position
2596   auto* lastVol = volumes.back();
2597   BOOST_CHECK_EQUAL(lastVol, vol2.get());
2598   auto lastBounds =
2599       dynamic_cast<const CylinderVolumeBounds*>(&lastVol->volumeBounds());
2600   BOOST_REQUIRE(lastBounds != nullptr);
2601   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMinR), rMin2);
2602   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMaxR), rMax2);
2603   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2604   BOOST_CHECK_CLOSE(lastVol->center(gctx)[eZ], 0, 1e-10);
2605 
2606   // No gap volumes should be created since only inner radius changed
2607   BOOST_CHECK_EQUAL(volumes.size(), 2);
2608 }
2609 
2610 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeRNegativeFlipped) {
2611   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2612   double hlZ = 400_mm;
2613   double rMin1 = 100_mm;
2614   double rMax1 = 200_mm;
2615   double rMin2 = 200_mm;
2616   double rMax2 = 300_mm;
2617 
2618   // Create two cylinder volumes stacked in r
2619   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin1, rMax1, hlZ);
2620   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin2, rMax2, hlZ);
2621 
2622   Transform3 transform = Transform3::Identity();
2623   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2624   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2625 
2626   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2627   // Test with Expand for outer radius and Gap for inner radius
2628   CylinderVolumeStack cylStack(
2629       gctx, volumes, AxisDirection::AxisR, VolumeAttachmentStrategy::Gap,
2630       {VolumeResizeStrategy::Gap, VolumeResizeStrategy::Expand}, *logger);
2631 
2632   // Update bounds to test only inner radius expansion
2633   auto newBounds = std::make_shared<CylinderVolumeBounds>(50_mm, rMax2, hlZ);
2634   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2635   // A gap volume should be created at inner radius
2636   BOOST_CHECK_EQUAL(volumes.size(), 3);  // 2 volumes + 1 gap volume
2637 
2638   // Check gap volume at inner radius
2639   auto* gapVol = volumes[0];
2640   auto gapBounds =
2641       dynamic_cast<const CylinderVolumeBounds*>(&gapVol->volumeBounds());
2642   BOOST_REQUIRE(gapBounds != nullptr);
2643   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2644   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
2645   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), rMin1);
2646   BOOST_CHECK_CLOSE(gapVol->center(gctx)[eZ], 0, 1e-10);
2647 
2648   // Check that first original volume maintains its size and position
2649   auto* originalFirstVol = volumes[1];
2650   BOOST_CHECK_EQUAL(originalFirstVol, vol1.get());
2651   auto originalFirstBounds = dynamic_cast<const CylinderVolumeBounds*>(
2652       &originalFirstVol->volumeBounds());
2653   BOOST_REQUIRE(originalFirstBounds != nullptr);
2654   BOOST_CHECK_EQUAL(
2655       originalFirstBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2656   BOOST_CHECK_EQUAL(originalFirstBounds->get(CylinderVolumeBounds::eMinR),
2657                     rMin1);
2658   BOOST_CHECK_EQUAL(originalFirstBounds->get(CylinderVolumeBounds::eMaxR),
2659                     rMax1);
2660   BOOST_CHECK_CLOSE(originalFirstVol->center(gctx)[eZ], 0, 1e-10);
2661 
2662   // Check that second volume maintains its size and position
2663   auto* originalSecondVol = volumes[2];
2664   BOOST_CHECK_EQUAL(originalSecondVol, vol2.get());
2665   auto originalSecondBounds = dynamic_cast<const CylinderVolumeBounds*>(
2666       &originalSecondVol->volumeBounds());
2667   BOOST_REQUIRE(originalSecondBounds != nullptr);
2668   BOOST_CHECK_EQUAL(originalSecondBounds->get(CylinderVolumeBounds::eMinR),
2669                     rMin2);
2670   BOOST_CHECK_EQUAL(originalSecondBounds->get(CylinderVolumeBounds::eMaxR),
2671                     rMax2);
2672   BOOST_CHECK_EQUAL(
2673       originalSecondBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2674   BOOST_CHECK_CLOSE(originalSecondVol->center(gctx)[eZ], 0, 1e-10);
2675 }
2676 
2677 BOOST_AUTO_TEST_CASE(RStackGapCreationWithUpdatedTransform) {
2678   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2679   double hlZ = 400_mm;
2680   double rMin1 = 100_mm;
2681   double rMax1 = 200_mm;
2682   double rMin2 = 200_mm;
2683   double rMax2 = 300_mm;
2684 
2685   // Create two cylinder volumes stacked in r
2686   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin1, rMax1, hlZ);
2687   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin2, rMax2, hlZ);
2688 
2689   Transform3 transform = Transform3::Identity();
2690   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2691   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2692 
2693   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2694   // Test with Gap for outer radius and Expand for inner radius
2695   CylinderVolumeStack cylStack(gctx, volumes, AxisDirection::AxisR,
2696                                VolumeAttachmentStrategy::Midpoint,
2697                                VolumeResizeStrategy::Gap, *logger);
2698 
2699   cylStack.update(
2700       gctx, std::make_shared<CylinderVolumeBounds>(50_mm, rMax2, hlZ + 5_mm),
2701       Transform3(Translation3(Vector3::UnitZ() * 5_mm)), *logger);
2702 
2703   auto& cylBounds =
2704       dynamic_cast<CylinderVolumeBounds&>(cylStack.volumeBounds());
2705 
2706   BOOST_CHECK_EQUAL(cylBounds.get(CylinderVolumeBounds::eMinR), 50_mm);
2707   BOOST_CHECK_EQUAL(cylBounds.get(CylinderVolumeBounds::eMaxR), rMax2);
2708   BOOST_CHECK_EQUAL(cylBounds.get(CylinderVolumeBounds::eHalfLengthZ),
2709                     hlZ + 5_mm);
2710 
2711   auto gapIt = std::ranges::find_if(volumes, [&](const auto* vol) {
2712     return vol != vol1.get() && vol != vol2.get();  // Find gap volume
2713   });
2714 
2715   BOOST_REQUIRE(gapIt != volumes.end());
2716 
2717   const auto* gap1 = *gapIt;
2718 
2719   BOOST_CHECK_EQUAL(vol1->center(gctx)[eZ], 5_mm);
2720   BOOST_CHECK_EQUAL(vol2->center(gctx)[eZ], 5_mm);
2721   BOOST_CHECK_EQUAL(gap1->center(gctx)[eZ], 5_mm);
2722 
2723   cylStack.update(
2724       gctx, std::make_shared<CylinderVolumeBounds>(50_mm, 350_mm, hlZ + 10_mm),
2725       Transform3(Translation3(Vector3::UnitZ() * 10_mm)), *logger);
2726 
2727   gapIt = std::ranges::find_if(volumes, [&](const auto* vol) {
2728     return vol != vol1.get() && vol != vol2.get() &&
2729            vol != gap1;  // Find gap volume
2730   });
2731 
2732   BOOST_REQUIRE(gapIt != volumes.end());
2733 
2734   const auto* gap2 = *gapIt;
2735 
2736   BOOST_CHECK_EQUAL(vol1->center(gctx)[eZ], 10_mm);
2737   BOOST_CHECK_EQUAL(vol2->center(gctx)[eZ], 10_mm);
2738   BOOST_CHECK_EQUAL(gap1->center(gctx)[eZ], 10_mm);
2739   BOOST_CHECK_EQUAL(gap2->center(gctx)[eZ], 10_mm);
2740 
2741   const auto& gap1Bounds =
2742       dynamic_cast<const CylinderVolumeBounds&>(gap1->volumeBounds());
2743 
2744   const auto& gap2Bounds =
2745       dynamic_cast<const CylinderVolumeBounds&>(gap2->volumeBounds());
2746 
2747   bounds1 =
2748       std::dynamic_pointer_cast<CylinderVolumeBounds>(vol1->volumeBoundsPtr());
2749   bounds2 =
2750       std::dynamic_pointer_cast<CylinderVolumeBounds>(vol2->volumeBoundsPtr());
2751 
2752   BOOST_REQUIRE(bounds1 != nullptr);
2753   BOOST_REQUIRE(bounds2 != nullptr);
2754 
2755   BOOST_CHECK_EQUAL(bounds1->get(CylinderVolumeBounds::eMinR), rMin1);
2756   BOOST_CHECK_EQUAL(bounds1->get(CylinderVolumeBounds::eMaxR), rMax1);
2757   BOOST_CHECK_EQUAL(bounds1->get(CylinderVolumeBounds::eHalfLengthZ),
2758                     hlZ + 10_mm);
2759 
2760   BOOST_CHECK_EQUAL(bounds2->get(CylinderVolumeBounds::eMinR), rMin2);
2761   BOOST_CHECK_EQUAL(bounds2->get(CylinderVolumeBounds::eMaxR), rMax2);
2762   BOOST_CHECK_EQUAL(bounds2->get(CylinderVolumeBounds::eHalfLengthZ),
2763                     hlZ + 10_mm);
2764 
2765   BOOST_CHECK_EQUAL(gap1Bounds.get(CylinderVolumeBounds::eMinR), 50_mm);
2766   BOOST_CHECK_EQUAL(gap1Bounds.get(CylinderVolumeBounds::eMaxR), rMin1);
2767   BOOST_CHECK_EQUAL(gap1Bounds.get(CylinderVolumeBounds::eHalfLengthZ),
2768                     hlZ + 10_mm);
2769 
2770   BOOST_CHECK_EQUAL(gap2Bounds.get(CylinderVolumeBounds::eMinR), rMax2);
2771   BOOST_CHECK_EQUAL(gap2Bounds.get(CylinderVolumeBounds::eMaxR), 350_mm);
2772   BOOST_CHECK_EQUAL(gap2Bounds.get(CylinderVolumeBounds::eHalfLengthZ),
2773                     hlZ + 10_mm);
2774 
2775   BOOST_CHECK_EQUAL(vol1->center(gctx)[eZ], 10_mm);
2776   BOOST_CHECK_EQUAL(vol2->center(gctx)[eZ], 10_mm);
2777   BOOST_CHECK_EQUAL(gap1->center(gctx)[eZ], 10_mm);
2778 }
2779 
2780 BOOST_AUTO_TEST_SUITE_END()
2781 
2782 }  // namespace ActsTests