Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-07-02 07:52:11

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