Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-28 08:22:09

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 or bevel 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         std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm,
1953                                                std::numbers::pi, 0.,
1954                                                0.3 * std::numbers::pi),
1955         std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm,
1956                                                std::numbers::pi, 0., 0.,
1957                                                0.3 * std::numbers::pi),
1958     };
1959 
1960     for (const auto& invalid : invalidVolumeBounds) {
1961       std::stringstream ss;
1962       ss << "Invalid bounds: " << *invalid;
1963       BOOST_TEST_CONTEXT(ss.str()) {
1964         std::vector<Volume*> volumes;
1965         auto vol1 = std::make_shared<Volume>(
1966             Transform3{Translation3{Vector3{0_mm, 0_mm, -500_mm}}},
1967             std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1968         volumes.push_back(vol1.get());
1969 
1970         {
1971           // have valid stack, try to assign extra
1972           CylinderVolumeStack cylStack(gctx, volumes, direction, strategy,
1973                                        VolumeResizeStrategy::Gap, *logger);
1974           BOOST_CHECK_THROW(
1975               cylStack.update(gctx, invalid, std::nullopt, *logger),
1976               std::invalid_argument);
1977         }
1978 
1979         {
1980           std::shared_ptr<Volume> vol;
1981           if (direction == AxisDirection::AxisZ) {
1982             vol = std::make_shared<Volume>(
1983                 Transform3{Translation3{Vector3{0_mm, 0_mm, 500_mm}}}, invalid);
1984           } else {
1985             invalid->set({
1986                 {CylinderVolumeBounds::eMinR, 400_mm},
1987                 {CylinderVolumeBounds::eMaxR, 600_mm},
1988             });
1989             vol = std::make_shared<Volume>(
1990                 Transform3{Translation3{Vector3{0_mm, 0_mm, 0_mm}}}, invalid);
1991           }
1992           volumes.push_back(vol.get());
1993           BOOST_CHECK_THROW(
1994               CylinderVolumeStack(gctx, volumes, direction, strategy,
1995                                   VolumeResizeStrategy::Gap, *logger),
1996               std::invalid_argument);
1997         }
1998       }
1999     }
2000   }
2001 }
2002 
2003 BOOST_DATA_TEST_CASE(JoinCylinderVolumeSingle,
2004                      (boost::unit_test::data::make(AxisDirection::AxisZ,
2005                                                    AxisDirection::AxisR) *
2006                       boost::unit_test::data::make(strategies)),
2007                      direction, strategy) {
2008   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2009   auto vol = std::make_shared<Volume>(
2010       Transform3::Identity() * Translation3{14_mm, 24_mm, 0_mm} *
2011           AngleAxis3(73_degree, Vector3::UnitX()),
2012       std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
2013 
2014   std::vector<Volume*> volumes{vol.get()};
2015 
2016   CylinderVolumeStack cylStack(gctx, volumes, direction, strategy,
2017                                VolumeResizeStrategy::Gap, *logger);
2018 
2019   // Cylinder stack has the same transform as bounds as the single input
2020   // volume
2021   BOOST_CHECK_EQUAL(volumes.size(), 1);
2022   BOOST_CHECK_EQUAL(volumes.at(0), vol.get());
2023   BOOST_CHECK_EQUAL(vol->localToGlobalTransform(gctx).matrix(),
2024                     cylStack.localToGlobalTransform(gctx).matrix());
2025   BOOST_CHECK_EQUAL(vol->volumeBounds(), cylStack.volumeBounds());
2026 }
2027 
2028 BOOST_AUTO_TEST_SUITE_END()
2029 BOOST_AUTO_TEST_SUITE_END()
2030 BOOST_AUTO_TEST_CASE(AsymmetricResizeZ) {
2031   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2032   double hlZ = 400_mm;
2033   double rMin = 100_mm;
2034   double rMax = 200_mm;
2035 
2036   // Create three cylinder volumes stacked in z
2037   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2038   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2039   auto bounds3 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2040 
2041   Transform3 transform1 = Transform3::Identity();
2042   transform1.translate(Vector3{0_mm, 0_mm, -2 * hlZ});
2043   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
2044 
2045   Transform3 transform2 = Transform3::Identity();
2046   transform2.translate(Vector3{0_mm, 0_mm, 0_mm});
2047   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
2048 
2049   Transform3 transform3 = Transform3::Identity();
2050   transform3.translate(Vector3{0_mm, 0_mm, 2 * hlZ});
2051   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
2052 
2053   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
2054   // Test with Gap for negative z and Expand for positive z
2055   CylinderVolumeStack cylStack(
2056       gctx, volumes, AxisDirection::AxisZ, VolumeAttachmentStrategy::Gap,
2057       {VolumeResizeStrategy::Gap, VolumeResizeStrategy::Expand}, *logger);
2058   // Initial stack spans [-3*hlZ, 3*hlZ]. Update bounds to test asymmetric
2059   // resize in z only New bounds should span [-4*hlZ, 4*hlZ] to ensure we only
2060   // grow
2061   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, 4 * hlZ);
2062   Transform3 newTransform =
2063       Transform3::Identity() * Translation3{0_mm, 0_mm, 0_mm};
2064 
2065   cylStack.update(gctx, newBounds, newTransform, *logger);
2066 
2067   // Check that we have one gap volume at negative z
2068   BOOST_CHECK_EQUAL(volumes.size(), 4);  // Original 3 + 1 gap volume
2069 
2070   // Check gap volume at negative z
2071   auto gapVol = volumes.front();
2072   auto gapBounds =
2073       dynamic_cast<const CylinderVolumeBounds*>(&gapVol->volumeBounds());
2074   BOOST_REQUIRE(gapBounds != nullptr);
2075   BOOST_CHECK_EQUAL(
2076       gapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2077       hlZ / 2);  // Half the original half-length to fill 1*hlZ gap
2078   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), rMin);
2079   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2080   BOOST_CHECK_CLOSE(gapVol->center(gctx)[eZ], -3.5 * hlZ,
2081                     1e-10);  // Center of [-4*hlZ, -3*hlZ]
2082 
2083   // Check that last volume was expanded in positive z
2084   auto* lastVol = volumes.back();
2085   BOOST_CHECK_EQUAL(lastVol, vol3.get());
2086   auto lastBounds =
2087       dynamic_cast<const CylinderVolumeBounds*>(&lastVol->volumeBounds());
2088   BOOST_REQUIRE(lastBounds != nullptr);
2089   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2090                     1.5 * hlZ);  // Original hlZ plus 0.5*hlZ expansion
2091   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMinR), rMin);
2092   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2093   BOOST_CHECK_CLOSE(lastVol->center(gctx)[eZ], 2.5 * hlZ,
2094                     1e-10);  // Center of [2*hlZ, 3*hlZ]
2095 
2096   // Check middle volumes maintain their size
2097   for (std::size_t i = 1; i < volumes.size() - 1; i++) {
2098     auto volBounds =
2099         dynamic_cast<const CylinderVolumeBounds*>(&volumes[i]->volumeBounds());
2100     BOOST_REQUIRE(volBounds != nullptr);
2101     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2102     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eMinR), rMin);
2103     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2104   }
2105   BOOST_CHECK_CLOSE(volumes[1]->center(gctx)[eZ], -2 * hlZ, 1e-10);
2106   BOOST_CHECK_CLOSE(volumes[2]->center(gctx)[eZ], 0, 1e-10);
2107 }
2108 
2109 BOOST_AUTO_TEST_CASE(AsymmetricResizeZFlipped) {
2110   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2111 
2112   double hlZ = 400_mm;
2113   double rMin = 100_mm;
2114   double rMax = 200_mm;
2115 
2116   // Create three cylinder volumes stacked in z
2117   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2118   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2119   auto bounds3 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2120 
2121   Transform3 transform1 = Transform3::Identity() * Translation3(0, 0, -2 * hlZ);
2122   Transform3 transform2 = Transform3::Identity();
2123   Transform3 transform3 = Transform3::Identity() * Translation3(0, 0, 2 * hlZ);
2124 
2125   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
2126   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
2127   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
2128 
2129   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
2130   // Test with Expand for inner radius and Gap for outer radius
2131   CylinderVolumeStack cylStack(
2132       gctx, volumes, AxisDirection::AxisZ, VolumeAttachmentStrategy::Gap,
2133       {VolumeResizeStrategy::Expand, VolumeResizeStrategy::Gap}, *logger);
2134 
2135   // Update bounds to test asymmetric expansion
2136   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, 4 * hlZ);
2137   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2138   // Check that we have one gap volume at positive z
2139   BOOST_CHECK_EQUAL(volumes.size(), 4);  // Original 3 + 1 gap volume
2140 
2141   // Check gap volume at positive z
2142   auto gapVol = volumes.back();
2143   auto gapBounds =
2144       dynamic_cast<const CylinderVolumeBounds*>(&gapVol->volumeBounds());
2145   BOOST_REQUIRE(gapBounds != nullptr);
2146   BOOST_CHECK_EQUAL(
2147       gapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2148       hlZ / 2);  // Half the original half-length to fill 1*hlZ gap
2149   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), rMin);
2150   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2151   BOOST_CHECK_CLOSE(gapVol->center(gctx)[eZ], 3.5 * hlZ,
2152                     1e-10);  // Center of [3*hlZ, 4*hlZ]
2153 
2154   // Check that first volume was expanded in positive z
2155   auto* firstVol = volumes.front();
2156   BOOST_CHECK_EQUAL(firstVol, vol1.get());
2157   auto firstBounds =
2158       dynamic_cast<const CylinderVolumeBounds*>(&firstVol->volumeBounds());
2159   BOOST_REQUIRE(firstBounds != nullptr);
2160   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2161                     1.5 * hlZ);  // Original hlZ plus 0.5*hlZ expansion
2162   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMinR), rMin);
2163   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2164   BOOST_CHECK_CLOSE(firstVol->center(gctx)[eZ], -2.5 * hlZ,
2165                     1e-10);  // Center of [-3*hlZ, -2*hlZ]
2166 
2167   // Check middle volumes maintain their size
2168   for (std::size_t i = 1; i < volumes.size() - 1; i++) {
2169     auto volBounds =
2170         dynamic_cast<const CylinderVolumeBounds*>(&volumes[i]->volumeBounds());
2171     BOOST_REQUIRE(volBounds != nullptr);
2172     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2173     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eMinR), rMin);
2174     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2175   }
2176   BOOST_CHECK_CLOSE(volumes[1]->center(gctx)[eZ], 0, 1e-10);
2177   BOOST_CHECK_CLOSE(volumes[2]->center(gctx)[eZ], 2 * hlZ, 1e-10);
2178 }
2179 
2180 BOOST_AUTO_TEST_CASE(AsymmetricResizeR) {
2181   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2182   double hlZ = 400_mm;
2183 
2184   // Create three cylinder volumes stacked in r with gaps
2185   auto bounds1 = std::make_shared<CylinderVolumeBounds>(100_mm, 200_mm, hlZ);
2186   auto bounds2 = std::make_shared<CylinderVolumeBounds>(200_mm, 300_mm, hlZ);
2187   auto bounds3 = std::make_shared<CylinderVolumeBounds>(300_mm, 400_mm, hlZ);
2188 
2189   Transform3 transform = Transform3::Identity();
2190   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2191   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2192   auto vol3 = std::make_shared<Volume>(transform, bounds3);
2193 
2194   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
2195   // Test with Gap for inner radius and Expand for outer radius
2196   CylinderVolumeStack cylStack(
2197       gctx, volumes, AxisDirection::AxisR, VolumeAttachmentStrategy::Midpoint,
2198       {VolumeResizeStrategy::Gap, VolumeResizeStrategy::Expand}, *logger);
2199 
2200   // Update bounds to test asymmetric resize in r only
2201   auto newBounds = std::make_shared<CylinderVolumeBounds>(50_mm, 500_mm, hlZ);
2202   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2203   // Check that we have one gap volume at inner radius
2204   BOOST_CHECK_EQUAL(volumes.size(), 4);  // Original 3 + 1 gap volume
2205 
2206   // Check gap volume at inner radius
2207   auto innerGap = volumes.front();
2208   auto innerGapBounds =
2209       dynamic_cast<const CylinderVolumeBounds*>(&innerGap->volumeBounds());
2210   BOOST_REQUIRE(innerGapBounds != nullptr);
2211   BOOST_CHECK_EQUAL(innerGapBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
2212   BOOST_CHECK_EQUAL(innerGapBounds->get(CylinderVolumeBounds::eMaxR), 100_mm);
2213   BOOST_CHECK_EQUAL(innerGapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2214                     hlZ);
2215 
2216   // Check that outer volume was expanded
2217   auto* outerVol = volumes.back();
2218   BOOST_CHECK_EQUAL(outerVol, vol3.get());
2219 
2220   auto outerBounds =
2221       dynamic_cast<const CylinderVolumeBounds*>(&outerVol->volumeBounds());
2222   BOOST_REQUIRE(outerBounds != nullptr);
2223   BOOST_CHECK_EQUAL(outerBounds->get(CylinderVolumeBounds::eMinR), 300_mm);
2224   BOOST_CHECK_EQUAL(outerBounds->get(CylinderVolumeBounds::eMaxR), 500_mm);
2225   BOOST_CHECK_EQUAL(outerBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2226 
2227   // Check middle volumes maintain their size
2228   for (std::size_t i = 1; i < volumes.size() - 1; i++) {
2229     auto volBounds =
2230         dynamic_cast<const CylinderVolumeBounds*>(&volumes[i]->volumeBounds());
2231     BOOST_REQUIRE(volBounds != nullptr);
2232     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2233   }
2234 }
2235 
2236 BOOST_AUTO_TEST_CASE(AsymmetricResizeRFlipped) {
2237   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2238   double hlZ = 400_mm;
2239 
2240   // Create three cylinder volumes stacked in r
2241   auto bounds1 = std::make_shared<CylinderVolumeBounds>(100_mm, 200_mm, hlZ);
2242   auto bounds2 = std::make_shared<CylinderVolumeBounds>(200_mm, 300_mm, hlZ);
2243   auto bounds3 = std::make_shared<CylinderVolumeBounds>(300_mm, 400_mm, hlZ);
2244 
2245   Transform3 transform = Transform3::Identity();
2246   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2247   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2248   auto vol3 = std::make_shared<Volume>(transform, bounds3);
2249 
2250   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
2251   // Test with Expand for inner radius and Gap for outer radius
2252   CylinderVolumeStack cylStack(
2253       gctx, volumes, AxisDirection::AxisR, VolumeAttachmentStrategy::Gap,
2254       {VolumeResizeStrategy::Expand, VolumeResizeStrategy::Gap}, *logger);
2255 
2256   // Update bounds to test asymmetric expansion
2257   auto newBounds = std::make_shared<CylinderVolumeBounds>(50_mm, 500_mm, hlZ);
2258   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2259   // Check that we have one gap volume at outer radius
2260   BOOST_CHECK_EQUAL(volumes.size(), 4);  // Original 3 + 1 gap volume
2261 
2262   // Check gap volume at outer radius
2263   auto outerGap = volumes.back();
2264   auto outerGapBounds =
2265       dynamic_cast<const CylinderVolumeBounds*>(&outerGap->volumeBounds());
2266   BOOST_REQUIRE(outerGapBounds != nullptr);
2267   BOOST_CHECK_EQUAL(outerGapBounds->get(CylinderVolumeBounds::eMinR), 400_mm);
2268   BOOST_CHECK_EQUAL(outerGapBounds->get(CylinderVolumeBounds::eMaxR), 500_mm);
2269   BOOST_CHECK_EQUAL(outerGapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2270                     hlZ);
2271 
2272   // Check that inner volume was expanded
2273   auto* innerVol = volumes.front();
2274   BOOST_CHECK_EQUAL(innerVol, vol1.get());
2275 
2276   auto innerBounds =
2277       dynamic_cast<const CylinderVolumeBounds*>(&innerVol->volumeBounds());
2278   BOOST_REQUIRE(innerBounds != nullptr);
2279   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
2280   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eMaxR), 200_mm);
2281   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2282 
2283   // Check middle volumes maintain their size
2284   for (std::size_t i = 1; i < volumes.size() - 1; i++) {
2285     auto volBounds =
2286         dynamic_cast<const CylinderVolumeBounds*>(&volumes[i]->volumeBounds());
2287     BOOST_REQUIRE(volBounds != nullptr);
2288     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2289   }
2290 }
2291 
2292 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeZ) {
2293   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2294   double hlZ = 400_mm;
2295   double rMin = 100_mm;
2296   double rMax = 200_mm;
2297 
2298   // Create two cylinder volumes stacked in z
2299   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2300   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2301 
2302   Transform3 transform1 = Transform3::Identity();
2303   transform1.translate(Vector3{0_mm, 0_mm, -hlZ});
2304   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
2305 
2306   Transform3 transform2 = Transform3::Identity();
2307   transform2.translate(Vector3{0_mm, 0_mm, hlZ});
2308   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
2309 
2310   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2311 
2312   // Test with Gap for negative z and Expand for positive z
2313   CylinderVolumeStack cylStack(
2314       gctx, volumes, AxisDirection::AxisZ, VolumeAttachmentStrategy::Gap,
2315       {VolumeResizeStrategy::Gap, VolumeResizeStrategy::Expand}, *logger);
2316 
2317   // Update bounds to test only positive z expansion
2318   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, 3 * hlZ);
2319   Transform3 newTransform =
2320       Transform3::Identity() * Translation3{0_mm, 0_mm, hlZ};
2321   cylStack.update(gctx, newBounds, newTransform, *logger);
2322   // Check that first volume maintains its size and position
2323   auto* firstVol = volumes.front();
2324   BOOST_CHECK_EQUAL(firstVol, vol1.get());
2325   auto firstBounds =
2326       dynamic_cast<const CylinderVolumeBounds*>(&firstVol->volumeBounds());
2327   BOOST_REQUIRE(firstBounds != nullptr);
2328   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2329   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMinR), rMin);
2330   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2331   BOOST_CHECK_CLOSE(firstVol->center(gctx)[eZ], -hlZ, 1e-10);
2332 
2333   // Check that second volume was expanded in positive z
2334   auto* lastVol = volumes.back();
2335   BOOST_CHECK_EQUAL(lastVol, vol2.get());
2336   auto lastBounds =
2337       dynamic_cast<const CylinderVolumeBounds*>(&lastVol->volumeBounds());
2338   BOOST_REQUIRE(lastBounds != nullptr);
2339   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2340                     2 * hlZ);
2341   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMinR), rMin);
2342   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2343   BOOST_CHECK_CLOSE(lastVol->center(gctx)[eZ], 2 * hlZ, 1e-10);
2344 
2345   // No gap volumes should be created since only positive z changed
2346   BOOST_CHECK_EQUAL(volumes.size(), 2);
2347 }
2348 
2349 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeZFlipped) {
2350   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2351   double hlZ = 400_mm;
2352   double rMin = 100_mm;
2353   double rMax = 200_mm;
2354 
2355   // Create two cylinder volumes stacked in z
2356   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2357   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2358 
2359   Transform3 transform1 = Transform3::Identity();
2360   transform1.translate(Vector3{0_mm, 0_mm, -hlZ});
2361   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
2362 
2363   Transform3 transform2 = Transform3::Identity();
2364   transform2.translate(Vector3{0_mm, 0_mm, hlZ});
2365   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
2366 
2367   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2368   // Test with Expand for negative z and Gap for positive z
2369   CylinderVolumeStack cylStack(
2370       gctx, volumes, AxisDirection::AxisZ, VolumeAttachmentStrategy::Gap,
2371       {VolumeResizeStrategy::Expand, VolumeResizeStrategy::Gap}, *logger);
2372 
2373   // Update bounds to test only positive z expansion
2374   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, 3 * hlZ);
2375   Transform3 newTransform =
2376       Transform3::Identity() * Translation3{0_mm, 0_mm, hlZ};
2377   cylStack.update(gctx, newBounds, newTransform, *logger);
2378   // Check that first volume maintains its size and position
2379   auto* firstVol = volumes.front();
2380   BOOST_CHECK_EQUAL(firstVol, vol1.get());
2381   auto firstBounds =
2382       dynamic_cast<const CylinderVolumeBounds*>(&firstVol->volumeBounds());
2383   BOOST_REQUIRE(firstBounds != nullptr);
2384   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2385   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMinR), rMin);
2386   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2387   BOOST_CHECK_CLOSE(firstVol->center(gctx)[eZ], -hlZ, 1e-10);
2388 
2389   // Check that second volume stays the same
2390   auto* midVol = volumes[1];
2391   BOOST_CHECK_EQUAL(midVol, vol2.get());
2392   auto midBounds =
2393       dynamic_cast<const CylinderVolumeBounds*>(&midVol->volumeBounds());
2394   BOOST_REQUIRE(midBounds != nullptr);
2395   BOOST_CHECK_EQUAL(midBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2396   BOOST_CHECK_EQUAL(midBounds->get(CylinderVolumeBounds::eMinR), rMin);
2397   BOOST_CHECK_EQUAL(midBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2398   BOOST_CHECK_CLOSE(midVol->center(gctx)[eZ], hlZ, 1e-10);
2399 
2400   // A gap volume should be created at positive z
2401   BOOST_CHECK_EQUAL(volumes.size(), 3);  // 2 volumes + 1 gap volume
2402 
2403   // Check gap volume at positive z
2404   auto* gapVol = volumes.back();
2405   auto gapBounds =
2406       dynamic_cast<const CylinderVolumeBounds*>(&gapVol->volumeBounds());
2407   BOOST_REQUIRE(gapBounds != nullptr);
2408   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2409   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), rMin);
2410   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2411   BOOST_CHECK_CLOSE(gapVol->center(gctx)[eZ], 3 * hlZ, 1e-10);
2412 }
2413 
2414 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeR) {
2415   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2416   double hlZ = 400_mm;
2417   double rMin1 = 100_mm;
2418   double rMax1 = 200_mm;
2419   double rMin2 = 200_mm;
2420   double rMax2 = 300_mm;
2421 
2422   // Create two cylinder volumes stacked in r
2423   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin1, rMax1, hlZ);
2424   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin2, rMax2, hlZ);
2425 
2426   Transform3 transform = Transform3::Identity();
2427   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2428   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2429 
2430   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2431 
2432   // Test with Gap for inner radius and Expand for outer radius
2433   CylinderVolumeStack cylStack(
2434       gctx, volumes, AxisDirection::AxisR, VolumeAttachmentStrategy::Gap,
2435       {VolumeResizeStrategy::Gap, VolumeResizeStrategy::Expand}, *logger);
2436 
2437   // Update bounds to test only outer radius expansion
2438   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin1, 500_mm, hlZ);
2439   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2440 
2441   // Check that inner volume maintains its size
2442   auto* innerVol = volumes.front();
2443   BOOST_CHECK_EQUAL(innerVol, vol1.get());
2444   auto innerBounds =
2445       dynamic_cast<const CylinderVolumeBounds*>(&innerVol->volumeBounds());
2446   BOOST_REQUIRE(innerBounds != nullptr);
2447   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eMinR), rMin1);
2448   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eMaxR), rMax1);
2449   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2450 
2451   // Check that outer volume was expanded only in outer radius
2452   auto* outerVol = volumes.back();
2453   BOOST_CHECK_EQUAL(outerVol, vol2.get());
2454   auto outerBounds =
2455       dynamic_cast<const CylinderVolumeBounds*>(&outerVol->volumeBounds());
2456   BOOST_REQUIRE(outerBounds != nullptr);
2457   BOOST_CHECK_EQUAL(outerBounds->get(CylinderVolumeBounds::eMinR), rMin2);
2458   BOOST_CHECK_EQUAL(outerBounds->get(CylinderVolumeBounds::eMaxR), 500_mm);
2459   BOOST_CHECK_EQUAL(outerBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2460 
2461   // No gap volumes should be created since only outer radius changed
2462   BOOST_CHECK_EQUAL(volumes.size(), 2);
2463 }
2464 
2465 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeRFlipped) {
2466   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2467   double hlZ = 400_mm;
2468   double rMin1 = 100_mm;
2469   double rMax1 = 200_mm;
2470   double rMin2 = 200_mm;
2471   double rMax2 = 300_mm;
2472 
2473   // Create two cylinder volumes stacked in r
2474   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin1, rMax1, hlZ);
2475   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin2, rMax2, hlZ);
2476 
2477   Transform3 transform = Transform3::Identity();
2478   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2479   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2480 
2481   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2482   // Test with Expand for inner radius and Gap for outer radius
2483   CylinderVolumeStack cylStack(
2484       gctx, volumes, AxisDirection::AxisR, VolumeAttachmentStrategy::Gap,
2485       {VolumeResizeStrategy::Expand, VolumeResizeStrategy::Gap}, *logger);
2486 
2487   // Update bounds to test only outer radius expansion
2488   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin1, 500_mm, hlZ);
2489   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2490   // Check that inner volume maintains its size
2491   auto* innerVol = volumes.front();
2492   BOOST_CHECK_EQUAL(innerVol, vol1.get());
2493   auto innerBounds =
2494       dynamic_cast<const CylinderVolumeBounds*>(&innerVol->volumeBounds());
2495   BOOST_REQUIRE(innerBounds != nullptr);
2496   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eMinR), rMin1);
2497   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eMaxR), rMax1);
2498   BOOST_CHECK_EQUAL(innerBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2499   BOOST_CHECK_CLOSE(innerVol->center(gctx)[eZ], 0, 1e-10);
2500 
2501   // Check that second volume maintains its size
2502   auto* midVol = volumes[1];
2503   BOOST_CHECK_EQUAL(midVol, vol2.get());
2504   auto midBounds =
2505       dynamic_cast<const CylinderVolumeBounds*>(&midVol->volumeBounds());
2506   BOOST_REQUIRE(midBounds != nullptr);
2507   BOOST_CHECK_EQUAL(midBounds->get(CylinderVolumeBounds::eMinR), rMin2);
2508   BOOST_CHECK_EQUAL(midBounds->get(CylinderVolumeBounds::eMaxR), rMax2);
2509   BOOST_CHECK_EQUAL(midBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2510   BOOST_CHECK_CLOSE(midVol->center(gctx)[eZ], 0, 1e-10);
2511 
2512   // A gap volume should be created at outer radius
2513   BOOST_CHECK_EQUAL(volumes.size(), 3);  // 2 volumes + 1 gap volume
2514 
2515   // Check gap volume at positive z
2516   auto* gapVol = volumes.back();
2517   auto gapBounds =
2518       dynamic_cast<const CylinderVolumeBounds*>(&gapVol->volumeBounds());
2519   BOOST_REQUIRE(gapBounds != nullptr);
2520   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2521   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), rMax2);
2522   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), 500_mm);
2523   BOOST_CHECK_CLOSE(gapVol->center(gctx)[eZ], 0, 1e-10);
2524 }
2525 
2526 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeZNegative) {
2527   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2528   double hlZ = 400_mm;
2529   double rMin = 100_mm;
2530   double rMax = 200_mm;
2531 
2532   // Create two cylinder volumes stacked in z
2533   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2534   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2535 
2536   Transform3 transform1 = Transform3::Identity();
2537   transform1.translate(Vector3{0_mm, 0_mm, -hlZ});
2538   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
2539 
2540   Transform3 transform2 = Transform3::Identity();
2541   transform2.translate(Vector3{0_mm, 0_mm, hlZ});
2542   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
2543 
2544   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2545   // Test with Gap for positive z and Expand for negative z
2546   CylinderVolumeStack cylStack(
2547       gctx, volumes, AxisDirection::AxisZ, VolumeAttachmentStrategy::Gap,
2548       {VolumeResizeStrategy::Expand, VolumeResizeStrategy::Gap}, *logger);
2549 
2550   // Update bounds to test only negative z expansion
2551   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, 3 * hlZ);
2552   Transform3 newTransform =
2553       Transform3::Identity() * Translation3{0_mm, 0_mm, -hlZ};
2554   cylStack.update(gctx, newBounds, newTransform, *logger);
2555   // Check that first volume was expanded in negative z
2556   auto* firstVol = volumes.front();
2557   BOOST_CHECK_EQUAL(firstVol, vol1.get());
2558   auto firstBounds =
2559       dynamic_cast<const CylinderVolumeBounds*>(&firstVol->volumeBounds());
2560   BOOST_REQUIRE(firstBounds != nullptr);
2561   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eHalfLengthZ),
2562                     2 * hlZ);
2563   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMinR), rMin);
2564   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2565   BOOST_CHECK_CLOSE(firstVol->center(gctx)[eZ], -2 * hlZ, 1e-10);
2566 
2567   // Check that second volume maintains its size and position
2568   auto* lastVol = volumes.back();
2569   BOOST_CHECK_EQUAL(lastVol, vol2.get());
2570   auto lastBounds =
2571       dynamic_cast<const CylinderVolumeBounds*>(&lastVol->volumeBounds());
2572   BOOST_REQUIRE(lastBounds != nullptr);
2573   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2574   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMinR), rMin);
2575   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2576   BOOST_CHECK_CLOSE(lastVol->center(gctx)[eZ], hlZ, 1e-10);
2577 
2578   // No gap volumes should be created since only negative z changed
2579   BOOST_CHECK_EQUAL(volumes.size(), 2);
2580 }
2581 
2582 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeZNegativeFlipped) {
2583   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2584   double hlZ = 400_mm;
2585   double rMin = 100_mm;
2586   double rMax = 200_mm;
2587 
2588   // Create two cylinder volumes stacked in z
2589   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2590   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2591 
2592   Transform3 transform1 = Transform3::Identity();
2593   transform1.translate(Vector3{0_mm, 0_mm, -hlZ});
2594   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
2595 
2596   Transform3 transform2 = Transform3::Identity();
2597   transform2.translate(Vector3{0_mm, 0_mm, hlZ});
2598   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
2599 
2600   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2601   // Test with Gap for negative z and Expand for positive z
2602   CylinderVolumeStack cylStack(
2603       gctx, volumes, AxisDirection::AxisZ, VolumeAttachmentStrategy::Gap,
2604       {VolumeResizeStrategy::Gap, VolumeResizeStrategy::Expand}, *logger);
2605 
2606   // Update bounds to test only negative z expansion
2607   auto newBounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, 3 * hlZ);
2608   Transform3 newTransform =
2609       Transform3::Identity() * Translation3{0_mm, 0_mm, -hlZ};
2610   cylStack.update(gctx, newBounds, newTransform, *logger);
2611 
2612   // A gap volume should be created at negative z
2613   BOOST_CHECK_EQUAL(volumes.size(), 3);  // 2 volumes + 1 gap volume
2614 
2615   // Check gap volume at negative z
2616   auto* gapVol = volumes[0];
2617   auto gapBounds =
2618       dynamic_cast<const CylinderVolumeBounds*>(&gapVol->volumeBounds());
2619   BOOST_REQUIRE(gapBounds != nullptr);
2620   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2621   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), rMin);
2622   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), rMax);
2623   BOOST_CHECK_CLOSE(gapVol->center(gctx)[eZ], -3 * hlZ, 1e-10);
2624 
2625   // Check that first original volume maintains its size and position
2626   auto* originalFirstVol = volumes[1];
2627   BOOST_CHECK_EQUAL(originalFirstVol, vol1.get());
2628   auto originalFirstBounds = dynamic_cast<const CylinderVolumeBounds*>(
2629       &originalFirstVol->volumeBounds());
2630   BOOST_REQUIRE(originalFirstBounds != nullptr);
2631   BOOST_CHECK_EQUAL(
2632       originalFirstBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2633   BOOST_CHECK_EQUAL(originalFirstBounds->get(CylinderVolumeBounds::eMinR),
2634                     rMin);
2635   BOOST_CHECK_EQUAL(originalFirstBounds->get(CylinderVolumeBounds::eMaxR),
2636                     rMax);
2637   BOOST_CHECK_CLOSE(originalFirstVol->center(gctx)[eZ], -hlZ, 1e-10);
2638 }
2639 
2640 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeRNegative) {
2641   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2642   double hlZ = 400_mm;
2643   double rMin1 = 100_mm;
2644   double rMax1 = 200_mm;
2645   double rMin2 = 200_mm;
2646   double rMax2 = 300_mm;
2647 
2648   // Create two cylinder volumes stacked in r
2649   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin1, rMax1, hlZ);
2650   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin2, rMax2, hlZ);
2651 
2652   Transform3 transform = Transform3::Identity();
2653   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2654   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2655 
2656   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2657   // Test with Gap for outer radius and Expand for inner radius
2658   CylinderVolumeStack cylStack(
2659       gctx, volumes, AxisDirection::AxisR, VolumeAttachmentStrategy::Gap,
2660       {VolumeResizeStrategy::Expand, VolumeResizeStrategy::Gap}, *logger);
2661 
2662   // Update bounds to test only inner radius expansion
2663   auto newBounds = std::make_shared<CylinderVolumeBounds>(50_mm, rMax2, hlZ);
2664   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2665   // Check that first volume was expanded in inner radius
2666   auto* firstVol = volumes.front();
2667   BOOST_CHECK_EQUAL(firstVol, vol1.get());
2668   auto firstBounds =
2669       dynamic_cast<const CylinderVolumeBounds*>(&firstVol->volumeBounds());
2670   BOOST_REQUIRE(firstBounds != nullptr);
2671   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
2672   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eMaxR), rMax1);
2673   BOOST_CHECK_EQUAL(firstBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2674   BOOST_CHECK_CLOSE(firstVol->center(gctx)[eZ], 0, 1e-10);
2675 
2676   // Check that second volume maintains its size and position
2677   auto* lastVol = volumes.back();
2678   BOOST_CHECK_EQUAL(lastVol, vol2.get());
2679   auto lastBounds =
2680       dynamic_cast<const CylinderVolumeBounds*>(&lastVol->volumeBounds());
2681   BOOST_REQUIRE(lastBounds != nullptr);
2682   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMinR), rMin2);
2683   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eMaxR), rMax2);
2684   BOOST_CHECK_EQUAL(lastBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2685   BOOST_CHECK_CLOSE(lastVol->center(gctx)[eZ], 0, 1e-10);
2686 
2687   // No gap volumes should be created since only inner radius changed
2688   BOOST_CHECK_EQUAL(volumes.size(), 2);
2689 }
2690 
2691 BOOST_AUTO_TEST_CASE(AsymmetricSingleSideResizeRNegativeFlipped) {
2692   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2693   double hlZ = 400_mm;
2694   double rMin1 = 100_mm;
2695   double rMax1 = 200_mm;
2696   double rMin2 = 200_mm;
2697   double rMax2 = 300_mm;
2698 
2699   // Create two cylinder volumes stacked in r
2700   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin1, rMax1, hlZ);
2701   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin2, rMax2, hlZ);
2702 
2703   Transform3 transform = Transform3::Identity();
2704   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2705   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2706 
2707   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2708   // Test with Expand for outer radius and Gap for inner radius
2709   CylinderVolumeStack cylStack(
2710       gctx, volumes, AxisDirection::AxisR, VolumeAttachmentStrategy::Gap,
2711       {VolumeResizeStrategy::Gap, VolumeResizeStrategy::Expand}, *logger);
2712 
2713   // Update bounds to test only inner radius expansion
2714   auto newBounds = std::make_shared<CylinderVolumeBounds>(50_mm, rMax2, hlZ);
2715   cylStack.update(gctx, newBounds, std::nullopt, *logger);
2716   // A gap volume should be created at inner radius
2717   BOOST_CHECK_EQUAL(volumes.size(), 3);  // 2 volumes + 1 gap volume
2718 
2719   // Check gap volume at inner radius
2720   auto* gapVol = volumes[0];
2721   auto gapBounds =
2722       dynamic_cast<const CylinderVolumeBounds*>(&gapVol->volumeBounds());
2723   BOOST_REQUIRE(gapBounds != nullptr);
2724   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2725   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
2726   BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), rMin1);
2727   BOOST_CHECK_CLOSE(gapVol->center(gctx)[eZ], 0, 1e-10);
2728 
2729   // Check that first original volume maintains its size and position
2730   auto* originalFirstVol = volumes[1];
2731   BOOST_CHECK_EQUAL(originalFirstVol, vol1.get());
2732   auto originalFirstBounds = dynamic_cast<const CylinderVolumeBounds*>(
2733       &originalFirstVol->volumeBounds());
2734   BOOST_REQUIRE(originalFirstBounds != nullptr);
2735   BOOST_CHECK_EQUAL(
2736       originalFirstBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2737   BOOST_CHECK_EQUAL(originalFirstBounds->get(CylinderVolumeBounds::eMinR),
2738                     rMin1);
2739   BOOST_CHECK_EQUAL(originalFirstBounds->get(CylinderVolumeBounds::eMaxR),
2740                     rMax1);
2741   BOOST_CHECK_CLOSE(originalFirstVol->center(gctx)[eZ], 0, 1e-10);
2742 
2743   // Check that second volume maintains its size and position
2744   auto* originalSecondVol = volumes[2];
2745   BOOST_CHECK_EQUAL(originalSecondVol, vol2.get());
2746   auto originalSecondBounds = dynamic_cast<const CylinderVolumeBounds*>(
2747       &originalSecondVol->volumeBounds());
2748   BOOST_REQUIRE(originalSecondBounds != nullptr);
2749   BOOST_CHECK_EQUAL(originalSecondBounds->get(CylinderVolumeBounds::eMinR),
2750                     rMin2);
2751   BOOST_CHECK_EQUAL(originalSecondBounds->get(CylinderVolumeBounds::eMaxR),
2752                     rMax2);
2753   BOOST_CHECK_EQUAL(
2754       originalSecondBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
2755   BOOST_CHECK_CLOSE(originalSecondVol->center(gctx)[eZ], 0, 1e-10);
2756 }
2757 
2758 BOOST_AUTO_TEST_CASE(RStackGapCreationWithUpdatedTransform) {
2759   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2760   double hlZ = 400_mm;
2761   double rMin1 = 100_mm;
2762   double rMax1 = 200_mm;
2763   double rMin2 = 200_mm;
2764   double rMax2 = 300_mm;
2765 
2766   // Create two cylinder volumes stacked in r
2767   auto bounds1 = std::make_shared<CylinderVolumeBounds>(rMin1, rMax1, hlZ);
2768   auto bounds2 = std::make_shared<CylinderVolumeBounds>(rMin2, rMax2, hlZ);
2769 
2770   Transform3 transform = Transform3::Identity();
2771   auto vol1 = std::make_shared<Volume>(transform, bounds1);
2772   auto vol2 = std::make_shared<Volume>(transform, bounds2);
2773 
2774   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
2775   // Test with Gap for outer radius and Expand for inner radius
2776   CylinderVolumeStack cylStack(gctx, volumes, AxisDirection::AxisR,
2777                                VolumeAttachmentStrategy::Midpoint,
2778                                VolumeResizeStrategy::Gap, *logger);
2779 
2780   cylStack.update(
2781       gctx, std::make_shared<CylinderVolumeBounds>(50_mm, rMax2, hlZ + 5_mm),
2782       Transform3(Translation3(Vector3::UnitZ() * 5_mm)), *logger);
2783 
2784   auto& cylBounds =
2785       dynamic_cast<CylinderVolumeBounds&>(cylStack.volumeBounds());
2786 
2787   BOOST_CHECK_EQUAL(cylBounds.get(CylinderVolumeBounds::eMinR), 50_mm);
2788   BOOST_CHECK_EQUAL(cylBounds.get(CylinderVolumeBounds::eMaxR), rMax2);
2789   BOOST_CHECK_EQUAL(cylBounds.get(CylinderVolumeBounds::eHalfLengthZ),
2790                     hlZ + 5_mm);
2791 
2792   auto gapIt = std::ranges::find_if(volumes, [&](const auto* vol) {
2793     return vol != vol1.get() && vol != vol2.get();  // Find gap volume
2794   });
2795 
2796   BOOST_REQUIRE(gapIt != volumes.end());
2797 
2798   const auto* gap1 = *gapIt;
2799 
2800   BOOST_CHECK_EQUAL(vol1->center(gctx)[eZ], 5_mm);
2801   BOOST_CHECK_EQUAL(vol2->center(gctx)[eZ], 5_mm);
2802   BOOST_CHECK_EQUAL(gap1->center(gctx)[eZ], 5_mm);
2803 
2804   cylStack.update(
2805       gctx, std::make_shared<CylinderVolumeBounds>(50_mm, 350_mm, hlZ + 10_mm),
2806       Transform3(Translation3(Vector3::UnitZ() * 10_mm)), *logger);
2807 
2808   gapIt = std::ranges::find_if(volumes, [&](const auto* vol) {
2809     return vol != vol1.get() && vol != vol2.get() &&
2810            vol != gap1;  // Find gap volume
2811   });
2812 
2813   BOOST_REQUIRE(gapIt != volumes.end());
2814 
2815   const auto* gap2 = *gapIt;
2816 
2817   BOOST_CHECK_EQUAL(vol1->center(gctx)[eZ], 10_mm);
2818   BOOST_CHECK_EQUAL(vol2->center(gctx)[eZ], 10_mm);
2819   BOOST_CHECK_EQUAL(gap1->center(gctx)[eZ], 10_mm);
2820   BOOST_CHECK_EQUAL(gap2->center(gctx)[eZ], 10_mm);
2821 
2822   const auto& gap1Bounds =
2823       dynamic_cast<const CylinderVolumeBounds&>(gap1->volumeBounds());
2824 
2825   const auto& gap2Bounds =
2826       dynamic_cast<const CylinderVolumeBounds&>(gap2->volumeBounds());
2827 
2828   bounds1 =
2829       std::dynamic_pointer_cast<CylinderVolumeBounds>(vol1->volumeBoundsPtr());
2830   bounds2 =
2831       std::dynamic_pointer_cast<CylinderVolumeBounds>(vol2->volumeBoundsPtr());
2832 
2833   BOOST_REQUIRE(bounds1 != nullptr);
2834   BOOST_REQUIRE(bounds2 != nullptr);
2835 
2836   BOOST_CHECK_EQUAL(bounds1->get(CylinderVolumeBounds::eMinR), rMin1);
2837   BOOST_CHECK_EQUAL(bounds1->get(CylinderVolumeBounds::eMaxR), rMax1);
2838   BOOST_CHECK_EQUAL(bounds1->get(CylinderVolumeBounds::eHalfLengthZ),
2839                     hlZ + 10_mm);
2840 
2841   BOOST_CHECK_EQUAL(bounds2->get(CylinderVolumeBounds::eMinR), rMin2);
2842   BOOST_CHECK_EQUAL(bounds2->get(CylinderVolumeBounds::eMaxR), rMax2);
2843   BOOST_CHECK_EQUAL(bounds2->get(CylinderVolumeBounds::eHalfLengthZ),
2844                     hlZ + 10_mm);
2845 
2846   BOOST_CHECK_EQUAL(gap1Bounds.get(CylinderVolumeBounds::eMinR), 50_mm);
2847   BOOST_CHECK_EQUAL(gap1Bounds.get(CylinderVolumeBounds::eMaxR), rMin1);
2848   BOOST_CHECK_EQUAL(gap1Bounds.get(CylinderVolumeBounds::eHalfLengthZ),
2849                     hlZ + 10_mm);
2850 
2851   BOOST_CHECK_EQUAL(gap2Bounds.get(CylinderVolumeBounds::eMinR), rMax2);
2852   BOOST_CHECK_EQUAL(gap2Bounds.get(CylinderVolumeBounds::eMaxR), 350_mm);
2853   BOOST_CHECK_EQUAL(gap2Bounds.get(CylinderVolumeBounds::eHalfLengthZ),
2854                     hlZ + 10_mm);
2855 
2856   BOOST_CHECK_EQUAL(vol1->center(gctx)[eZ], 10_mm);
2857   BOOST_CHECK_EQUAL(vol2->center(gctx)[eZ], 10_mm);
2858   BOOST_CHECK_EQUAL(gap1->center(gctx)[eZ], 10_mm);
2859 }
2860 
2861 // A change in the radial bounds that is smaller than the on-surface tolerance
2862 // must be treated as "no change" and must not spawn a spurious, near-zero
2863 // thickness gap volume. This is the r-direction analogue of the z-direction
2864 // reproduction (see ResizeReproduction2): the r-resize path currently compares
2865 // radii exactly instead of within tolerance, so it creates a degenerate gap
2866 // shell whose inner and outer radius are effectively identical.
2867 BOOST_AUTO_TEST_CASE(RStackGapCreationTolerance) {
2868   const auto gctx = Acts::GeometryContext::dangerouslyDefaultConstruct();
2869   const double hlZ = 400_mm;
2870   const double rMin = 100_mm;
2871   const double rMax = 200_mm;
2872 
2873   // Sub-tolerance perturbation of the radial bounds
2874   const double eps = s_onSurfaceTolerance / 2.0;
2875 
2876   BOOST_TEST_CONTEXT("Outer radius") {
2877     auto bounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2878     auto vol = std::make_shared<Volume>(Transform3::Identity(), bounds);
2879 
2880     std::vector<Volume*> volumes = {vol.get()};
2881     CylinderVolumeStack cylStack(gctx, volumes, AxisDirection::AxisR,
2882                                  VolumeAttachmentStrategy::Gap,
2883                                  VolumeResizeStrategy::Gap, *logger);
2884 
2885     BOOST_CHECK(cylStack.gaps().empty());
2886 
2887     // Grow the outer radius by less than the tolerance
2888     cylStack.update(
2889         gctx, std::make_shared<CylinderVolumeBounds>(rMin, rMax + eps, hlZ),
2890         std::nullopt, *logger);
2891 
2892     // No gap volume should have been created for a sub-tolerance change
2893     BOOST_CHECK(cylStack.gaps().empty());
2894     BOOST_CHECK_EQUAL(volumes.size(), 1);
2895   }
2896 
2897   BOOST_TEST_CONTEXT("Inner radius") {
2898     auto bounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
2899     auto vol = std::make_shared<Volume>(Transform3::Identity(), bounds);
2900 
2901     std::vector<Volume*> volumes = {vol.get()};
2902     CylinderVolumeStack cylStack(gctx, volumes, AxisDirection::AxisR,
2903                                  VolumeAttachmentStrategy::Gap,
2904                                  VolumeResizeStrategy::Gap, *logger);
2905 
2906     BOOST_CHECK(cylStack.gaps().empty());
2907 
2908     // Extend the inner radius inward by less than the tolerance
2909     cylStack.update(
2910         gctx, std::make_shared<CylinderVolumeBounds>(rMin - eps, rMax, hlZ),
2911         std::nullopt, *logger);
2912 
2913     // No gap volume should have been created for a sub-tolerance change
2914     BOOST_CHECK(cylStack.gaps().empty());
2915     BOOST_CHECK_EQUAL(volumes.size(), 1);
2916   }
2917 }
2918 
2919 BOOST_AUTO_TEST_SUITE_END()
2920 
2921 }  // namespace ActsTests