Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-01-18 09:12:38

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/Tests/CommonHelpers/FloatComparisons.hpp"
0023 #include "Acts/Utilities/BinningType.hpp"
0024 #include "Acts/Utilities/Logger.hpp"
0025 #include "Acts/Utilities/Zip.hpp"
0026 
0027 #include <numbers>
0028 
0029 using namespace Acts::UnitLiterals;
0030 
0031 namespace Acts::Test {
0032 
0033 auto logger = Acts::getDefaultLogger("UnitTests", Acts::Logging::VERBOSE);
0034 
0035 struct Fixture {
0036   Logging::Level m_level;
0037   Fixture() {
0038     m_level = Acts::Logging::getFailureThreshold();
0039     Acts::Logging::setFailureThreshold(Acts::Logging::FATAL);
0040   }
0041 
0042   ~Fixture() { Acts::Logging::setFailureThreshold(m_level); }
0043 };
0044 
0045 BOOST_FIXTURE_TEST_SUITE(Geometry, Fixture)
0046 
0047 static const std::vector<CylinderVolumeStack::AttachmentStrategy> strategies = {
0048     CylinderVolumeStack::AttachmentStrategy::Gap,
0049     CylinderVolumeStack::AttachmentStrategy::First,
0050     CylinderVolumeStack::AttachmentStrategy::Second,
0051     CylinderVolumeStack::AttachmentStrategy::Midpoint,
0052 };
0053 
0054 static const std::vector<CylinderVolumeStack::ResizeStrategy> resizeStrategies =
0055     {
0056         CylinderVolumeStack::ResizeStrategy::Expand,
0057         CylinderVolumeStack::ResizeStrategy::Gap,
0058 };
0059 
0060 BOOST_AUTO_TEST_SUITE(CylinderVolumeStackTest)
0061 BOOST_AUTO_TEST_SUITE(ZDirection)
0062 
0063 BOOST_DATA_TEST_CASE(Baseline,
0064                      (boost::unit_test::data::xrange(-135, 180, 45) *
0065                       boost::unit_test::data::xrange(0, 2, 1) *
0066                       boost::unit_test::data::make(0.8, 1.0, 1.2) *
0067                       boost::unit_test::data::make(Vector3{0_mm, 0_mm, 0_mm},
0068                                                    Vector3{20_mm, 0_mm, 0_mm},
0069                                                    Vector3{0_mm, 20_mm, 0_mm},
0070                                                    Vector3{20_mm, 20_mm, 0_mm},
0071                                                    Vector3{0_mm, 0_mm, 20_mm}) *
0072                       boost::unit_test::data::make(strategies)),
0073                      angle, rotate, shift, offset, strategy) {
0074   double hlZ = 400_mm;
0075 
0076   // Cylinder volumes which already line up, but have different1 radii
0077   auto bounds1 = std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, hlZ);
0078   auto bounds2 = std::make_shared<CylinderVolumeBounds>(200_mm, 600_mm, hlZ);
0079   auto bounds3 = std::make_shared<CylinderVolumeBounds>(300_mm, 500_mm, hlZ);
0080 
0081   Transform3 base =
0082       AngleAxis3(angle * 1_degree, Vector3::UnitX()) * Translation3(offset);
0083 
0084   Transform3 transform1 = base;
0085   transform1.translate(Vector3{0_mm, 0_mm, -2 * hlZ * shift});
0086   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
0087 
0088   Transform3 transform2 = base;
0089   transform2.translate(Vector3{0_mm, 0_mm, 0_mm});
0090   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
0091 
0092   Transform3 transform3 = base;
0093   transform3.translate(Vector3{0_mm, 0_mm, 2 * hlZ * shift});
0094   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
0095 
0096   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
0097   // Rotate to simulate unsorted volumes: all results should be the same!
0098   std::rotate(volumes.begin(), volumes.begin() + rotate, volumes.end());
0099 
0100   auto origVolumes = volumes;
0101 
0102   std::vector<CylinderVolumeBounds> originalBounds;
0103   std::transform(
0104       volumes.begin(), volumes.end(), std::back_inserter(originalBounds),
0105       [](const auto& vol) {
0106         return *dynamic_cast<const CylinderVolumeBounds*>(&vol->volumeBounds());
0107       });
0108 
0109   if (shift < 1.0) {
0110     BOOST_CHECK_THROW(
0111         CylinderVolumeStack(volumes, AxisDirection::AxisZ, strategy,
0112                             CylinderVolumeStack::ResizeStrategy::Gap, *logger),
0113         std::invalid_argument);
0114     return;
0115   }
0116   CylinderVolumeStack cylStack(volumes, AxisDirection::AxisZ, strategy,
0117                                CylinderVolumeStack::ResizeStrategy::Gap,
0118                                *logger);
0119 
0120   auto stackBounds =
0121       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0122   BOOST_REQUIRE(stackBounds != nullptr);
0123 
0124   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
0125   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMaxR), 600_mm);
0126   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0127                     hlZ + 2 * hlZ * shift);
0128   CHECK_CLOSE_OR_SMALL(cylStack.transform().matrix(), base.matrix(), 1e-10,
0129                        1e-14);
0130 
0131   // All volumes (including gaps) are cylinders and have the same radial bounds
0132   for (const auto& volume : volumes) {
0133     const auto* cylinderBounds =
0134         dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
0135     BOOST_REQUIRE(cylinderBounds != nullptr);
0136     BOOST_CHECK_EQUAL(cylinderBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
0137     BOOST_CHECK_EQUAL(cylinderBounds->get(CylinderVolumeBounds::eMaxR), 600_mm);
0138   }
0139 
0140   // Volumes are sorted in (local) z
0141   for (std::size_t i = 0; i < volumes.size() - 1; ++i) {
0142     const auto& a = volumes.at(i);
0143     const auto& b = volumes.at(i + 1);
0144 
0145     BOOST_CHECK_LT((base.inverse() * a->center())[eZ],
0146                    (base.inverse() * b->center())[eZ]);
0147   }
0148 
0149   if (shift <= 1.0) {
0150     // No gap volumes were added
0151     BOOST_CHECK_EQUAL(volumes.size(), 3);
0152 
0153     // No expansion, original volumes did not move
0154     BOOST_CHECK_EQUAL(vol1->transform().matrix(), transform1.matrix());
0155     BOOST_CHECK_EQUAL(vol2->transform().matrix(), transform2.matrix());
0156     BOOST_CHECK_EQUAL(vol3->transform().matrix(), transform3.matrix());
0157 
0158     for (const auto& [volume, bounds] : zip(origVolumes, originalBounds)) {
0159       const auto* newBounds =
0160           dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
0161       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0162                         bounds.get(CylinderVolumeBounds::eHalfLengthZ));
0163     }
0164   } else {
0165     if (strategy == CylinderVolumeStack::AttachmentStrategy::Gap) {
0166       // Gap volumes were added
0167       BOOST_CHECK_EQUAL(volumes.size(), 5);
0168       auto gap1 = volumes.at(1);
0169       auto gap2 = volumes.at(3);
0170 
0171       BOOST_TEST_MESSAGE("Gap 1: " << gap1->transform().matrix());
0172       BOOST_TEST_MESSAGE("Gap 2: " << gap2->transform().matrix());
0173 
0174       const auto* gapBounds1 =
0175           dynamic_cast<const CylinderVolumeBounds*>(&gap1->volumeBounds());
0176       const auto* gapBounds2 =
0177           dynamic_cast<const CylinderVolumeBounds*>(&gap2->volumeBounds());
0178 
0179       double gapHlZ = (shift - 1.0) * hlZ;
0180 
0181       BOOST_CHECK(std::abs(gapBounds1->get(CylinderVolumeBounds::eHalfLengthZ) -
0182                            gapHlZ) < 1e-10);
0183       BOOST_CHECK(std::abs(gapBounds2->get(CylinderVolumeBounds::eHalfLengthZ) -
0184                            gapHlZ) < 1e-10);
0185 
0186       double gap1Z = (-2 * hlZ * shift) + hlZ + gapHlZ;
0187       double gap2Z = (2 * hlZ * shift) - hlZ - gapHlZ;
0188 
0189       Transform3 gap1Transform = base * Translation3{0_mm, 0_mm, gap1Z};
0190       Transform3 gap2Transform = base * Translation3{0_mm, 0_mm, gap2Z};
0191 
0192       CHECK_CLOSE_OR_SMALL(gap1->transform().matrix(), gap1Transform.matrix(),
0193                            1e-10, 1e-14);
0194       CHECK_CLOSE_OR_SMALL(gap2->transform().matrix(), gap2Transform.matrix(),
0195                            1e-10, 1e-14);
0196 
0197       // Original volumes did not changes bounds
0198       for (const auto& [volume, bounds] : zip(origVolumes, originalBounds)) {
0199         const auto* newBounds =
0200             dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
0201         BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0202                           bounds.get(CylinderVolumeBounds::eHalfLengthZ));
0203       }
0204 
0205       // No expansion, original volumes did not move
0206       BOOST_CHECK_EQUAL(vol1->transform().matrix(), transform1.matrix());
0207       BOOST_CHECK_EQUAL(vol2->transform().matrix(), transform2.matrix());
0208       BOOST_CHECK_EQUAL(vol3->transform().matrix(), transform3.matrix());
0209 
0210     } else if (strategy == CylinderVolumeStack::AttachmentStrategy::First) {
0211       // No gap volumes were added
0212       BOOST_CHECK_EQUAL(volumes.size(), 3);
0213 
0214       double wGap = (shift - 1.0) * hlZ * 2;
0215 
0216       // Volume 1 got bigger and shifted right
0217       auto newBounds1 =
0218           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
0219       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0220                         hlZ + wGap / 2.0);
0221       double pZ1 = -2 * hlZ * shift + wGap / 2.0;
0222       Transform3 expectedTransform1 = base * Translation3{0_mm, 0_mm, pZ1};
0223       CHECK_CLOSE_OR_SMALL(vol1->transform().matrix(),
0224                            expectedTransform1.matrix(), 1e-10, 1e-14);
0225 
0226       // Volume 2 got bigger and shifted left
0227       auto newBounds2 =
0228           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
0229       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0230                         hlZ + wGap / 2.0);
0231       double pZ2 = wGap / 2.0;
0232       Transform3 expectedTransform2 = base * Translation3{0_mm, 0_mm, pZ2};
0233       CHECK_CLOSE_OR_SMALL(vol2->transform().matrix(),
0234                            expectedTransform2.matrix(), 1e-10, 1e-14);
0235 
0236       // Volume 3 stayed the same
0237       auto newBounds3 =
0238           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
0239       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eHalfLengthZ),
0240                         hlZ);
0241       double pZ3 = 2 * hlZ * shift;
0242       Transform3 expectedTransform3 = base * Translation3{0_mm, 0_mm, pZ3};
0243       CHECK_CLOSE_OR_SMALL(vol3->transform().matrix(),
0244                            expectedTransform3.matrix(), 1e-10, 1e-14);
0245     } else if (strategy == CylinderVolumeStack::AttachmentStrategy::Second) {
0246       // No gap volumes were added
0247       BOOST_CHECK_EQUAL(volumes.size(), 3);
0248 
0249       double wGap = (shift - 1.0) * hlZ * 2;
0250 
0251       // Volume 1 stayed the same
0252       auto newBounds1 =
0253           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
0254       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0255                         hlZ);
0256       double pZ1 = -2 * hlZ * shift;
0257       Transform3 expectedTransform1 = base * Translation3{0_mm, 0_mm, pZ1};
0258       CHECK_CLOSE_OR_SMALL(vol1->transform().matrix(),
0259                            expectedTransform1.matrix(), 1e-10, 1e-14);
0260 
0261       // Volume 2 got bigger and shifted left
0262       auto newBounds2 =
0263           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
0264       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0265                         hlZ + wGap / 2.0);
0266       double pZ2 = -wGap / 2.0;
0267       Transform3 expectedTransform2 = base * Translation3{0_mm, 0_mm, pZ2};
0268       CHECK_CLOSE_OR_SMALL(vol2->transform().matrix(),
0269                            expectedTransform2.matrix(), 1e-10, 1e-14);
0270 
0271       // Volume 3 got bigger and shifted left
0272       auto newBounds3 =
0273           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
0274       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eHalfLengthZ),
0275                         hlZ + wGap / 2.0);
0276       double pZ3 = 2 * hlZ * shift - wGap / 2.0;
0277       Transform3 expectedTransform3 = base * Translation3{0_mm, 0_mm, pZ3};
0278       CHECK_CLOSE_OR_SMALL(vol3->transform().matrix(),
0279                            expectedTransform3.matrix(), 1e-10, 1e-14);
0280     } else if (strategy == CylinderVolumeStack::AttachmentStrategy::Midpoint) {
0281       // No gap volumes were added
0282       BOOST_CHECK_EQUAL(volumes.size(), 3);
0283 
0284       double wGap = (shift - 1.0) * hlZ * 2;
0285 
0286       // Volume 1 got bigger and shifted right
0287       auto newBounds1 =
0288           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
0289       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0290                         hlZ + wGap / 4.0);
0291       double pZ1 = -2 * hlZ * shift + wGap / 4.0;
0292       Transform3 expectedTransform1 = base * Translation3{0_mm, 0_mm, pZ1};
0293       CHECK_CLOSE_OR_SMALL(vol1->transform().matrix(),
0294                            expectedTransform1.matrix(), 1e-10, 1e-14);
0295 
0296       // Volume 2 got bigger but didn't move
0297       auto newBounds2 =
0298           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
0299       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0300                         hlZ + wGap / 2.0);
0301       CHECK_CLOSE_OR_SMALL(vol2->transform().matrix(), base.matrix(), 1e-10,
0302                            1e-14);
0303 
0304       // Volume 3 got bigger and shifted left
0305       auto newBounds3 =
0306           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
0307       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eHalfLengthZ),
0308                         hlZ + wGap / 4.0);
0309       double pZ3 = 2 * hlZ * shift - wGap / 4.0;
0310       Transform3 expectedTransform3 = base * Translation3{0_mm, 0_mm, pZ3};
0311       CHECK_CLOSE_OR_SMALL(vol3->transform().matrix(),
0312                            expectedTransform3.matrix(), 1e-10, 1e-14);
0313     }
0314   }
0315 }
0316 
0317 BOOST_AUTO_TEST_CASE(Asymmetric) {
0318   double hlZ1 = 200_mm;
0319   double pZ1 = -1100_mm;
0320   double hlZ2 = 600_mm;
0321   double pZ2 = -200_mm;
0322   double hlZ3 = 400_mm;
0323   double pZ3 = 850_mm;
0324 
0325   // Cylinder volumes which already line up, but have different1 radii
0326   auto bounds1 = std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, hlZ1);
0327   auto bounds2 = std::make_shared<CylinderVolumeBounds>(200_mm, 600_mm, hlZ2);
0328   auto bounds3 = std::make_shared<CylinderVolumeBounds>(300_mm, 500_mm, hlZ3);
0329 
0330   Transform3 transform1 = Transform3::Identity();
0331   transform1.translate(Vector3{0_mm, 0_mm, pZ1});
0332   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
0333 
0334   Transform3 transform2 = Transform3::Identity();
0335   transform2.translate(Vector3{0_mm, 0_mm, pZ2});
0336   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
0337 
0338   Transform3 transform3 = Transform3::Identity();
0339   transform3.translate(Vector3{0_mm, 0_mm, pZ3});
0340   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
0341 
0342   std::vector<Volume*> volumes = {vol2.get(), vol1.get(), vol3.get()};
0343 
0344   CylinderVolumeStack cylStack(volumes, AxisDirection::AxisZ,
0345                                CylinderVolumeStack::AttachmentStrategy::Gap,
0346                                CylinderVolumeStack::ResizeStrategy::Gap,
0347                                *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                                CylinderVolumeStack::ResizeStrategy::Gap,
0389                                *logger);
0390 
0391   auto stackBounds =
0392       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0393   BOOST_REQUIRE(stackBounds != nullptr);
0394   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
0395   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMaxR), 400_mm);
0396   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0397                     2 * hlZ + gap / 2.0);
0398 
0399   auto newBounds1 =
0400       dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
0401   auto newBounds2 =
0402       dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
0403 
0404   for (const auto& bounds : {newBounds1, newBounds2}) {
0405     BOOST_CHECK_EQUAL(bounds->get(CylinderVolumeBounds::eMinR), 100_mm);
0406     BOOST_CHECK_EQUAL(bounds->get(CylinderVolumeBounds::eMaxR), 400_mm);
0407   }
0408 
0409   if (strategy == CylinderVolumeStack::AttachmentStrategy::Gap) {
0410     // Volumes stayed at the same position, not resized
0411     BOOST_CHECK_EQUAL(vol1->center()[eZ], -hlZ - gap / 2.0 + shift);
0412     BOOST_CHECK_EQUAL(vol2->center()[eZ], hlZ + gap / 2.0 + shift);
0413     BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
0414     BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
0415   } else if (strategy == CylinderVolumeStack::AttachmentStrategy::First) {
0416     // Left volume moved, got resized
0417     BOOST_CHECK_EQUAL(vol1->center()[eZ], -hlZ + shift);
0418     BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0419                       hlZ + gap / 2.0);
0420     // Right volume stayed the same
0421     BOOST_CHECK_EQUAL(vol2->center()[eZ], hlZ + gap / 2.0 + shift);
0422     BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
0423   } else if (strategy == CylinderVolumeStack::AttachmentStrategy::Second) {
0424     // Left volume stayed the same
0425     BOOST_CHECK_EQUAL(vol1->center()[eZ], -hlZ - gap / 2.0 + shift);
0426     BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
0427     // Right volume moved, got resized
0428     BOOST_CHECK_EQUAL(vol2->center()[eZ], hlZ + shift);
0429     BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0430                       hlZ + gap / 2.0);
0431   } else if (strategy == CylinderVolumeStack::AttachmentStrategy::Midpoint) {
0432     // Left volume moved, got resized
0433     BOOST_CHECK_EQUAL(vol1->center()[eZ], -hlZ - gap / 4.0 + shift);
0434     BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0435                       hlZ + gap / 4.0);
0436 
0437     // Right volume moved, got resized
0438     BOOST_CHECK_EQUAL(vol2->center()[eZ], hlZ + gap / 4.0 + shift);
0439     BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0440                       hlZ + gap / 4.0);
0441   }
0442 }
0443 
0444 BOOST_DATA_TEST_CASE(UpdateStack,
0445                      (boost::unit_test::data::xrange(-135, 180, 45) *
0446                       boost::unit_test::data::make(Vector3{0_mm, 0_mm, 0_mm},
0447                                                    Vector3{20_mm, 0_mm, 0_mm},
0448                                                    Vector3{0_mm, 20_mm, 0_mm},
0449                                                    Vector3{20_mm, 20_mm, 0_mm},
0450                                                    Vector3{0_mm, 0_mm, 20_mm}) *
0451                       boost::unit_test::data::make(-100_mm, 0_mm, 100_mm) *
0452                       boost::unit_test::data::make(resizeStrategies)),
0453                      angle, offset, zshift, strategy) {
0454   double hlZ = 400_mm;
0455 
0456   // Cylinder volumes which already line up, but have different1 radii
0457   auto bounds1 = std::make_shared<CylinderVolumeBounds>(100_mm, 600_mm, hlZ);
0458   auto bounds2 = std::make_shared<CylinderVolumeBounds>(100_mm, 600_mm, hlZ);
0459   auto bounds3 = std::make_shared<CylinderVolumeBounds>(100_mm, 600_mm, hlZ);
0460 
0461   Transform3 base = AngleAxis3(angle * 1_degree, Vector3::UnitX()) *
0462                     Translation3(offset + Vector3{0_mm, 0_mm, zshift});
0463 
0464   Transform3 transform1 = base;
0465   transform1.translate(Vector3{0_mm, 0_mm, -2 * hlZ});
0466   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
0467 
0468   Transform3 transform2 = base;
0469   transform2.translate(Vector3{0_mm, 0_mm, 0_mm});
0470   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
0471 
0472   Transform3 transform3 = base;
0473   transform3.translate(Vector3{0_mm, 0_mm, 2 * hlZ});
0474   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
0475 
0476   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
0477   std::vector<Volume*> originalVolumes = volumes;
0478 
0479   std::vector<Transform3> originalTransforms = {transform1, transform2,
0480                                                 transform3};
0481 
0482   CylinderVolumeStack cylStack(
0483       volumes, AxisDirection::AxisZ,
0484       CylinderVolumeStack::AttachmentStrategy::Gap,  // should not make a
0485                                                      // difference
0486       strategy, *logger);
0487 
0488   const auto* originalBounds =
0489       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0490 
0491   auto assertOriginalBounds = [&]() {
0492     const auto* cylBounds =
0493         dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0494     BOOST_REQUIRE(cylBounds != nullptr);
0495     BOOST_CHECK_EQUAL(cylBounds, originalBounds);
0496     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
0497     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 600_mm);
0498     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0499                       3 * hlZ);
0500   };
0501 
0502   assertOriginalBounds();
0503 
0504   {
0505     // Assign a copy of the identical bounds gives identical bounds
0506     auto bounds = std::make_shared<CylinderVolumeBounds>(
0507         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0508     cylStack.update(bounds, std::nullopt, *logger);
0509     assertOriginalBounds();
0510   }
0511 
0512   {
0513     // Cannot increase mininmum r
0514     auto bounds = std::make_shared<CylinderVolumeBounds>(
0515         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0516     bounds->set(CylinderVolumeBounds::eMinR, 200_mm);
0517     BOOST_CHECK_THROW(cylStack.update(bounds, std::nullopt, *logger),
0518                       std::invalid_argument);
0519     assertOriginalBounds();
0520   }
0521 
0522   {
0523     // Cannot decrease maximum r
0524     auto bounds = std::make_shared<CylinderVolumeBounds>(
0525         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0526     bounds->set(CylinderVolumeBounds::eMaxR, 500_mm);
0527     BOOST_CHECK_THROW(cylStack.update(bounds, std::nullopt, *logger),
0528                       std::invalid_argument);
0529     assertOriginalBounds();
0530   }
0531 
0532   {
0533     // Cannot decrease half length z
0534     auto bounds = std::make_shared<CylinderVolumeBounds>(
0535         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0536     bounds->set(CylinderVolumeBounds::eHalfLengthZ, 2 * hlZ);
0537     BOOST_CHECK_THROW(cylStack.update(bounds, std::nullopt, *logger),
0538                       std::invalid_argument);
0539     assertOriginalBounds();
0540   }
0541 
0542   {
0543     // Reduce minimum r
0544     auto bounds = std::make_shared<CylinderVolumeBounds>(
0545         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0546     bounds->set(CylinderVolumeBounds::eMinR, 50_mm);
0547     cylStack.update(bounds, std::nullopt, *logger);
0548     const auto* cylBounds =
0549         dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0550     BOOST_REQUIRE(cylBounds != nullptr);
0551     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
0552     // Rest unchanged
0553     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 600_mm);
0554     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0555                       3 * hlZ);
0556 
0557     // No gap volumes were added
0558     BOOST_CHECK_EQUAL(volumes.size(), 3);
0559 
0560     // All volumes reduces min r to accommodate
0561     for (const auto& [volume, origTransform] :
0562          zip(volumes, originalTransforms)) {
0563       const auto* newBounds =
0564           dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
0565       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
0566       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMaxR), 600_mm);
0567       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0568                         hlZ);
0569 
0570       // Position stayed the same
0571       BOOST_CHECK_EQUAL(volume->transform().matrix(), origTransform.matrix());
0572     }
0573   }
0574 
0575   {
0576     // Increase maximum r
0577     auto bounds = std::make_shared<CylinderVolumeBounds>(
0578         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0579     bounds->set(CylinderVolumeBounds::eMaxR, 700_mm);
0580     cylStack.update(bounds, std::nullopt, *logger);
0581     const auto* cylBounds =
0582         dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0583     BOOST_REQUIRE(cylBounds != nullptr);
0584     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 700_mm);
0585     // Rest as before
0586     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
0587     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0588                       3 * hlZ);
0589 
0590     // No gap volumes were added
0591     BOOST_CHECK_EQUAL(volumes.size(), 3);
0592 
0593     // All volumes reduces min r to accommodate
0594     for (const auto& [volume, origTransform] :
0595          zip(volumes, originalTransforms)) {
0596       const auto* newBounds =
0597           dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
0598       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
0599       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMaxR), 700_mm);
0600       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0601                         hlZ);
0602 
0603       // Position stayed the same
0604       BOOST_CHECK_EQUAL(volume->transform().matrix(), origTransform.matrix());
0605     }
0606   }
0607 
0608   {
0609     // Increase half length z
0610     auto bounds = std::make_shared<CylinderVolumeBounds>(
0611         dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0612     bounds->set(CylinderVolumeBounds::eHalfLengthZ, 4 * hlZ);
0613     cylStack.update(bounds, std::nullopt, *logger);
0614     const auto* cylBounds =
0615         dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0616     BOOST_REQUIRE(cylBounds != nullptr);
0617     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0618                       4 * hlZ);
0619 
0620     // Rest as before
0621     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
0622     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 700_mm);
0623 
0624     if (strategy == CylinderVolumeStack::ResizeStrategy::Expand) {
0625       // No gap volumes were added
0626       BOOST_CHECK_EQUAL(volumes.size(), 3);
0627 
0628       // Volume 1 got bigger and shifted left
0629       auto newBounds1 =
0630           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
0631       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0632                         hlZ + hlZ / 2.0);
0633       Transform3 expectedTransform1 =
0634           base * Translation3{0_mm, 0_mm, -2 * hlZ - hlZ / 2.0};
0635       BOOST_CHECK_EQUAL(vol1->transform().matrix(),
0636                         expectedTransform1.matrix());
0637 
0638       // Volume 2 stayed the same
0639       auto newBounds2 =
0640           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
0641       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0642                         hlZ);
0643       BOOST_CHECK_EQUAL(vol2->transform().matrix(), transform2.matrix());
0644 
0645       // Volume 3 got bigger and shifted right
0646       auto newBounds3 =
0647           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
0648       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eHalfLengthZ),
0649                         hlZ + hlZ / 2.0);
0650       Transform3 expectedTransform3 =
0651           base * Translation3{0_mm, 0_mm, 2 * hlZ + hlZ / 2.0};
0652       BOOST_CHECK_EQUAL(vol3->transform().matrix(),
0653                         expectedTransform3.matrix());
0654     } else if (strategy == CylinderVolumeStack::ResizeStrategy::Gap) {
0655       // Gap volumes were added
0656       BOOST_CHECK_EQUAL(volumes.size(), 5);
0657 
0658       for (const auto& [volume, origTransform] :
0659            zip(originalVolumes, originalTransforms)) {
0660         const auto* newBounds =
0661             dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
0662         BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
0663         BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMaxR), 700_mm);
0664         BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0665                           hlZ);
0666         // Position stayed the same
0667         BOOST_CHECK_EQUAL(volume->transform().matrix(), origTransform.matrix());
0668       }
0669 
0670       auto gap1 = volumes.front();
0671       auto gap2 = volumes.back();
0672 
0673       const auto* gapBounds1 =
0674           dynamic_cast<const CylinderVolumeBounds*>(&gap1->volumeBounds());
0675       const auto* gapBounds2 =
0676           dynamic_cast<const CylinderVolumeBounds*>(&gap2->volumeBounds());
0677 
0678       BOOST_CHECK_EQUAL(gapBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
0679                         hlZ / 2.0);
0680       BOOST_CHECK_EQUAL(gapBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
0681                         hlZ / 2.0);
0682 
0683       Transform3 gap1Transform =
0684           base * Translation3{0_mm, 0_mm, -3 * hlZ - hlZ / 2.0};
0685       Transform3 gap2Transform =
0686           base * Translation3{0_mm, 0_mm, 3 * hlZ + hlZ / 2.0};
0687 
0688       CHECK_CLOSE_OR_SMALL(gap1->transform().matrix(), gap1Transform.matrix(),
0689                            1e-10, 1e-14);
0690       CHECK_CLOSE_OR_SMALL(gap2->transform().matrix(), gap2Transform.matrix(),
0691                            1e-10, 1e-14);
0692     }
0693   }
0694 }
0695 
0696 BOOST_DATA_TEST_CASE(
0697     UpdateStackOneSided,
0698     (boost::unit_test::data::make(-1.0, 1.0) ^
0699      boost::unit_test::data::make(CylinderVolumeStack::ResizeStrategy::Gap,
0700                                   CylinderVolumeStack::ResizeStrategy::Expand)),
0701     f, strategy) {
0702   auto trf = Transform3::Identity();
0703 
0704   auto trf1 = trf * Translation3{Vector3{0_mm, 0_mm, -500_mm}};
0705   auto vol1 = std::make_shared<Volume>(
0706       trf1, std::make_shared<CylinderVolumeBounds>(100_mm, 300_mm, 400_mm));
0707 
0708   auto trf2 = trf * Translation3{Vector3{0_mm, 0_mm, 500_mm}};
0709   auto vol2 = std::make_shared<Volume>(
0710       trf2, std::make_shared<CylinderVolumeBounds>(100_mm, 300_mm, 400_mm));
0711 
0712   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
0713 
0714   CylinderVolumeStack cylStack{volumes, AxisDirection::AxisZ,
0715                                CylinderVolumeStack::AttachmentStrategy::Gap,
0716                                strategy, *logger};
0717   const auto* originalBounds =
0718       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0719 
0720   // Increase halflength by 50mm
0721   auto newBounds = std::make_shared<CylinderVolumeBounds>(
0722       dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
0723   newBounds->set(CylinderVolumeBounds::eHalfLengthZ, 950_mm);
0724   // Shift to +z by 50mm
0725   trf *= Translation3{Vector3{0_mm, 0_mm, f * 50_mm}};
0726   // -> left edge should stay at -400mm, right edge should be at 500mm or the
0727   // other direction
0728 
0729   auto checkUnchanged = [&]() {
0730     const auto* cylBounds =
0731         dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0732     BOOST_REQUIRE(cylBounds != nullptr);
0733     BOOST_CHECK_EQUAL(*cylBounds, *originalBounds);
0734   };
0735 
0736   // Invalid: shift too far in z
0737   BOOST_CHECK_THROW(
0738       cylStack.update(newBounds, trf * Translation3{Vector3{0, 0, f * 20_mm}},
0739                       *logger),
0740       std::invalid_argument);
0741   checkUnchanged();
0742 
0743   // Invalid: shift in x
0744   BOOST_CHECK_THROW(
0745       cylStack.update(newBounds, trf * Translation3{Vector3{10_mm, 0, 0}},
0746                       *logger),
0747       std::invalid_argument);
0748   checkUnchanged();
0749 
0750   // Invalid: shift in y
0751   BOOST_CHECK_THROW(
0752       cylStack.update(newBounds, trf * Translation3{Vector3{0, 10_mm, 0}},
0753                       *logger),
0754       std::invalid_argument);
0755   checkUnchanged();
0756 
0757   // Invalid: rotation
0758   BOOST_CHECK_THROW(
0759       cylStack.update(newBounds, trf * AngleAxis3{10_degree, Vector3::UnitY()},
0760                       *logger),
0761       std::invalid_argument);
0762   checkUnchanged();
0763 
0764   cylStack.update(newBounds, trf, *logger);
0765 
0766   BOOST_CHECK_EQUAL(cylStack.transform().matrix(), trf.matrix());
0767   const auto* cylBounds =
0768       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
0769   BOOST_REQUIRE(cylBounds != nullptr);
0770   BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ), 950_mm);
0771 
0772   // All volumes including gaps should have same r size
0773   for (const auto* vol : volumes) {
0774     const auto* volBounds =
0775         dynamic_cast<const CylinderVolumeBounds*>(&vol->volumeBounds());
0776     BOOST_REQUIRE(volBounds != nullptr);
0777     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
0778     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eMaxR), 300_mm);
0779   }
0780 
0781   if (strategy == CylinderVolumeStack::ResizeStrategy::Expand) {
0782     // No gaps were added, there was one gap initially
0783     BOOST_CHECK_EQUAL(volumes.size(), 3);
0784     const Volume* vol = nullptr;
0785     if (f < 0.0) {
0786       // first volume should have gotten bigger
0787       vol = volumes.front();
0788     } else {
0789       // last volume should have gotten bigger
0790       vol = volumes.back();
0791     }
0792 
0793     const auto* volBounds =
0794         dynamic_cast<const CylinderVolumeBounds*>(&vol->volumeBounds());
0795     BOOST_REQUIRE(volBounds != nullptr);
0796     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0797                       450_mm);
0798     BOOST_CHECK_EQUAL(vol->center()[eZ], f * 550_mm);
0799   } else if (strategy == CylinderVolumeStack::ResizeStrategy::Gap) {
0800     // One gap volume was added
0801     BOOST_CHECK_EQUAL(volumes.size(), 4);
0802 
0803     const Volume* gap = nullptr;
0804     if (f < 0.0) {
0805       gap = volumes.front();
0806     } else {
0807       gap = volumes.back();
0808     }
0809     const auto* gapBounds =
0810         dynamic_cast<const CylinderVolumeBounds*>(&gap->volumeBounds());
0811     BOOST_REQUIRE(gapBounds != nullptr);
0812 
0813     BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0814                       50_mm);
0815     BOOST_CHECK_EQUAL(gap->center()[eZ], f * 950_mm);
0816   }
0817 }
0818 
0819 BOOST_AUTO_TEST_CASE(ResizeReproduction1) {
0820   Transform3 trf1 =
0821       Transform3::Identity() * Translation3{Vector3::UnitZ() * -2000};
0822   auto bounds1 = std::make_shared<CylinderVolumeBounds>(70, 100, 100.0);
0823   Volume vol1{trf1, bounds1};
0824 
0825   std::vector<Volume*> volumes = {&vol1};
0826   CylinderVolumeStack stack(volumes, AxisDirection::AxisZ,
0827                             CylinderVolumeStack::AttachmentStrategy::Gap,
0828                             CylinderVolumeStack::ResizeStrategy::Gap, *logger);
0829 
0830   Transform3 trf2 =
0831       Transform3::Identity() * Translation3{Vector3::UnitZ() * -1500};
0832   stack.update(std::make_shared<CylinderVolumeBounds>(30.0, 100, 600), trf2,
0833                *logger);
0834 
0835   std::cout << stack.volumeBounds() << std::endl;
0836   std::cout << stack.transform().matrix() << std::endl;
0837 
0838   Transform3 trf3 =
0839       Transform3::Identity() * Translation3{Vector3::UnitZ() * -1600};
0840   stack.update(std::make_shared<CylinderVolumeBounds>(30.0, 100, 700), trf3,
0841                *logger);
0842 }
0843 
0844 BOOST_AUTO_TEST_CASE(ResizeReproduction2) {
0845   // The numbers are tuned a bit to reproduce the faulty behavior
0846   Transform3 trf1 =
0847       Transform3::Identity() * Translation3{Vector3::UnitZ() * 263};
0848   auto bounds1 = std::make_shared<CylinderVolumeBounds>(30, 100, 4.075);
0849   Volume vol1{trf1, bounds1};
0850 
0851   std::vector<Volume*> volumes = {&vol1};
0852   CylinderVolumeStack stack(volumes, AxisDirection::AxisZ,
0853                             CylinderVolumeStack::AttachmentStrategy::Gap,
0854                             CylinderVolumeStack::ResizeStrategy::Gap, *logger);
0855 
0856   Transform3 trf2 =
0857       Transform3::Identity() * Translation3{Vector3::UnitZ() * 260.843};
0858   stack.update(std::make_shared<CylinderVolumeBounds>(30.0, 100, 6.232), trf2,
0859                *logger);
0860 
0861   std::cout << stack.volumeBounds() << std::endl;
0862   std::cout << stack.transform().matrix() << std::endl;
0863 
0864   Transform3 trf3 =
0865       Transform3::Identity() * Translation3{Vector3::UnitZ() * 1627.31};
0866   stack.update(std::make_shared<CylinderVolumeBounds>(30.0, 100, 1372.699),
0867                trf3, *logger);
0868 }
0869 
0870 //   original size
0871 // <--------------->
0872 // +---------------+
0873 // |               |
0874 // |               |
0875 // |   Volume 1    |
0876 // |               |
0877 // |               |
0878 // +---------------+
0879 //         first resize
0880 // <-------------------------->
0881 // +---------------+----------+
0882 // |               |          |
0883 // |               |          |
0884 // |   Volume 1    |   Gap    |
0885 // |               |          |      Gap is
0886 // |               |          |      reused!--+
0887 // +---------------+----------+               |
0888 //             second resize                  |
0889 // <----------------------------------->      |
0890 // +---------------+-------------------+      |
0891 // |               |                   |      |
0892 // |               |                   |      |
0893 // |   Volume 1    |        Gap        |<-----+
0894 // |               |                   |
0895 // |               |                   |
0896 // +---------------+-------------------+
0897 //
0898 BOOST_AUTO_TEST_CASE(ResizeGapMultiple) {
0899   Transform3 trf = Transform3::Identity();
0900   auto bounds = std::make_shared<CylinderVolumeBounds>(70, 100, 100.0);
0901   Volume vol{trf, bounds};
0902 
0903   BOOST_TEST_CONTEXT("Positive") {
0904     std::vector<Volume*> volumes = {&vol};
0905     CylinderVolumeStack stack(volumes, AxisDirection::AxisZ,
0906                               CylinderVolumeStack::AttachmentStrategy::Gap,
0907                               CylinderVolumeStack::ResizeStrategy::Gap,
0908                               *logger);
0909 
0910     BOOST_CHECK_EQUAL(volumes.size(), 1);
0911     BOOST_CHECK(stack.gaps().empty());
0912 
0913     stack.update(std::make_shared<CylinderVolumeBounds>(30.0, 100, 200),
0914                  trf * Translation3{Vector3::UnitZ() * 100}, *logger);
0915     BOOST_CHECK_EQUAL(volumes.size(), 2);
0916     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
0917 
0918     BOOST_CHECK_EQUAL(stack.gaps().front()->center()[eZ], 200.0);
0919     const auto* cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
0920         &stack.gaps().front()->volumeBounds());
0921     BOOST_REQUIRE_NE(cylBounds, nullptr);
0922     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0923                       100.0);
0924 
0925     stack.update(std::make_shared<CylinderVolumeBounds>(30.0, 100, 300),
0926                  trf * Translation3{Vector3::UnitZ() * 200}, *logger);
0927 
0928     BOOST_CHECK_EQUAL(volumes.size(), 2);
0929     // No additional gap volume was added!
0930     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
0931 
0932     BOOST_CHECK_EQUAL(stack.gaps().front()->center()[eZ], 300.0);
0933     cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
0934         &stack.gaps().front()->volumeBounds());
0935     BOOST_REQUIRE_NE(cylBounds, nullptr);
0936     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0937                       200.0);
0938   }
0939 
0940   BOOST_TEST_CONTEXT("Negative") {
0941     std::vector<Volume*> volumes = {&vol};
0942     CylinderVolumeStack stack(volumes, AxisDirection::AxisZ,
0943                               CylinderVolumeStack::AttachmentStrategy::Gap,
0944                               CylinderVolumeStack::ResizeStrategy::Gap,
0945                               *logger);
0946 
0947     BOOST_CHECK_EQUAL(volumes.size(), 1);
0948     BOOST_CHECK(stack.gaps().empty());
0949 
0950     stack.update(std::make_shared<CylinderVolumeBounds>(30.0, 100, 200),
0951                  trf * Translation3{Vector3::UnitZ() * -100}, *logger);
0952     BOOST_CHECK_EQUAL(volumes.size(), 2);
0953     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
0954 
0955     BOOST_CHECK_EQUAL(stack.gaps().front()->center()[eZ], -200.0);
0956     const auto* cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
0957         &stack.gaps().front()->volumeBounds());
0958     BOOST_REQUIRE_NE(cylBounds, nullptr);
0959     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0960                       100.0);
0961 
0962     stack.update(std::make_shared<CylinderVolumeBounds>(30.0, 100, 300),
0963                  trf * Translation3{Vector3::UnitZ() * -200}, *logger);
0964 
0965     BOOST_CHECK_EQUAL(volumes.size(), 2);
0966     // No additional gap volume was added!
0967     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
0968 
0969     BOOST_CHECK_EQUAL(stack.gaps().front()->center()[eZ], -300.0);
0970     cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
0971         &stack.gaps().front()->volumeBounds());
0972     BOOST_REQUIRE_NE(cylBounds, nullptr);
0973     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
0974                       200.0);
0975   }
0976 }
0977 
0978 BOOST_AUTO_TEST_SUITE_END()
0979 
0980 BOOST_AUTO_TEST_SUITE(RDirection)
0981 
0982 BOOST_DATA_TEST_CASE(Baseline,
0983                      (boost::unit_test::data::xrange(-135, 180, 45) *
0984                       boost::unit_test::data::xrange(0, 2, 1) *
0985                       boost::unit_test::data::make(-0.1, 0.0, 0.1) *
0986                       boost::unit_test::data::make(Vector3{0_mm, 0_mm, 0_mm},
0987                                                    Vector3{20_mm, 0_mm, 0_mm},
0988                                                    Vector3{0_mm, 20_mm, 0_mm},
0989                                                    Vector3{20_mm, 20_mm, 0_mm},
0990                                                    Vector3{0_mm, 0_mm, 20_mm}) *
0991                       boost::unit_test::data::make(strategies)),
0992                      angle, rotate, f, offset, strategy) {
0993   double hlZ = 400_mm;
0994 
0995   double fInner = 1.0 + f;
0996   double fOuter = 1.0 - f;
0997 
0998   // Cylinder volumes which already line up in r but have different z and hl
0999   auto bounds1 = std::make_shared<CylinderVolumeBounds>(fInner * 100_mm,
1000                                                         fOuter * 300_mm, hlZ);
1001   auto bounds2 = std::make_shared<CylinderVolumeBounds>(fInner * 300_mm,
1002                                                         fOuter * 600_mm, hlZ);
1003   auto bounds3 = std::make_shared<CylinderVolumeBounds>(fInner * 600_mm,
1004                                                         fOuter * 900_mm, hlZ);
1005 
1006   Transform3 base =
1007       AngleAxis3(angle * 1_degree, Vector3::UnitX()) * Translation3(offset);
1008 
1009   // volumes are shifted in z
1010 
1011   Transform3 transform1 = base;
1012   transform1.translate(Vector3{0_mm, 0_mm, 20_mm});
1013   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
1014 
1015   Transform3 transform2 = base;
1016   transform2.translate(Vector3{0_mm, 0_mm, -30_mm});
1017   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
1018 
1019   Transform3 transform3 = base;
1020   transform3.translate(Vector3{0_mm, 0_mm, 40_mm});
1021   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
1022 
1023   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
1024   // Rotate to simulate unsorted volumes: all results should be the same!
1025   std::rotate(volumes.begin(), volumes.begin() + rotate, volumes.end());
1026 
1027   std::vector<Volume*> origVolumes = volumes;
1028 
1029   std::vector<CylinderVolumeBounds> originalBounds;
1030   std::transform(
1031       volumes.begin(), volumes.end(), std::back_inserter(originalBounds),
1032       [](const auto& vol) {
1033         return dynamic_cast<const CylinderVolumeBounds&>(vol->volumeBounds());
1034       });
1035 
1036   if (f < 0.0) {
1037     BOOST_CHECK_THROW(
1038         CylinderVolumeStack(volumes, AxisDirection::AxisR, strategy,
1039                             CylinderVolumeStack::ResizeStrategy::Gap, *logger),
1040         std::invalid_argument);
1041     return;
1042   }
1043 
1044   CylinderVolumeStack cylStack(volumes, AxisDirection::AxisR, strategy,
1045                                CylinderVolumeStack::ResizeStrategy::Gap,
1046                                *logger);
1047 
1048   auto stackBounds =
1049       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
1050   BOOST_REQUIRE(stackBounds != nullptr);
1051 
1052   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMinR),
1053                     fInner * 100_mm);
1054   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eMaxR),
1055                     fOuter * 900_mm);
1056   double expectedHalfLengthZ = (40_mm + 30_mm + 2 * hlZ) / 2.0;
1057   BOOST_CHECK_EQUAL(stackBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1058                     expectedHalfLengthZ);
1059 
1060   // After synchronization, all volumes should have the same z position and half
1061   // length
1062   // This includes possible gap volumes!
1063   Transform3 commonTransform = base * Translation3{0_mm, 0_mm, 5_mm};
1064 
1065   CHECK_CLOSE_OR_SMALL(cylStack.transform().matrix(), commonTransform.matrix(),
1066                        1e-10, 1e-14);
1067 
1068   for (const auto& volume : volumes) {
1069     const auto* cylinderBounds =
1070         dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
1071     BOOST_REQUIRE(cylinderBounds != nullptr);
1072     BOOST_CHECK_EQUAL(cylinderBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1073                       expectedHalfLengthZ);
1074   }
1075 
1076   BOOST_CHECK_EQUAL(
1077       dynamic_cast<const CylinderVolumeBounds&>(vol1->volumeBounds())
1078           .get(CylinderVolumeBounds::eMinR),
1079       fInner * 100_mm);
1080 
1081   BOOST_CHECK_EQUAL(
1082       dynamic_cast<const CylinderVolumeBounds&>(vol3->volumeBounds())
1083           .get(CylinderVolumeBounds::eMaxR),
1084       fOuter * 900_mm);
1085 
1086   // Volumes are sorted in r
1087   for (std::size_t i = 0; i < volumes.size() - 1; ++i) {
1088     const auto& a = volumes.at(i);
1089     const auto& b = volumes.at(i + 1);
1090 
1091     const auto* aBounds =
1092         dynamic_cast<const CylinderVolumeBounds*>(&a->volumeBounds());
1093     const auto* bBounds =
1094         dynamic_cast<const CylinderVolumeBounds*>(&b->volumeBounds());
1095 
1096     double aMidR = (aBounds->get(CylinderVolumeBounds::eMinR) +
1097                     aBounds->get(CylinderVolumeBounds::eMaxR)) /
1098                    2.0;
1099 
1100     double bMidR = (bBounds->get(CylinderVolumeBounds::eMinR) +
1101                     bBounds->get(CylinderVolumeBounds::eMaxR)) /
1102                    2.0;
1103 
1104     BOOST_CHECK_LT(aMidR, bMidR);
1105   }
1106 
1107   if (f == 0.0) {
1108     // No gap volumes were added
1109     BOOST_CHECK_EQUAL(volumes.size(), 3);
1110 
1111     // Original volumes did not change r bounds
1112     for (const auto& [volume, origCylBounds] :
1113          zip(origVolumes, originalBounds)) {
1114       const auto* newBounds =
1115           dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
1116       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMinR),
1117                         origCylBounds.get(CylinderVolumeBounds::eMinR));
1118       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMaxR),
1119                         origCylBounds.get(CylinderVolumeBounds::eMaxR));
1120     }
1121   } else {
1122     const auto* newBounds1 =
1123         dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
1124     const auto* newBounds2 =
1125         dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
1126     const auto* newBounds3 =
1127         dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
1128     if (strategy == CylinderVolumeStack::AttachmentStrategy::Gap) {
1129       // Two gap volumes were added
1130       BOOST_CHECK_EQUAL(volumes.size(), 5);
1131 
1132       // Original volumes did not change r bounds
1133       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMinR),
1134                         fInner * 100_mm);
1135       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMaxR),
1136                         fOuter * 300_mm);
1137       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMinR),
1138                         fInner * 300_mm);
1139       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMaxR),
1140                         fOuter * 600_mm);
1141       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMinR),
1142                         fInner * 600_mm);
1143       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMaxR),
1144                         fOuter * 900_mm);
1145 
1146       auto gap1 = volumes.at(1);
1147       auto gap2 = volumes.at(3);
1148 
1149       const auto* gapBounds1 =
1150           dynamic_cast<const CylinderVolumeBounds*>(&gap1->volumeBounds());
1151       const auto* gapBounds2 =
1152           dynamic_cast<const CylinderVolumeBounds*>(&gap2->volumeBounds());
1153 
1154       BOOST_CHECK_EQUAL(gapBounds1->get(CylinderVolumeBounds::eMinR),
1155                         fOuter * 300_mm);
1156       BOOST_CHECK_EQUAL(gapBounds1->get(CylinderVolumeBounds::eMaxR),
1157                         fInner * 300_mm);
1158       BOOST_CHECK_EQUAL(gapBounds2->get(CylinderVolumeBounds::eMinR),
1159                         fOuter * 600_mm);
1160       BOOST_CHECK_EQUAL(gapBounds2->get(CylinderVolumeBounds::eMaxR),
1161                         fInner * 600_mm);
1162 
1163     } else if (strategy == CylinderVolumeStack::AttachmentStrategy::First) {
1164       // No gap volumes were added
1165       BOOST_CHECK_EQUAL(volumes.size(), 3);
1166 
1167       // Volume 1 got bigger and grew to meet Volume 2
1168       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMinR),
1169                         fInner * 100_mm);
1170       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMaxR),
1171                         fInner * 300_mm);
1172 
1173       // Volume 2 got bigger and grew to meet Volume 3
1174       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMinR),
1175                         fInner * 300_mm);
1176       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMaxR),
1177                         fInner * 600_mm);
1178 
1179       // Volume 3 stayed the same
1180       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMinR),
1181                         fInner * 600_mm);
1182       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMaxR),
1183                         fOuter * 900_mm);
1184 
1185     } else if (strategy == CylinderVolumeStack::AttachmentStrategy::Second) {
1186       // No gap volumes were added
1187       BOOST_CHECK_EQUAL(volumes.size(), 3);
1188 
1189       // Volume 1 stayed the same
1190       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMinR),
1191                         fInner * 100_mm);
1192       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMaxR),
1193                         fOuter * 300_mm);
1194 
1195       // Volume 2 got bigger and grew inward to meet Volume 1
1196       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMinR),
1197                         fOuter * 300_mm);
1198       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMaxR),
1199                         fOuter * 600_mm);
1200 
1201       // Volume 3 got bigger and grew inward to meet Volume 2
1202       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMinR),
1203                         fOuter * 600_mm);
1204       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMaxR),
1205                         fOuter * 900_mm);
1206     } else if (strategy == CylinderVolumeStack::AttachmentStrategy::Midpoint) {
1207       // No gap volumes were added
1208       BOOST_CHECK_EQUAL(volumes.size(), 3);
1209 
1210       // Volume 1 grew outward to meet Volume 2 half way
1211       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMinR),
1212                         fInner * 100_mm);
1213       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMaxR),
1214                         (fOuter * 300_mm + fInner * 300_mm) / 2.0);
1215 
1216       // Volume 2 grew inward and outward to meet Volume 1 and 3 half way
1217       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMinR),
1218                         (fOuter * 300_mm + fInner * 300_mm) / 2.0);
1219       BOOST_CHECK_EQUAL(newBounds2->get(CylinderVolumeBounds::eMaxR),
1220                         (fOuter * 600_mm + fInner * 600_mm) / 2.0);
1221 
1222       // Volume 3 grew inward to meet Volume 2 half way
1223       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMinR),
1224                         (fOuter * 600_mm + fInner * 600_mm) / 2.0);
1225       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMaxR),
1226                         fOuter * 900_mm);
1227     }
1228   }
1229 }
1230 
1231 BOOST_DATA_TEST_CASE(UpdateStack,
1232                      (boost::unit_test::data::xrange(-135, 180, 45) *
1233                       boost::unit_test::data::make(Vector3{0_mm, 0_mm, 0_mm},
1234                                                    Vector3{20_mm, 0_mm, 0_mm},
1235                                                    Vector3{0_mm, 20_mm, 0_mm},
1236                                                    Vector3{20_mm, 20_mm, 0_mm},
1237                                                    Vector3{0_mm, 0_mm, 20_mm}) *
1238                       boost::unit_test::data::make(-100_mm, 0_mm, 100_mm) *
1239                       boost::unit_test::data::make(resizeStrategies)),
1240                      angle, offset, zshift, strategy) {
1241   double hlZ = 400_mm;
1242 
1243   // Cylinder volumes which already line up in r but have different z and hl
1244   auto bounds1 = std::make_shared<CylinderVolumeBounds>(100_mm, 300_mm, hlZ);
1245   auto bounds2 = std::make_shared<CylinderVolumeBounds>(300_mm, 600_mm, hlZ);
1246   auto bounds3 = std::make_shared<CylinderVolumeBounds>(600_mm, 900_mm, hlZ);
1247 
1248   Transform3 base = AngleAxis3(angle * 1_degree, Vector3::UnitX()) *
1249                     Translation3(offset + Vector3{0, 0, zshift});
1250 
1251   // volumes are shifted in z
1252   auto vol1 = std::make_shared<Volume>(base, bounds1);
1253   auto vol2 = std::make_shared<Volume>(base, bounds2);
1254   auto vol3 = std::make_shared<Volume>(base, bounds3);
1255 
1256   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
1257   std::vector<Volume*> originalVolumes = volumes;
1258 
1259   std::vector<CylinderVolumeBounds> originalBounds;
1260 
1261   std::transform(
1262       volumes.begin(), volumes.end(), std::back_inserter(originalBounds),
1263       [](const auto& vol) {
1264         return *dynamic_cast<const CylinderVolumeBounds*>(&vol->volumeBounds());
1265       });
1266 
1267   const CylinderVolumeBounds* originalOuterBounds = nullptr;
1268 
1269   std::unique_ptr<CylinderVolumeStack> cylStack;
1270 
1271   auto resetCylStack = [&]() {
1272     volumes = originalVolumes;
1273 
1274     for (const auto& [volume, origBounds] : zip(volumes, originalBounds)) {
1275       volume->assignVolumeBounds(
1276           std::make_shared<CylinderVolumeBounds>(origBounds));
1277     }
1278 
1279     cylStack = std::make_unique<CylinderVolumeStack>(
1280         volumes, AxisDirection::AxisR,
1281         CylinderVolumeStack::AttachmentStrategy::Gap,  // should not make a
1282                                                        // difference
1283         strategy, *logger);
1284 
1285     originalOuterBounds =
1286         dynamic_cast<const CylinderVolumeBounds*>(&cylStack->volumeBounds());
1287   };
1288 
1289   resetCylStack();
1290 
1291   auto assertInitialVolumesUnchanged = [&]() {
1292     for (const auto& [volume, origCylBounds] :
1293          zip(originalVolumes, originalBounds)) {
1294       const auto* newBounds =
1295           dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
1296       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMinR),
1297                         origCylBounds.get(CylinderVolumeBounds::eMinR));
1298       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMaxR),
1299                         origCylBounds.get(CylinderVolumeBounds::eMaxR));
1300       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1301                         origCylBounds.get(CylinderVolumeBounds::eHalfLengthZ));
1302       BOOST_CHECK_EQUAL(volume->transform().matrix(), base.matrix());
1303     }
1304   };
1305 
1306   auto assertOriginalBounds = [&]() {
1307     const auto* cylBounds =
1308         dynamic_cast<const CylinderVolumeBounds*>(&cylStack->volumeBounds());
1309     BOOST_REQUIRE(cylBounds != nullptr);
1310     BOOST_CHECK_EQUAL(cylBounds, originalOuterBounds);
1311     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
1312     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 900_mm);
1313     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
1314   };
1315 
1316   assertOriginalBounds();
1317 
1318   {
1319     // Assign a copy of the identical bounds gives identical bounds
1320     auto bounds = std::make_shared<CylinderVolumeBounds>(
1321         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1322     cylStack->update(bounds, std::nullopt, *logger);
1323     assertOriginalBounds();
1324   }
1325 
1326   {
1327     // Cannot increase mininmum r
1328     auto bounds = std::make_shared<CylinderVolumeBounds>(
1329         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1330     bounds->set(CylinderVolumeBounds::eMinR, 200_mm);
1331     BOOST_CHECK_THROW(cylStack->update(bounds, std::nullopt, *logger),
1332                       std::invalid_argument);
1333     assertOriginalBounds();
1334   }
1335 
1336   {
1337     // Cannot decrease maximum r
1338     auto bounds = std::make_shared<CylinderVolumeBounds>(
1339         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1340     bounds->set(CylinderVolumeBounds::eMaxR, 500_mm);
1341     BOOST_CHECK_THROW(cylStack->update(bounds, std::nullopt, *logger),
1342                       std::invalid_argument);
1343     assertOriginalBounds();
1344   }
1345 
1346   {
1347     // Cannot decrease half length z
1348     auto bounds = std::make_shared<CylinderVolumeBounds>(
1349         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1350     bounds->set(CylinderVolumeBounds::eHalfLengthZ, 0.5 * hlZ);
1351     BOOST_CHECK_THROW(cylStack->update(bounds, std::nullopt, *logger),
1352                       std::invalid_argument);
1353     assertOriginalBounds();
1354   }
1355 
1356   {
1357     // Reduce minimum r
1358     auto bounds = std::make_shared<CylinderVolumeBounds>(
1359         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1360     bounds->set(CylinderVolumeBounds::eMinR, 50_mm);
1361     cylStack->update(bounds, std::nullopt, *logger);
1362     const auto* cylBounds =
1363         dynamic_cast<const CylinderVolumeBounds*>(&cylStack->volumeBounds());
1364     BOOST_REQUIRE(cylBounds != nullptr);
1365     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
1366     // Rest unchanged
1367     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 900_mm);
1368     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
1369 
1370     if (strategy == CylinderVolumeStack::ResizeStrategy::Expand) {
1371       // No gap volumes were added
1372       BOOST_CHECK_EQUAL(volumes.size(), 3);
1373 
1374       // Innermost volume reduced r size
1375       const auto* newBounds1 =
1376           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
1377       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMinR), 50_mm);
1378       // Position stayed the same
1379       BOOST_CHECK_EQUAL(vol1->transform().matrix(), base.matrix());
1380 
1381       // Other volumes are unchanged
1382       const auto* newBounds2 =
1383           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
1384       BOOST_CHECK_EQUAL(*newBounds2, originalBounds[1]);
1385       BOOST_CHECK_EQUAL(vol2->transform().matrix(), base.matrix());
1386 
1387       const auto* newBounds3 =
1388           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
1389       BOOST_CHECK_EQUAL(*newBounds3, originalBounds[2]);
1390       BOOST_CHECK_EQUAL(vol3->transform().matrix(), base.matrix());
1391 
1392     } else if (strategy == CylinderVolumeStack::ResizeStrategy::Gap) {
1393       // One gap volume was added
1394       BOOST_CHECK_EQUAL(volumes.size(), 4);
1395 
1396       auto gap = volumes.front();
1397       auto gapBounds =
1398           dynamic_cast<const CylinderVolumeBounds*>(&gap->volumeBounds());
1399       BOOST_REQUIRE(gapBounds != nullptr);
1400       BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), 50_mm);
1401       BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), 100_mm);
1402       BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1403                         hlZ);
1404       BOOST_CHECK_EQUAL(gap->transform().matrix(), base.matrix());
1405 
1406       // Other volumes are unchanged
1407       assertInitialVolumesUnchanged();
1408     }
1409   }
1410 
1411   resetCylStack();
1412 
1413   {
1414     // Increase maximum r
1415     auto bounds = std::make_shared<CylinderVolumeBounds>(
1416         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1417     bounds->set(CylinderVolumeBounds::eMaxR, 1000_mm);
1418     cylStack->update(bounds, std::nullopt, *logger);
1419     const auto* cylBounds =
1420         dynamic_cast<const CylinderVolumeBounds*>(&cylStack->volumeBounds());
1421     BOOST_REQUIRE(cylBounds != nullptr);
1422     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 1000_mm);
1423     // Rest as before
1424     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
1425     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
1426 
1427     if (strategy == CylinderVolumeStack::ResizeStrategy::Expand) {
1428       // No gap volumes were added
1429       BOOST_CHECK_EQUAL(volumes.size(), 3);
1430 
1431       // Outermost volume increased r size
1432       const auto* newBounds3 =
1433           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
1434       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMaxR), 1000_mm);
1435       // Position stayed the same
1436       BOOST_CHECK_EQUAL(vol3->transform().matrix(), base.matrix());
1437 
1438       // Other volumes are unchanged
1439       const auto* newBounds1 =
1440           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
1441       BOOST_CHECK_EQUAL(*newBounds1, originalBounds[0]);
1442       BOOST_CHECK_EQUAL(vol1->transform().matrix(), base.matrix());
1443 
1444       const auto* newBounds2 =
1445           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
1446       BOOST_CHECK_EQUAL(*newBounds2, originalBounds[1]);
1447       BOOST_CHECK_EQUAL(vol2->transform().matrix(), base.matrix());
1448 
1449     } else if (strategy == CylinderVolumeStack::ResizeStrategy::Gap) {
1450       // One gap volume was added
1451       BOOST_CHECK_EQUAL(volumes.size(), 4);
1452 
1453       auto gap = volumes.back();
1454       auto gapBounds =
1455           dynamic_cast<const CylinderVolumeBounds*>(&gap->volumeBounds());
1456       BOOST_REQUIRE(gapBounds != nullptr);
1457       BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMinR), 900_mm);
1458       BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eMaxR), 1000_mm);
1459       BOOST_CHECK_EQUAL(gapBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1460                         hlZ);
1461       BOOST_CHECK_EQUAL(gap->transform().matrix(), base.matrix());
1462 
1463       // Other volumes are unchanged
1464       assertInitialVolumesUnchanged();
1465     }
1466   }
1467 
1468   resetCylStack();
1469 
1470   {
1471     // Decrease r min and increase r max
1472     auto bounds = std::make_shared<CylinderVolumeBounds>(
1473         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1474     bounds->set({
1475         {CylinderVolumeBounds::eMinR, 0_mm},
1476         {CylinderVolumeBounds::eMaxR, 1100_mm},
1477     });
1478 
1479     cylStack->update(bounds, std::nullopt, *logger);
1480     const auto* cylBounds =
1481         dynamic_cast<const CylinderVolumeBounds*>(&cylStack->volumeBounds());
1482     BOOST_REQUIRE(cylBounds != nullptr);
1483     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 1100_mm);
1484     // Rest as before
1485     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 0_mm);
1486     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
1487 
1488     if (strategy == CylinderVolumeStack::ResizeStrategy::Expand) {
1489       // No gap volumes were added
1490       BOOST_CHECK_EQUAL(volumes.size(), 3);
1491 
1492       // Innermost volume reduced r size
1493       const auto* newBounds1 =
1494           dynamic_cast<const CylinderVolumeBounds*>(&vol1->volumeBounds());
1495       BOOST_CHECK_EQUAL(newBounds1->get(CylinderVolumeBounds::eMinR), 0_mm);
1496       // Position stayed the same
1497       BOOST_CHECK_EQUAL(vol1->transform().matrix(), base.matrix());
1498 
1499       // Middle volume is unchanged
1500       const auto* newBounds2 =
1501           dynamic_cast<const CylinderVolumeBounds*>(&vol2->volumeBounds());
1502       BOOST_CHECK_EQUAL(*newBounds2, originalBounds[1]);
1503       BOOST_CHECK_EQUAL(vol2->transform().matrix(), base.matrix());
1504 
1505       // Outermost volume increased r size
1506       const auto* newBounds3 =
1507           dynamic_cast<const CylinderVolumeBounds*>(&vol3->volumeBounds());
1508       BOOST_CHECK_EQUAL(newBounds3->get(CylinderVolumeBounds::eMaxR), 1100_mm);
1509       // Position stayed the same
1510       BOOST_CHECK_EQUAL(vol3->transform().matrix(), base.matrix());
1511 
1512     } else if (strategy == CylinderVolumeStack::ResizeStrategy::Gap) {
1513       // One gap volume was added
1514       BOOST_CHECK_EQUAL(volumes.size(), 5);
1515 
1516       auto gap1 = volumes.front();
1517       auto gapBounds1 =
1518           dynamic_cast<const CylinderVolumeBounds*>(&gap1->volumeBounds());
1519       BOOST_REQUIRE(gapBounds1 != nullptr);
1520       BOOST_CHECK_EQUAL(gapBounds1->get(CylinderVolumeBounds::eMinR), 0_mm);
1521       BOOST_CHECK_EQUAL(gapBounds1->get(CylinderVolumeBounds::eMaxR), 100_mm);
1522       BOOST_CHECK_EQUAL(gapBounds1->get(CylinderVolumeBounds::eHalfLengthZ),
1523                         hlZ);
1524       BOOST_CHECK_EQUAL(gap1->transform().matrix(), base.matrix());
1525 
1526       auto gap2 = volumes.back();
1527       auto gapBounds2 =
1528           dynamic_cast<const CylinderVolumeBounds*>(&gap2->volumeBounds());
1529       BOOST_REQUIRE(gapBounds2 != nullptr);
1530       BOOST_CHECK_EQUAL(gapBounds2->get(CylinderVolumeBounds::eMinR), 900_mm);
1531       BOOST_CHECK_EQUAL(gapBounds2->get(CylinderVolumeBounds::eMaxR), 1100_mm);
1532       BOOST_CHECK_EQUAL(gapBounds2->get(CylinderVolumeBounds::eHalfLengthZ),
1533                         hlZ);
1534 
1535       // Other volumes are unchanged
1536       assertInitialVolumesUnchanged();
1537     }
1538   }
1539 
1540   resetCylStack();
1541 
1542   {
1543     // Increase half length z
1544     auto bounds = std::make_shared<CylinderVolumeBounds>(
1545         dynamic_cast<const CylinderVolumeBounds&>(cylStack->volumeBounds()));
1546     bounds->set(CylinderVolumeBounds::eHalfLengthZ, 2 * hlZ);
1547     cylStack->update(bounds, std::nullopt, *logger);
1548     const auto* cylBounds =
1549         dynamic_cast<const CylinderVolumeBounds*>(&cylStack->volumeBounds());
1550     BOOST_REQUIRE(cylBounds != nullptr);
1551     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1552                       2 * hlZ);
1553 
1554     // Rest as before
1555     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
1556     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 900_mm);
1557 
1558     // No gap volumes were added
1559     BOOST_CHECK_EQUAL(volumes.size(), 3);
1560 
1561     for (const auto& [volume, origCylBounds] :
1562          zip(originalVolumes, originalBounds)) {
1563       const auto* newBounds =
1564           dynamic_cast<const CylinderVolumeBounds*>(&volume->volumeBounds());
1565       // Radii are all as before
1566       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMinR),
1567                         origCylBounds.get(CylinderVolumeBounds::eMinR));
1568       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eMaxR),
1569                         origCylBounds.get(CylinderVolumeBounds::eMaxR));
1570 
1571       // Half length z is changed on all
1572       BOOST_CHECK_EQUAL(newBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1573                         2 * hlZ);
1574 
1575       // Position stayed the same
1576       BOOST_CHECK_EQUAL(volume->transform().matrix(), base.matrix());
1577     }
1578   }
1579 }
1580 
1581 BOOST_DATA_TEST_CASE(
1582     UpdateStackOneSided,
1583     (boost::unit_test::data::make(-1.0, 1.0) ^
1584      boost::unit_test::data::make(CylinderVolumeStack::ResizeStrategy::Gap,
1585                                   CylinderVolumeStack::ResizeStrategy::Expand)),
1586     f, strategy) {
1587   // Strategy should not affect the sizing here at all
1588 
1589   auto trf = Transform3::Identity();
1590 
1591   auto vol1 = std::make_shared<Volume>(
1592       trf, std::make_shared<CylinderVolumeBounds>(100_mm, 300_mm, 400_mm));
1593 
1594   auto vol2 = std::make_shared<Volume>(
1595       trf, std::make_shared<CylinderVolumeBounds>(400_mm, 600_mm, 400_mm));
1596 
1597   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
1598 
1599   CylinderVolumeStack cylStack{volumes, AxisDirection::AxisR,
1600                                CylinderVolumeStack::AttachmentStrategy::Gap,
1601                                strategy, *logger};
1602   const auto* originalBounds =
1603       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
1604 
1605   // Increase halflength by 50mm
1606   auto newBounds = std::make_shared<CylinderVolumeBounds>(
1607       dynamic_cast<const CylinderVolumeBounds&>(cylStack.volumeBounds()));
1608   newBounds->set(CylinderVolumeBounds::eHalfLengthZ, 450_mm);
1609   // Shift to +z by 50mm
1610   trf *= Translation3{Vector3{0_mm, 0_mm, f * 50_mm}};
1611   // -> left edge should stay at -400mm, right edge should be at 500mm
1612 
1613   auto checkUnchanged = [&]() {
1614     const auto* cylBounds =
1615         dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
1616     BOOST_REQUIRE(cylBounds != nullptr);
1617     BOOST_CHECK_EQUAL(*cylBounds, *originalBounds);
1618   };
1619 
1620   // Invalid: shift too far in z
1621   BOOST_CHECK_THROW(
1622       cylStack.update(newBounds, trf * Translation3{Vector3{0, 0, f * 20_mm}},
1623                       *logger),
1624       std::invalid_argument);
1625   checkUnchanged();
1626 
1627   // Invalid: shift in x
1628   BOOST_CHECK_THROW(
1629       cylStack.update(newBounds, trf * Translation3{Vector3{10_mm, 0, 0}},
1630                       *logger),
1631       std::invalid_argument);
1632   checkUnchanged();
1633 
1634   // Invalid: shift in y
1635   BOOST_CHECK_THROW(
1636       cylStack.update(newBounds, trf * Translation3{Vector3{0, 10_mm, 0}},
1637                       *logger),
1638       std::invalid_argument);
1639   checkUnchanged();
1640 
1641   // Invalid: rotation
1642   BOOST_CHECK_THROW(
1643       cylStack.update(newBounds, trf * AngleAxis3{10_degree, Vector3::UnitY()},
1644                       *logger),
1645       std::invalid_argument);
1646   checkUnchanged();
1647 
1648   cylStack.update(newBounds, trf, *logger);
1649 
1650   BOOST_CHECK_EQUAL(cylStack.transform().matrix(), trf.matrix());
1651   const auto* cylBounds =
1652       dynamic_cast<const CylinderVolumeBounds*>(&cylStack.volumeBounds());
1653   BOOST_REQUIRE(cylBounds != nullptr);
1654   BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 100_mm);
1655   BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 600_mm);
1656 
1657   // All volumes including gaps should have same new position and halflength
1658   for (const auto* vol : volumes) {
1659     const auto* volBounds =
1660         dynamic_cast<const CylinderVolumeBounds*>(&vol->volumeBounds());
1661     BOOST_REQUIRE(volBounds != nullptr);
1662     BOOST_CHECK_EQUAL(vol->transform().matrix(), trf.matrix());
1663     BOOST_CHECK_EQUAL(volBounds->get(CylinderVolumeBounds::eHalfLengthZ),
1664                       450_mm);
1665   }
1666 }
1667 
1668 BOOST_AUTO_TEST_CASE(ResizeGapMultiple) {
1669   Transform3 trf = Transform3::Identity();
1670   auto bounds = std::make_shared<CylinderVolumeBounds>(100, 200, 100);
1671   Volume vol{trf, bounds};
1672 
1673   BOOST_TEST_CONTEXT("Outer") {
1674     std::vector<Volume*> volumes = {&vol};
1675     CylinderVolumeStack stack(volumes, AxisDirection::AxisR,
1676                               CylinderVolumeStack::AttachmentStrategy::Gap,
1677                               CylinderVolumeStack::ResizeStrategy::Gap,
1678                               *logger);
1679 
1680     BOOST_CHECK_EQUAL(volumes.size(), 1);
1681     BOOST_CHECK(stack.gaps().empty());
1682 
1683     stack.update(std::make_shared<CylinderVolumeBounds>(100, 250, 100), trf,
1684                  *logger);
1685     BOOST_CHECK_EQUAL(volumes.size(), 2);
1686     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
1687 
1688     const auto* cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
1689         &stack.gaps().front()->volumeBounds());
1690     BOOST_REQUIRE_NE(cylBounds, nullptr);
1691     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 200);
1692     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 250);
1693 
1694     stack.update(std::make_shared<CylinderVolumeBounds>(100, 300, 100), trf,
1695                  *logger);
1696 
1697     BOOST_CHECK_EQUAL(volumes.size(), 2);
1698     // No additional gap volume was added!
1699     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
1700 
1701     cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
1702         &stack.gaps().front()->volumeBounds());
1703     BOOST_REQUIRE_NE(cylBounds, nullptr);
1704     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 200);
1705     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 300);
1706   }
1707 
1708   BOOST_TEST_CONTEXT("Inner") {
1709     std::vector<Volume*> volumes = {&vol};
1710     CylinderVolumeStack stack(volumes, AxisDirection::AxisR,
1711                               CylinderVolumeStack::AttachmentStrategy::Gap,
1712                               CylinderVolumeStack::ResizeStrategy::Gap,
1713                               *logger);
1714 
1715     BOOST_CHECK_EQUAL(volumes.size(), 1);
1716     BOOST_CHECK(stack.gaps().empty());
1717 
1718     stack.update(std::make_shared<CylinderVolumeBounds>(50, 200, 100), trf,
1719                  *logger);
1720     BOOST_CHECK_EQUAL(volumes.size(), 2);
1721     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
1722 
1723     const auto* cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
1724         &stack.gaps().front()->volumeBounds());
1725     BOOST_REQUIRE_NE(cylBounds, nullptr);
1726     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 50);
1727     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 100);
1728 
1729     stack.update(std::make_shared<CylinderVolumeBounds>(0, 200, 100), trf,
1730                  *logger);
1731 
1732     BOOST_CHECK_EQUAL(volumes.size(), 2);
1733     // No additional gap volume was added!
1734     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
1735 
1736     cylBounds = dynamic_cast<const CylinderVolumeBounds*>(
1737         &stack.gaps().front()->volumeBounds());
1738     BOOST_REQUIRE_NE(cylBounds, nullptr);
1739     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMinR), 0);
1740     BOOST_CHECK_EQUAL(cylBounds->get(CylinderVolumeBounds::eMaxR), 100);
1741   }
1742 }
1743 
1744 BOOST_AUTO_TEST_SUITE_END()
1745 
1746 BOOST_AUTO_TEST_SUITE(Common)
1747 
1748 BOOST_DATA_TEST_CASE(JoinCylinderVolumesInvalidDirection,
1749                      boost::unit_test::data::make(strategies), strategy) {
1750   std::vector<Volume*> volumes;
1751   auto vol1 = std::make_shared<Volume>(
1752       Transform3::Identity(),
1753       std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1754   volumes.push_back(vol1.get());
1755 
1756   // Single volume invalid direction still gives an error
1757   BOOST_CHECK_THROW(
1758       CylinderVolumeStack(volumes, AxisDirection::AxisY, strategy),
1759       std::invalid_argument);
1760 
1761   auto vol2 = std::make_shared<Volume>(
1762       Transform3::Identity(),
1763       std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1764   volumes.push_back(vol2.get());
1765 
1766   BOOST_CHECK_THROW(
1767       CylinderVolumeStack(volumes, AxisDirection::AxisY, strategy),
1768       std::invalid_argument);
1769 }
1770 
1771 BOOST_DATA_TEST_CASE(JoinCylinderVolumesInvalidInput,
1772                      (boost::unit_test::data::make(strategies) *
1773                       boost::unit_test::data::make(Acts::AxisDirection::AxisZ,
1774                                                    Acts::AxisDirection::AxisR)),
1775                      strategy, direction) {
1776   BOOST_TEST_CONTEXT("Empty Volume") {
1777     std::vector<Volume*> volumes;
1778     BOOST_CHECK_THROW(CylinderVolumeStack(volumes, direction, strategy),
1779                       std::invalid_argument);
1780   }
1781 
1782   BOOST_TEST_CONTEXT("Volumes rotated relative to each other") {
1783     // At this time, all rotations are considered invalid, even around z
1784     for (const Vector3 axis : {Vector3::UnitX(), Vector3::UnitY()}) {
1785       std::vector<Volume*> volumes;
1786       auto vol1 = std::make_shared<Volume>(
1787           Transform3{Translation3{Vector3{0_mm, 0_mm, -500_mm}}},
1788           std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1789       volumes.push_back(vol1.get());
1790 
1791       BOOST_TEST_MESSAGE("Axis: " << axis);
1792       auto vol2 = std::make_shared<Volume>(
1793           Transform3{Translation3{Vector3{0_mm, 0_mm, 500_mm}} *
1794                      AngleAxis3(1_degree, axis)},
1795           std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1796       volumes.push_back(vol2.get());
1797 
1798       BOOST_CHECK_THROW(CylinderVolumeStack(
1799                             volumes, direction, strategy,
1800                             CylinderVolumeStack::ResizeStrategy::Gap, *logger),
1801                         std::invalid_argument);
1802     }
1803   }
1804 
1805   BOOST_TEST_CONTEXT("Volumes shifted in the xy plane relative to each other") {
1806     for (const Vector3& shift :
1807          {Vector3{5_mm, 0, 0}, Vector3{0, -5_mm, 0}, Vector3{2_mm, -2_mm, 0}}) {
1808       std::vector<Volume*> volumes;
1809       auto vol1 = std::make_shared<Volume>(
1810           Transform3{Translation3{Vector3{0_mm, 0_mm, -500_mm}}},
1811           std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1812       volumes.push_back(vol1.get());
1813 
1814       auto vol2 = std::make_shared<Volume>(
1815           Transform3{Translation3{Vector3{0_mm, 0_mm, 500_mm} + shift}},
1816           std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1817       volumes.push_back(vol2.get());
1818 
1819       BOOST_CHECK_THROW(CylinderVolumeStack(
1820                             volumes, direction, strategy,
1821                             CylinderVolumeStack::ResizeStrategy::Gap, *logger),
1822                         std::invalid_argument);
1823     }
1824   }
1825 
1826   BOOST_TEST_CONTEXT("Volume has phi values or bevel values") {
1827     std::vector<std::shared_ptr<CylinderVolumeBounds>> invalidVolumeBounds = {
1828         std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm,
1829                                                0.2 * std::numbers::pi),
1830 
1831         std::make_shared<CylinderVolumeBounds>(
1832             100_mm, 400_mm, 400_mm, std::numbers::pi, 0.3 * std::numbers::pi),
1833 
1834         std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm,
1835                                                std::numbers::pi, 0.,
1836                                                0.3 * std::numbers::pi),
1837         std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm,
1838                                                std::numbers::pi, 0., 0.,
1839                                                0.3 * std::numbers::pi),
1840     };
1841 
1842     for (const auto& invalid : invalidVolumeBounds) {
1843       std::stringstream ss;
1844       ss << "Invalid bounds: " << *invalid;
1845       BOOST_TEST_CONTEXT(ss.str()) {
1846         std::vector<Volume*> volumes;
1847         auto vol1 = std::make_shared<Volume>(
1848             Transform3{Translation3{Vector3{0_mm, 0_mm, -500_mm}}},
1849             std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1850         volumes.push_back(vol1.get());
1851 
1852         {
1853           // have valid stack, try to assign extra
1854           CylinderVolumeStack cylStack(volumes, direction, strategy,
1855                                        CylinderVolumeStack::ResizeStrategy::Gap,
1856                                        *logger);
1857           BOOST_CHECK_THROW(cylStack.update(invalid, std::nullopt, *logger),
1858                             std::invalid_argument);
1859         }
1860 
1861         {
1862           std::shared_ptr<Volume> vol;
1863           if (direction == AxisDirection::AxisZ) {
1864             vol = std::make_shared<Volume>(
1865                 Transform3{Translation3{Vector3{0_mm, 0_mm, 500_mm}}}, invalid);
1866           } else {
1867             invalid->set({
1868                 {CylinderVolumeBounds::eMinR, 400_mm},
1869                 {CylinderVolumeBounds::eMaxR, 600_mm},
1870             });
1871             vol = std::make_shared<Volume>(
1872                 Transform3{Translation3{Vector3{0_mm, 0_mm, 0_mm}}}, invalid);
1873           }
1874           volumes.push_back(vol.get());
1875           BOOST_CHECK_THROW(
1876               CylinderVolumeStack(volumes, direction, strategy,
1877                                   CylinderVolumeStack::ResizeStrategy::Gap,
1878                                   *logger),
1879               std::invalid_argument);
1880         }
1881       }
1882     }
1883   }
1884 }
1885 
1886 BOOST_DATA_TEST_CASE(JoinCylinderVolumeSingle,
1887                      (boost::unit_test::data::make(Acts::AxisDirection::AxisZ,
1888                                                    Acts::AxisDirection::AxisR) *
1889                       boost::unit_test::data::make(strategies)),
1890                      direction, strategy) {
1891   auto vol = std::make_shared<Volume>(
1892       Transform3::Identity() * Translation3{14_mm, 24_mm, 0_mm} *
1893           AngleAxis3(73_degree, Vector3::UnitX()),
1894       std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 400_mm));
1895 
1896   std::vector<Volume*> volumes{vol.get()};
1897 
1898   CylinderVolumeStack cylStack(volumes, direction, strategy,
1899                                CylinderVolumeStack::ResizeStrategy::Gap,
1900                                *logger);
1901 
1902   // Cylinder stack has the same transform as bounds as the single input
1903   // volume
1904   BOOST_CHECK_EQUAL(volumes.size(), 1);
1905   BOOST_CHECK_EQUAL(volumes.at(0), vol.get());
1906   BOOST_CHECK_EQUAL(vol->transform().matrix(), cylStack.transform().matrix());
1907   BOOST_CHECK_EQUAL(vol->volumeBounds(), cylStack.volumeBounds());
1908 }
1909 
1910 BOOST_AUTO_TEST_SUITE_END()
1911 BOOST_AUTO_TEST_SUITE_END()
1912 BOOST_AUTO_TEST_SUITE_END()
1913 
1914 }  // namespace Acts::Test