Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-10-30 07:55:17

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