Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-26 08:04:35

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