Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-17 07:51: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/Tolerance.hpp"
0020 #include "Acts/Definitions/Units.hpp"
0021 #include "Acts/Geometry/CuboidVolumeBounds.hpp"
0022 #include "Acts/Geometry/CuboidVolumeStack.hpp"
0023 #include "Acts/Geometry/VolumeAttachmentStrategy.hpp"
0024 #include "Acts/Geometry/VolumeResizeStrategy.hpp"
0025 #include "Acts/Utilities/AxisDefinitions.hpp"
0026 #include "Acts/Utilities/Logger.hpp"
0027 #include "Acts/Utilities/ThrowAssert.hpp"
0028 #include "Acts/Utilities/Zip.hpp"
0029 #include "ActsTests/CommonHelpers/FloatComparisons.hpp"
0030 
0031 #include <cassert>
0032 #include <initializer_list>
0033 #include <stdexcept>
0034 
0035 using namespace Acts;
0036 using namespace Acts::UnitLiterals;
0037 
0038 namespace ActsTests {
0039 
0040 auto logger = getDefaultLogger("UnitTests", Logging::VERBOSE);
0041 
0042 const auto gctx = GeometryContext::dangerouslyDefaultConstruct();
0043 
0044 struct Fixture {
0045   Logging::Level m_level;
0046   Fixture() {
0047     m_level = Logging::getFailureThreshold();
0048     Logging::setFailureThreshold(Logging::FATAL);
0049   }
0050 
0051   ~Fixture() { Logging::setFailureThreshold(m_level); }
0052 };
0053 
0054 BOOST_FIXTURE_TEST_SUITE(GeometrySuite, Fixture)
0055 
0056 static const std::vector<VolumeAttachmentStrategy> strategies = {
0057     VolumeAttachmentStrategy::Gap,
0058     VolumeAttachmentStrategy::First,
0059     VolumeAttachmentStrategy::Second,
0060     VolumeAttachmentStrategy::Midpoint,
0061 };
0062 
0063 static const std::vector<VolumeResizeStrategy> resizeStrategies = {
0064     VolumeResizeStrategy::Expand,
0065     VolumeResizeStrategy::Gap,
0066 };
0067 
0068 BOOST_AUTO_TEST_SUITE(CuboidVolumeStackTest)
0069 
0070 BOOST_DATA_TEST_CASE(BaselineLocal,
0071                      (boost::unit_test::data::xrange(-135, 180, 45) *
0072                       boost::unit_test::data::xrange(0, 2, 1) *
0073                       boost::unit_test::data::make(0.8, 1.0, 1.2) *
0074                       boost::unit_test::data::make(Vector3{0_mm, 0_mm, 0_mm},
0075                                                    Vector3{20_mm, 0_mm, 0_mm},
0076                                                    Vector3{0_mm, 20_mm, 0_mm},
0077                                                    Vector3{20_mm, 20_mm, 0_mm},
0078                                                    Vector3{0_mm, 0_mm, 20_mm}) *
0079                       boost::unit_test::data::make(strategies) *
0080                       boost::unit_test::data::make(AxisDirection::AxisX,
0081                                                    AxisDirection::AxisY,
0082                                                    AxisDirection::AxisZ)),
0083                      angle, rotate, shift, offset, strategy, dir) {
0084   double halfDir = 400_mm;
0085 
0086   auto [dirOrth1, dirOrth2] = CuboidVolumeStack::getOrthogonalAxes(dir);
0087 
0088   auto dirIdx = CuboidVolumeStack::axisToIndex(dir);
0089   auto dirOrth1Idx = CuboidVolumeStack::axisToIndex(dirOrth1);
0090 
0091   auto boundDir = CuboidVolumeBounds::boundsFromAxisDirection(dir);
0092   auto boundDirOrth1 = CuboidVolumeBounds::boundsFromAxisDirection(dirOrth1);
0093   auto boundDirOrth2 = CuboidVolumeBounds::boundsFromAxisDirection(dirOrth2);
0094 
0095   auto bounds1 = std::make_shared<CuboidVolumeBounds>(
0096       std::initializer_list<std::pair<CuboidVolumeBounds::BoundValues, double>>{
0097           {boundDir, halfDir},
0098           {boundDirOrth1, 100_mm},
0099           {boundDirOrth2, 400_mm}});
0100 
0101   auto bounds2 = std::make_shared<CuboidVolumeBounds>(
0102       std::initializer_list<std::pair<CuboidVolumeBounds::BoundValues, double>>{
0103           {boundDir, halfDir},
0104           {boundDirOrth1, 200_mm},
0105           {boundDirOrth2, 600_mm}});
0106 
0107   auto bounds3 = std::make_shared<CuboidVolumeBounds>(
0108       std::initializer_list<std::pair<CuboidVolumeBounds::BoundValues, double>>{
0109           {boundDir, halfDir},
0110           {boundDirOrth1, 300_mm},
0111           {boundDirOrth2, 500_mm}});
0112 
0113   Transform3 base = AngleAxis3(angle * 1_degree, Vector3::Unit(dirOrth1Idx)) *
0114                     Translation3(offset);
0115 
0116   Translation3 translation1(Vector3::Unit(dirIdx) * (-2 * halfDir * shift));
0117   Transform3 transform1 = base * translation1;
0118   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
0119 
0120   Transform3 transform2 = base;
0121   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
0122 
0123   Translation3 translation3(Vector3::Unit(dirIdx) * (2 * halfDir * shift));
0124   Transform3 transform3 = base * translation3;
0125   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
0126 
0127   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
0128   // Rotate to simulate unsorted volumes: all results should be the same!
0129   std::rotate(volumes.begin(), volumes.begin() + rotate, volumes.end());
0130 
0131   auto origVolumes = volumes;
0132 
0133   std::vector<CuboidVolumeBounds> originalBounds;
0134   std::transform(volumes.begin(), volumes.end(),
0135                  std::back_inserter(originalBounds), [](const auto& vol) {
0136                    const auto* res =
0137                        dynamic_cast<CuboidVolumeBounds*>(&vol->volumeBounds());
0138                    throw_assert(res != nullptr, "");
0139                    return *res;
0140                  });
0141 
0142   if (shift < 1.0) {
0143     BOOST_CHECK_THROW(CuboidVolumeStack(gctx, volumes, dir, strategy,
0144                                         VolumeResizeStrategy::Gap, *logger),
0145                       std::invalid_argument);
0146     return;
0147   }
0148   CuboidVolumeStack stack(gctx, volumes, dir, strategy,
0149                           VolumeResizeStrategy::Gap, *logger);
0150 
0151   auto stackBounds =
0152       dynamic_cast<const CuboidVolumeBounds*>(&stack.volumeBounds());
0153   BOOST_REQUIRE(stackBounds != nullptr);
0154 
0155   BOOST_CHECK_CLOSE(stackBounds->get(boundDirOrth1), 300_mm, 1e-6);
0156   BOOST_CHECK_CLOSE(stackBounds->get(boundDirOrth2), 600_mm, 1e-6);
0157   BOOST_CHECK_CLOSE(stackBounds->get(boundDir), halfDir + 2 * halfDir * shift,
0158                     1e-6);
0159   CHECK_CLOSE_OR_SMALL(stack.localToGlobalTransform(gctx).matrix(),
0160                        base.matrix(), 1e-10, 1e-12);
0161 
0162   // All volumes (including gaps) are cuboids and have the same orthogonal
0163   // bounds
0164   for (const auto& volume : volumes) {
0165     const auto* cuboidBounds =
0166         dynamic_cast<const CuboidVolumeBounds*>(&volume->volumeBounds());
0167     BOOST_REQUIRE(cuboidBounds != nullptr);
0168     BOOST_CHECK_CLOSE(cuboidBounds->get(boundDirOrth1), 300_mm, 1e-6);
0169     BOOST_CHECK_CLOSE(cuboidBounds->get(boundDirOrth2), 600_mm, 1e-6);
0170   }
0171 
0172   // Volumes are sorted in (local) stacking direction
0173   for (std::size_t i = 0; i < volumes.size() - 1; ++i) {
0174     const auto& a = volumes.at(i);
0175     const auto& b = volumes.at(i + 1);
0176 
0177     BOOST_CHECK_LT((base.inverse() * a->center(gctx))[dirIdx],
0178                    (base.inverse() * b->center(gctx))[dirIdx]);
0179   }
0180 
0181   if (shift <= 1.0) {
0182     // No gap volumes were added
0183     BOOST_CHECK_EQUAL(volumes.size(), 3);
0184 
0185     // No expansion, original volumes did not move
0186     BOOST_CHECK_EQUAL(vol1->localToGlobalTransform(gctx).matrix(),
0187                       transform1.matrix());
0188     BOOST_CHECK_EQUAL(vol2->localToGlobalTransform(gctx).matrix(),
0189                       transform2.matrix());
0190     BOOST_CHECK_EQUAL(vol3->localToGlobalTransform(gctx).matrix(),
0191                       transform3.matrix());
0192 
0193     for (const auto& [volume, bounds] : zip(origVolumes, originalBounds)) {
0194       const auto* newBounds =
0195           dynamic_cast<const CuboidVolumeBounds*>(&volume->volumeBounds());
0196       BOOST_CHECK_CLOSE(newBounds->get(boundDir), bounds.get(boundDir), 1e-6);
0197     }
0198   } else {
0199     if (strategy == VolumeAttachmentStrategy::Gap) {
0200       // Gap volumes were added
0201       BOOST_CHECK_EQUAL(volumes.size(), 5);
0202       auto gap1 = volumes.at(1);
0203       auto gap2 = volumes.at(3);
0204 
0205       BOOST_TEST_MESSAGE(
0206           "Gap 1: " << gap1->localToGlobalTransform(gctx).matrix());
0207       BOOST_TEST_MESSAGE(
0208           "Gap 2: " << gap2->localToGlobalTransform(gctx).matrix());
0209 
0210       const auto* gapBounds1 =
0211           dynamic_cast<const CuboidVolumeBounds*>(&gap1->volumeBounds());
0212       const auto* gapBounds2 =
0213           dynamic_cast<const CuboidVolumeBounds*>(&gap2->volumeBounds());
0214 
0215       double gapHlDir = (shift - 1.0) * halfDir;
0216 
0217       BOOST_CHECK(std::abs(gapBounds1->get(boundDir) - gapHlDir) < 1e-12);
0218       BOOST_CHECK(std::abs(gapBounds2->get(boundDir) - gapHlDir) < 1e-12);
0219 
0220       double gap1Dir = (-2 * halfDir * shift) + halfDir + gapHlDir;
0221       double gap2Dir = (2 * halfDir * shift) - halfDir - gapHlDir;
0222 
0223       Translation3 gap1Translation(Vector3::Unit(dirIdx) * gap1Dir);
0224       Translation3 gap2Translation(Vector3::Unit(dirIdx) * gap2Dir);
0225 
0226       Transform3 gap1Transform = base * gap1Translation;
0227       Transform3 gap2Transform = base * gap2Translation;
0228 
0229       CHECK_CLOSE_OR_SMALL(gap1->localToGlobalTransform(gctx).matrix(),
0230                            gap1Transform.matrix(), 1e-10, 1e-12);
0231       CHECK_CLOSE_OR_SMALL(gap2->localToGlobalTransform(gctx).matrix(),
0232                            gap2Transform.matrix(), 1e-10, 1e-12);
0233 
0234       // Original volumes did not changes bounds
0235       for (const auto& [volume, bounds] : zip(origVolumes, originalBounds)) {
0236         const auto* newBounds =
0237             dynamic_cast<const CuboidVolumeBounds*>(&volume->volumeBounds());
0238         BOOST_CHECK_CLOSE(newBounds->get(boundDir), bounds.get(boundDir), 1e-6);
0239       }
0240 
0241       // No expansion, original volumes did not move
0242       BOOST_CHECK_EQUAL(vol1->localToGlobalTransform(gctx).matrix(),
0243                         transform1.matrix());
0244       BOOST_CHECK_EQUAL(vol2->localToGlobalTransform(gctx).matrix(),
0245                         transform2.matrix());
0246       BOOST_CHECK_EQUAL(vol3->localToGlobalTransform(gctx).matrix(),
0247                         transform3.matrix());
0248     } else if (strategy == VolumeAttachmentStrategy::First) {
0249       // No gap volumes were added
0250       BOOST_CHECK_EQUAL(volumes.size(), 3);
0251 
0252       double wGap = (shift - 1.0) * halfDir * 2;
0253 
0254       // Volume 1 got bigger and shifted right
0255       auto newBounds1 =
0256           dynamic_cast<const CuboidVolumeBounds*>(&vol1->volumeBounds());
0257       BOOST_CHECK_CLOSE(newBounds1->get(boundDir), halfDir + wGap / 2.0, 1e-6);
0258       double pDir1 = -2 * halfDir * shift + wGap / 2.0;
0259       Translation3 expectedTranslation1(Vector3::Unit(dirIdx) * pDir1);
0260       Transform3 expectedTransform1 = base * expectedTranslation1;
0261       CHECK_CLOSE_OR_SMALL(vol1->localToGlobalTransform(gctx).matrix(),
0262                            expectedTransform1.matrix(), 1e-10, 1e-12);
0263 
0264       // Volume 2 got bigger and shifted left
0265       auto newBounds2 =
0266           dynamic_cast<const CuboidVolumeBounds*>(&vol2->volumeBounds());
0267       BOOST_CHECK_CLOSE(newBounds2->get(boundDir), halfDir + wGap / 2.0, 1e-6);
0268       double pDir2 = wGap / 2.0;
0269       Translation3 expectedTranslation2(Vector3::Unit(dirIdx) * pDir2);
0270       Transform3 expectedTransform2 = base * expectedTranslation2;
0271       CHECK_CLOSE_OR_SMALL(vol2->localToGlobalTransform(gctx).matrix(),
0272                            expectedTransform2.matrix(), 1e-10, 1e-12);
0273 
0274       // Volume 3 stayed the same
0275       auto newBounds3 =
0276           dynamic_cast<const CuboidVolumeBounds*>(&vol3->volumeBounds());
0277       BOOST_CHECK_CLOSE(newBounds3->get(boundDir), halfDir, 1e-6);
0278       double pDir3 = 2 * halfDir * shift;
0279       Translation3 expectedTranslation3(Vector3::Unit(dirIdx) * pDir3);
0280       Transform3 expectedTransform3 = base * expectedTranslation3;
0281       CHECK_CLOSE_OR_SMALL(vol3->localToGlobalTransform(gctx).matrix(),
0282                            expectedTransform3.matrix(), 1e-10, 1e-12);
0283     } else if (strategy == VolumeAttachmentStrategy::Second) {
0284       // No gap volumes were added
0285       BOOST_CHECK_EQUAL(volumes.size(), 3);
0286 
0287       double wGap = (shift - 1.0) * halfDir * 2;
0288 
0289       // Volume 1 stayed the same
0290       auto newBounds1 =
0291           dynamic_cast<const CuboidVolumeBounds*>(&vol1->volumeBounds());
0292       BOOST_CHECK_CLOSE(newBounds1->get(boundDir), halfDir, 1e-6);
0293       double pDir1 = -2 * halfDir * shift;
0294       Translation3 expectedTranslation1(Vector3::Unit(dirIdx) * pDir1);
0295       Transform3 expectedTransform1 = base * expectedTranslation1;
0296       CHECK_CLOSE_OR_SMALL(vol1->localToGlobalTransform(gctx).matrix(),
0297                            expectedTransform1.matrix(), 1e-10, 1e-12);
0298 
0299       // Volume 2 got bigger and shifted left
0300       auto newBounds2 =
0301           dynamic_cast<const CuboidVolumeBounds*>(&vol2->volumeBounds());
0302       BOOST_CHECK_CLOSE(newBounds2->get(boundDir), halfDir + wGap / 2.0, 1e-6);
0303       double pDir2 = -wGap / 2.0;
0304       Translation3 expectedTranslation2(Vector3::Unit(dirIdx) * pDir2);
0305       Transform3 expectedTransform2 = base * expectedTranslation2;
0306       CHECK_CLOSE_OR_SMALL(vol2->localToGlobalTransform(gctx).matrix(),
0307                            expectedTransform2.matrix(), 1e-10, 1e-12);
0308 
0309       // Volume 3 got bigger and shifted left
0310       auto newBounds3 =
0311           dynamic_cast<const CuboidVolumeBounds*>(&vol3->volumeBounds());
0312       BOOST_CHECK_CLOSE(newBounds3->get(boundDir), halfDir + wGap / 2.0, 1e-6);
0313       double pDir3 = 2 * halfDir * shift - wGap / 2.0;
0314       Translation3 expectedTranslation3(Vector3::Unit(dirIdx) * pDir3);
0315       Transform3 expectedTransform3 = base * expectedTranslation3;
0316       CHECK_CLOSE_OR_SMALL(vol3->localToGlobalTransform(gctx).matrix(),
0317                            expectedTransform3.matrix(), 1e-10, 1e-12);
0318     } else if (strategy == VolumeAttachmentStrategy::Midpoint) {
0319       // No gap volumes were added
0320       BOOST_CHECK_EQUAL(volumes.size(), 3);
0321 
0322       double wGap = (shift - 1.0) * halfDir * 2;
0323 
0324       // Volume 1 got bigger and shifted right
0325       auto newBounds1 =
0326           dynamic_cast<const CuboidVolumeBounds*>(&vol1->volumeBounds());
0327       BOOST_CHECK_CLOSE(newBounds1->get(boundDir), halfDir + wGap / 4.0, 1e-6);
0328       double pDir1 = -2 * halfDir * shift + wGap / 4.0;
0329       Translation3 expectedTranslation1(Vector3::Unit(dirIdx) * pDir1);
0330       Transform3 expectedTransform1 = base * expectedTranslation1;
0331       CHECK_CLOSE_OR_SMALL(vol1->localToGlobalTransform(gctx).matrix(),
0332                            expectedTransform1.matrix(), 1e-10, 1e-12);
0333 
0334       // Volume 2 got bigger but didn't move
0335       auto newBounds2 =
0336           dynamic_cast<const CuboidVolumeBounds*>(&vol2->volumeBounds());
0337       BOOST_CHECK_CLOSE(newBounds2->get(boundDir), halfDir + wGap / 2.0, 1e-6);
0338       CHECK_CLOSE_OR_SMALL(vol2->localToGlobalTransform(gctx).matrix(),
0339                            base.matrix(), 1e-10, 1e-12);
0340 
0341       // Volume 3 got bigger and shifted left
0342       auto newBounds3 =
0343           dynamic_cast<const CuboidVolumeBounds*>(&vol3->volumeBounds());
0344       BOOST_CHECK_CLOSE(newBounds3->get(boundDir), halfDir + wGap / 4.0, 1e-6);
0345       double pDir3 = 2 * halfDir * shift - wGap / 4.0;
0346       Translation3 expectedTranslation3(Vector3::Unit(dirIdx) * pDir3);
0347       Transform3 expectedTransform3 = base * expectedTranslation3;
0348       CHECK_CLOSE_OR_SMALL(vol3->localToGlobalTransform(gctx).matrix(),
0349                            expectedTransform3.matrix(), 1e-10, 1e-12);
0350     }
0351   }
0352 }
0353 
0354 BOOST_DATA_TEST_CASE(Asymmetric,
0355                      boost::unit_test::data::make(AxisDirection::AxisX,
0356                                                   AxisDirection::AxisY,
0357                                                   AxisDirection::AxisZ),
0358                      dir) {
0359   double halfDir1 = 200_mm;
0360   double pDir1 = -1100_mm;
0361   double halfDir2 = 600_mm;
0362   double pDir2 = -200_mm;
0363   double halfDir3 = 400_mm;
0364   double pDir3 = 850_mm;
0365 
0366   auto [dirOrth1, dirOrth2] = CuboidVolumeStack::getOrthogonalAxes(dir);
0367 
0368   auto dirIdx = CuboidVolumeStack::axisToIndex(dir);
0369 
0370   auto boundDir = CuboidVolumeBounds::boundsFromAxisDirection(dir);
0371   auto boundDirOrth1 = CuboidVolumeBounds::boundsFromAxisDirection(dirOrth1);
0372   auto boundDirOrth2 = CuboidVolumeBounds::boundsFromAxisDirection(dirOrth2);
0373 
0374   auto bounds1 = std::make_shared<CuboidVolumeBounds>(
0375       std::initializer_list<std::pair<CuboidVolumeBounds::BoundValues, double>>{
0376           {boundDir, halfDir1},
0377           {boundDirOrth1, 100_mm},
0378           {boundDirOrth2, 400_mm}});
0379 
0380   auto bounds2 = std::make_shared<CuboidVolumeBounds>(
0381       std::initializer_list<std::pair<CuboidVolumeBounds::BoundValues, double>>{
0382           {boundDir, halfDir2},
0383           {boundDirOrth1, 200_mm},
0384           {boundDirOrth2, 600_mm}});
0385 
0386   auto bounds3 = std::make_shared<CuboidVolumeBounds>(
0387       std::initializer_list<std::pair<CuboidVolumeBounds::BoundValues, double>>{
0388           {boundDir, halfDir3},
0389           {boundDirOrth1, 300_mm},
0390           {boundDirOrth2, 500_mm}});
0391 
0392   Translation3 translation1(Vector3::Unit(dirIdx) * pDir1);
0393   Transform3 transform1(translation1);
0394   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
0395 
0396   Translation3 translation2(Vector3::Unit(dirIdx) * pDir2);
0397   Transform3 transform2(translation2);
0398   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
0399 
0400   Translation3 translation3(Vector3::Unit(dirIdx) * pDir3);
0401   Transform3 transform3(translation3);
0402   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
0403 
0404   std::vector<Volume*> volumes = {vol2.get(), vol1.get(), vol3.get()};
0405 
0406   CuboidVolumeStack stack(gctx, volumes, dir, VolumeAttachmentStrategy::Gap,
0407                           VolumeResizeStrategy::Gap, *logger);
0408   BOOST_CHECK_EQUAL(volumes.size(), 5);
0409 
0410   auto stackBounds =
0411       dynamic_cast<const CuboidVolumeBounds*>(&stack.volumeBounds());
0412   BOOST_REQUIRE(stackBounds != nullptr);
0413 
0414   BOOST_CHECK_CLOSE(stackBounds->get(boundDirOrth1), 300_mm, 1e-6);
0415   BOOST_CHECK_CLOSE(stackBounds->get(boundDirOrth2), 600_mm, 1e-6);
0416   BOOST_CHECK_CLOSE(stackBounds->get(boundDir),
0417                     (std::abs(pDir1 - halfDir1) + pDir3 + halfDir3) / 2.0,
0418                     1e-6);
0419 
0420   double midDir = (pDir1 - halfDir1 + pDir3 + halfDir3) / 2.0;
0421   Translation3 expectedTranslation(Vector3::Unit(dirIdx) * midDir);
0422   Transform3 expectedTransform = Transform3::Identity() * expectedTranslation;
0423   CHECK_CLOSE_OR_SMALL(stack.localToGlobalTransform(gctx).matrix(),
0424                        expectedTransform.matrix(), 1e-10, 1e-12);
0425 }
0426 
0427 BOOST_DATA_TEST_CASE(UpdateStack,
0428                      (boost::unit_test::data::xrange(-135, 180, 45) *
0429                       boost::unit_test::data::make(Vector3{0_mm, 0_mm, 0_mm},
0430                                                    Vector3{20_mm, 0_mm, 0_mm},
0431                                                    Vector3{0_mm, 20_mm, 0_mm},
0432                                                    Vector3{20_mm, 20_mm, 0_mm},
0433                                                    Vector3{0_mm, 0_mm, 20_mm}) *
0434                       boost::unit_test::data::make(-100_mm, 0_mm, 100_mm) *
0435                       boost::unit_test::data::make(resizeStrategies) *
0436                       boost::unit_test::data::make(AxisDirection::AxisX,
0437                                                    AxisDirection::AxisY,
0438                                                    AxisDirection::AxisZ)),
0439                      angle, offset, zshift, strategy, dir) {
0440   double halfDir = 400_mm;
0441 
0442   auto [dirOrth1, dirOrth2] = CuboidVolumeStack::getOrthogonalAxes(dir);
0443 
0444   auto dirIdx = CuboidVolumeStack::axisToIndex(dir);
0445   auto dirOrth1Idx = CuboidVolumeStack::axisToIndex(dirOrth1);
0446 
0447   auto boundDir = CuboidVolumeBounds::boundsFromAxisDirection(dir);
0448   auto boundDirOrth1 = CuboidVolumeBounds::boundsFromAxisDirection(dirOrth1);
0449   auto boundDirOrth2 = CuboidVolumeBounds::boundsFromAxisDirection(dirOrth2);
0450 
0451   auto bounds1 = std::make_shared<CuboidVolumeBounds>(
0452       std::initializer_list<std::pair<CuboidVolumeBounds::BoundValues, double>>{
0453           {boundDir, halfDir},
0454           {boundDirOrth1, 100_mm},
0455           {boundDirOrth2, 600_mm}});
0456 
0457   auto bounds2 = std::make_shared<CuboidVolumeBounds>(
0458       std::initializer_list<std::pair<CuboidVolumeBounds::BoundValues, double>>{
0459           {boundDir, halfDir},
0460           {boundDirOrth1, 100_mm},
0461           {boundDirOrth2, 600_mm}});
0462 
0463   auto bounds3 = std::make_shared<CuboidVolumeBounds>(
0464       std::initializer_list<std::pair<CuboidVolumeBounds::BoundValues, double>>{
0465           {boundDir, halfDir},
0466           {boundDirOrth1, 100_mm},
0467           {boundDirOrth2, 600_mm}});
0468 
0469   Vector3 shift = Vector3::Unit(dirIdx) * zshift;
0470   Transform3 base = AngleAxis3(angle * 1_degree, Vector3::Unit(dirOrth1Idx)) *
0471                     Translation3(offset + shift);
0472 
0473   Translation3 translation1(Vector3::Unit(dirIdx) * -2 * halfDir);
0474   Transform3 transform1 = base * translation1;
0475   auto vol1 = std::make_shared<Volume>(transform1, bounds1);
0476 
0477   Transform3 transform2 = base;
0478   auto vol2 = std::make_shared<Volume>(transform2, bounds2);
0479 
0480   Translation3 translation3(Vector3::Unit(dirIdx) * 2 * halfDir);
0481   Transform3 transform3 = base * translation3;
0482   auto vol3 = std::make_shared<Volume>(transform3, bounds3);
0483 
0484   std::vector<Volume*> volumes = {vol1.get(), vol2.get(), vol3.get()};
0485   std::vector<Volume*> originalVolumes = volumes;
0486 
0487   std::vector<Transform3> originalTransforms = {transform1, transform2,
0488                                                 transform3};
0489 
0490   CuboidVolumeStack stack(gctx, volumes, dir,
0491                           VolumeAttachmentStrategy::Gap,  // should not make a
0492                                                           // difference
0493                           strategy, *logger);
0494 
0495   const auto* originalBounds =
0496       dynamic_cast<const CuboidVolumeBounds*>(&stack.volumeBounds());
0497 
0498   auto assertOriginalBounds = [&]() {
0499     const auto* bounds =
0500         dynamic_cast<const CuboidVolumeBounds*>(&stack.volumeBounds());
0501     BOOST_REQUIRE(bounds != nullptr);
0502     BOOST_CHECK_EQUAL(bounds, originalBounds);
0503     BOOST_CHECK_CLOSE(bounds->get(boundDirOrth1), 100_mm, 1e-6);
0504     BOOST_CHECK_CLOSE(bounds->get(boundDirOrth2), 600_mm, 1e-6);
0505     BOOST_CHECK_CLOSE(bounds->get(boundDir), 3 * halfDir, 1e-6);
0506   };
0507 
0508   assertOriginalBounds();
0509 
0510   {
0511     // Assign a copy of the identical bounds gives identical bounds
0512     auto bounds = std::make_shared<CuboidVolumeBounds>(
0513         dynamic_cast<const CuboidVolumeBounds&>(stack.volumeBounds()));
0514     stack.update(gctx, bounds, std::nullopt, *logger);
0515     assertOriginalBounds();
0516   }
0517 
0518   {
0519     // Cannot decrease half length
0520     auto bounds = std::make_shared<CuboidVolumeBounds>(
0521         dynamic_cast<const CuboidVolumeBounds&>(stack.volumeBounds()));
0522     bounds->set(boundDirOrth1, 20_mm);
0523     BOOST_CHECK_THROW(stack.update(gctx, bounds, std::nullopt, *logger),
0524                       std::invalid_argument);
0525     assertOriginalBounds();
0526   }
0527 
0528   {
0529     // Cannot decrease half length
0530     auto bounds = std::make_shared<CuboidVolumeBounds>(
0531         dynamic_cast<const CuboidVolumeBounds&>(stack.volumeBounds()));
0532     bounds->set(boundDirOrth2, 200_mm);
0533     BOOST_CHECK_THROW(stack.update(gctx, bounds, std::nullopt, *logger),
0534                       std::invalid_argument);
0535     assertOriginalBounds();
0536   }
0537 
0538   {
0539     // Cannot decrease half length
0540     auto bounds = std::make_shared<CuboidVolumeBounds>(
0541         dynamic_cast<const CuboidVolumeBounds&>(stack.volumeBounds()));
0542     bounds->set(boundDir, 2 * halfDir);
0543     BOOST_CHECK_THROW(stack.update(gctx, bounds, std::nullopt, *logger),
0544                       std::invalid_argument);
0545     assertOriginalBounds();
0546   }
0547 
0548   {
0549     // Increase half length
0550     auto bounds = std::make_shared<CuboidVolumeBounds>(
0551         dynamic_cast<const CuboidVolumeBounds&>(stack.volumeBounds()));
0552     bounds->set(boundDirOrth1, 700_mm);
0553     stack.update(gctx, bounds, std::nullopt, *logger);
0554     const auto* updatedBounds =
0555         dynamic_cast<const CuboidVolumeBounds*>(&stack.volumeBounds());
0556     BOOST_REQUIRE(updatedBounds != nullptr);
0557     BOOST_CHECK_CLOSE(updatedBounds->get(boundDirOrth1), 700_mm, 1e-6);
0558     BOOST_CHECK_CLOSE(updatedBounds->get(boundDirOrth2), 600_mm, 1e-6);
0559     BOOST_CHECK_CLOSE(updatedBounds->get(boundDir), 3 * halfDir, 1e-6);
0560 
0561     // No gap volumes were added
0562     BOOST_CHECK_EQUAL(volumes.size(), 3);
0563 
0564     // All volumes increase half x to accommodate
0565     for (const auto& [volume, origTransform] :
0566          zip(volumes, originalTransforms)) {
0567       const auto* newBounds =
0568           dynamic_cast<const CuboidVolumeBounds*>(&volume->volumeBounds());
0569       BOOST_CHECK_CLOSE(newBounds->get(boundDirOrth1), 700_mm, 1e-6);
0570       BOOST_CHECK_CLOSE(newBounds->get(boundDirOrth2), 600_mm, 1e-6);
0571       BOOST_CHECK_CLOSE(newBounds->get(boundDir), halfDir, 1e-6);
0572 
0573       // Position stayed the same
0574       BOOST_CHECK_EQUAL(volume->localToGlobalTransform(gctx).matrix(),
0575                         origTransform.matrix());
0576     }
0577   }
0578   {
0579     // Increase half length
0580     auto bounds = std::make_shared<CuboidVolumeBounds>(
0581         dynamic_cast<const CuboidVolumeBounds&>(stack.volumeBounds()));
0582     bounds->set(boundDirOrth2, 700_mm);
0583     stack.update(gctx, bounds, std::nullopt, *logger);
0584     const auto* updatedBounds =
0585         dynamic_cast<const CuboidVolumeBounds*>(&stack.volumeBounds());
0586     BOOST_REQUIRE(updatedBounds != nullptr);
0587     BOOST_CHECK_CLOSE(updatedBounds->get(boundDirOrth1), 700_mm, 1e-6);
0588     BOOST_CHECK_CLOSE(updatedBounds->get(boundDirOrth2), 700_mm, 1e-6);
0589     BOOST_CHECK_CLOSE(updatedBounds->get(boundDir), 3 * halfDir, 1e-6);
0590 
0591     // No gap volumes were added
0592     BOOST_CHECK_EQUAL(volumes.size(), 3);
0593 
0594     // All volumes increase half y to accommodate
0595     for (const auto& [volume, origTransform] :
0596          zip(volumes, originalTransforms)) {
0597       const auto* newBounds =
0598           dynamic_cast<const CuboidVolumeBounds*>(&volume->volumeBounds());
0599       BOOST_CHECK_CLOSE(newBounds->get(boundDirOrth1), 700_mm, 1e-6);
0600       BOOST_CHECK_CLOSE(newBounds->get(boundDirOrth2), 700_mm, 1e-6);
0601       BOOST_CHECK_CLOSE(newBounds->get(boundDir), halfDir, 1e-6);
0602 
0603       // Position stayed the same
0604       BOOST_CHECK_EQUAL(volume->localToGlobalTransform(gctx).matrix(),
0605                         origTransform.matrix());
0606     }
0607   }
0608 
0609   {
0610     // Increase half length
0611     auto bounds = std::make_shared<CuboidVolumeBounds>(
0612         dynamic_cast<const CuboidVolumeBounds&>(stack.volumeBounds()));
0613     bounds->set(boundDir, 4 * halfDir);
0614     stack.update(gctx, bounds, std::nullopt, *logger);
0615     const auto* updatedBounds =
0616         dynamic_cast<const CuboidVolumeBounds*>(&stack.volumeBounds());
0617     BOOST_REQUIRE(updatedBounds != nullptr);
0618     BOOST_CHECK_CLOSE(updatedBounds->get(boundDir), 4 * halfDir, 1e-6);
0619     BOOST_CHECK_CLOSE(updatedBounds->get(boundDirOrth1), 700_mm, 1e-6);
0620     BOOST_CHECK_CLOSE(updatedBounds->get(boundDirOrth2), 700_mm, 1e-6);
0621 
0622     if (strategy == VolumeResizeStrategy::Expand) {
0623       // No gap volumes were added
0624       BOOST_CHECK_EQUAL(volumes.size(), 3);
0625 
0626       // Volume 1 got bigger and shifted left
0627       auto newBounds1 =
0628           dynamic_cast<const CuboidVolumeBounds*>(&vol1->volumeBounds());
0629       BOOST_CHECK_CLOSE(newBounds1->get(boundDir), halfDir + halfDir / 2.0,
0630                         1e-6);
0631       auto expectedTranslation1 =
0632           Translation3(Vector3::Unit(dirIdx) * (-2 * halfDir - halfDir / 2.0));
0633       Transform3 expectedTransform1 = base * expectedTranslation1;
0634       CHECK_CLOSE_OR_SMALL(vol1->localToGlobalTransform(gctx).matrix(),
0635                            expectedTransform1.matrix(), 1e-10, 1e-12);
0636 
0637       // Volume 2 stayed the same
0638       auto newBounds2 =
0639           dynamic_cast<const CuboidVolumeBounds*>(&vol2->volumeBounds());
0640       BOOST_CHECK_CLOSE(newBounds2->get(boundDir), halfDir, 1e-6);
0641       CHECK_CLOSE_OR_SMALL(vol2->localToGlobalTransform(gctx).matrix(),
0642                            transform2.matrix(), 1e-10, 1e-12);
0643 
0644       // Volume 3 got bigger and shifted right
0645       auto newBounds3 =
0646           dynamic_cast<const CuboidVolumeBounds*>(&vol3->volumeBounds());
0647       BOOST_CHECK_CLOSE(newBounds3->get(boundDir), halfDir + halfDir / 2.0,
0648                         1e-6);
0649       auto expectedTranslation3 =
0650           Translation3(Vector3::Unit(dirIdx) * (2 * halfDir + halfDir / 2.0));
0651       Transform3 expectedTransform3 = base * expectedTranslation3;
0652       CHECK_CLOSE_OR_SMALL(vol3->localToGlobalTransform(gctx).matrix(),
0653                            expectedTransform3.matrix(), 1e-10, 1e-12);
0654     } else if (strategy == VolumeResizeStrategy::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 CuboidVolumeBounds*>(&volume->volumeBounds());
0662         BOOST_CHECK_CLOSE(newBounds->get(boundDirOrth1), 700_mm, 1e-6);
0663         BOOST_CHECK_CLOSE(newBounds->get(boundDirOrth2), 700_mm, 1e-6);
0664         BOOST_CHECK_CLOSE(newBounds->get(boundDir), halfDir, 1e-6);
0665         // Position stayed the same
0666         CHECK_CLOSE_OR_SMALL(volume->localToGlobalTransform(gctx).matrix(),
0667                              origTransform.matrix(), 1e-10, 1e-12);
0668       }
0669 
0670       auto gap1 = volumes.front();
0671       auto gap2 = volumes.back();
0672 
0673       const auto* gapBounds1 =
0674           dynamic_cast<const CuboidVolumeBounds*>(&gap1->volumeBounds());
0675       const auto* gapBounds2 =
0676           dynamic_cast<const CuboidVolumeBounds*>(&gap2->volumeBounds());
0677 
0678       BOOST_CHECK_CLOSE(gapBounds1->get(boundDir), halfDir / 2.0, 1e-6);
0679       BOOST_CHECK_CLOSE(gapBounds2->get(boundDir), halfDir / 2.0, 1e-6);
0680       auto gap1Translation =
0681           Translation3(Vector3::Unit(dirIdx) * (-3 * halfDir - halfDir / 2.0));
0682       Transform3 gap1Transform = base * gap1Translation;
0683 
0684       auto gap2Translation =
0685           Translation3(Vector3::Unit(dirIdx) * (3 * halfDir + halfDir / 2.0));
0686       Transform3 gap2Transform = base * gap2Translation;
0687       CHECK_CLOSE_OR_SMALL(gap1->localToGlobalTransform(gctx).matrix(),
0688                            gap1Transform.matrix(), 1e-10, 1e-12);
0689       CHECK_CLOSE_OR_SMALL(gap2->localToGlobalTransform(gctx).matrix(),
0690                            gap2Transform.matrix(), 1e-10, 1e-12);
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      boost::unit_test::data::make(AxisDirection::AxisX, AxisDirection::AxisY,
0701                                   AxisDirection::AxisZ)),
0702     f, strategy, dir) {
0703   auto [dirOrth1, dirOrth2] = CuboidVolumeStack::getOrthogonalAxes(dir);
0704 
0705   auto dirIdx = CuboidVolumeStack::axisToIndex(dir);
0706   auto dirOrth1Idx = CuboidVolumeStack::axisToIndex(dirOrth1);
0707   auto dirOrth2Idx = CuboidVolumeStack::axisToIndex(dirOrth2);
0708 
0709   auto boundDir = CuboidVolumeBounds::boundsFromAxisDirection(dir);
0710   auto boundDirOrth1 = CuboidVolumeBounds::boundsFromAxisDirection(dirOrth1);
0711   auto boundDirOrth2 = CuboidVolumeBounds::boundsFromAxisDirection(dirOrth2);
0712 
0713   auto bounds1 = std::make_shared<CuboidVolumeBounds>(
0714       std::initializer_list<std::pair<CuboidVolumeBounds::BoundValues, double>>{
0715           {boundDir, 400_mm},
0716           {boundDirOrth1, 100_mm},
0717           {boundDirOrth2, 300_mm}});
0718 
0719   auto bounds2 = std::make_shared<CuboidVolumeBounds>(
0720       std::initializer_list<std::pair<CuboidVolumeBounds::BoundValues, double>>{
0721           {boundDir, 400_mm},
0722           {boundDirOrth1, 100_mm},
0723           {boundDirOrth2, 300_mm}});
0724 
0725   auto trf = Transform3::Identity();
0726 
0727   auto translation1 = Translation3(Vector3::Unit(dirIdx) * -500_mm);
0728   auto trf1 = trf * translation1;
0729   auto vol1 = std::make_shared<Volume>(trf1, bounds1);
0730 
0731   auto translation2 = Translation3(Vector3::Unit(dirIdx) * 500_mm);
0732   auto trf2 = trf * translation2;
0733   auto vol2 = std::make_shared<Volume>(trf2, bounds2);
0734 
0735   std::vector<Volume*> volumes = {vol1.get(), vol2.get()};
0736 
0737   CuboidVolumeStack stack{gctx,     volumes, dir, VolumeAttachmentStrategy::Gap,
0738                           strategy, *logger};
0739   const auto* originalBounds =
0740       dynamic_cast<const CuboidVolumeBounds*>(&stack.volumeBounds());
0741 
0742   // Increase half length by 50mm
0743   auto newBounds = std::make_shared<CuboidVolumeBounds>(
0744       dynamic_cast<const CuboidVolumeBounds&>(stack.volumeBounds()));
0745   newBounds->set(boundDir, 950_mm);
0746   // Shift to +stacking direction by 50mm
0747   auto delta = Translation3(Vector3::Unit(dirIdx) * f * 50_mm);
0748   trf *= delta;
0749   // -> left edge should stay at -400mm, right edge should be at 500mm or the
0750   // other direction
0751   auto checkUnchanged = [&]() {
0752     const auto* bounds =
0753         dynamic_cast<const CuboidVolumeBounds*>(&stack.volumeBounds());
0754     BOOST_REQUIRE(bounds != nullptr);
0755     BOOST_CHECK_EQUAL(*bounds, *originalBounds);
0756   };
0757 
0758   // Invalid: shift too far in merging direction
0759   BOOST_CHECK_THROW(
0760       auto errDelta = Translation3(Vector3::Unit(dirIdx) * f * 20_mm);
0761       stack.update(gctx, newBounds, trf * errDelta, *logger),
0762       std::invalid_argument);
0763   checkUnchanged();
0764 
0765   // Invalid: shift in orthogonal direction
0766   BOOST_CHECK_THROW(
0767       auto errDelta = Translation3(Vector3::Unit(dirOrth1Idx) * 10_mm);
0768       stack.update(gctx, newBounds, trf * errDelta, *logger),
0769       std::invalid_argument);
0770   checkUnchanged();
0771 
0772   // Invalid: shift in orthogonal direction
0773   BOOST_CHECK_THROW(
0774       auto errDelta = Translation3(Vector3::Unit(dirOrth2Idx) * 10_mm);
0775       stack.update(gctx, newBounds, trf * errDelta, *logger),
0776       std::invalid_argument);
0777   checkUnchanged();
0778 
0779   // Invalid: rotation
0780   BOOST_CHECK_THROW(
0781       stack.update(gctx, newBounds,
0782                    trf * AngleAxis3{10_degree, Vector3::Unit(dirOrth1Idx)},
0783                    *logger),
0784       std::invalid_argument);
0785   checkUnchanged();
0786 
0787   stack.update(gctx, newBounds, trf, *logger);
0788 
0789   CHECK_CLOSE_OR_SMALL(stack.localToGlobalTransform(gctx).matrix(),
0790                        trf.matrix(), 1e-10, 1e-12);
0791   const auto* bounds =
0792       dynamic_cast<const CuboidVolumeBounds*>(&stack.volumeBounds());
0793   BOOST_REQUIRE(bounds != nullptr);
0794   BOOST_CHECK_CLOSE(bounds->get(boundDir), 950_mm, 1e-6);
0795 
0796   // All volumes including gaps should have same size in orthogonal plane
0797   for (const auto* vol : volumes) {
0798     const auto* volBounds =
0799         dynamic_cast<const CuboidVolumeBounds*>(&vol->volumeBounds());
0800     BOOST_REQUIRE(volBounds != nullptr);
0801     BOOST_CHECK_CLOSE(volBounds->get(boundDirOrth1), 100_mm, 1e-6);
0802     BOOST_CHECK_CLOSE(volBounds->get(boundDirOrth2), 300_mm, 1e-6);
0803   }
0804 
0805   if (strategy == VolumeResizeStrategy::Expand) {
0806     // No gaps were added, there was one gap initially
0807     BOOST_CHECK_EQUAL(volumes.size(), 3);
0808     const Volume* vol = nullptr;
0809     if (f < 0.0) {
0810       // first volume should have gotten bigger
0811       vol = volumes.front();
0812     } else {
0813       // last volume should have gotten bigger
0814       vol = volumes.back();
0815     }
0816 
0817     const auto* volBounds =
0818         dynamic_cast<const CuboidVolumeBounds*>(&vol->volumeBounds());
0819     BOOST_REQUIRE(volBounds != nullptr);
0820     BOOST_CHECK_CLOSE(volBounds->get(boundDir), 450_mm, 1e-6);
0821     BOOST_CHECK_EQUAL(vol->center(gctx)[dirIdx], f * 550_mm);
0822   } else if (strategy == VolumeResizeStrategy::Gap) {
0823     // One gap volume was added
0824     BOOST_CHECK_EQUAL(volumes.size(), 4);
0825 
0826     const Volume* gap = nullptr;
0827     if (f < 0.0) {
0828       gap = volumes.front();
0829     } else {
0830       gap = volumes.back();
0831     }
0832     const auto* gapBounds =
0833         dynamic_cast<const CuboidVolumeBounds*>(&gap->volumeBounds());
0834     BOOST_REQUIRE(gapBounds != nullptr);
0835 
0836     BOOST_CHECK_CLOSE(gapBounds->get(boundDir), 50_mm, 1e-6);
0837     BOOST_CHECK_EQUAL(gap->center(gctx)[dirIdx], f * 950_mm);
0838   }
0839 }
0840 
0841 //   original size
0842 // <--------------->
0843 // +---------------+
0844 // |               |
0845 // |               |
0846 // |   Volume 1    |
0847 // |               |
0848 // |               |
0849 // +---------------+
0850 //         first resize
0851 // <-------------------------->
0852 // +---------------+----------+
0853 // |               |          |
0854 // |               |          |
0855 // |   Volume 1    |   Gap    |
0856 // |               |          |      Gap is
0857 // |               |          |      reused!--+
0858 // +---------------+----------+               |
0859 //             second resize                  |
0860 // <----------------------------------->      |
0861 // +---------------+-------------------+      |
0862 // |               |                   |      |
0863 // |               |                   |      |
0864 // |   Volume 1    |        Gap        |<-----+
0865 // |               |                   |
0866 // |               |                   |
0867 // +---------------+-------------------+
0868 //
0869 BOOST_DATA_TEST_CASE(ResizeGapMultiple,
0870                      boost::unit_test::data::make(AxisDirection::AxisX,
0871                                                   AxisDirection::AxisY,
0872                                                   AxisDirection::AxisZ),
0873                      dir) {
0874   auto [dirOrth1, dirOrth2] = CuboidVolumeStack::getOrthogonalAxes(dir);
0875 
0876   auto dirIdx = CuboidVolumeStack::axisToIndex(dir);
0877 
0878   auto boundDir = CuboidVolumeBounds::boundsFromAxisDirection(dir);
0879   auto boundDirOrth1 = CuboidVolumeBounds::boundsFromAxisDirection(dirOrth1);
0880   auto boundDirOrth2 = CuboidVolumeBounds::boundsFromAxisDirection(dirOrth2);
0881 
0882   auto bounds = std::make_shared<CuboidVolumeBounds>(
0883       std::initializer_list<std::pair<CuboidVolumeBounds::BoundValues, double>>{
0884           {boundDir, 100}, {boundDirOrth1, 70}, {boundDirOrth2, 100}});
0885   Transform3 trf = Transform3::Identity();
0886   Volume vol{trf, bounds};
0887 
0888   BOOST_TEST_CONTEXT("Positive") {
0889     std::vector<Volume*> volumes = {&vol};
0890     CuboidVolumeStack stack(gctx, volumes, dir, VolumeAttachmentStrategy::Gap,
0891                             VolumeResizeStrategy::Gap, *logger);
0892 
0893     BOOST_CHECK_EQUAL(volumes.size(), 1);
0894     BOOST_CHECK(stack.gaps().empty());
0895 
0896     auto newBounds1 = std::make_shared<CuboidVolumeBounds>(
0897         std::initializer_list<
0898             std::pair<CuboidVolumeBounds::BoundValues, double>>{
0899             {boundDir, 200}, {boundDirOrth1, 70}, {boundDirOrth2, 100}});
0900     stack.update(gctx, newBounds1,
0901                  trf * Translation3{Vector3::Unit(dirIdx) * 100}, *logger);
0902     BOOST_CHECK_EQUAL(volumes.size(), 2);
0903     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
0904 
0905     BOOST_CHECK_EQUAL(stack.gaps().front()->center(gctx)[dirIdx], 200.0);
0906     const auto* updatedBounds = dynamic_cast<const CuboidVolumeBounds*>(
0907         &stack.gaps().front()->volumeBounds());
0908     BOOST_REQUIRE_NE(updatedBounds, nullptr);
0909     BOOST_CHECK_CLOSE(updatedBounds->get(boundDir), 100.0, 1e-6);
0910 
0911     auto newBounds2 = std::make_shared<CuboidVolumeBounds>(
0912         std::initializer_list<
0913             std::pair<CuboidVolumeBounds::BoundValues, double>>{
0914             {boundDir, 300}, {boundDirOrth1, 70}, {boundDirOrth2, 100}});
0915     stack.update(gctx, newBounds2,
0916                  trf * Translation3{Vector3::Unit(dirIdx) * 200}, *logger);
0917 
0918     BOOST_CHECK_EQUAL(volumes.size(), 2);
0919     // No additional gap volume was added!
0920     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
0921 
0922     BOOST_CHECK_EQUAL(stack.gaps().front()->center(gctx)[dirIdx], 300.0);
0923     updatedBounds = dynamic_cast<const CuboidVolumeBounds*>(
0924         &stack.gaps().front()->volumeBounds());
0925     BOOST_REQUIRE_NE(updatedBounds, nullptr);
0926     BOOST_CHECK_CLOSE(updatedBounds->get(boundDir), 200.0, 1e-6);
0927   }
0928 
0929   BOOST_TEST_CONTEXT("Negative") {
0930     std::vector<Volume*> volumes = {&vol};
0931     CuboidVolumeStack stack(gctx, volumes, dir, VolumeAttachmentStrategy::Gap,
0932                             VolumeResizeStrategy::Gap, *logger);
0933 
0934     BOOST_CHECK_EQUAL(volumes.size(), 1);
0935     BOOST_CHECK(stack.gaps().empty());
0936 
0937     auto newBounds1 = std::make_shared<CuboidVolumeBounds>(
0938         std::initializer_list<
0939             std::pair<CuboidVolumeBounds::BoundValues, double>>{
0940             {boundDir, 200}, {boundDirOrth1, 70}, {boundDirOrth2, 100}});
0941     stack.update(gctx, newBounds1,
0942                  trf * Translation3{Vector3::Unit(dirIdx) * -100}, *logger);
0943     BOOST_CHECK_EQUAL(volumes.size(), 2);
0944     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
0945 
0946     BOOST_CHECK_EQUAL(stack.gaps().front()->center(gctx)[dirIdx], -200.0);
0947     const auto* updatedBounds = dynamic_cast<const CuboidVolumeBounds*>(
0948         &stack.gaps().front()->volumeBounds());
0949     BOOST_REQUIRE_NE(updatedBounds, nullptr);
0950     BOOST_CHECK_CLOSE(updatedBounds->get(boundDir), 100.0, 1e-6);
0951 
0952     auto newBounds2 = std::make_shared<CuboidVolumeBounds>(
0953         std::initializer_list<
0954             std::pair<CuboidVolumeBounds::BoundValues, double>>{
0955             {boundDir, 300}, {boundDirOrth1, 70}, {boundDirOrth2, 100}});
0956     stack.update(gctx, newBounds2,
0957                  trf * Translation3{Vector3::Unit(dirIdx) * -200}, *logger);
0958 
0959     BOOST_CHECK_EQUAL(volumes.size(), 2);
0960     // No additional gap volume was added!
0961     BOOST_CHECK_EQUAL(stack.gaps().size(), 1);
0962 
0963     BOOST_CHECK_EQUAL(stack.gaps().front()->center(gctx)[dirIdx], -300.0);
0964     updatedBounds = dynamic_cast<const CuboidVolumeBounds*>(
0965         &stack.gaps().front()->volumeBounds());
0966     BOOST_REQUIRE_NE(updatedBounds, nullptr);
0967     BOOST_CHECK_CLOSE(updatedBounds->get(boundDir), 200.0, 1e-6);
0968   }
0969 }
0970 
0971 BOOST_DATA_TEST_CASE(InvalidDirection, boost::unit_test::data::make(strategies),
0972                      strategy) {
0973   std::vector<Volume*> volumes;
0974   auto vol1 = std::make_shared<Volume>(
0975       Transform3::Identity(),
0976       std::make_shared<CuboidVolumeBounds>(100_mm, 400_mm, 400_mm));
0977   volumes.push_back(vol1.get());
0978 
0979   // Single volume invalid direction still gives an error
0980   BOOST_CHECK_THROW(
0981       CuboidVolumeStack(gctx, volumes, AxisDirection::AxisR, strategy),
0982       std::invalid_argument);
0983 
0984   auto vol2 = std::make_shared<Volume>(
0985       Transform3::Identity(),
0986       std::make_shared<CuboidVolumeBounds>(100_mm, 400_mm, 400_mm));
0987   volumes.push_back(vol2.get());
0988 
0989   BOOST_CHECK_THROW(
0990       CuboidVolumeStack(gctx, volumes, AxisDirection::AxisR, strategy),
0991       std::invalid_argument);
0992 }
0993 
0994 BOOST_DATA_TEST_CASE(InvalidInput,
0995                      (boost::unit_test::data::make(strategies) *
0996                       boost::unit_test::data::make(AxisDirection::AxisX,
0997                                                    AxisDirection::AxisY,
0998                                                    AxisDirection::AxisZ)),
0999                      strategy, direction) {
1000   BOOST_TEST_CONTEXT("Empty Volume") {
1001     std::vector<Volume*> volumes;
1002     BOOST_CHECK_THROW(CuboidVolumeStack(gctx, volumes, direction, strategy),
1003                       std::invalid_argument);
1004   }
1005 
1006   BOOST_TEST_CONTEXT("Volumes rotated relative to each other") {
1007     // At this time, all rotations are considered invalid, even around
1008     // orientation
1009     for (const Vector3 axis : {Vector3::UnitX(), Vector3::UnitY()}) {
1010       std::vector<Volume*> volumes;
1011       auto vol1 = std::make_shared<Volume>(
1012           Transform3{Translation3{Vector3{0_mm, 0_mm, -500_mm}}},
1013           std::make_shared<CuboidVolumeBounds>(100_mm, 400_mm, 400_mm));
1014       volumes.push_back(vol1.get());
1015 
1016       BOOST_TEST_MESSAGE("Axis: " << axis);
1017       auto vol2 = std::make_shared<Volume>(
1018           Transform3{Translation3{Vector3{0_mm, 0_mm, 500_mm}} *
1019                      AngleAxis3(1_degree, axis)},
1020           std::make_shared<CuboidVolumeBounds>(100_mm, 400_mm, 400_mm));
1021       volumes.push_back(vol2.get());
1022 
1023       BOOST_CHECK_THROW(CuboidVolumeStack(gctx, volumes, direction, strategy,
1024                                           VolumeResizeStrategy::Gap, *logger),
1025                         std::invalid_argument);
1026     }
1027   }
1028 
1029   BOOST_TEST_CONTEXT(
1030       "Volumes shifted in the orthogonal plane relative to each other") {
1031     for (const Vector3& shift :
1032          {Vector3{5_mm, 0, 0}, Vector3{0, -5_mm, 0}, Vector3{2_mm, -2_mm, 0}}) {
1033       std::vector<Volume*> volumes;
1034       auto vol1 = std::make_shared<Volume>(
1035           Transform3{Translation3{Vector3{0_mm, 0_mm, -500_mm}}},
1036           std::make_shared<CuboidVolumeBounds>(100_mm, 400_mm, 400_mm));
1037       volumes.push_back(vol1.get());
1038 
1039       auto vol2 = std::make_shared<Volume>(
1040           Transform3{Translation3{Vector3{0_mm, 0_mm, 500_mm} + shift}},
1041           std::make_shared<CuboidVolumeBounds>(100_mm, 400_mm, 400_mm));
1042       volumes.push_back(vol2.get());
1043 
1044       BOOST_CHECK_THROW(CuboidVolumeStack(gctx, volumes, direction, strategy,
1045                                           VolumeResizeStrategy::Gap, *logger),
1046                         std::invalid_argument);
1047     }
1048   }
1049 }
1050 
1051 BOOST_DATA_TEST_CASE(JoinCuboidVolumeSingle,
1052                      (boost::unit_test::data::make(AxisDirection::AxisX,
1053                                                    AxisDirection::AxisY,
1054                                                    AxisDirection::AxisZ) *
1055                       boost::unit_test::data::make(strategies)),
1056                      direction, strategy) {
1057   auto vol = std::make_shared<Volume>(
1058       Transform3::Identity() * Translation3{14_mm, 24_mm, 0_mm} *
1059           AngleAxis3(73_degree, Vector3::UnitX()),
1060       std::make_shared<CuboidVolumeBounds>(100_mm, 400_mm, 400_mm));
1061 
1062   std::vector<Volume*> volumes{vol.get()};
1063 
1064   CuboidVolumeStack stack(gctx, volumes, direction, strategy,
1065                           VolumeResizeStrategy::Gap, *logger);
1066 
1067   // Cuboid stack has the same transform as bounds as the single input
1068   // volume
1069   BOOST_CHECK_EQUAL(volumes.size(), 1);
1070   BOOST_CHECK_EQUAL(volumes.at(0), vol.get());
1071   BOOST_CHECK_EQUAL(vol->localToGlobalTransform(gctx).matrix(),
1072                     stack.localToGlobalTransform(gctx).matrix());
1073   BOOST_CHECK_EQUAL(vol->volumeBounds(), stack.volumeBounds());
1074 }
1075 
1076 // A change in the stacking-direction bounds that is smaller than the on-surface
1077 // tolerance must be treated as "no change" and must not spawn a spurious,
1078 // near-zero thickness gap volume. This is the cuboid analogue of the cylinder
1079 // r-direction reproduction (see CylinderVolumeStackTests::RStackGapCreation-
1080 // Tolerance). The cuboid gap-resize path already guards its comparisons with
1081 // the tolerance, so this acts as a regression guard.
1082 BOOST_DATA_TEST_CASE(GapCreationTolerance,
1083                      boost::unit_test::data::make(AxisDirection::AxisX,
1084                                                   AxisDirection::AxisY,
1085                                                   AxisDirection::AxisZ),
1086                      dir) {
1087   auto boundDir = CuboidVolumeBounds::boundsFromAxisDirection(dir);
1088   auto [dirOrth1, dirOrth2] = CuboidVolumeStack::getOrthogonalAxes(dir);
1089   auto boundDirOrth1 = CuboidVolumeBounds::boundsFromAxisDirection(dirOrth1);
1090   auto boundDirOrth2 = CuboidVolumeBounds::boundsFromAxisDirection(dirOrth2);
1091   auto dirIdx = CuboidVolumeStack::axisToIndex(dir);
1092 
1093   auto makeBounds = [&](double hl) {
1094     return std::make_shared<CuboidVolumeBounds>(
1095         std::initializer_list<
1096             std::pair<CuboidVolumeBounds::BoundValues, double>>{
1097             {boundDir, hl}, {boundDirOrth1, 70}, {boundDirOrth2, 100}});
1098   };
1099 
1100   // Sub-tolerance perturbation of the half length along the stacking direction
1101   const double eps = s_onSurfaceTolerance / 2.0;
1102 
1103   for (const double sign : {+1.0, -1.0}) {
1104     BOOST_TEST_CONTEXT("sign=" << sign) {
1105       Volume vol{Transform3::Identity(), makeBounds(100)};
1106       std::vector<Volume*> volumes = {&vol};
1107       CuboidVolumeStack stack(gctx, volumes, dir, VolumeAttachmentStrategy::Gap,
1108                               VolumeResizeStrategy::Gap, *logger);
1109 
1110       BOOST_CHECK(stack.gaps().empty());
1111 
1112       // Grow the half length by less than the tolerance, on one side only
1113       stack.update(
1114           gctx, makeBounds(100 + eps / 2.0),
1115           Transform3{Translation3{Vector3::Unit(dirIdx) * sign * eps / 2.0}},
1116           *logger);
1117 
1118       // No gap volume should have been created for a sub-tolerance change
1119       BOOST_CHECK(stack.gaps().empty());
1120       BOOST_CHECK_EQUAL(volumes.size(), 1);
1121     }
1122   }
1123 }
1124 
1125 BOOST_AUTO_TEST_SUITE_END()
1126 BOOST_AUTO_TEST_SUITE_END()
1127 
1128 }  // namespace ActsTests