Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-05-18 07:49:27

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