Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-06-30 07:53:11

0001 // This file is part of the ACTS project.
0002 //
0003 // Copyright (C) 2016 CERN for the benefit of the ACTS project
0004 //
0005 // This Source Code Form is subject to the terms of the Mozilla Public
0006 // License, v. 2.0. If a copy of the MPL was not distributed with this
0007 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
0008 
0009 #include <boost/test/data/test_case.hpp>
0010 #include <boost/test/tools/old/interface.hpp>
0011 #include <boost/test/unit_test.hpp>
0012 #include <boost/test/unit_test_suite.hpp>
0013 
0014 #include "Acts/Definitions/Units.hpp"
0015 #include "Acts/Geometry/Blueprint.hpp"
0016 #include "Acts/Geometry/BlueprintNode.hpp"
0017 #include "Acts/Geometry/ContainerBlueprintNode.hpp"
0018 #include "Acts/Geometry/CylinderVolumeBounds.hpp"
0019 #include "Acts/Geometry/CylinderVolumeStack.hpp"
0020 #include "Acts/Geometry/GeometryContext.hpp"
0021 #include "Acts/Geometry/LayerBlueprintNode.hpp"
0022 #include "Acts/Geometry/MaterialDesignatorBlueprintNode.hpp"
0023 #include "Acts/Geometry/StaticBlueprintNode.hpp"
0024 #include "Acts/Geometry/TrackingVolume.hpp"
0025 #include "Acts/Geometry/TrapezoidVolumeBounds.hpp"
0026 #include "Acts/Geometry/VolumeAttachmentStrategy.hpp"
0027 #include "Acts/Material/BinnedSurfaceMaterial.hpp"
0028 #include "Acts/Material/HomogeneousSurfaceMaterial.hpp"
0029 #include "Acts/Material/Material.hpp"
0030 #include "Acts/Material/MaterialSlab.hpp"
0031 #include "Acts/Material/ProtoSurfaceMaterial.hpp"
0032 #include "Acts/Surfaces/RectangleBounds.hpp"
0033 #include "Acts/Tests/CommonHelpers/DetectorElementStub.hpp"
0034 #include "Acts/Utilities/BinningType.hpp"
0035 #include "Acts/Utilities/Logger.hpp"
0036 #include "Acts/Utilities/ProtoAxis.hpp"
0037 
0038 #include <fstream>
0039 #include <memory>
0040 #include <stdexcept>
0041 #include <vector>
0042 
0043 using namespace Acts::UnitLiterals;
0044 
0045 using Acts::Experimental::Blueprint;
0046 using Acts::Experimental::BlueprintNode;
0047 using Acts::Experimental::BlueprintOptions;
0048 using Acts::Experimental::LayerBlueprintNode;
0049 using Acts::Experimental::MaterialDesignatorBlueprintNode;
0050 using Acts::Experimental::StaticBlueprintNode;
0051 
0052 namespace Acts::Test {
0053 
0054 auto logger = Acts::getDefaultLogger("UnitTests", Acts::Logging::VERBOSE);
0055 
0056 GeometryContext gctx;
0057 
0058 namespace {
0059 
0060 auto nameLookup(const TrackingGeometry& geo) {
0061   return [&](const std::string& name) -> const TrackingVolume& {
0062     const TrackingVolume* volume = nullptr;
0063 
0064     geo.visitVolumes([&](const TrackingVolume* v) {
0065       if (v->volumeName() == name) {
0066         volume = v;
0067       }
0068     });
0069 
0070     if (volume == nullptr) {
0071       throw std::runtime_error("Volume not found: " + name);
0072     }
0073     return *volume;
0074   };
0075 }
0076 
0077 std::size_t countVolumes(const TrackingGeometry& geo) {
0078   std::size_t nVolumes = 0;
0079   geo.visitVolumes([&](const TrackingVolume* /*volume*/) { nVolumes++; });
0080   return nVolumes;
0081 }
0082 
0083 }  // namespace
0084 
0085 BOOST_AUTO_TEST_SUITE(Geometry);
0086 
0087 BOOST_AUTO_TEST_SUITE(BlueprintNodeTest);
0088 
0089 BOOST_AUTO_TEST_CASE(InvalidRoot) {
0090   Logging::ScopedFailureThreshold threshold{Logging::Level::FATAL};
0091 
0092   Blueprint::Config cfg;
0093   Blueprint root{cfg};
0094   BOOST_CHECK_THROW(root.construct({}, gctx, *logger), std::logic_error);
0095 
0096   // More than one child is also invalid
0097   auto cylBounds = std::make_shared<CylinderVolumeBounds>(10_mm, 20_mm, 100_mm);
0098   root.addChild(
0099       std::make_unique<StaticBlueprintNode>(std::make_unique<TrackingVolume>(
0100           Transform3::Identity(), cylBounds, "child1")));
0101   root.addChild(
0102       std::make_unique<StaticBlueprintNode>(std::make_unique<TrackingVolume>(
0103           Transform3::Identity(), cylBounds, "child2")));
0104 
0105   BOOST_CHECK_THROW(root.construct({}, gctx, *logger), std::logic_error);
0106 }
0107 
0108 class DummyNode : public BlueprintNode {
0109  public:
0110   explicit DummyNode(const std::string& name) : m_name(name) {}
0111 
0112   const std::string& name() const override { return m_name; }
0113 
0114   Volume& build(const BlueprintOptions& /*options*/,
0115                 const GeometryContext& /*gctx*/,
0116                 const Acts::Logger& /*logger*/) override {
0117     throw std::logic_error("Not implemented");
0118   }
0119 
0120   PortalShellBase& connect(const BlueprintOptions& /*options*/,
0121                            const GeometryContext& /*gctx*/,
0122                            const Logger& /*logger */) override {
0123     throw std::logic_error("Not implemented");
0124   }
0125 
0126   void finalize(const BlueprintOptions& /*options*/,
0127                 const GeometryContext& /*gctx*/, TrackingVolume& /*parent*/,
0128                 const Logger& /*logger*/) override {
0129     throw std::logic_error("Not implemented");
0130   }
0131 
0132  private:
0133   std::string m_name;
0134 };
0135 
0136 BOOST_AUTO_TEST_CASE(RootCannotBeChild) {
0137   auto node = std::make_shared<DummyNode>("node");
0138   Blueprint::Config cfg;
0139   auto root = std::make_shared<Blueprint>(cfg);
0140 
0141   BOOST_CHECK_THROW(node->addChild(root), std::invalid_argument);
0142 }
0143 
0144 BOOST_AUTO_TEST_CASE(AddChildInvalid) {
0145   auto node = std::make_shared<DummyNode>("node");
0146 
0147   // Add self
0148   BOOST_CHECK_THROW(node->addChild(node), std::invalid_argument);
0149 
0150   // Add nullptr
0151   BOOST_CHECK_THROW(node->addChild(nullptr), std::invalid_argument);
0152 
0153   auto nodeB = std::make_shared<DummyNode>("nodeB");
0154   auto nodeC = std::make_shared<DummyNode>("nodeC");
0155 
0156   node->addChild(nodeB);
0157   nodeB->addChild(nodeC);
0158   BOOST_CHECK_THROW(nodeC->addChild(node), std::invalid_argument);
0159 
0160   // already has parent, can't be added as a child anywhere else
0161   BOOST_CHECK_THROW(node->addChild(nodeC), std::invalid_argument);
0162 }
0163 
0164 BOOST_AUTO_TEST_CASE(Depth) {
0165   auto node1 = std::make_shared<DummyNode>("node1");
0166   auto node2 = std::make_shared<DummyNode>("node2");
0167   auto node3 = std::make_shared<DummyNode>("node3");
0168 
0169   BOOST_CHECK_EQUAL(node1->depth(), 0);
0170   BOOST_CHECK_EQUAL(node2->depth(), 0);
0171   BOOST_CHECK_EQUAL(node3->depth(), 0);
0172 
0173   node2->addChild(node3);
0174   BOOST_CHECK_EQUAL(node2->depth(), 0);
0175   BOOST_CHECK_EQUAL(node3->depth(), 1);
0176 
0177   node1->addChild(node2);
0178   BOOST_CHECK_EQUAL(node1->depth(), 0);
0179   BOOST_CHECK_EQUAL(node2->depth(), 1);
0180   BOOST_CHECK_EQUAL(node3->depth(), 2);
0181 }
0182 
0183 BOOST_AUTO_TEST_CASE(Static) {
0184   Blueprint::Config cfg;
0185   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0186   cfg.envelope[AxisDirection::AxisR] = {1_mm, 2_mm};
0187   Blueprint root{cfg};
0188 
0189   double hlZ = 30_mm;
0190   auto cylBounds = std::make_shared<CylinderVolumeBounds>(10_mm, 20_mm, hlZ);
0191   auto cyl = std::make_unique<TrackingVolume>(Transform3::Identity(), cylBounds,
0192                                               "child");
0193 
0194   root.addStaticVolume(std::move(cyl));
0195 
0196   BOOST_CHECK_EQUAL(root.children().size(), 1);
0197 
0198   auto tGeometry = root.construct({}, gctx, *logger);
0199 
0200   BOOST_REQUIRE(tGeometry);
0201 
0202   BOOST_CHECK(tGeometry->geometryVersion() ==
0203               TrackingGeometry::GeometryVersion::Gen3);
0204 
0205   BOOST_CHECK_EQUAL(tGeometry->highestTrackingVolume()->volumes().size(), 1);
0206 
0207   BOOST_CHECK_EQUAL(countVolumes(*tGeometry), 2);
0208 
0209   auto lookup = nameLookup(*tGeometry);
0210   BOOST_CHECK_EQUAL(lookup("child").volumeBounds().type(),
0211                     VolumeBounds::BoundsType::eCylinder);
0212   auto actCyl =
0213       dynamic_cast<const CylinderVolumeBounds&>(lookup("child").volumeBounds());
0214   // Size as given
0215   BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMinR), 10_mm);
0216   BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMaxR), 20_mm);
0217   BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
0218 
0219   BOOST_CHECK_EQUAL(lookup("World").volumeBounds().type(),
0220                     VolumeBounds::BoundsType::eCylinder);
0221   auto worldCyl =
0222       dynamic_cast<const CylinderVolumeBounds&>(lookup("World").volumeBounds());
0223   BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMinR), 9_mm);
0224   BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMaxR), 22_mm);
0225   BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eHalfLengthZ),
0226                     hlZ + 20_mm);
0227 
0228   BOOST_CHECK_EQUAL(lookup("World").portals().size(), 8);
0229 }
0230 
0231 BOOST_AUTO_TEST_CASE(CylinderContainer) {
0232   Blueprint::Config cfg;
0233   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0234   cfg.envelope[AxisDirection::AxisR] = {2_mm, 20_mm};
0235   auto root = std::make_unique<Blueprint>(cfg);
0236 
0237   auto& cyl = root->addCylinderContainer("Container", AxisDirection::AxisZ);
0238   cyl.setAttachmentStrategy(VolumeAttachmentStrategy::Gap);
0239 
0240   double z0 = -200_mm;
0241   double hlZ = 30_mm;
0242   auto cylBounds = std::make_shared<CylinderVolumeBounds>(10_mm, 20_mm, hlZ);
0243   for (std::size_t i = 0; i < 3; i++) {
0244     auto childCyl = std::make_unique<TrackingVolume>(
0245         Transform3::Identity() *
0246             Translation3{Vector3{0, 0, z0 + i * 2 * hlZ * 1.2}},
0247         cylBounds, "child" + std::to_string(i));
0248     cyl.addStaticVolume(std::move(childCyl));
0249   }
0250 
0251   auto tGeometry = root->construct({}, gctx, *logger);
0252   BOOST_CHECK(tGeometry->geometryVersion() ==
0253               TrackingGeometry::GeometryVersion::Gen3);
0254 
0255   // 4 real volumes + 2 gaps
0256   BOOST_CHECK_EQUAL(countVolumes(*tGeometry), 6);
0257 
0258   auto lookup = nameLookup(*tGeometry);
0259   BOOST_CHECK_EQUAL(lookup("World").volumeBounds().type(),
0260                     VolumeBounds::BoundsType::eCylinder);
0261   auto worldCyl =
0262       dynamic_cast<const CylinderVolumeBounds&>(lookup("World").volumeBounds());
0263   BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMinR), 8_mm);
0264   BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMaxR), 40_mm);
0265   BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eHalfLengthZ), 122_mm);
0266 
0267   BOOST_CHECK_EQUAL(lookup("World").portals().size(), 8);
0268 
0269   for (std::size_t i = 0; i < 3; i++) {
0270     const auto& vol{lookup("child" + std::to_string(i))};
0271     BOOST_CHECK_EQUAL(vol.volumeBounds().type(),
0272                       VolumeBounds::BoundsType::eCylinder);
0273     auto actCyl = dynamic_cast<const CylinderVolumeBounds&>(vol.volumeBounds());
0274     BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMinR), 10_mm);
0275     BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMaxR), 20_mm);
0276     BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eHalfLengthZ), hlZ);
0277   }
0278 
0279   for (std::size_t i = 0; i < 2; i++) {
0280     const auto& vol{lookup("Container::Gap" + std::to_string(i + 1))};
0281     BOOST_CHECK_EQUAL(vol.volumeBounds().type(),
0282                       VolumeBounds::BoundsType::eCylinder);
0283     auto gapCyl = dynamic_cast<const CylinderVolumeBounds&>(vol.volumeBounds());
0284     BOOST_CHECK_EQUAL(gapCyl.get(CylinderVolumeBounds::eMinR), 10_mm);
0285     BOOST_CHECK_EQUAL(gapCyl.get(CylinderVolumeBounds::eMaxR), 20_mm);
0286     BOOST_CHECK_EQUAL(gapCyl.get(CylinderVolumeBounds::eHalfLengthZ), 6_mm);
0287   }
0288 }
0289 
0290 BOOST_AUTO_TEST_CASE(Confined) {
0291   Transform3 base{Transform3::Identity()};
0292 
0293   Blueprint::Config cfg;
0294   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0295   cfg.envelope[AxisDirection::AxisR] = {2_mm, 20_mm};
0296   auto root = std::make_unique<Blueprint>(cfg);
0297 
0298   root->addStaticVolume(
0299       base, std::make_shared<CylinderVolumeBounds>(50_mm, 400_mm, 1000_mm),
0300       "PixelWrapper", [&](auto& wrap) {
0301         double rMin = 100_mm;
0302         double rMax = 350_mm;
0303         double hlZ = 100_mm;
0304 
0305         wrap.addStaticVolume(
0306             base * Translation3{Vector3{0, 0, -600_mm}},
0307             std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ),
0308             "PixelNeg1");
0309 
0310         wrap.addStaticVolume(
0311             base * Translation3{Vector3{0, 0, -200_mm}},
0312             std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ),
0313             "PixelNeg2");
0314 
0315         wrap.addStaticVolume(
0316             base * Translation3{Vector3{0, 0, 200_mm}},
0317             std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ),
0318             "PixelPos1");
0319 
0320         wrap.addStaticVolume(
0321             base * Translation3{Vector3{0, 0, 600_mm}},
0322             std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ),
0323             "PixelPos2");
0324       });
0325 
0326   auto trackingGeometry = root->construct({}, gctx, *logger);
0327 
0328   // overall dimensions are the wrapper volume + envelope
0329   auto lookup = nameLookup(*trackingGeometry);
0330 
0331   BOOST_CHECK_EQUAL(lookup("World").volumeBounds().type(),
0332                     VolumeBounds::BoundsType::eCylinder);
0333   auto worldCyl =
0334       dynamic_cast<const CylinderVolumeBounds&>(lookup("World").volumeBounds());
0335   BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMinR), 48_mm);
0336   BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMaxR), 420_mm);
0337   BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eHalfLengthZ), 1020_mm);
0338 
0339   // 4 outer portals and 4 inner
0340   BOOST_CHECK_EQUAL(lookup("World").portals().size(), 8);
0341   BOOST_CHECK_EQUAL(lookup("World").volumes().size(), 1);
0342 
0343   BOOST_CHECK_EQUAL(lookup("PixelWrapper").volumeBounds().type(),
0344                     VolumeBounds::BoundsType::eCylinder);
0345 
0346   auto wrapperCyl = dynamic_cast<const CylinderVolumeBounds&>(
0347       lookup("PixelWrapper").volumeBounds());
0348   BOOST_CHECK_EQUAL(wrapperCyl.get(CylinderVolumeBounds::eMinR), 50_mm);
0349   BOOST_CHECK_EQUAL(wrapperCyl.get(CylinderVolumeBounds::eMaxR), 400_mm);
0350   BOOST_CHECK_EQUAL(wrapperCyl.get(CylinderVolumeBounds::eHalfLengthZ),
0351                     1000_mm);
0352   BOOST_CHECK_EQUAL(lookup("PixelWrapper").portals().size(), 4 + 4 * 4);
0353   BOOST_CHECK_EQUAL(lookup("PixelWrapper").volumes().size(), 4);
0354 
0355   for (const auto& name :
0356        {"PixelNeg1", "PixelNeg2", "PixelPos1", "PixelPos2"}) {
0357     BOOST_CHECK_EQUAL(lookup(name).volumeBounds().type(),
0358                       VolumeBounds::BoundsType::eCylinder);
0359     auto actCyl =
0360         dynamic_cast<const CylinderVolumeBounds&>(lookup(name).volumeBounds());
0361     BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMinR), 100_mm);
0362     BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMaxR), 350_mm);
0363     BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eHalfLengthZ), 100_mm);
0364     BOOST_CHECK_EQUAL(lookup(name).portals().size(), 4);
0365   }
0366 }
0367 
0368 BOOST_AUTO_TEST_CASE(ConfinedWithShared) {
0369   Transform3 base{Transform3::Identity()};
0370 
0371   constexpr double rMin = 100_mm;
0372   constexpr double rMax = 350_mm;
0373   constexpr double hlZ = 100_mm;
0374 
0375   auto sharedBounds = std::make_shared<CylinderVolumeBounds>(rMin, rMax, hlZ);
0376   Blueprint::Config cfg;
0377   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0378   cfg.envelope[AxisDirection::AxisR] = {2_mm, 20_mm};
0379   auto root = std::make_unique<Blueprint>(cfg);
0380 
0381   root->addCylinderContainer(
0382       "PixelWrapper", AxisDirection::AxisZ, [&](auto& wrap) {
0383         wrap.addStaticVolume(base * Translation3{Vector3{0, 0, -750_mm}},
0384                              sharedBounds, "PixelNeg1");
0385 
0386         wrap.addStaticVolume(base * Translation3{Vector3{0, 0, -200_mm}},
0387                              sharedBounds, "PixelNeg2");
0388 
0389         wrap.addStaticVolume(base * Translation3{Vector3{0, 0, 200_mm}},
0390                              sharedBounds, "PixelPos1");
0391 
0392         wrap.addStaticVolume(base * Translation3{Vector3{0, 0, 975_mm}},
0393                              sharedBounds, "PixelPos2");
0394       });
0395   auto trackingGeometry = root->construct({}, gctx, *logger);
0396   // overall dimensions are the wrapper volume + envelope
0397   auto lookup = nameLookup(*trackingGeometry);
0398   BOOST_CHECK_EQUAL(lookup("World").volumeBounds().type(),
0399                     VolumeBounds::BoundsType::eCylinder);
0400   auto worldCyl =
0401       dynamic_cast<const CylinderVolumeBounds&>(lookup("World").volumeBounds());
0402   BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMinR), 98_mm);
0403   BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMaxR), 370_mm);
0404   BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eHalfLengthZ), 982.5_mm);
0405   // 4 outer portals and 4 inner
0406   BOOST_CHECK_EQUAL(lookup("World").portals().size(), 8);
0407   BOOST_CHECK_EQUAL(lookup("World").volumes().size(), 4);
0408 
0409   constexpr std::array<double, 4> expHalfL{187.5_mm, 237.5_mm, 293.75_mm,
0410                                            243.75_mm};
0411   const std::array<std::string, 4> volNames{"PixelNeg1", "PixelNeg2",
0412                                             "PixelPos1", "PixelPos2"};
0413   for (std::size_t v = 0; v < 4; ++v) {
0414     const auto& testMe{lookup(volNames[v])};
0415     BOOST_CHECK_EQUAL(testMe.volumeBounds().type(),
0416                       VolumeBounds::BoundsType::eCylinder);
0417     BOOST_CHECK_EQUAL(testMe.volumeBoundsPtr() != sharedBounds, true);
0418 
0419     auto actCyl =
0420         dynamic_cast<const CylinderVolumeBounds&>(testMe.volumeBounds());
0421     BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMinR), 100_mm);
0422     BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMaxR), 350_mm);
0423     BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eHalfLengthZ),
0424                       expHalfL[v]);
0425     BOOST_CHECK_EQUAL(testMe.portals().size(), 4);
0426     if (v + 1 == 4) {
0427       break;
0428     }
0429     const auto& nextVol = lookup(volNames[(v + 1)]);
0430     const Acts::Vector3 outside =
0431         testMe.transform().translation() +
0432         Acts::Vector3{150_mm, 0.,
0433                       actCyl.get(CylinderVolumeBounds::eHalfLengthZ) - 0.5_mm};
0434     BOOST_CHECK_EQUAL(nextVol.inside(outside), false);
0435     const Acts::Vector3 inside =
0436         testMe.transform().translation() +
0437         Acts::Vector3{150_mm, 0.,
0438                       actCyl.get(CylinderVolumeBounds::eHalfLengthZ) + 0.5_mm};
0439     BOOST_CHECK_EQUAL(nextVol.inside(inside), true);
0440   }
0441 }
0442 
0443 BOOST_AUTO_TEST_CASE(DiscLayer) {
0444   double yrot = 45_degree;
0445   Transform3 base = Transform3::Identity() * AngleAxis3{yrot, Vector3::UnitY()};
0446 
0447   std::vector<std::shared_ptr<Surface>> surfaces;
0448   std::vector<std::unique_ptr<DetectorElementBase>> elements;
0449   double r = 300_mm;
0450   std::size_t nSensors = 8;
0451   double thickness = 2.5_mm;
0452   auto recBounds = std::make_shared<RectangleBounds>(40_mm, 60_mm);
0453 
0454   double deltaPhi = 2 * std::numbers::pi / nSensors;
0455   for (std::size_t i = 0; i < nSensors; i++) {
0456     // Create a fan of sensors
0457 
0458     Transform3 trf = base * AngleAxis3{deltaPhi * i, Vector3::UnitZ()} *
0459                      Translation3(Vector3::UnitX() * r);
0460 
0461     if (i % 2 == 0) {
0462       trf = trf * Translation3{Vector3::UnitZ() * 5_mm};
0463     }
0464 
0465     auto& element = elements.emplace_back(
0466         std::make_unique<DetectorElementStub>(trf, recBounds, thickness));
0467 
0468     element->surface().assignDetectorElement(*element);
0469 
0470     surfaces.push_back(element->surface().getSharedPtr());
0471   }
0472 
0473   std::function<void(LayerBlueprintNode&)> withSurfaces =
0474       [&surfaces, &base](LayerBlueprintNode& layer) {
0475         layer.setSurfaces(surfaces)
0476             .setLayerType(LayerBlueprintNode::LayerType::Disc)
0477             .setEnvelope(ExtentEnvelope{{
0478                 .z = {0.1_mm, 0.1_mm},
0479                 .r = {1_mm, 1_mm},
0480             }})
0481             .setTransform(base);
0482       };
0483 
0484   std::function<void(LayerBlueprintNode&)> withProtoLayer =
0485       [&surfaces, &base](LayerBlueprintNode& layer) {
0486         MutableProtoLayer protoLayer{gctx, surfaces, base.inverse()};
0487         layer.setProtoLayer(protoLayer)
0488             .setLayerType(LayerBlueprintNode::LayerType::Disc)
0489             .setEnvelope(ExtentEnvelope{{
0490                 .z = {0.1_mm, 0.1_mm},
0491                 .r = {1_mm, 1_mm},
0492             }});
0493       };
0494 
0495   for (const auto& func : {withSurfaces, withProtoLayer}) {
0496     Blueprint root{{.envelope = ExtentEnvelope{{
0497                         .z = {2_mm, 2_mm},
0498                         .r = {3_mm, 5_mm},
0499                     }}}};
0500 
0501     root.addLayer("Layer0", [&](auto& layer) { func(layer); });
0502 
0503     auto trackingGeometry = root.construct({}, gctx, *logger);
0504 
0505     std::size_t nSurfaces = 0;
0506 
0507     trackingGeometry->visitSurfaces([&](const Surface* surface) {
0508       if (surface->associatedDetectorElement() != nullptr) {
0509         nSurfaces++;
0510       }
0511     });
0512 
0513     BOOST_CHECK_EQUAL(nSurfaces, surfaces.size());
0514     BOOST_CHECK_EQUAL(countVolumes(*trackingGeometry), 2);
0515     auto lookup = nameLookup(*trackingGeometry);
0516 
0517     BOOST_CHECK_EQUAL(lookup("Layer0").volumeBounds().type(),
0518                       VolumeBounds::BoundsType::eCylinder);
0519     auto layerCyl = dynamic_cast<const CylinderVolumeBounds&>(
0520         lookup("Layer0").volumeBounds());
0521     BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eMinR), 258.9999999_mm,
0522                       1e-6);
0523     BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eMaxR),
0524                       346.25353003_mm, 1e-6);
0525     BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eHalfLengthZ), 3.85_mm,
0526                       1e-6);
0527   }
0528 }
0529 
0530 BOOST_AUTO_TEST_CASE(CylinderLayer) {
0531   double yrot = 0_degree;
0532   Transform3 base = Transform3::Identity() * AngleAxis3{yrot, Vector3::UnitY()};
0533 
0534   std::vector<std::shared_ptr<Surface>> surfaces;
0535   std::vector<std::unique_ptr<DetectorElementBase>> elements;
0536 
0537   double r = 300_mm;
0538   std::size_t nStaves = 10;
0539   int nSensorsPerStave = 8;
0540   double thickness = 0;
0541   double hlPhi = 40_mm;
0542   double hlZ = 60_mm;
0543   auto recBounds = std::make_shared<RectangleBounds>(hlPhi, hlZ);
0544 
0545   double deltaPhi = 2 * std::numbers::pi / nStaves;
0546 
0547   for (std::size_t istave = 0; istave < nStaves; istave++) {
0548     for (int isensor = -nSensorsPerStave; isensor <= nSensorsPerStave;
0549          isensor++) {
0550       double z = isensor * (2 * hlZ + 5_mm);
0551 
0552       Transform3 trf = base * Translation3(Vector3::UnitZ() * z) *
0553                        AngleAxis3{deltaPhi * istave, Vector3::UnitZ()} *
0554                        Translation3(Vector3::UnitX() * r) *
0555                        AngleAxis3{10_degree, Vector3::UnitZ()} *
0556                        AngleAxis3{90_degree, Vector3::UnitY()} *
0557                        AngleAxis3{90_degree, Vector3::UnitZ()};
0558       auto& element = elements.emplace_back(
0559           std::make_unique<DetectorElementStub>(trf, recBounds, thickness));
0560       element->surface().assignDetectorElement(*element);
0561       surfaces.push_back(element->surface().getSharedPtr());
0562     }
0563   }
0564 
0565   std::function<void(LayerBlueprintNode&)> withSurfaces =
0566       [&surfaces, &base](LayerBlueprintNode& layer) {
0567         layer.setSurfaces(surfaces)
0568             .setLayerType(LayerBlueprintNode::LayerType::Cylinder)
0569             .setEnvelope(ExtentEnvelope{{
0570                 .z = {10_mm, 10_mm},
0571                 .r = {20_mm, 10_mm},
0572             }})
0573             .setTransform(base);
0574       };
0575 
0576   std::function<void(LayerBlueprintNode&)> withProtoLayer =
0577       [&surfaces, &base](LayerBlueprintNode& layer) {
0578         MutableProtoLayer protoLayer{gctx, surfaces, base.inverse()};
0579         layer.setProtoLayer(protoLayer)
0580             .setLayerType(LayerBlueprintNode::LayerType::Cylinder)
0581             .setEnvelope(ExtentEnvelope{{
0582                 .z = {10_mm, 10_mm},
0583                 .r = {20_mm, 10_mm},
0584             }});
0585       };
0586 
0587   for (const auto& func : {withSurfaces, withProtoLayer}) {
0588     Blueprint root{{.envelope = ExtentEnvelope{{
0589                         .z = {2_mm, 2_mm},
0590                         .r = {3_mm, 5_mm},
0591                     }}}};
0592 
0593     root.addLayer("Layer0", [&](auto& layer) { func(layer); });
0594 
0595     auto trackingGeometry = root.construct({}, gctx, *logger);
0596 
0597     std::size_t nSurfaces = 0;
0598 
0599     trackingGeometry->visitSurfaces([&](const Surface* surface) {
0600       if (surface->associatedDetectorElement() != nullptr) {
0601         nSurfaces++;
0602       }
0603     });
0604 
0605     BOOST_CHECK_EQUAL(nSurfaces, surfaces.size());
0606     BOOST_CHECK_EQUAL(countVolumes(*trackingGeometry), 2);
0607     auto lookup = nameLookup(*trackingGeometry);
0608 
0609     BOOST_CHECK_EQUAL(lookup("Layer0").volumeBounds().type(),
0610                       VolumeBounds::BoundsType::eCylinder);
0611     auto layerCyl = dynamic_cast<const CylinderVolumeBounds&>(
0612         lookup("Layer0").volumeBounds());
0613     BOOST_CHECK_EQUAL(lookup("Layer0").portals().size(), 4);
0614     BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eMinR), 275.6897761_mm,
0615                       1e-6);
0616     BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eMaxR), 319.4633358_mm,
0617                       1e-6);
0618     BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eHalfLengthZ), 1070_mm,
0619                       1e-6);
0620   }
0621 }
0622 
0623 BOOST_AUTO_TEST_CASE(Material) {
0624   Blueprint::Config cfg;
0625   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0626   cfg.envelope[AxisDirection::AxisR] = {1_mm, 2_mm};
0627   Blueprint root{cfg};
0628 
0629   double hlZ = 30_mm;
0630   auto cylBounds = std::make_shared<CylinderVolumeBounds>(10_mm, 20_mm, hlZ);
0631   auto cyl = std::make_unique<TrackingVolume>(Transform3::Identity(), cylBounds,
0632                                               "child");
0633 
0634   using enum AxisDirection;
0635   using enum CylinderVolumeBounds::Face;
0636   using enum AxisBoundaryType;
0637 
0638   root.addMaterial("Material", [&](auto& mat) {
0639     mat.configureFace(NegativeDisc, {AxisR, Bound, 5}, {AxisPhi, Bound, 10});
0640     mat.configureFace(PositiveDisc, {AxisR, Bound, 15}, {AxisPhi, Bound, 20});
0641     mat.configureFace(OuterCylinder, {AxisRPhi, Bound, 25}, {AxisZ, Bound, 30});
0642 
0643     mat.addStaticVolume(std::move(cyl));
0644   });
0645 
0646   auto trackingGeometry = root.construct({}, gctx, *logger);
0647 
0648   BOOST_CHECK_EQUAL(countVolumes(*trackingGeometry), 2);
0649   auto lookup = nameLookup(*trackingGeometry);
0650   auto& child = lookup("child");
0651 
0652   // Check negative disc material
0653   const auto* negDisc = child.portals()
0654                             .at(static_cast<std::size_t>(NegativeDisc))
0655                             .surface()
0656                             .surfaceMaterial();
0657   BOOST_REQUIRE_NE(negDisc, nullptr);
0658   const auto& negDiscMat =
0659       dynamic_cast<const ProtoGridSurfaceMaterial&>(*negDisc);
0660   // Check positive disc material
0661   const auto* posDisc = child.portals()
0662                             .at(static_cast<std::size_t>(PositiveDisc))
0663                             .surface()
0664                             .surfaceMaterial();
0665   BOOST_REQUIRE_NE(posDisc, nullptr);
0666   const auto& posDiscMat =
0667       dynamic_cast<const ProtoGridSurfaceMaterial&>(*posDisc);
0668 
0669   BOOST_CHECK_EQUAL(negDiscMat.binning().at(0).getAxis().getNBins(), 5);
0670   BOOST_CHECK_EQUAL(negDiscMat.binning().at(1).getAxis().getNBins(), 10);
0671   BOOST_CHECK_EQUAL(posDiscMat.binning().at(0).getAxis().getNBins(), 15);
0672   BOOST_CHECK_EQUAL(posDiscMat.binning().at(1).getAxis().getNBins(), 20);
0673 
0674   // Check outer cylinder material
0675   const auto* outerCyl = child.portals()
0676                              .at(static_cast<std::size_t>(OuterCylinder))
0677                              .surface()
0678                              .surfaceMaterial();
0679   BOOST_REQUIRE_NE(outerCyl, nullptr);
0680   const auto& outerCylMat =
0681       dynamic_cast<const ProtoGridSurfaceMaterial&>(*outerCyl);
0682   BOOST_CHECK_EQUAL(outerCylMat.binning().at(0).getAxis().getNBins(), 25);
0683   BOOST_CHECK_EQUAL(outerCylMat.binning().at(1).getAxis().getNBins(), 30);
0684 
0685   // Check that other faces have no material
0686   for (std::size_t i = 0; i < child.portals().size(); i++) {
0687     if (i != static_cast<std::size_t>(NegativeDisc) &&
0688         i != static_cast<std::size_t>(PositiveDisc) &&
0689         i != static_cast<std::size_t>(OuterCylinder)) {
0690       BOOST_CHECK_EQUAL(child.portals().at(i).surface().surfaceMaterial(),
0691                         nullptr);
0692     }
0693   }
0694 }
0695 
0696 BOOST_AUTO_TEST_CASE(MaterialInvalidAxisDirections) {
0697   Blueprint::Config cfg;
0698   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0699   cfg.envelope[AxisDirection::AxisR] = {1_mm, 2_mm};
0700   Blueprint root{cfg};
0701 
0702   using enum AxisDirection;
0703   using enum AxisBoundaryType;
0704 
0705   // Test invalid axis direction combinations for cylinder faces
0706   BOOST_CHECK_THROW(
0707       root.addMaterial("Material",
0708                        [&](auto& mat) {
0709                          mat.configureFace(
0710                              CylinderVolumeBounds::Face::NegativeDisc,
0711                              {AxisZ, Bound, 5}, {AxisPhi, Bound, 10});
0712                        }),
0713       std::invalid_argument);
0714 
0715   BOOST_CHECK_THROW(
0716       root.addMaterial("Material",
0717                        [&](auto& mat) {
0718                          mat.configureFace(
0719                              CylinderVolumeBounds::Face::OuterCylinder,
0720                              {AxisR, Bound, 5}, {AxisR, Bound, 10});
0721                        }),
0722       std::invalid_argument);
0723 
0724   // Test invalid axis direction combinations for cuboid faces
0725   BOOST_CHECK_THROW(
0726       root.addMaterial("Material",
0727                        [&](auto& mat) {
0728                          mat.configureFace(
0729                              CuboidVolumeBounds::Face::NegativeXFace,
0730                              {AxisX, Bound, 5}, {AxisZ, Bound, 10});
0731                        }),
0732       std::invalid_argument);
0733 
0734   BOOST_CHECK_THROW(
0735       root.addMaterial("Material",
0736                        [&](auto& mat) {
0737                          mat.configureFace(
0738                              CuboidVolumeBounds::Face::PositiveYFace,
0739                              {AxisY, Bound, 5}, {AxisX, Bound, 10});
0740                        }),
0741       std::invalid_argument);
0742 
0743   BOOST_CHECK_THROW(
0744       root.addMaterial("Material",
0745                        [&](auto& mat) {
0746                          mat.configureFace(
0747                              CuboidVolumeBounds::Face::NegativeZFace,
0748                              {AxisZ, Bound, 5}, {AxisY, Bound, 10});
0749                        }),
0750       std::invalid_argument);
0751 }
0752 
0753 BOOST_AUTO_TEST_CASE(MaterialMixedVolumeTypes) {
0754   Blueprint::Config cfg;
0755   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0756   cfg.envelope[AxisDirection::AxisR] = {1_mm, 2_mm};
0757   Blueprint root{cfg};
0758 
0759   using enum AxisDirection;
0760   using enum AxisBoundaryType;
0761 
0762   // Configure for cylinder first, then try to add cuboid - should throw
0763   BOOST_CHECK_THROW(
0764       root.addMaterial(
0765           "Material",
0766           [&](auto& mat) {
0767             mat.configureFace(CylinderVolumeBounds::Face::NegativeDisc,
0768                               {AxisR, Bound, 5}, {AxisPhi, Bound, 10});
0769             mat.configureFace(CuboidVolumeBounds::Face::NegativeXFace,
0770                               {AxisX, Bound, 5}, {AxisY, Bound, 10});
0771           }),
0772       std::runtime_error);
0773 
0774   // Configure for cuboid first, then try to add cylinder - should throw
0775   BOOST_CHECK_THROW(
0776       root.addMaterial(
0777           "Material",
0778           [&](auto& mat) {
0779             mat.configureFace(CuboidVolumeBounds::Face::NegativeXFace,
0780                               {AxisX, Bound, 5}, {AxisY, Bound, 10});
0781             mat.configureFace(CylinderVolumeBounds::Face::NegativeDisc,
0782                               {AxisR, Bound, 5}, {AxisPhi, Bound, 10});
0783           }),
0784       std::runtime_error);
0785 }
0786 
0787 BOOST_AUTO_TEST_CASE(MaterialCuboid) {
0788   Blueprint::Config cfg;
0789   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0790   cfg.envelope[AxisDirection::AxisR] = {1_mm, 2_mm};
0791   Blueprint root{cfg};
0792 
0793   using enum AxisDirection;
0794   using enum AxisBoundaryType;
0795   using enum CuboidVolumeBounds::Face;
0796 
0797   double hlX = 30_mm;
0798   double hlY = 40_mm;
0799   double hlZ = 50_mm;
0800   auto cuboidBounds = std::make_shared<CuboidVolumeBounds>(hlX, hlY, hlZ);
0801   auto cuboid = std::make_unique<TrackingVolume>(Transform3::Identity(),
0802                                                  cuboidBounds, "child");
0803 
0804   auto mat = std::make_shared<MaterialDesignatorBlueprintNode>("Material");
0805 
0806   // Configure material for different faces with different binning
0807   mat->configureFace(NegativeXFace, {AxisX, Bound, 5}, {AxisY, Bound, 10});
0808   mat->configureFace(PositiveXFace, {AxisX, Bound, 15}, {AxisY, Bound, 20});
0809   mat->configureFace(NegativeYFace, {AxisX, Bound, 25}, {AxisY, Bound, 30});
0810   mat->configureFace(PositiveYFace, {AxisX, Bound, 35}, {AxisY, Bound, 40});
0811   mat->configureFace(NegativeZFace, {AxisX, Bound, 45}, {AxisY, Bound, 50});
0812   mat->configureFace(PositiveZFace, {AxisX, Bound, 55}, {AxisY, Bound, 60});
0813 
0814   mat->addChild(std::make_shared<StaticBlueprintNode>(std::move(cuboid)));
0815 
0816   root.addChild(mat);
0817 
0818   auto trackingGeometry =
0819       root.construct({}, gctx, *logger->clone(std::nullopt, Logging::VERBOSE));
0820 
0821   BOOST_REQUIRE(trackingGeometry);
0822 
0823   auto lookup = nameLookup(*trackingGeometry);
0824   auto& child = lookup("child");
0825 
0826   // Check that material is attached to all faces
0827   for (std::size_t i = 0; i < child.portals().size(); i++) {
0828     const auto* material = child.portals().at(i).surface().surfaceMaterial();
0829     BOOST_REQUIRE_NE(material, nullptr);
0830 
0831     const auto& gridMaterial =
0832         dynamic_cast<const ProtoGridSurfaceMaterial&>(*material);
0833 
0834     // Check binning based on face
0835     CuboidVolumeBounds::Face face = static_cast<CuboidVolumeBounds::Face>(i);
0836     switch (face) {
0837       case NegativeXFace:
0838         BOOST_CHECK_EQUAL(gridMaterial.binning().at(0).getAxis().getNBins(), 5);
0839         BOOST_CHECK_EQUAL(gridMaterial.binning().at(1).getAxis().getNBins(),
0840                           10);
0841         break;
0842       case PositiveXFace:
0843         BOOST_CHECK_EQUAL(gridMaterial.binning().at(0).getAxis().getNBins(),
0844                           15);
0845         BOOST_CHECK_EQUAL(gridMaterial.binning().at(1).getAxis().getNBins(),
0846                           20);
0847         break;
0848       case NegativeYFace:
0849         BOOST_CHECK_EQUAL(gridMaterial.binning().at(0).getAxis().getNBins(),
0850                           25);
0851         BOOST_CHECK_EQUAL(gridMaterial.binning().at(1).getAxis().getNBins(),
0852                           30);
0853         break;
0854       case PositiveYFace:
0855         BOOST_CHECK_EQUAL(gridMaterial.binning().at(0).getAxis().getNBins(),
0856                           35);
0857         BOOST_CHECK_EQUAL(gridMaterial.binning().at(1).getAxis().getNBins(),
0858                           40);
0859         break;
0860       case NegativeZFace:
0861         BOOST_CHECK_EQUAL(gridMaterial.binning().at(0).getAxis().getNBins(),
0862                           45);
0863         BOOST_CHECK_EQUAL(gridMaterial.binning().at(1).getAxis().getNBins(),
0864                           50);
0865         break;
0866       case PositiveZFace:
0867         BOOST_CHECK_EQUAL(gridMaterial.binning().at(0).getAxis().getNBins(),
0868                           55);
0869         BOOST_CHECK_EQUAL(gridMaterial.binning().at(1).getAxis().getNBins(),
0870                           60);
0871         break;
0872     }
0873   }
0874 }
0875 
0876 BOOST_AUTO_TEST_CASE(HomogeneousMaterialCylinder) {
0877   Blueprint::Config cfg;
0878   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0879   cfg.envelope[AxisDirection::AxisR] = {1_mm, 2_mm};
0880   Blueprint root{cfg};
0881 
0882   double hlZ = 30_mm;
0883   auto cylBounds = std::make_shared<CylinderVolumeBounds>(10_mm, 20_mm, hlZ);
0884   auto cyl = std::make_unique<TrackingVolume>(Transform3::Identity(), cylBounds,
0885                                               "child");
0886 
0887   using enum CylinderVolumeBounds::Face;
0888 
0889   // Create some homogeneous materials with different properties
0890   auto testMaterial = Acts::Material::fromMolarDensity(
0891       9.370_cm, 46.52_cm, 28.0855, 14, (2.329 / 28.0855) * 1_mol / 1_cm3);
0892 
0893   auto negDiscMat = std::make_shared<HomogeneousSurfaceMaterial>(
0894       MaterialSlab(testMaterial, 0.1_mm));
0895   auto posDiscMat = std::make_shared<HomogeneousSurfaceMaterial>(
0896       MaterialSlab(testMaterial, 0.2_mm));
0897   auto outerCylMat = std::make_shared<HomogeneousSurfaceMaterial>(
0898       MaterialSlab(testMaterial, 0.3_mm));
0899 
0900   root.addMaterial("Material", [&](auto& mat) {
0901     mat.configureFace(NegativeDisc, negDiscMat);
0902     mat.configureFace(PositiveDisc, posDiscMat);
0903     mat.configureFace(OuterCylinder, outerCylMat);
0904 
0905     mat.addStaticVolume(std::move(cyl));
0906   });
0907 
0908   auto trackingGeometry = root.construct({}, gctx, *logger);
0909 
0910   BOOST_CHECK_EQUAL(countVolumes(*trackingGeometry), 2);
0911   auto lookup = nameLookup(*trackingGeometry);
0912   auto& child = lookup("child");
0913 
0914   // Check negative disc material
0915   const auto* negDisc = child.portals()
0916                             .at(static_cast<std::size_t>(NegativeDisc))
0917                             .surface()
0918                             .surfaceMaterial();
0919   BOOST_REQUIRE_NE(negDisc, nullptr);
0920   BOOST_CHECK_EQUAL(negDisc, negDiscMat.get());
0921 
0922   // Check positive disc material
0923   const auto* posDisc = child.portals()
0924                             .at(static_cast<std::size_t>(PositiveDisc))
0925                             .surface()
0926                             .surfaceMaterial();
0927   BOOST_REQUIRE_NE(posDisc, nullptr);
0928   BOOST_CHECK_EQUAL(posDisc, posDiscMat.get());
0929 
0930   // Check outer cylinder material
0931   const auto* outerCyl = child.portals()
0932                              .at(static_cast<std::size_t>(OuterCylinder))
0933                              .surface()
0934                              .surfaceMaterial();
0935   BOOST_REQUIRE_NE(outerCyl, nullptr);
0936   BOOST_CHECK_EQUAL(outerCyl, outerCylMat.get());
0937 
0938   // Check that other faces have no material
0939   for (std::size_t i = 0; i < child.portals().size(); i++) {
0940     if (i != static_cast<std::size_t>(NegativeDisc) &&
0941         i != static_cast<std::size_t>(PositiveDisc) &&
0942         i != static_cast<std::size_t>(OuterCylinder)) {
0943       BOOST_CHECK_EQUAL(child.portals().at(i).surface().surfaceMaterial(),
0944                         nullptr);
0945     }
0946   }
0947 }
0948 
0949 BOOST_AUTO_TEST_CASE(HomogeneousMaterialCuboid) {
0950   Blueprint::Config cfg;
0951   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0952   cfg.envelope[AxisDirection::AxisR] = {1_mm, 2_mm};
0953   Blueprint root{cfg};
0954 
0955   using enum CuboidVolumeBounds::Face;
0956 
0957   double hlX = 30_mm;
0958   double hlY = 40_mm;
0959   double hlZ = 50_mm;
0960   auto cuboidBounds = std::make_shared<CuboidVolumeBounds>(hlX, hlY, hlZ);
0961   auto cuboid = std::make_unique<TrackingVolume>(Transform3::Identity(),
0962                                                  cuboidBounds, "child");
0963 
0964   // Create different homogeneous materials for each face
0965   auto testMaterial = Acts::Material::fromMolarDensity(
0966       9.370_cm, 46.52_cm, 28.0855, 14, (2.329 / 28.0855) * 1_mol / 1_cm3);
0967 
0968   auto negXMat = std::make_shared<HomogeneousSurfaceMaterial>(
0969       MaterialSlab(testMaterial, 0.1_mm));
0970   auto posXMat = std::make_shared<HomogeneousSurfaceMaterial>(
0971       MaterialSlab(testMaterial, 0.2_mm));
0972   auto negYMat = std::make_shared<HomogeneousSurfaceMaterial>(
0973       MaterialSlab(testMaterial, 0.3_mm));
0974   auto posYMat = std::make_shared<HomogeneousSurfaceMaterial>(
0975       MaterialSlab(testMaterial, 0.4_mm));
0976   auto negZMat = std::make_shared<HomogeneousSurfaceMaterial>(
0977       MaterialSlab(testMaterial, 0.5_mm));
0978   auto posZMat = std::make_shared<HomogeneousSurfaceMaterial>(
0979       MaterialSlab(testMaterial, 0.6_mm));
0980 
0981   root.addMaterial("Material", [&](auto& mat) {
0982     mat.configureFace(NegativeXFace, negXMat);
0983     mat.configureFace(PositiveXFace, posXMat);
0984     mat.configureFace(NegativeYFace, negYMat);
0985     mat.configureFace(PositiveYFace, posYMat);
0986     mat.configureFace(NegativeZFace, negZMat);
0987     mat.configureFace(PositiveZFace, posZMat);
0988 
0989     mat.addStaticVolume(std::move(cuboid));
0990   });
0991 
0992   auto trackingGeometry = root.construct({}, gctx, *logger);
0993 
0994   BOOST_REQUIRE(trackingGeometry);
0995 
0996   auto lookup = nameLookup(*trackingGeometry);
0997   auto& child = lookup("child");
0998 
0999   // Check that material is attached to all faces with correct properties
1000   for (std::size_t i = 0; i < child.portals().size(); i++) {
1001     const auto* material = child.portals().at(i).surface().surfaceMaterial();
1002     BOOST_REQUIRE_NE(material, nullptr);
1003 
1004     const auto* homMaterial =
1005         dynamic_cast<const HomogeneousSurfaceMaterial*>(material);
1006     BOOST_REQUIRE_NE(homMaterial, nullptr);
1007 
1008     // Check thickness based on face
1009     CuboidVolumeBounds::Face face = static_cast<CuboidVolumeBounds::Face>(i);
1010     switch (face) {
1011       case NegativeXFace:
1012         BOOST_CHECK_EQUAL(homMaterial, negXMat.get());
1013         break;
1014       case PositiveXFace:
1015         BOOST_CHECK_EQUAL(homMaterial, posXMat.get());
1016         break;
1017       case NegativeYFace:
1018         BOOST_CHECK_EQUAL(homMaterial, negYMat.get());
1019         break;
1020       case PositiveYFace:
1021         BOOST_CHECK_EQUAL(homMaterial, posYMat.get());
1022         break;
1023       case NegativeZFace:
1024         BOOST_CHECK_EQUAL(homMaterial, negZMat.get());
1025         break;
1026       case PositiveZFace:
1027         BOOST_CHECK_EQUAL(homMaterial, posZMat.get());
1028         break;
1029     }
1030   }
1031 }
1032 
1033 BOOST_AUTO_TEST_CASE(HomogeneousMaterialMixedVolumeTypes) {
1034   Blueprint::Config cfg;
1035   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
1036   cfg.envelope[AxisDirection::AxisR] = {1_mm, 2_mm};
1037   Blueprint root{cfg};
1038 
1039   auto testMaterial = Acts::Material::fromMolarDensity(
1040       9.370_cm, 46.52_cm, 28.0855, 14, (2.329 / 28.0855) * 1_mol / 1_cm3);
1041 
1042   auto material = std::make_shared<HomogeneousSurfaceMaterial>(
1043       MaterialSlab(testMaterial, 0.1_mm));
1044 
1045   // Configure for cylinder first, then try to add cuboid - should throw
1046   BOOST_CHECK_THROW(
1047       root.addMaterial("Material",
1048                        [&](auto& mat) {
1049                          mat.configureFace(
1050                              CylinderVolumeBounds::Face::NegativeDisc,
1051                              material);
1052                          mat.configureFace(
1053                              CuboidVolumeBounds::Face::NegativeXFace, material);
1054                        }),
1055       std::runtime_error);
1056 
1057   // Configure for cuboid first, then try to add cylinder - should throw
1058   BOOST_CHECK_THROW(
1059       root.addMaterial("Material",
1060                        [&](auto& mat) {
1061                          mat.configureFace(
1062                              CuboidVolumeBounds::Face::NegativeXFace, material);
1063                          mat.configureFace(
1064                              CylinderVolumeBounds::Face::NegativeDisc,
1065                              material);
1066                        }),
1067       std::runtime_error);
1068 }
1069 
1070 BOOST_AUTO_TEST_CASE(LayerCenterOfGravity) {
1071   // Test disc layer with center of gravity disabled
1072   {
1073     double yrot = 45_degree;
1074     Transform3 base =
1075         Transform3::Identity() * AngleAxis3{yrot, Vector3::UnitY()};
1076 
1077     std::vector<std::shared_ptr<Surface>> surfaces;
1078     std::vector<std::unique_ptr<DetectorElementBase>> elements;
1079     double r = 300_mm;
1080     std::size_t nSensors = 8;
1081     double thickness = 2.5_mm;
1082     auto recBounds = std::make_shared<RectangleBounds>(40_mm, 60_mm);
1083 
1084     double deltaPhi = 2 * std::numbers::pi / nSensors;
1085     for (std::size_t i = 0; i < nSensors; i++) {
1086       Transform3 trf = base * AngleAxis3{deltaPhi * i, Vector3::UnitZ()} *
1087                        Translation3(Vector3::UnitX() * r);
1088 
1089       if (i % 2 == 0) {
1090         trf = trf * Translation3{Vector3::UnitZ() * 5_mm};
1091       }
1092 
1093       auto& element = elements.emplace_back(
1094           std::make_unique<DetectorElementStub>(trf, recBounds, thickness));
1095 
1096       element->surface().assignDetectorElement(*element);
1097       surfaces.push_back(element->surface().getSharedPtr());
1098     }
1099 
1100     Blueprint root{{.envelope = ExtentEnvelope{{
1101                         .z = {2_mm, 2_mm},
1102                         .r = {3_mm, 5_mm},
1103                     }}}};
1104 
1105     root.addLayer("Layer0", [&](auto& layer) {
1106       layer.setSurfaces(surfaces)
1107           .setLayerType(LayerBlueprintNode::LayerType::Disc)
1108           .setEnvelope(ExtentEnvelope{{
1109               .z = {0.1_mm, 0.1_mm},
1110               .r = {1_mm, 1_mm},
1111           }})
1112           .setTransform(base)
1113           .setUseCenterOfGravity(false, false, false);  // Disable all axes
1114     });
1115 
1116     auto trackingGeometry = root.construct({}, gctx, *logger);
1117     auto lookup = nameLookup(*trackingGeometry);
1118 
1119     BOOST_CHECK_EQUAL(lookup("Layer0").volumeBounds().type(),
1120                       VolumeBounds::BoundsType::eCylinder);
1121 
1122     auto layerCyl = dynamic_cast<const CylinderVolumeBounds&>(
1123         lookup("Layer0").volumeBounds());
1124 
1125     // With center of gravity disabled, the layer should be at the origin
1126     BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eMinR), 258.9999999_mm,
1127                       1e-6);
1128     BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eMaxR),
1129                       346.25353003_mm, 1e-6);
1130     BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eHalfLengthZ), 3.85_mm,
1131                       1e-6);
1132   }
1133 
1134   // Test cylinder layer with center of gravity disabled
1135   {
1136     double yrot = 0_degree;
1137     Transform3 base =
1138         Transform3::Identity() * AngleAxis3{yrot, Vector3::UnitY()};
1139 
1140     std::vector<std::shared_ptr<Surface>> surfaces;
1141     std::vector<std::unique_ptr<DetectorElementBase>> elements;
1142 
1143     double r = 300_mm;
1144     std::size_t nStaves = 10;
1145     int nSensorsPerStave = 8;
1146     double thickness = 0;
1147     double hlPhi = 40_mm;
1148     double hlZ = 60_mm;
1149     auto recBounds = std::make_shared<RectangleBounds>(hlPhi, hlZ);
1150 
1151     double deltaPhi = 2 * std::numbers::pi / nStaves;
1152 
1153     for (std::size_t istave = 0; istave < nStaves; istave++) {
1154       for (int isensor = -nSensorsPerStave; isensor <= nSensorsPerStave;
1155            isensor++) {
1156         double z = isensor * (2 * hlZ + 5_mm);
1157 
1158         Transform3 trf = base * Translation3(Vector3::UnitZ() * z) *
1159                          AngleAxis3{deltaPhi * istave, Vector3::UnitZ()} *
1160                          Translation3(Vector3::UnitX() * r) *
1161                          AngleAxis3{10_degree, Vector3::UnitZ()} *
1162                          AngleAxis3{90_degree, Vector3::UnitY()} *
1163                          AngleAxis3{90_degree, Vector3::UnitZ()};
1164         auto& element = elements.emplace_back(
1165             std::make_unique<DetectorElementStub>(trf, recBounds, thickness));
1166         element->surface().assignDetectorElement(*element);
1167         surfaces.push_back(element->surface().getSharedPtr());
1168       }
1169     }
1170 
1171     Blueprint root{{.envelope = ExtentEnvelope{{
1172                         .z = {2_mm, 2_mm},
1173                         .r = {3_mm, 5_mm},
1174                     }}}};
1175 
1176     root.addLayer("Layer0", [&](auto& layer) {
1177       layer.setSurfaces(surfaces)
1178           .setLayerType(LayerBlueprintNode::LayerType::Cylinder)
1179           .setEnvelope(ExtentEnvelope{{
1180               .z = {10_mm, 10_mm},
1181               .r = {20_mm, 10_mm},
1182           }})
1183           .setTransform(base)
1184           .setUseCenterOfGravity(false, false, false);  // Disable all axes
1185     });
1186 
1187     auto trackingGeometry = root.construct({}, gctx, *logger);
1188     auto lookup = nameLookup(*trackingGeometry);
1189     BOOST_CHECK_EQUAL(lookup("Layer0").volumeBounds().type(),
1190                       VolumeBounds::BoundsType::eCylinder);
1191 
1192     auto layerCyl = dynamic_cast<const CylinderVolumeBounds&>(
1193         lookup("Layer0").volumeBounds());
1194 
1195     // With center of gravity disabled, the layer should be at the origin
1196     BOOST_CHECK_EQUAL(lookup("Layer0").portals().size(), 4);
1197     BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eMinR), 275.6897761_mm,
1198                       1e-6);
1199     BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eMaxR), 319.4633358_mm,
1200                       1e-6);
1201     BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eHalfLengthZ), 1070_mm,
1202                       1e-6);
1203   }
1204 }
1205 
1206 BOOST_AUTO_TEST_CASE(GeometryIdnetifiersForPortals) {
1207   Blueprint::Config cfg;
1208   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
1209   cfg.envelope[AxisDirection::AxisR] = {1_mm, 2_mm};
1210   Blueprint root{cfg};
1211 
1212   auto& cubcontainer =
1213       root.addCuboidContainer("CuboidContainer", AxisDirection::AxisX);
1214   auto parentBounds = std::make_shared<CuboidVolumeBounds>(1_m, 20_mm, 20_mm);
1215   auto parentVol = std::make_unique<TrackingVolume>(Transform3::Identity(),
1216                                                     parentBounds, "parent");
1217   parentVol->assignGeometryId(Acts::GeometryIdentifier{}.withVolume(1));
1218   auto parentNode = std::make_shared<StaticBlueprintNode>(std::move(parentVol));
1219   std::size_t nChambers = 50;
1220   // start from the edge of the parent volume
1221   double startX = -1000. + 3. + 0.5;
1222   Transform3 trf = Transform3(Translation3(startX, 0, 0));
1223   auto tbounds =
1224       std::make_shared<TrapezoidVolumeBounds>(3_mm, 3_mm, 10_mm, 15_mm);
1225 
1226   for (std::size_t i = 0; i < nChambers; i++) {
1227     // move the chambers position
1228     trf.translation() += Vector3::UnitX() * i * 7_mm;
1229 
1230     auto childVol = std::make_unique<TrackingVolume>(
1231         trf, tbounds, "child" + std::to_string(i));
1232     childVol->assignGeometryId(
1233         Acts::GeometryIdentifier{}.withVolume(1).withLayer(i + 1));
1234     auto childNode = std::make_shared<StaticBlueprintNode>(std::move(childVol));
1235     parentNode->addChild(std::move(childNode));
1236   }
1237 
1238   cubcontainer.addChild(std::move(parentNode));
1239 
1240   auto trackingGeometry = root.construct({}, gctx, *logger);
1241 }
1242 
1243 BOOST_AUTO_TEST_SUITE_END();
1244 
1245 BOOST_AUTO_TEST_SUITE_END();
1246 
1247 }  // namespace Acts::Test