Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-12 08:22:12

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/Algebra.hpp"
0015 #include "Acts/Definitions/Units.hpp"
0016 #include "Acts/Geometry/Blueprint.hpp"
0017 #include "Acts/Geometry/ContainerBlueprintNode.hpp"
0018 #include "Acts/Geometry/CylinderVolumeBounds.hpp"
0019 #include "Acts/Geometry/GeometryContext.hpp"
0020 #include "Acts/Geometry/LayerBlueprintNode.hpp"
0021 #include "Acts/Geometry/MaterialDesignatorBlueprintNode.hpp"
0022 #include "Acts/Geometry/Portal.hpp"
0023 #include "Acts/Geometry/PortalDesignatorBlueprintNode.hpp"
0024 #include "Acts/Geometry/TrackingGeometry.hpp"
0025 #include "Acts/Geometry/TrackingVolume.hpp"
0026 #include "Acts/Geometry/VolumeAttachmentStrategy.hpp"
0027 #include "Acts/Geometry/VolumeResizeStrategy.hpp"
0028 #include "Acts/Material/MergedMaterialMarker.hpp"
0029 #include "Acts/Navigation/INavigationPolicy.hpp"
0030 #include "Acts/Navigation/NavigationStream.hpp"
0031 #include "Acts/Surfaces/RectangleBounds.hpp"
0032 #include "Acts/Surfaces/Surface.hpp"
0033 #include "Acts/Utilities/AxisSpec.hpp"
0034 #include "Acts/Utilities/Logger.hpp"
0035 #include "Acts/Visualization/GeometryView3D.hpp"
0036 #include "Acts/Visualization/ObjVisualization3D.hpp"
0037 #include "ActsTests/CommonHelpers/DetectorElementStub.hpp"
0038 
0039 #include <fstream>
0040 #include <random>
0041 #include <vector>
0042 
0043 using namespace Acts;
0044 using namespace UnitLiterals;
0045 
0046 using Acts::Blueprint;
0047 using Acts::LayerBlueprintNode;
0048 using Acts::MaterialDesignatorBlueprintNode;
0049 
0050 namespace ActsTests {
0051 
0052 auto logger = getDefaultLogger("UnitTests", Logging::DEBUG);
0053 
0054 auto gctx = GeometryContext::dangerouslyDefaultConstruct();
0055 
0056 inline std::vector<std::shared_ptr<Surface>> makeFanLayer(
0057     const Transform3& base,
0058     std::vector<std::unique_ptr<SurfacePlacementBase>>& elements,
0059     double r = 300_mm, std::size_t nSensors = 8, double thickness = 0) {
0060   auto recBounds = std::make_shared<RectangleBounds>(40_mm, 60_mm);
0061 
0062   double deltaPhi = 2 * std::numbers::pi / nSensors;
0063   std::vector<std::shared_ptr<Surface>> surfaces;
0064   for (std::size_t i = 0; i < nSensors; i++) {
0065     // Create a fan of sensors
0066 
0067     Transform3 trf = base * AngleAxis3{deltaPhi * i, Vector3::UnitZ()} *
0068                      Translation3(Vector3::UnitX() * r);
0069 
0070     if (i % 2 == 0) {
0071       trf = trf * Translation3{Vector3::UnitZ() * 5_mm};
0072     }
0073 
0074     auto& element = elements.emplace_back(
0075         std::make_unique<DetectorElementStub>(trf, recBounds, thickness));
0076 
0077     element->surface().assignSurfacePlacement(*element);
0078 
0079     surfaces.push_back(element->surface().getSharedPtr());
0080   }
0081   return surfaces;
0082 }
0083 
0084 inline std::vector<std::shared_ptr<Surface>> makeBarrelLayer(
0085     const Transform3& base,
0086     std::vector<std::unique_ptr<SurfacePlacementBase>>& elements,
0087     double r = 300_mm, std::size_t nStaves = 10, int nSensorsPerStave = 8,
0088     double thickness = 0, double hlPhi = 40_mm, double hlZ = 60_mm) {
0089   auto recBounds = std::make_shared<RectangleBounds>(hlPhi, hlZ);
0090 
0091   double deltaPhi = 2 * std::numbers::pi / nStaves;
0092   std::vector<std::shared_ptr<Surface>> surfaces;
0093 
0094   for (std::size_t istave = 0; istave < nStaves; istave++) {
0095     for (int isensor = -nSensorsPerStave; isensor <= nSensorsPerStave;
0096          isensor++) {
0097       double z = isensor * (2 * hlZ + 5_mm);
0098 
0099       Transform3 trf = base * Translation3(Vector3::UnitZ() * z) *
0100                        AngleAxis3{deltaPhi * istave, Vector3::UnitZ()} *
0101                        Translation3(Vector3::UnitX() * r) *
0102                        AngleAxis3{10_degree, Vector3::UnitZ()} *
0103                        AngleAxis3{90_degree, Vector3::UnitY()} *
0104                        AngleAxis3{90_degree, Vector3::UnitZ()};
0105       auto& element = elements.emplace_back(
0106           std::make_unique<DetectorElementStub>(trf, recBounds, thickness));
0107       element->surface().assignSurfacePlacement(*element);
0108       surfaces.push_back(element->surface().getSharedPtr());
0109     }
0110   }
0111 
0112   return surfaces;
0113 }
0114 
0115 }  // namespace ActsTests
0116 
0117 using namespace ActsTests;
0118 
0119 BOOST_AUTO_TEST_SUITE(GeometrySuite);
0120 
0121 void pseudoNavigation(const TrackingGeometry& trackingGeometry,
0122                       Vector3 position, const Vector3& direction,
0123                       std::ostream& csv, std::size_t run,
0124                       std::size_t substepsPerCm, const Logger& logger) {
0125   ACTS_VERBOSE("start navigation " << run);
0126   ACTS_VERBOSE("dir: " << direction.transpose());
0127   ACTS_VERBOSE(direction.norm());
0128 
0129   std::mt19937 rng{static_cast<unsigned int>(run)};
0130   std::uniform_real_distribution<> dist{0.01, 0.99};
0131 
0132   const auto* volume =
0133       trackingGeometry.resolveLowestTrackingVolume(gctx, position).value();
0134   BOOST_REQUIRE_NE(volume, nullptr);
0135   ACTS_VERBOSE(volume->volumeName());
0136 
0137   NavigationStream main;
0138   const TrackingVolume* currentVolume = volume;
0139 
0140   csv << run << "," << position[0] << "," << position[1] << "," << position[2];
0141   csv << "," << volume->geometryId().volume();
0142   csv << "," << volume->geometryId().boundary();
0143   csv << "," << volume->geometryId().sensitive();
0144   csv << std::endl;
0145 
0146   ACTS_VERBOSE("start pseudo navigation");
0147 
0148   for (std::size_t i = 0; i < 100; i++) {
0149     main = NavigationStream{};
0150     AppendOnlyNavigationStream stream{main};
0151 
0152     NavigationArguments navArgs{.position = position, .direction = direction};
0153     NavigationPolicyStateManager stateManager;
0154     currentVolume->navigationPolicy()->createState(gctx, navArgs, stateManager,
0155                                                    logger);
0156     auto policyState = stateManager.currentState();
0157     currentVolume->initializeNavigationCandidates(gctx, navArgs, policyState,
0158                                                   stream, logger);
0159 
0160     ACTS_VERBOSE(main.candidates().size() << " candidates");
0161 
0162     for (const auto& candidate : main.candidates()) {
0163       ACTS_VERBOSE(" -> " << candidate.surface().geometryId());
0164       ACTS_VERBOSE("    " << candidate.surface().toStream(gctx));
0165     }
0166 
0167     ACTS_VERBOSE("initializing candidates");
0168     main.initialize(gctx, {position, direction}, BoundaryTolerance::None());
0169 
0170     ACTS_VERBOSE(main.candidates().size() << " candidates remaining");
0171 
0172     for (const auto& candidate : main.candidates()) {
0173       ACTS_VERBOSE(" -> " << candidate.surface().geometryId());
0174       ACTS_VERBOSE("    " << candidate.surface().toStream(gctx));
0175     }
0176 
0177     if (main.currentCandidate().surface().isOnSurface(gctx, position,
0178                                                       direction)) {
0179       ACTS_VERBOSE("Already on surface at initialization, skipping candidate");
0180 
0181       auto id = main.currentCandidate().surface().geometryId();
0182       csv << run << "," << position[0] << "," << position[1] << ","
0183           << position[2];
0184       csv << "," << id.volume();
0185       csv << "," << id.boundary();
0186       csv << "," << id.sensitive();
0187       csv << std::endl;
0188       if (!main.switchToNextCandidate()) {
0189         ACTS_WARNING("candidates exhausted unexpectedly");
0190         break;
0191       }
0192     }
0193 
0194     auto writeIntersection = [&](const Vector3& pos, const Surface& surface) {
0195       csv << run << "," << pos[0] << "," << pos[1] << "," << pos[2];
0196       csv << "," << surface.geometryId().volume();
0197       csv << "," << surface.geometryId().boundary();
0198       csv << "," << surface.geometryId().sensitive();
0199       csv << std::endl;
0200     };
0201 
0202     bool terminated = false;
0203     while (main.remainingCandidates() > 0) {
0204       const auto& candidate = main.currentCandidate();
0205 
0206       ACTS_VERBOSE(candidate.position().transpose());
0207 
0208       ACTS_VERBOSE("moving to position: " << position.transpose() << " (r="
0209                                           << VectorHelpers::perp(position)
0210                                           << ")");
0211 
0212       Vector3 delta = candidate.position() - position;
0213 
0214       std::size_t substeps =
0215           std::max(1l, std::lround(delta.norm() / 10_cm * substepsPerCm));
0216 
0217       for (std::size_t j = 0; j < substeps; j++) {
0218         // position += delta / (substeps + 1);
0219         Vector3 subpos = position + dist(rng) * delta;
0220         csv << run << "," << subpos[0] << "," << subpos[1] << "," << subpos[2];
0221         csv << "," << currentVolume->geometryId().volume();
0222         csv << ",0,0";  // zero boundary and sensitive ids
0223         csv << std::endl;
0224       }
0225 
0226       position = candidate.position();
0227       ACTS_VERBOSE("                 -> "
0228                    << position.transpose()
0229                    << " (r=" << VectorHelpers::perp(position) << ")");
0230 
0231       writeIntersection(position, candidate.surface());
0232 
0233       if (candidate.isPortalTarget()) {
0234         ACTS_VERBOSE("On portal: " << candidate.surface().toStream(gctx));
0235         currentVolume =
0236             candidate.portal().resolveVolume(gctx, position, direction).value();
0237 
0238         if (currentVolume == nullptr) {
0239           ACTS_VERBOSE("switched to nullptr -> we're done");
0240           terminated = true;
0241         }
0242         break;
0243 
0244       } else {
0245         ACTS_VERBOSE("Not on portal");
0246       }
0247 
0248       main.switchToNextCandidate();
0249     }
0250 
0251     if (terminated) {
0252       ACTS_VERBOSE("Terminate pseudo navigation");
0253       break;
0254     }
0255 
0256     ACTS_VERBOSE("switched to " << currentVolume->volumeName());
0257 
0258     ACTS_VERBOSE("-----");
0259   }
0260 }
0261 
0262 BOOST_AUTO_TEST_CASE(NodeApiTestContainers) {
0263   // Transform3 base{AngleAxis3{30_degree, Vector3{1, 0, 0}}};
0264   Transform3 base{Transform3::Identity()};
0265 
0266   std::vector<std::unique_ptr<SurfacePlacementBase>> detectorElements;
0267   auto makeFan = [&](const Transform3& layerBase, auto&&..., double r,
0268                      std::size_t nSensors, double thickness) {
0269     return makeFanLayer(layerBase, detectorElements, r, nSensors, thickness);
0270   };
0271 
0272   Blueprint::Config cfg;
0273   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0274   cfg.envelope[AxisDirection::AxisR] = {0_mm, 20_mm};
0275   auto root = std::make_unique<Blueprint>(cfg);
0276 
0277   root->addMaterial("GlobalMaterial", [&](MaterialDesignatorBlueprintNode&
0278                                               mat) {
0279     using enum AxisDirection;
0280     using enum AxisBoundaryType;
0281     using enum CylinderVolumeBounds::Face;
0282 
0283     // Configure cylinder faces with proper binning
0284     mat.configureFace(OuterCylinder,
0285                       AxisSpec::DeferredEquidistant(20, AxisRPhi),
0286                       AxisSpec::DeferredEquidistant(20, AxisZ));
0287     mat.configureFace(NegativeDisc, AxisSpec::DeferredEquidistant(15, AxisR),
0288                       AxisSpec::DeferredEquidistant(25, AxisPhi));
0289     mat.configureFace(PositiveDisc, AxisSpec::DeferredEquidistant(15, AxisR),
0290                       AxisSpec::DeferredEquidistant(25, AxisPhi));
0291 
0292     mat.addCylinderContainer("Detector", AxisDirection::AxisR, [&](auto& det) {
0293       det.addCylinderContainer("Pixel", AxisDirection::AxisZ, [&](auto& cyl) {
0294         cyl.setAttachmentStrategy(VolumeAttachmentStrategy::Gap)
0295             .setResizeStrategy(VolumeResizeStrategy::Gap);
0296 
0297         cyl.addCylinderContainer(
0298             "PixelNegativeEndcap", AxisDirection::AxisZ, [&](auto& ec) {
0299               ec.setAttachmentStrategy(VolumeAttachmentStrategy::Gap);
0300 
0301               auto makeLayer = [&](const Transform3& trf, auto& layer) {
0302                 std::vector<std::shared_ptr<Surface>> surfaces;
0303                 auto layerSurfaces = makeFan(trf, 300_mm, 10, 2_mm);
0304                 std::copy(layerSurfaces.begin(), layerSurfaces.end(),
0305                           std::back_inserter(surfaces));
0306                 layerSurfaces = makeFan(trf, 500_mm, 16, 2_mm);
0307                 std::copy(layerSurfaces.begin(), layerSurfaces.end(),
0308                           std::back_inserter(surfaces));
0309 
0310                 layer.setSurfaces(surfaces)
0311                     .setLayerType(LayerBlueprintNode::LayerType::Disc)
0312                     .setEnvelope(ExtentEnvelope{{
0313                         .z = {5_mm, 5_mm},
0314                         .r = {10_mm, 20_mm},
0315                     }})
0316                     .setTransform(base);
0317               };
0318 
0319               ec.addLayer("PixelNeg1", [&](auto& layer) {
0320                 makeLayer(base * Translation3{Vector3{0, 0, -700_mm}}, layer);
0321               });
0322 
0323               ec.addLayer("PixelNeg2", [&](auto& layer) {
0324                 makeLayer(base * Translation3{Vector3{0, 0, -500_mm}}, layer);
0325               });
0326             });
0327 
0328         cyl.addCylinderContainer(
0329             "PixelBarrel", AxisDirection::AxisR, [&](auto& brl) {
0330               brl.setAttachmentStrategy(VolumeAttachmentStrategy::Gap)
0331                   .setResizeStrategy(VolumeResizeStrategy::Gap);
0332 
0333               auto makeLayer = [&](const std::string& name, double r,
0334                                    std::size_t nStaves, int nSensorsPerStave) {
0335                 brl.addLayer(name, [&](auto& layer) {
0336                   std::vector<std::shared_ptr<Surface>> surfaces =
0337                       makeBarrelLayer(base, detectorElements, r, nStaves,
0338                                       nSensorsPerStave, 2.5_mm, 10_mm, 20_mm);
0339 
0340                   layer.setSurfaces(surfaces)
0341                       .setLayerType(LayerBlueprintNode::LayerType::Cylinder)
0342                       .setEnvelope(ExtentEnvelope{{
0343                           .z = {5_mm, 5_mm},
0344                           .r = {1_mm, 1_mm},
0345                       }})
0346                       .setTransform(base);
0347                 });
0348               };
0349 
0350               makeLayer("PixelLayer0", 30_mm, 18, 5);
0351               makeLayer("PixelLayer1", 90_mm, 30, 6);
0352 
0353               brl.addStaticVolume(base,
0354                                   std::make_shared<CylinderVolumeBounds>(
0355                                       100_mm, 110_mm, 250_mm),
0356                                   "PixelSupport");
0357 
0358               makeLayer("PixelLayer2", 150_mm, 40, 7);
0359               makeLayer("PixelLayer3", 250_mm, 70, 8);
0360             });
0361 
0362         auto& ec =
0363             cyl.addCylinderContainer("PixelPosWrapper", AxisDirection::AxisR);
0364         ec.setResizeStrategy(VolumeResizeStrategy::Gap);
0365         ec.addStaticVolume(std::make_unique<TrackingVolume>(
0366             base * Translation3{Vector3{0, 0, 600_mm}},
0367             std::make_shared<CylinderVolumeBounds>(150_mm, 390_mm, 200_mm),
0368             "PixelPositiveEndcap"));
0369       });
0370 
0371       det.addStaticVolume(
0372           base, std::make_shared<CylinderVolumeBounds>(0_mm, 23_mm, 1000_mm),
0373           "BeamPipe");
0374     });
0375   });
0376 
0377   std::ofstream dot{"api_test_container.dot"};
0378   root->graphviz(dot);
0379 
0380   auto trackingGeometry = root->construct({}, gctx, *logger);
0381 
0382   BOOST_REQUIRE(trackingGeometry);
0383   BOOST_CHECK(trackingGeometry->geometryVersion() ==
0384               TrackingGeometry::GeometryVersion::Gen3);
0385 
0386   trackingGeometry->visitVolumes([&](const TrackingVolume* volume) {
0387     std::cout << volume->volumeName() << std::endl;
0388     std::cout << " -> id: " << volume->geometryId() << std::endl;
0389     std::cout << " -> " << volume->portals().size() << " portals" << std::endl;
0390   });
0391 
0392   ObjVisualization3D vis;
0393 
0394   trackingGeometry->visualize(vis, gctx);
0395 
0396   vis.write("api_test_container.obj");
0397 
0398   Vector3 position = Vector3::Zero();
0399   std::ofstream csv{"api_test_container.csv"};
0400   csv << "x,y,z,volume,boundary,sensitive" << std::endl;
0401 
0402   std::mt19937 rnd{42};
0403 
0404   std::uniform_real_distribution<> dist{-1, 1};
0405 
0406   double etaWidth = 3;
0407   double thetaMin = 2 * std::atan(std::exp(-etaWidth));
0408   double thetaMax = 2 * std::atan(std::exp(etaWidth));
0409   std::uniform_real_distribution<> thetaDist{thetaMin, thetaMax};
0410 
0411   using namespace UnitLiterals;
0412 
0413   for (std::size_t i = 0; i < 5000; i++) {
0414     double theta = thetaDist(rnd);
0415     double phi = 2 * std::numbers::pi * dist(rnd);
0416 
0417     Vector3 direction;
0418     direction[0] = std::sin(theta) * std::cos(phi);
0419     direction[1] = std::sin(theta) * std::sin(phi);
0420     direction[2] = std::cos(theta);
0421 
0422     pseudoNavigation(*trackingGeometry, position, direction, csv, i, 2,
0423                      *logger->clone(std::nullopt, Logging::DEBUG));
0424   }
0425 }
0426 
0427 BOOST_AUTO_TEST_CASE(NodeApiTestCuboid) {
0428   Transform3 base{Transform3::Identity()};
0429 
0430   Blueprint::Config cfg;
0431   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0432   cfg.envelope[AxisDirection::AxisR] = {0_mm, 20_mm};
0433   auto root = std::make_unique<Blueprint>(cfg);
0434 
0435   root->addMaterial("GlobalMaterial", [&](MaterialDesignatorBlueprintNode&
0436                                               mat) {
0437     using enum AxisDirection;
0438     using enum AxisBoundaryType;
0439     using enum CuboidVolumeBounds::Face;
0440 
0441     // Configure valid axis combinations for each face type
0442     mat.configureFace(NegativeXFace, AxisSpec::DeferredEquidistant(20, AxisX),
0443                       AxisSpec::DeferredEquidistant(20, AxisY));
0444     mat.configureFace(PositiveXFace, AxisSpec::DeferredEquidistant(20, AxisX),
0445                       AxisSpec::DeferredEquidistant(20, AxisY));
0446     mat.configureFace(NegativeYFace, AxisSpec::DeferredEquidistant(15, AxisX),
0447                       AxisSpec::DeferredEquidistant(25, AxisY));
0448     mat.configureFace(PositiveYFace, AxisSpec::DeferredEquidistant(15, AxisX),
0449                       AxisSpec::DeferredEquidistant(25, AxisY));
0450     mat.configureFace(NegativeZFace, AxisSpec::DeferredEquidistant(15, AxisX),
0451                       AxisSpec::DeferredEquidistant(25, AxisY));
0452     mat.configureFace(PositiveZFace, AxisSpec::DeferredEquidistant(15, AxisX),
0453                       AxisSpec::DeferredEquidistant(25, AxisY));
0454 
0455     mat.addStaticVolume(
0456         base, std::make_shared<CuboidVolumeBounds>(100_mm, 100_mm, 100_mm),
0457         "TestVolume");
0458   });
0459 
0460   auto trackingGeometry = root->construct({}, gctx, *logger);
0461   BOOST_REQUIRE(trackingGeometry);
0462   BOOST_CHECK(trackingGeometry->geometryVersion() ==
0463               TrackingGeometry::GeometryVersion::Gen3);
0464 }
0465 
0466 // Reproduces the "material on a merged portal" failure: material is designated
0467 // on a portal face that is subsequently merged during container stacking. The
0468 // material designator wraps a child of a z-stacking container; since stacking
0469 // in z merges the OuterCylinder portals of all children, the designated face
0470 // cannot survive the merge.
0471 //
0472 // This is detected early by the container node, before the stack shell is
0473 // built, producing a node-scoped error. The deeper shell-level reporting is
0474 // exercised directly in the CylinderPortalShell tests.
0475 BOOST_AUTO_TEST_CASE(MaterialOnMergedPortalThrows) {
0476   Transform3 base{Transform3::Identity()};
0477 
0478   Blueprint::Config cfg;
0479   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0480   cfg.envelope[AxisDirection::AxisR] = {0_mm, 20_mm};
0481   auto root = std::make_unique<Blueprint>(cfg);
0482 
0483   root->addCylinderContainer("Stack", AxisDirection::AxisZ, [&](auto& stack) {
0484     using enum AxisDirection;
0485     using enum AxisBoundaryType;
0486     using enum CylinderVolumeBounds::Face;
0487 
0488     // First child: a static volume whose OuterCylinder face is given material.
0489     // This is the face that the parent z-stack will try to merge.
0490     stack.addMaterial("Material", [&](auto& mat) {
0491       mat.configureFace(OuterCylinder,
0492                         AxisSpec::DeferredEquidistant(20, AxisRPhi),
0493                         AxisSpec::DeferredEquidistant(20, AxisZ));
0494       mat.addStaticVolume(
0495           base * Translation3{Vector3{0, 0, -200_mm}},
0496           std::make_shared<CylinderVolumeBounds>(0_mm, 100_mm, 100_mm),
0497           "VolumeA");
0498     });
0499 
0500     // Second child: plain static volume on the other side in z.
0501     stack.addStaticVolume(
0502         base * Translation3{Vector3{0, 0, 200_mm}},
0503         std::make_shared<CylinderVolumeBounds>(0_mm, 100_mm, 100_mm),
0504         "VolumeB");
0505   });
0506 
0507   // The early-detection error should name the offending face, the shell
0508   // involved, and explain that material was placed on a merged face.
0509   bool thrown = false;
0510   {
0511     Logging::ScopedFailureThreshold threshold{Logging::Level::FATAL};
0512     try {
0513       root->construct({}, gctx, *logger);
0514     } catch (const PortalMergingException& e) {
0515       thrown = true;
0516       std::string msg = e.what();
0517       BOOST_CHECK(msg.find("OuterCylinder") != std::string::npos);
0518       BOOST_CHECK(msg.find("VolumeA") != std::string::npos);
0519       BOOST_CHECK(msg.find("material") != std::string::npos);
0520     }
0521   }
0522   BOOST_CHECK(thrown);
0523 }
0524 
0525 BOOST_AUTO_TEST_CASE(MaterialOnMergedPortalKeepGoing) {
0526   // Same blueprint as MaterialOnMergedPortalThrows, but constructed with the
0527   // keep-going option. Construction must succeed, and the merged outer cylinder
0528   // surface must carry a MergedMaterialMarker.
0529   Transform3 base{Transform3::Identity()};
0530 
0531   Blueprint::Config cfg;
0532   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0533   cfg.envelope[AxisDirection::AxisR] = {0_mm, 20_mm};
0534   auto root = std::make_unique<Blueprint>(cfg);
0535 
0536   root->addCylinderContainer("Stack", AxisDirection::AxisZ, [&](auto& stack) {
0537     using enum AxisDirection;
0538     using enum AxisBoundaryType;
0539     using enum CylinderVolumeBounds::Face;
0540 
0541     stack.addMaterial("Material", [&](auto& mat) {
0542       mat.configureFace(OuterCylinder,
0543                         AxisSpec::DeferredEquidistant(20, AxisRPhi),
0544                         AxisSpec::DeferredEquidistant(20, AxisZ));
0545       mat.addStaticVolume(
0546           base * Translation3{Vector3{0, 0, -200_mm}},
0547           std::make_shared<CylinderVolumeBounds>(0_mm, 100_mm, 100_mm),
0548           "VolumeA");
0549     });
0550 
0551     stack.addStaticVolume(
0552         base * Translation3{Vector3{0, 0, 200_mm}},
0553         std::make_shared<CylinderVolumeBounds>(0_mm, 100_mm, 100_mm),
0554         "VolumeB");
0555   });
0556 
0557   BlueprintOptions options;
0558   options.keepGoingOnMaterialMergeFailure = true;
0559 
0560   std::unique_ptr<const TrackingGeometry> trackingGeometry;
0561   {
0562     Logging::ScopedFailureThreshold threshold{Logging::Level::FATAL};
0563     BOOST_REQUIRE_NO_THROW(trackingGeometry =
0564                                root->construct(options, gctx, *logger));
0565   }
0566   BOOST_REQUIRE(trackingGeometry != nullptr);
0567 
0568   std::size_t markerCount = 0;
0569   trackingGeometry->visitSurfaces(
0570       [&](const Surface* surface) {
0571         if (surface != nullptr && surface->surfaceMaterial() != nullptr &&
0572             dynamic_cast<const MergedMaterialMarker*>(
0573                 surface->surfaceMaterial()) != nullptr) {
0574           ++markerCount;
0575         }
0576       },
0577       false);
0578   BOOST_CHECK_GE(markerCount, 1u);
0579 }
0580 
0581 BOOST_AUTO_TEST_CASE(MaterialOnMergedPortalKeepGoingSingleChildFalseWarning) {
0582   // Reproducer for a false-positive in the early material-clash check:
0583   // a single-child AxisZ stack must NOT trigger a merge-material error because
0584   // no portal merge actually happens.  With keepGoingOnMaterialMergeFailure at
0585   // its default (false / strict), construction must succeed without throwing.
0586   Transform3 base{Transform3::Identity()};
0587 
0588   Blueprint::Config cfg;
0589   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0590   cfg.envelope[AxisDirection::AxisR] = {0_mm, 20_mm};
0591   auto root = std::make_unique<Blueprint>(cfg);
0592 
0593   root->addCylinderContainer("Stack", AxisDirection::AxisZ, [&](auto& stack) {
0594     using enum AxisDirection;
0595     using enum AxisBoundaryType;
0596     using enum CylinderVolumeBounds::Face;
0597 
0598     stack.addMaterial("Material", [&](auto& mat) {
0599       mat.configureFace(OuterCylinder,
0600                         AxisSpec::DeferredEquidistant(20, AxisRPhi),
0601                         AxisSpec::DeferredEquidistant(20, AxisZ));
0602       mat.addStaticVolume(
0603           base, std::make_shared<CylinderVolumeBounds>(0_mm, 100_mm, 100_mm),
0604           "VolumeA");
0605     });
0606   });
0607 
0608   // Use strict mode (keepGoingOnMaterialMergeFailure = false by default).
0609   // The single-child stack must not be mistaken for a real merge, so no
0610   // exception should be thrown.
0611   BlueprintOptions options;
0612 
0613   std::unique_ptr<const TrackingGeometry> trackingGeometry;
0614   BOOST_REQUIRE_NO_THROW(trackingGeometry =
0615                              root->construct(options, gctx, *logger));
0616   BOOST_REQUIRE(trackingGeometry != nullptr);
0617 
0618   std::size_t markerCount = 0;
0619   trackingGeometry->visitSurfaces(
0620       [&](const Surface* surface) {
0621         if (surface != nullptr && surface->surfaceMaterial() != nullptr &&
0622             dynamic_cast<const MergedMaterialMarker*>(
0623                 surface->surfaceMaterial()) != nullptr) {
0624           ++markerCount;
0625         }
0626       },
0627       false);
0628   BOOST_CHECK_EQUAL(markerCount, 0u);
0629 }
0630 
0631 // Tag the fused face between two z-stacked volumes and look the portal back up
0632 // from the final geometry. The tagged portal is shared by both volumes.
0633 BOOST_AUTO_TEST_CASE(PortalTagLookup) {
0634   using Acts::PortalDesignatorBlueprintNode;
0635   Transform3 base{Transform3::Identity()};
0636 
0637   Blueprint::Config cfg;
0638   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0639   cfg.envelope[AxisDirection::AxisR] = {0_mm, 20_mm};
0640   auto root = std::make_unique<Blueprint>(cfg);
0641 
0642   root->addCylinderContainer("Stack", AxisDirection::AxisZ, [&](auto& stack) {
0643     using enum CylinderVolumeBounds::Face;
0644 
0645     // Tag VolumeA's PositiveDisc, the face fused with VolumeB's NegativeDisc.
0646     stack.addPortalDesignator("Tags", [&](auto& tags) {
0647       tags.tagFace(PositiveDisc, "tracker_calo_boundary");
0648       tags.addStaticVolume(
0649           base * Translation3{Vector3{0, 0, -200_mm}},
0650           std::make_shared<CylinderVolumeBounds>(0_mm, 100_mm, 100_mm),
0651           "VolumeA");
0652     });
0653 
0654     stack.addStaticVolume(
0655         base * Translation3{Vector3{0, 0, 200_mm}},
0656         std::make_shared<CylinderVolumeBounds>(0_mm, 100_mm, 100_mm),
0657         "VolumeB");
0658   });
0659 
0660   std::unique_ptr<const TrackingGeometry> trackingGeometry =
0661       root->construct({}, gctx, *logger);
0662   BOOST_REQUIRE(trackingGeometry != nullptr);
0663 
0664   const Portal* portal = trackingGeometry->findPortal("tracker_calo_boundary");
0665   BOOST_REQUIRE(portal != nullptr);
0666 
0667   BOOST_CHECK(trackingGeometry->findPortal("does_not_exist") == nullptr);
0668 
0669   // The tagged portal must be the shared portal reachable from both volumes.
0670   auto containsPortal = [&](const TrackingVolume& volume) {
0671     for (const auto& p : volume.portals()) {
0672       if (&p == portal) {
0673         return true;
0674       }
0675     }
0676     return false;
0677   };
0678 
0679   const TrackingVolume* volumeA = nullptr;
0680   const TrackingVolume* volumeB = nullptr;
0681   trackingGeometry->apply([&](const TrackingVolume& volume) {
0682     if (volume.volumeName() == "VolumeA") {
0683       volumeA = &volume;
0684     } else if (volume.volumeName() == "VolumeB") {
0685       volumeB = &volume;
0686     }
0687   });
0688 
0689   BOOST_REQUIRE(volumeA != nullptr);
0690   BOOST_REQUIRE(volumeB != nullptr);
0691   BOOST_CHECK(containsPortal(*volumeA));
0692   BOOST_CHECK(containsPortal(*volumeB));
0693 }
0694 
0695 // Tagging two distinct (non-fused) portals with the same label must be detected
0696 // as a collision when the geometry is closed.
0697 BOOST_AUTO_TEST_CASE(PortalTagDuplicateThrows) {
0698   using Acts::PortalDesignatorBlueprintNode;
0699   Transform3 base{Transform3::Identity()};
0700 
0701   Blueprint::Config cfg;
0702   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0703   cfg.envelope[AxisDirection::AxisR] = {0_mm, 20_mm};
0704   auto root = std::make_unique<Blueprint>(cfg);
0705 
0706   root->addCylinderContainer("Stack", AxisDirection::AxisZ, [&](auto& stack) {
0707     using enum CylinderVolumeBounds::Face;
0708 
0709     // Tag VolumeA's NegativeDisc (an outer boundary, not fused).
0710     stack.addPortalDesignator("TagsA", [&](auto& tags) {
0711       tags.tagFace(NegativeDisc, "dup");
0712       tags.addStaticVolume(
0713           base * Translation3{Vector3{0, 0, -200_mm}},
0714           std::make_shared<CylinderVolumeBounds>(0_mm, 100_mm, 100_mm),
0715           "VolumeA");
0716     });
0717 
0718     // Tag VolumeB's PositiveDisc (a different outer boundary) with the same
0719     // tag.
0720     stack.addPortalDesignator("TagsB", [&](auto& tags) {
0721       tags.tagFace(PositiveDisc, "dup");
0722       tags.addStaticVolume(
0723           base * Translation3{Vector3{0, 0, 200_mm}},
0724           std::make_shared<CylinderVolumeBounds>(0_mm, 100_mm, 100_mm),
0725           "VolumeB");
0726     });
0727   });
0728 
0729   Logging::ScopedFailureThreshold threshold{Logging::Level::FATAL};
0730   BOOST_CHECK_THROW(root->construct({}, gctx, *logger), std::invalid_argument);
0731 }
0732 
0733 // Same as PortalTagLookup, but for a cuboid x-stack: VolumeA's PositiveXFace is
0734 // fused with VolumeB's NegativeXFace.
0735 BOOST_AUTO_TEST_CASE(PortalTagLookupCuboid) {
0736   using Acts::PortalDesignatorBlueprintNode;
0737   Transform3 base{Transform3::Identity()};
0738 
0739   Blueprint::Config cfg;
0740   cfg.envelope[AxisDirection::AxisX] = {20_mm, 20_mm};
0741   cfg.envelope[AxisDirection::AxisY] = {20_mm, 20_mm};
0742   cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm};
0743   auto root = std::make_unique<Blueprint>(cfg);
0744 
0745   root->addCuboidContainer("Stack", AxisDirection::AxisX, [&](auto& stack) {
0746     using enum CuboidVolumeBounds::Face;
0747 
0748     stack.addPortalDesignator("Tags", [&](auto& tags) {
0749       tags.tagFace(PositiveXFace, "cuboid_boundary");
0750       tags.addStaticVolume(
0751           base * Translation3{Vector3{-200_mm, 0, 0}},
0752           std::make_shared<CuboidVolumeBounds>(100_mm, 100_mm, 100_mm),
0753           "VolumeA");
0754     });
0755 
0756     stack.addStaticVolume(
0757         base * Translation3{Vector3{200_mm, 0, 0}},
0758         std::make_shared<CuboidVolumeBounds>(100_mm, 100_mm, 100_mm),
0759         "VolumeB");
0760   });
0761 
0762   std::unique_ptr<const TrackingGeometry> trackingGeometry =
0763       root->construct({}, gctx, *logger);
0764   BOOST_REQUIRE(trackingGeometry != nullptr);
0765 
0766   const Portal* portal = trackingGeometry->findPortal("cuboid_boundary");
0767   BOOST_REQUIRE(portal != nullptr);
0768 
0769   auto containsPortal = [&](const TrackingVolume& volume) {
0770     for (const auto& p : volume.portals()) {
0771       if (&p == portal) {
0772         return true;
0773       }
0774     }
0775     return false;
0776   };
0777 
0778   const TrackingVolume* volumeA = nullptr;
0779   const TrackingVolume* volumeB = nullptr;
0780   trackingGeometry->apply([&](const TrackingVolume& volume) {
0781     if (volume.volumeName() == "VolumeA") {
0782       volumeA = &volume;
0783     } else if (volume.volumeName() == "VolumeB") {
0784       volumeB = &volume;
0785     }
0786   });
0787 
0788   BOOST_REQUIRE(volumeA != nullptr);
0789   BOOST_REQUIRE(volumeB != nullptr);
0790   BOOST_CHECK(containsPortal(*volumeA));
0791   BOOST_CHECK(containsPortal(*volumeB));
0792 }
0793 
0794 BOOST_AUTO_TEST_SUITE_END();