From 661ab124a7103e8be4ae50071f288219a743ea42 Mon Sep 17 00:00:00 2001 From: "Alexander J. Pfleger" <70842573+AJPfleger@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:58:12 +0200 Subject: [PATCH 01/24] fix(gx2f): new error `UsedUnreachableMeasurements` (#3653) This fixes a super rare bug. 1 in 17k segfaulted at `calculateTrackQuantities(track)`. This was, because the `tipIndex` was set outside the actual track. The wrong `tipIndex` comes from the last propagation where we didn't try to hit all `inputMeasurements` on `insertExternalSurface`s. The created track contains measurement information, that is missing in the final track. We could think about having this as an outlier or refit ignoring the missed measurements. For now, I would just return an error, because: - We have no proper outlier definition in the GX2F yet - Refitting should be maybe a user-task. The whole refitting logic with a dropped measurement might make the whole fitter quite complicated - The error is so rare, that it is fine, to just avoid crashing We discussed an would refrain from creating a unit test, since it is quite difficult to reproduce this edge case in an isolated environment. Due to its rarity, it might be not worth the time. --- .../TrackFitting/GlobalChiSquareFitter.hpp | 37 +++++++++++++++++-- .../GlobalChiSquareFitterError.hpp | 1 + .../GlobalChiSquareFitterError.cpp | 2 + 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/Core/include/Acts/TrackFitting/GlobalChiSquareFitter.hpp b/Core/include/Acts/TrackFitting/GlobalChiSquareFitter.hpp index 2de65a78a07..92a6a4f76ba 100644 --- a/Core/include/Acts/TrackFitting/GlobalChiSquareFitter.hpp +++ b/Core/include/Acts/TrackFitting/GlobalChiSquareFitter.hpp @@ -610,10 +610,10 @@ class Gx2Fitter { // Check if we can stop to propagate if (result.measurementStates == inputMeasurements->size()) { - ACTS_INFO("Actor: finish: All measurements have been found."); + ACTS_DEBUG("Actor: finish: All measurements have been found."); result.finished = true; } else if (state.navigation.navigationBreak) { - ACTS_INFO("Actor: finish: navigationBreak."); + ACTS_DEBUG("Actor: finish: navigationBreak."); result.finished = true; } @@ -1218,7 +1218,7 @@ class Gx2Fitter { // We only consider states with a measurement (and/or material) if (!stateHasMeasurement && !doMaterial) { - ACTS_INFO(" Skip state."); + ACTS_DEBUG(" Skip state."); continue; } @@ -1388,6 +1388,9 @@ class Gx2Fitter { // Propagate again with the final covariance matrix. This is necessary to // obtain the propagated covariance for each state. + // We also need to recheck the result and find the tipIndex, because at this + // step, we will not ignore the boundary checks for measurement surfaces. We + // want to create trackstates only on surfaces, that we actually hit. if (gx2fOptions.nUpdateMax > 0) { ACTS_VERBOSE("final deltaParams:\n" << deltaParams); ACTS_VERBOSE("Propagate with the final covariance."); @@ -1413,7 +1416,33 @@ class Gx2Fitter { auto& r = propagatorState.template get>(); r.fittedStates = &trackContainer.trackStateContainer(); - m_propagator.template propagate(propagatorState); + auto propagationResult = m_propagator.template propagate(propagatorState); + + // Run the fitter + auto result = m_propagator.template makeResult(std::move(propagatorState), + propagationResult, + propagatorOptions, false); + + if (!result.ok()) { + ACTS_ERROR("Propagation failed: " << result.error()); + return result.error(); + } + + auto& propRes = *result; + GX2FResult gx2fResult = std::move(propRes.template get()); + + if (!gx2fResult.result.ok()) { + ACTS_INFO("GlobalChiSquareFitter failed in actor: " + << gx2fResult.result.error() << ", " + << gx2fResult.result.error().message()); + return gx2fResult.result.error(); + } + + if (tipIndex != gx2fResult.lastMeasurementIndex) { + ACTS_INFO("Final fit used unreachable measurements."); + return Experimental::GlobalChiSquareFitterError:: + UsedUnreachableMeasurements; + } } if (!trackContainer.hasColumn( diff --git a/Core/include/Acts/TrackFitting/GlobalChiSquareFitterError.hpp b/Core/include/Acts/TrackFitting/GlobalChiSquareFitterError.hpp index 7c95750a3f6..5bc4c55b3e2 100644 --- a/Core/include/Acts/TrackFitting/GlobalChiSquareFitterError.hpp +++ b/Core/include/Acts/TrackFitting/GlobalChiSquareFitterError.hpp @@ -19,6 +19,7 @@ enum class GlobalChiSquareFitterError { DidNotConverge = 2, NotEnoughMeasurements = 3, UpdatePushedToNewVolume = 4, + UsedUnreachableMeasurements = 5, }; std::error_code make_error_code( diff --git a/Core/src/TrackFitting/GlobalChiSquareFitterError.cpp b/Core/src/TrackFitting/GlobalChiSquareFitterError.cpp index 98beceb8cb1..3a3cc713173 100644 --- a/Core/src/TrackFitting/GlobalChiSquareFitterError.cpp +++ b/Core/src/TrackFitting/GlobalChiSquareFitterError.cpp @@ -32,6 +32,8 @@ class GlobalChiSquareFitterErrorCategory : public std::error_category { return "Gx2f: Not enough measurements."; case GlobalChiSquareFitterError::UpdatePushedToNewVolume: return "Gx2f: Update pushed the parameters to a new volume."; + case GlobalChiSquareFitterError::UsedUnreachableMeasurements: + return "Gx2f: Final fit used unreachable measurements."; default: return "unknown"; } From af00555e0490808d5b04ebb2d709d5c03eb63600 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 2 Oct 2024 11:24:58 +0200 Subject: [PATCH 02/24] feat(geo): TrackingVolume gets surface storage (#3675) Part of #3502 This PR adds storage for surfaces (with shared ownership) inside tracking volumes. Blocked by: - https://github.com/acts-project/acts/pull/3673 --- Core/include/Acts/Geometry/TrackingVolume.hpp | 20 +++++++++++++++++++ Core/src/Geometry/TrackingVolume.cpp | 18 +++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/Core/include/Acts/Geometry/TrackingVolume.hpp b/Core/include/Acts/Geometry/TrackingVolume.hpp index 290e5c21425..b208eefa54b 100644 --- a/Core/include/Acts/Geometry/TrackingVolume.hpp +++ b/Core/include/Acts/Geometry/TrackingVolume.hpp @@ -322,6 +322,25 @@ class TrackingVolume : public Volume { /// @param portal The portal to add void addPortal(std::shared_ptr portal); + using MutableSurfaceRange = + detail::TransformRange>>; + using SurfaceRange = + detail::TransformRange>>; + + /// Return all surfaces registered under this tracking volume + /// @return the range of surfaces + SurfaceRange surfaces() const; + + /// Return mutable view of the registered surfaces under this tracking volume + /// @return the range of surfaces + MutableSurfaceRange surfaces(); + + /// Add a surface to this tracking volume + /// @param surface The surface to add + void addSurface(std::shared_ptr surface); + /// Add a child volume to this tracking volume /// @param volume The volume to add /// @note The @p volume will have its mother volume assigned to @p this. @@ -516,6 +535,7 @@ class TrackingVolume : public Volume { std::vector> m_volumes; std::vector> m_portals; + std::vector> m_surfaces; }; } // namespace Acts diff --git a/Core/src/Geometry/TrackingVolume.cpp b/Core/src/Geometry/TrackingVolume.cpp index d2c1124a1ae..79ffdc9d180 100644 --- a/Core/src/Geometry/TrackingVolume.cpp +++ b/Core/src/Geometry/TrackingVolume.cpp @@ -663,7 +663,25 @@ TrackingVolume::MutablePortalRange TrackingVolume::portals() { } void TrackingVolume::addPortal(std::shared_ptr portal) { + if (portal == nullptr) { + throw std::invalid_argument("Portal is nullptr"); + } m_portals.push_back(std::move(portal)); } +TrackingVolume::SurfaceRange TrackingVolume::surfaces() const { + return SurfaceRange{m_surfaces}; +} + +TrackingVolume::MutableSurfaceRange TrackingVolume::surfaces() { + return MutableSurfaceRange{m_surfaces}; +} + +void TrackingVolume::addSurface(std::shared_ptr surface) { + if (surface == nullptr) { + throw std::invalid_argument("Surface is nullptr"); + } + m_surfaces.push_back(std::move(surface)); +} + } // namespace Acts From d4681222fbf1fa3d84cdc8061ebc296bafa78928 Mon Sep 17 00:00:00 2001 From: Carlo Varni <75478407+CarloVarni@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:06:53 +0200 Subject: [PATCH 03/24] fix: Fix Warning messages (#3679) Fix following messages: ```console /__w/acts/acts/build/../CI/test_coverage.py:44: SyntaxWarning: invalid escape sequence '\d' map(int, re.match("gcovr (\d+\.\d+)", gcovr_version_text).group(1).split(".")) /__w/acts/acts/build/../CI/test_coverage.py:66: SyntaxWarning: invalid escape sequence '\.' excludes = ["-e", "../Tests/", "-e", ".*json\.hpp"] ``` --- CI/test_coverage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CI/test_coverage.py b/CI/test_coverage.py index 06b13a06abb..0488a71d85d 100755 --- a/CI/test_coverage.py +++ b/CI/test_coverage.py @@ -41,7 +41,7 @@ def call(cmd): ret, gcovr_version_text = check_output(["gcovr", "--version"]) gcovr_version = tuple( - map(int, re.match("gcovr (\d+\.\d+)", gcovr_version_text).group(1).split(".")) + map(int, re.match(r"gcovr (\d+\.\d+)", gcovr_version_text).group(1).split(".")) ) extra_flags = [] @@ -63,7 +63,7 @@ def call(cmd): if not os.path.exists(coverage_dir): os.makedirs(coverage_dir) -excludes = ["-e", "../Tests/", "-e", ".*json\.hpp"] +excludes = ["-e", "../Tests/", "-e", r".*json\.hpp"] # create the html report call( From e617154100486f1a9429b8643c2d15bb5282ab17 Mon Sep 17 00:00:00 2001 From: "Alexander J. Pfleger" <70842573+AJPfleger@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:46:16 +0200 Subject: [PATCH 04/24] refactor: use `std::ranges:find`, `find_if`, `find_if_not` (#3614) --- .../Acts/TrackFitting/KalmanFitter.hpp | 7 ++--- .../Vertexing/AdaptiveMultiVertexFitter.hpp | 4 +-- Core/src/Geometry/CylinderVolumeBuilder.cpp | 21 ++++++-------- Core/src/Geometry/Layer.cpp | 10 +++---- Core/src/Surfaces/VerticesHelper.cpp | 6 ++-- Core/src/TrackFinding/MeasurementSelector.cpp | 8 ++--- Core/src/Utilities/BinningType.cpp | 3 +- .../Vertexing/AdaptiveMultiVertexFinder.cpp | 15 ++++------ Core/src/Vertexing/IterativeVertexFinder.cpp | 29 +++++++------------ .../Generators/Pythia8ProcessGenerator.cpp | 9 +++--- .../MappingMaterialDecorator.hpp | 13 ++++----- .../Vertexing/src/VertexingHelpers.hpp | 4 +-- .../src/Utilities/EventDataTransforms.cpp | 2 +- .../src/Validation/TrackClassification.cpp | 7 ++--- .../ActsExamples/Io/Csv/CsvInputOutput.hpp | 3 +- Examples/Io/EDM4hep/src/EDM4hepReader.cpp | 8 ++--- Examples/Io/Root/src/RootSimHitReader.cpp | 6 ++-- .../ActSVG/src/SurfaceArraySvgConverter.cpp | 17 +++++------ .../Detray/src/DetrayGeometryConverter.cpp | 4 ++- Plugins/FpeMonitoring/src/FpeMonitor.cpp | 22 ++++++-------- .../Json/src/DetectorVolumeJsonConverter.cpp | 3 +- Plugins/Json/src/MaterialMapJsonConverter.cpp | 10 +++---- Plugins/Json/src/PortalJsonConverter.cpp | 2 +- 23 files changed, 89 insertions(+), 124 deletions(-) diff --git a/Core/include/Acts/TrackFitting/KalmanFitter.hpp b/Core/include/Acts/TrackFitting/KalmanFitter.hpp index f55b0fac1cf..b0fe848c17b 100644 --- a/Core/include/Acts/TrackFitting/KalmanFitter.hpp +++ b/Core/include/Acts/TrackFitting/KalmanFitter.hpp @@ -484,11 +484,8 @@ class KalmanFitter { result.fittedStates->applyBackwards( result.lastMeasurementIndex, [&](auto trackState) { auto fSurface = &trackState.referenceSurface(); - auto surface_it = std::find_if( - result.passedAgainSurfaces.begin(), - result.passedAgainSurfaces.end(), - [=](const Surface* s) { return s == fSurface; }); - if (surface_it == result.passedAgainSurfaces.end()) { + if (!rangeContainsValue(result.passedAgainSurfaces, + fSurface)) { // If reversed filtering missed this surface, then there is // no smoothed parameter trackState.unset(TrackStatePropMask::Smoothed); diff --git a/Core/include/Acts/Vertexing/AdaptiveMultiVertexFitter.hpp b/Core/include/Acts/Vertexing/AdaptiveMultiVertexFitter.hpp index 73884fd31f3..91f18618673 100644 --- a/Core/include/Acts/Vertexing/AdaptiveMultiVertexFitter.hpp +++ b/Core/include/Acts/Vertexing/AdaptiveMultiVertexFitter.hpp @@ -22,6 +22,7 @@ #include "Acts/Vertexing/VertexingError.hpp" #include "Acts/Vertexing/VertexingOptions.hpp" +#include #include namespace Acts { @@ -81,8 +82,7 @@ class AdaptiveMultiVertexFitter { Result removeVertexFromCollection(Vertex& vtxToRemove, const Logger& logger) { - auto it = std::find(vertexCollection.begin(), vertexCollection.end(), - &vtxToRemove); + auto it = std::ranges::find(vertexCollection, &vtxToRemove); // Check if the value was found before erasing if (it == vertexCollection.end()) { ACTS_ERROR("vtxToRemove is not part of vertexCollection."); diff --git a/Core/src/Geometry/CylinderVolumeBuilder.cpp b/Core/src/Geometry/CylinderVolumeBuilder.cpp index bb741167519..47de379d9be 100644 --- a/Core/src/Geometry/CylinderVolumeBuilder.cpp +++ b/Core/src/Geometry/CylinderVolumeBuilder.cpp @@ -274,19 +274,17 @@ Acts::CylinderVolumeBuilder::trackingVolume( double tolerance = m_cfg.ringTolerance; // Search for the rmin value - and insert if necessary double rMin = discBounds->rMin(); - auto innerSearch = std::find_if( - innerRadii.begin(), innerRadii.end(), [&](double reference) { - return std::abs(rMin - reference) < tolerance; - }); + auto innerSearch = std::ranges::find_if(innerRadii, [&](double r) { + return std::abs(rMin - r) < tolerance; + }); if (innerSearch == innerRadii.end()) { innerRadii.push_back(rMin); } // Search for the rmax value - and insert if necessary double rMax = discBounds->rMax(); - auto outerSearch = std::find_if( - outerRadii.begin(), outerRadii.end(), [&](double reference) { - return std::abs(rMax - reference) < tolerance; - }); + auto outerSearch = std::ranges::find_if(outerRadii, [&](double r) { + return std::abs(rMax - r) < tolerance; + }); if (outerSearch == outerRadii.end()) { outerRadii.push_back(rMax); } @@ -356,10 +354,9 @@ Acts::CylinderVolumeBuilder::trackingVolume( double test = elay->surfaceRepresentation().binningPositionValue( gctx, BinningValue::binR); // Find the right bin - auto ringVolume = std::find_if( - volumeRminRmax.begin(), volumeRminRmax.end(), - [&](const auto& reference) { - return (test > reference.first && test < reference.second); + auto ringVolume = + std::ranges::find_if(volumeRminRmax, [&](const auto& vrr) { + return (test > vrr.first && test < vrr.second); }); if (ringVolume != volumeRminRmax.end()) { unsigned int ringBin = diff --git a/Core/src/Geometry/Layer.cpp b/Core/src/Geometry/Layer.cpp index dca511b3978..024f99e9c42 100644 --- a/Core/src/Geometry/Layer.cpp +++ b/Core/src/Geometry/Layer.cpp @@ -126,11 +126,9 @@ Acts::Layer::compatibleSurfaces( double farLimit = options.farLimit; auto isUnique = [&](const SurfaceIntersection& b) { - auto find_it = std::find_if( - sIntersections.begin(), sIntersections.end(), [&b](const auto& a) { - return a.object() == b.object() && a.index() == b.index(); - }); - return find_it == sIntersections.end(); + return std::ranges::none_of(sIntersections, [&b](const auto& a) { + return a.object() == b.object() && a.index() == b.index(); + }); }; // lemma 0 : accept the surface @@ -140,7 +138,7 @@ Acts::Layer::compatibleSurfaces( if (sensitive && options.resolveSensitive) { return true; } - // next option: it's a material surface and you want to have it + // next option: it's a material surface, and you want to have it if (options.resolveMaterial && sf.surfaceMaterial() != nullptr) { return true; } diff --git a/Core/src/Surfaces/VerticesHelper.cpp b/Core/src/Surfaces/VerticesHelper.cpp index 3ff4785fe37..448ee97e8da 100644 --- a/Core/src/Surfaces/VerticesHelper.cpp +++ b/Core/src/Surfaces/VerticesHelper.cpp @@ -35,11 +35,9 @@ std::vector Acts::detail::VerticesHelper::phiSegments( if (!phiRefs.empty()) { for (const auto& phiRef : phiRefs) { // Trying to find the right patch - auto match = std::find_if( - phiSegments.begin(), phiSegments.end(), [&](ActsScalar phiSeg) { + if (std::ranges::none_of(phiSegments, [&](ActsScalar phiSeg) { return std::abs(phiSeg - phiRef) < phiTolerance; - }); - if (match == phiSegments.end()) { + })) { phiSegments.push_back(phiRef); } } diff --git a/Core/src/TrackFinding/MeasurementSelector.cpp b/Core/src/TrackFinding/MeasurementSelector.cpp index cde6670e4ca..130c8381536 100644 --- a/Core/src/TrackFinding/MeasurementSelector.cpp +++ b/Core/src/TrackFinding/MeasurementSelector.cpp @@ -118,10 +118,10 @@ MeasurementSelector::Cuts MeasurementSelector::getCutsByTheta( // look at the positive half of the Z axis const double constrainedTheta = std::min(theta, M_PI - theta); - auto it = std::find_if(config.begin(), config.end(), - [constrainedTheta](const InternalCutBin& cuts) { - return constrainedTheta < cuts.maxTheta; - }); + auto it = std::ranges::find_if( + config, [constrainedTheta](const InternalCutBin& cuts) { + return constrainedTheta < cuts.maxTheta; + }); assert(it != config.end()); return {it->maxNumMeasurements, it->maxChi2Measurement, it->maxChi2Outlier}; } diff --git a/Core/src/Utilities/BinningType.cpp b/Core/src/Utilities/BinningType.cpp index e1ed3ecea71..8faa7dcd856 100644 --- a/Core/src/Utilities/BinningType.cpp +++ b/Core/src/Utilities/BinningType.cpp @@ -32,8 +32,7 @@ const std::vector& allBinningValues() { } BinningValue binningValueFromName(const std::string& name) { - auto it = - std::find(s_binningValueNames.begin(), s_binningValueNames.end(), name); + auto it = std::ranges::find(s_binningValueNames, name); if (it == s_binningValueNames.end()) { throw std::invalid_argument("Unknown binning value name: " + name); } diff --git a/Core/src/Vertexing/AdaptiveMultiVertexFinder.cpp b/Core/src/Vertexing/AdaptiveMultiVertexFinder.cpp index 655ee0b218f..34d20e32e0a 100644 --- a/Core/src/Vertexing/AdaptiveMultiVertexFinder.cpp +++ b/Core/src/Vertexing/AdaptiveMultiVertexFinder.cpp @@ -12,6 +12,8 @@ #include "Acts/Vertexing/IVertexFinder.hpp" #include "Acts/Vertexing/VertexingError.hpp" +#include + namespace Acts { Result> AdaptiveMultiVertexFinder::find( @@ -364,10 +366,7 @@ std::pair AdaptiveMultiVertexFinder::checkVertexAndCompatibleTracks( !m_cfg.useFastCompatibility)) { // TODO: Understand why looking for compatible tracks only in seed tracks // and not also in all tracks - auto foundIter = - std::find_if(seedTracks.begin(), seedTracks.end(), - [&trk](auto seedTrk) { return trk == seedTrk; }); - if (foundIter != seedTracks.end()) { + if (rangeContainsValue(seedTracks, trk)) { nCompatibleTracks++; ACTS_DEBUG("Compatible track found."); @@ -399,9 +398,7 @@ auto AdaptiveMultiVertexFinder::removeCompatibleTracksFromSeedTracks( trkAtVtx.chi2Track < m_cfg.maxVertexChi2 && !m_cfg.useFastCompatibility)) { // Find and remove track from seedTracks - auto foundSeedIter = - std::find_if(seedTracks.begin(), seedTracks.end(), - [&trk](auto seedTrk) { return trk == seedTrk; }); + auto foundSeedIter = std::ranges::find(seedTracks, trk); if (foundSeedIter != seedTracks.end()) { seedTracks.erase(foundSeedIter); removedSeedTracks.push_back(trk); @@ -425,9 +422,7 @@ bool AdaptiveMultiVertexFinder::removeTrackIfIncompatible( double compatibility = trkAtVtx.vertexCompatibility; if (compatibility > maxCompatibility) { // Try to find track in seed tracks - auto foundSeedIter = - std::find_if(seedTracks.begin(), seedTracks.end(), - [&trk](auto seedTrk) { return trk == seedTrk; }); + auto foundSeedIter = std::ranges::find(seedTracks, trk); if (foundSeedIter != seedTracks.end()) { maxCompatibility = compatibility; maxCompSeedIt = foundSeedIter; diff --git a/Core/src/Vertexing/IterativeVertexFinder.cpp b/Core/src/Vertexing/IterativeVertexFinder.cpp index e455dca47ec..9c03ba8a8f8 100644 --- a/Core/src/Vertexing/IterativeVertexFinder.cpp +++ b/Core/src/Vertexing/IterativeVertexFinder.cpp @@ -222,10 +222,9 @@ inline void Acts::IterativeVertexFinder::removeTracks( const BoundTrackParameters& params = m_cfg.extractParameters(trk); // Find track in seedTracks auto foundIter = - std::find_if(seedTracks.begin(), seedTracks.end(), - [¶ms, this](const auto seedTrk) { - return params == m_cfg.extractParameters(seedTrk); - }); + std::ranges::find_if(seedTracks, [¶ms, this](const auto seedTrk) { + return params == m_cfg.extractParameters(seedTrk); + }); if (foundIter != seedTracks.end()) { // Remove track from seed tracks seedTracks.erase(foundIter); @@ -284,10 +283,7 @@ Acts::Result Acts::IterativeVertexFinder::removeUsedCompatibleTracks( } // Find and remove track from seedTracks auto foundSeedIter = - std::find_if(seedTracks.begin(), seedTracks.end(), - [&trackAtVtx](const auto& seedTrk) { - return trackAtVtx.originalParams == seedTrk; - }); + std::ranges::find(seedTracks, trackAtVtx.originalParams); if (foundSeedIter != seedTracks.end()) { seedTracks.erase(foundSeedIter); } else { @@ -296,10 +292,7 @@ Acts::Result Acts::IterativeVertexFinder::removeUsedCompatibleTracks( // Find and remove track from tracksToFit auto foundFitIter = - std::find_if(tracksToFit.begin(), tracksToFit.end(), - [&trackAtVtx](const auto& fitTrk) { - return trackAtVtx.originalParams == fitTrk; - }); + std::ranges::find(tracksToFit, trackAtVtx.originalParams); if (foundFitIter != tracksToFit.end()) { tracksToFit.erase(foundFitIter); } else { @@ -334,9 +327,7 @@ Acts::Result Acts::IterativeVertexFinder::removeUsedCompatibleTracks( // check if sufficiently compatible with last fitted vertex // (quite loose constraint) if (chi2 < m_cfg.maximumChi2cutForSeeding) { - auto foundIter = - std::find_if(seedTracks.begin(), seedTracks.end(), - [&trk](const auto& seedTrk) { return trk == seedTrk; }); + auto foundIter = std::ranges::find(seedTracks, trk); if (foundIter != seedTracks.end()) { // Remove track from seed tracks seedTracks.erase(foundIter); @@ -345,8 +336,8 @@ Acts::Result Acts::IterativeVertexFinder::removeUsedCompatibleTracks( } else { // Track not compatible with vertex // Remove track from current vertex - auto foundIter = std::find_if( - tracksAtVertex.begin(), tracksAtVertex.end(), + auto foundIter = std::ranges::find_if( + tracksAtVertex, [&trk](auto trkAtVtx) { return trk == trkAtVtx.originalParams; }); if (foundIter != tracksAtVertex.end()) { // Remove track from seed tracks @@ -495,8 +486,8 @@ Acts::Result Acts::IterativeVertexFinder::reassignTracksToNewVertex( // delete it later // when all tracks used to fit current vertex are deleted seedTracks.push_back(tracksIter->originalParams); - // seedTracks.push_back(*std::find_if( - // origTracks.begin(), origTracks.end(), + // seedTracks.push_back(*std::ranges::find_if( + // origTracks, // [&origParams, this](auto origTrack) { // return origParams == m_extractParameters(*origTrack); // })); diff --git a/Examples/Algorithms/GeneratorsPythia8/ActsExamples/Generators/Pythia8ProcessGenerator.cpp b/Examples/Algorithms/GeneratorsPythia8/ActsExamples/Generators/Pythia8ProcessGenerator.cpp index 91de17b9eef..808bf931c6a 100644 --- a/Examples/Algorithms/GeneratorsPythia8/ActsExamples/Generators/Pythia8ProcessGenerator.cpp +++ b/Examples/Algorithms/GeneratorsPythia8/ActsExamples/Generators/Pythia8ProcessGenerator.cpp @@ -154,11 +154,10 @@ Pythia8Generator::operator()(RandomEngine& rng) { // check if an existing vertex is close enough auto it = - std::find_if(vertices.begin(), vertices.end(), - [&pos4, this](const SimVertex& other) { - return (pos4.head<3>() - other.position()).norm() < - m_cfg.spatialVertexThreshold; - }); + std::ranges::find_if(vertices, [&pos4, this](const SimVertex& v) { + return (pos4.head<3>() - v.position()).norm() < + m_cfg.spatialVertexThreshold; + }); if (it != vertices.end()) { particleId.setVertexSecondary(std::distance(vertices.begin(), it)); diff --git a/Examples/Algorithms/MaterialMapping/include/ActsExamples/MaterialMapping/MappingMaterialDecorator.hpp b/Examples/Algorithms/MaterialMapping/include/ActsExamples/MaterialMapping/MappingMaterialDecorator.hpp index 565b04c9ae3..b06e538885c 100644 --- a/Examples/Algorithms/MaterialMapping/include/ActsExamples/MaterialMapping/MappingMaterialDecorator.hpp +++ b/Examples/Algorithms/MaterialMapping/include/ActsExamples/MaterialMapping/MappingMaterialDecorator.hpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -93,14 +94,10 @@ class MappingMaterialDecorator : public IMaterialDecorator { /// /// @param volume to be looped onto void volumeLoop(const Acts::TrackingVolume* tVolume) { - auto sameId = - [tVolume]( - const std::pair>& pair) { - return (tVolume->geometryId() == pair.first); - }; - if (std::find_if(m_volumeMaterialMap.begin(), m_volumeMaterialMap.end(), - sameId) != m_volumeMaterialMap.end()) { + auto sameId = [tVolume](const auto& pair) { + return (tVolume->geometryId() == pair.first); + }; + if (std::ranges::any_of(m_volumeMaterialMap, sameId)) { // this volume was already visited return; } diff --git a/Examples/Algorithms/Vertexing/src/VertexingHelpers.hpp b/Examples/Algorithms/Vertexing/src/VertexingHelpers.hpp index 9d3afe19646..3222578a3da 100644 --- a/Examples/Algorithms/Vertexing/src/VertexingHelpers.hpp +++ b/Examples/Algorithms/Vertexing/src/VertexingHelpers.hpp @@ -15,6 +15,7 @@ #include "ActsExamples/Framework/AlgorithmContext.hpp" #include "ActsExamples/Framework/DataHandle.hpp" +#include #include #include @@ -55,8 +56,7 @@ inline ProtoVertexContainer makeProtoVertices( protoVertex.reserve(vertex.tracks().size()); for (const auto& track : vertex.tracks()) { - auto it = std::find(inputTracks.begin(), inputTracks.end(), - track.originalParams); + auto it = std::ranges::find(inputTracks, track.originalParams); if (it != inputTracks.end()) { protoVertex.push_back(std::distance(inputTracks.begin(), it)); } else { diff --git a/Examples/Framework/src/Utilities/EventDataTransforms.cpp b/Examples/Framework/src/Utilities/EventDataTransforms.cpp index 9e0cbbff3fd..a82df0cf5a3 100644 --- a/Examples/Framework/src/Utilities/EventDataTransforms.cpp +++ b/Examples/Framework/src/Utilities/EventDataTransforms.cpp @@ -37,7 +37,7 @@ const ActsExamples::SimSpacePoint* ActsExamples::findSpacePointForIndex( }); }; - auto found = std::find_if(spacepoints.begin(), spacepoints.end(), match); + auto found = std::ranges::find_if(spacepoints, match); if (found == spacepoints.end()) { return nullptr; diff --git a/Examples/Framework/src/Validation/TrackClassification.cpp b/Examples/Framework/src/Validation/TrackClassification.cpp index 0341dc09e5a..a89f7c2a07a 100644 --- a/Examples/Framework/src/Validation/TrackClassification.cpp +++ b/Examples/Framework/src/Validation/TrackClassification.cpp @@ -25,10 +25,9 @@ inline void increaseHitCount( std::vector& particleHitCounts, ActsFatras::Barcode particleId) { // linear search since there is no ordering - auto it = std::find_if(particleHitCounts.begin(), particleHitCounts.end(), - [=](const ActsExamples::ParticleHitCount& phc) { - return (phc.particleId == particleId); - }); + auto it = std::ranges::find_if(particleHitCounts, [=](const auto& phc) { + return (phc.particleId == particleId); + }); // either increase count if we saw the particle before or add it if (it != particleHitCounts.end()) { it->hitCount += 1u; diff --git a/Examples/Io/Csv/include/ActsExamples/Io/Csv/CsvInputOutput.hpp b/Examples/Io/Csv/include/ActsExamples/Io/Csv/CsvInputOutput.hpp index 3cdbe789e3c..7f4e7b894c3 100644 --- a/Examples/Io/Csv/include/ActsExamples/Io/Csv/CsvInputOutput.hpp +++ b/Examples/Io/Csv/include/ActsExamples/Io/Csv/CsvInputOutput.hpp @@ -38,6 +38,7 @@ #include "Acts/Utilities/Concepts.hpp" #include "Acts/Utilities/Helpers.hpp" +#include #include #include #include @@ -605,7 +606,7 @@ inline void NamedTupleDsvReader::parse_header( m_extra_columns.clear(); for (std::size_t i = 0; i < m_columns.size(); ++i) { // find the position of the column in the tuple. - auto it = std::find(names.begin(), names.end(), m_columns[i]); + auto it = std::ranges::find(names, m_columns[i]); if (it != names.end()) { // establish mapping between column and tuple item position m_tuple_column_map[std::distance(names.begin(), it)] = i; diff --git a/Examples/Io/EDM4hep/src/EDM4hepReader.cpp b/Examples/Io/EDM4hep/src/EDM4hepReader.cpp index ec84083f2aa..dfb11c84c9e 100644 --- a/Examples/Io/EDM4hep/src/EDM4hepReader.cpp +++ b/Examples/Io/EDM4hep/src/EDM4hepReader.cpp @@ -181,11 +181,9 @@ ProcessCode EDM4hepReader::read(const AlgorithmContext& ctx) { vtxPos /= Acts::UnitConstants::mm; // linear search for vector - auto it = std::find_if( - primaryVertices.begin(), primaryVertices.end(), - [&vtxPos]( - const std::pair>& - pair) { return pair.first == vtxPos; }); + auto it = std::ranges::find_if(primaryVertices, [&vtxPos](const auto& v) { + return v.first == vtxPos; + }); if (it == primaryVertices.end()) { ACTS_DEBUG("Found primary vertex at " << vtx.x << ", " << vtx.y << ", " diff --git a/Examples/Io/Root/src/RootSimHitReader.cpp b/Examples/Io/Root/src/RootSimHitReader.cpp index 1e503d8711c..4f0d805dc7e 100644 --- a/Examples/Io/Root/src/RootSimHitReader.cpp +++ b/Examples/Io/Root/src/RootSimHitReader.cpp @@ -108,9 +108,9 @@ std::pair RootSimHitReader::availableEvents() const { } ProcessCode RootSimHitReader::read(const AlgorithmContext& context) { - auto it = std::find_if( - m_eventMap.begin(), m_eventMap.end(), - [&](const auto& a) { return std::get<0>(a) == context.eventNumber; }); + auto it = std::ranges::find_if(m_eventMap, [&](const auto& a) { + return std::get<0>(a) == context.eventNumber; + }); if (it == m_eventMap.end()) { // explicitly warn if it happens for the first or last event as that might diff --git a/Plugins/ActSVG/src/SurfaceArraySvgConverter.cpp b/Plugins/ActSVG/src/SurfaceArraySvgConverter.cpp index f2acf870864..794bb1d43b5 100644 --- a/Plugins/ActSVG/src/SurfaceArraySvgConverter.cpp +++ b/Plugins/ActSVG/src/SurfaceArraySvgConverter.cpp @@ -13,6 +13,8 @@ #include "Acts/Surfaces/SurfaceArray.hpp" #include "Acts/Surfaces/SurfaceBounds.hpp" +#include + std::tuple, Acts::Svg::ProtoGrid, std::vector > Acts::Svg::SurfaceArrayConverter::convert( @@ -93,11 +95,9 @@ Acts::Svg::SurfaceArrayConverter::convert( auto sameBounds = [&](const SurfaceBounds* test) { return ((*test) == sBounds); }; - // Check if you have this template object already - auto tBounds = - std::find_if(templateBounds.begin(), templateBounds.end(), sameBounds); - // New reference bounds and new reference object - if (tBounds == templateBounds.end()) { + // Check if you have this template object already before creating new + // reference bounds and new reference object + if (std::ranges::none_of(templateBounds, sameBounds)) { // Let's get the right style SurfaceConverter::Options sOptions; sOptions.templateSurface = true; @@ -107,7 +107,7 @@ Acts::Svg::SurfaceArrayConverter::convert( sOptions.style = *sfStyle; } - // Create a referese surface and reference object from it + // Create a reference surface and reference object from it auto referenceSurface = SurfaceConverter::convert(gctx, *sf, sOptions); auto referenceObject = View::xy(referenceSurface, @@ -148,8 +148,7 @@ Acts::Svg::SurfaceArrayConverter::convert( return ((*test) == sBounds); }; // Check if you have this template object already - auto tBounds = std::find_if(templateBounds.begin(), templateBounds.end(), - sameBounds); + auto tBounds = std::ranges::find_if(templateBounds, sameBounds); // New reference bounds and new reference object if (tBounds != templateBounds.end()) { std::size_t tObject = std::distance(templateBounds.begin(), tBounds); @@ -196,7 +195,7 @@ Acts::Svg::SurfaceArrayConverter::convert( auto bSurfaces = surfaceArray.neighbors(bCenter); std::vector binnAssoc; for (const auto& bs : bSurfaces) { - auto candidate = std::find(surfaces.begin(), surfaces.end(), bs); + auto candidate = std::ranges::find(surfaces, bs); if (candidate != surfaces.end()) { binnAssoc.push_back(std::distance(surfaces.begin(), candidate)); } diff --git a/Plugins/Detray/src/DetrayGeometryConverter.cpp b/Plugins/Detray/src/DetrayGeometryConverter.cpp index da57d91c2bb..664900841f0 100644 --- a/Plugins/Detray/src/DetrayGeometryConverter.cpp +++ b/Plugins/Detray/src/DetrayGeometryConverter.cpp @@ -21,6 +21,8 @@ #include "Acts/Surfaces/Surface.hpp" #include "Acts/Surfaces/SurfaceBounds.hpp" +#include + #include using namespace detray; @@ -36,7 +38,7 @@ namespace { int findVolume( const Acts::Experimental::DetectorVolume* volume, const std::vector& volumes) { - auto candidate = std::find(volumes.begin(), volumes.end(), volume); + auto candidate = std::ranges::find(volumes, volume); if (candidate != volumes.end()) { return std::distance(volumes.begin(), candidate); } diff --git a/Plugins/FpeMonitoring/src/FpeMonitor.cpp b/Plugins/FpeMonitoring/src/FpeMonitor.cpp index 3b08145668e..8b4feb5f1d2 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitor.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitor.cpp @@ -87,10 +87,9 @@ void FpeMonitor::Result::add(FpeType type, void *stackPtr, auto st = std::make_unique( boost::stacktrace::stacktrace::from_dump(stackPtr, bufferSize)); - auto it = std::find_if( - m_stracktraces.begin(), m_stracktraces.end(), [&](const FpeInfo &el) { - return areFpesEquivalent({el.type, *el.st}, {type, *st}); - }); + auto it = std::ranges::find_if(m_stracktraces, [&](const FpeInfo &el) { + return areFpesEquivalent({el.type, *el.st}, {type, *st}); + }); if (it != m_stracktraces.end()) { it->count += 1; @@ -101,10 +100,9 @@ void FpeMonitor::Result::add(FpeType type, void *stackPtr, bool FpeMonitor::Result::contains( FpeType type, const boost::stacktrace::stacktrace &st) const { - return std::find_if(m_stracktraces.begin(), m_stracktraces.end(), - [&](const FpeInfo &el) { - return areFpesEquivalent({el.type, *el.st}, {type, st}); - }) != m_stracktraces.end(); + return std::ranges::any_of(m_stracktraces, [&](const FpeInfo &el) { + return areFpesEquivalent({el.type, *el.st}, {type, st}); + }); } FpeMonitor::Result &FpeMonitor::result() { @@ -167,11 +165,9 @@ void FpeMonitor::Result::deduplicate() { m_stracktraces.clear(); for (auto &info : copy) { - auto it = std::find_if(m_stracktraces.begin(), m_stracktraces.end(), - [&info](const FpeInfo &el) { - return areFpesEquivalent({el.type, *el.st}, - {info.type, *info.st}); - }); + auto it = std::ranges::find_if(m_stracktraces, [&info](const FpeInfo &el) { + return areFpesEquivalent({el.type, *el.st}, {info.type, *info.st}); + }); if (it != m_stracktraces.end()) { it->count += info.count; continue; diff --git a/Plugins/Json/src/DetectorVolumeJsonConverter.cpp b/Plugins/Json/src/DetectorVolumeJsonConverter.cpp index 19628c078ab..3d5c03c020d 100644 --- a/Plugins/Json/src/DetectorVolumeJsonConverter.cpp +++ b/Plugins/Json/src/DetectorVolumeJsonConverter.cpp @@ -21,6 +21,7 @@ #include "Acts/Plugins/Json/VolumeBoundsJsonConverter.hpp" #include "Acts/Utilities/Enumerate.hpp" +#include #include nlohmann::json Acts::DetectorVolumeJsonConverter::toJson( @@ -59,7 +60,7 @@ nlohmann::json Acts::DetectorVolumeJsonConverter::toJson( nlohmann::json jPortals; if (!portals.empty()) { for (const auto* p : volume.portals()) { - auto it = std::find(portals.begin(), portals.end(), p); + auto it = std::ranges::find(portals, p); if (it != portals.end()) { jPortals.push_back(std::distance(portals.begin(), it)); } else { diff --git a/Plugins/Json/src/MaterialMapJsonConverter.cpp b/Plugins/Json/src/MaterialMapJsonConverter.cpp index e27c069157c..76fc01f8569 100644 --- a/Plugins/Json/src/MaterialMapJsonConverter.cpp +++ b/Plugins/Json/src/MaterialMapJsonConverter.cpp @@ -336,12 +336,10 @@ void Acts::MaterialMapJsonConverter::convertToHierarchy( std::pair>& surfaceHierarchy, const Acts::TrackingVolume* tVolume) { - auto sameId = - [tVolume]( - const std::pair& - pair) { return (tVolume->geometryId() == pair.first); }; - if (std::find_if(volumeHierarchy.begin(), volumeHierarchy.end(), sameId) != - volumeHierarchy.end()) { + auto sameId = [tVolume](const auto& pair) { + return (tVolume->geometryId() == pair.first); + }; + if (std::ranges::any_of(volumeHierarchy, sameId)) { // this volume was already visited return; } diff --git a/Plugins/Json/src/PortalJsonConverter.cpp b/Plugins/Json/src/PortalJsonConverter.cpp index a7044f7b6dd..e39b6efca17 100644 --- a/Plugins/Json/src/PortalJsonConverter.cpp +++ b/Plugins/Json/src/PortalJsonConverter.cpp @@ -40,7 +40,7 @@ namespace { int findVolume( const Acts::Experimental::DetectorVolume* volume, const std::vector& volumes) { - auto candidate = std::find(volumes.begin(), volumes.end(), volume); + auto candidate = std::ranges::find(volumes, volume); if (candidate != volumes.end()) { return std::distance(volumes.begin(), candidate); } From e7dceab2c4a06a6e614f9be3f019a81d82940459 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Wed, 2 Oct 2024 19:56:21 +0200 Subject: [PATCH 05/24] feat: Allow volume constrain for propagation (#3470) Add optional volume constrains to the propagation which are executed by the `VolumeConstraintAborter`. This is directly exercised in the track finding and wired to python. This is generally useful if track finding / fitting should be done in a sub detector region. Same goes for simulation and extrapolation. --- .../Acts/Propagator/PropagatorOptions.hpp | 15 ++++- .../Acts/Propagator/StandardAborters.hpp | 56 ++++++++++++++++++- .../CombinatorialKalmanFilter.hpp | 9 ++- .../TrackFinding/TrackFindingAlgorithm.hpp | 11 +++- .../src/TrackFindingAlgorithm.cpp | 18 ++++-- .../python/acts/examples/reconstruction.py | 24 +++++++- Examples/Python/src/TrackFinding.cpp | 6 +- Examples/Scripts/Python/full_chain_itk.py | 4 +- Examples/Scripts/Python/full_chain_odd.py | 21 ++++++- .../Fatras/Kernel/SimulationActorTests.cpp | 10 ++++ 10 files changed, 152 insertions(+), 22 deletions(-) diff --git a/Core/include/Acts/Propagator/PropagatorOptions.hpp b/Core/include/Acts/Propagator/PropagatorOptions.hpp index 039aed013a5..78e7edab2fe 100644 --- a/Core/include/Acts/Propagator/PropagatorOptions.hpp +++ b/Core/include/Acts/Propagator/PropagatorOptions.hpp @@ -33,13 +33,22 @@ struct PurePropagatorPlainOptions { /// Absolute maximum path length double pathLimit = std::numeric_limits::max(); - /// Required tolerance to reach surface - double surfaceTolerance = s_onSurfaceTolerance; - /// Loop protection step, it adapts the pathLimit bool loopProtection = true; /// Allowed loop fraction, 1 is a full loop double loopFraction = 0.5; + + /// Required tolerance to reach surface + double surfaceTolerance = s_onSurfaceTolerance; + + /// Constrain the propagation to selected volumes + /// @note ignored if empty + /// @note requires `VolumeConstraintAborter` aborter + std::vector constrainToVolumeIds; + /// Additional volumes to be considered as end of world + /// @note ignored if empty + /// @note requires `VolumeConstraintAborter` aborter + std::vector endOfWorldVolumeIds; }; } // namespace detail diff --git a/Core/include/Acts/Propagator/StandardAborters.hpp b/Core/include/Acts/Propagator/StandardAborters.hpp index ce460a945d7..706b0ce0697 100644 --- a/Core/include/Acts/Propagator/StandardAborters.hpp +++ b/Core/include/Acts/Propagator/StandardAborters.hpp @@ -161,7 +161,7 @@ struct ForcedSurfaceReached : SurfaceReached { : SurfaceReached(std::numeric_limits::lowest()) {} }; -/// This is the condition that the end of World has been reached +/// This is the condition that the end of world has been reached /// it then triggers an propagation abort struct EndOfWorldReached { /// boolean operator for abort condition without using the result @@ -181,6 +181,60 @@ struct EndOfWorldReached { } }; +/// This is the condition that the end of world has been reached +/// it then triggers a propagation abort +struct VolumeConstraintAborter { + /// boolean operator for abort condition without using the result + /// + /// @tparam propagator_state_t Type of the propagator state + /// @tparam navigator_t Type of the navigator + /// + /// @param [in,out] state The propagation state object + /// @param [in] navigator The navigator object + /// @param logger a logger instance + template + bool checkAbort(propagator_state_t& state, const stepper_t& /*stepper*/, + const navigator_t& navigator, const Logger& logger) const { + const auto& constrainToVolumeIds = state.options.constrainToVolumeIds; + const auto& endOfWorldVolumeIds = state.options.endOfWorldVolumeIds; + + if (constrainToVolumeIds.empty() && endOfWorldVolumeIds.empty()) { + return false; + } + const auto* currentVolume = navigator.currentVolume(state.navigation); + + // We need a volume to check its ID + if (currentVolume == nullptr) { + return false; + } + + const auto currentVolumeId = + static_cast(currentVolume->geometryId().volume()); + + if (!constrainToVolumeIds.empty() && + std::find(constrainToVolumeIds.begin(), constrainToVolumeIds.end(), + currentVolumeId) == constrainToVolumeIds.end()) { + ACTS_VERBOSE( + "VolumeConstraintAborter aborter | Abort with volume constrain " + << currentVolumeId); + return true; + } + + if (!endOfWorldVolumeIds.empty() && + std::find(endOfWorldVolumeIds.begin(), endOfWorldVolumeIds.end(), + currentVolumeId) != endOfWorldVolumeIds.end()) { + ACTS_VERBOSE( + "VolumeConstraintAborter aborter | Abort with additional end of " + "world volume " + << currentVolumeId); + return true; + } + + return false; + } +}; + /// Aborter that checks if the propagation has reached any surface struct AnySurfaceReached { template pixelVolumes; - std::set stripVolumes; + std::vector pixelVolumeIds; + std::vector stripVolumeIds; - /// additional track selector settings + // additional track selector settings std::size_t maxPixelHoles = std::numeric_limits::max(); std::size_t maxStripHoles = std::numeric_limits::max(); + + /// The volume ids to constrain the track finding to + std::vector constrainToVolumeIds; + /// The volume ids to stop the track finding at + std::vector endOfWorldVolumeIds; }; /// Constructor of the track finding algorithm diff --git a/Examples/Algorithms/TrackFinding/src/TrackFindingAlgorithm.cpp b/Examples/Algorithms/TrackFinding/src/TrackFindingAlgorithm.cpp index 0a5ae601d20..a6d5efc69e6 100644 --- a/Examples/Algorithms/TrackFinding/src/TrackFindingAlgorithm.cpp +++ b/Examples/Algorithms/TrackFinding/src/TrackFindingAlgorithm.cpp @@ -217,16 +217,18 @@ class BranchStopper { } bool tooManyHolesPS = false; - if (!(m_cfg.pixelVolumes.empty() && m_cfg.stripVolumes.empty())) { + if (!(m_cfg.pixelVolumeIds.empty() && m_cfg.stripVolumeIds.empty())) { auto& branchState = branchStateAccessor(track); // count both holes and outliers as holes for pixel/strip counts if (trackState.typeFlags().test(Acts::TrackStateFlag::HoleFlag) || trackState.typeFlags().test(Acts::TrackStateFlag::OutlierFlag)) { - if (m_cfg.pixelVolumes.contains( - trackState.referenceSurface().geometryId().volume())) { + auto volumeId = trackState.referenceSurface().geometryId().volume(); + if (std::find(m_cfg.pixelVolumeIds.begin(), m_cfg.pixelVolumeIds.end(), + volumeId) != m_cfg.pixelVolumeIds.end()) { ++branchState.nPixelHoles; - } else if (m_cfg.stripVolumes.contains( - trackState.referenceSurface().geometryId().volume())) { + } else if (std::find(m_cfg.stripVolumeIds.begin(), + m_cfg.stripVolumeIds.end(), + volumeId) != m_cfg.stripVolumeIds.end()) { ++branchState.nStripHoles; } } @@ -350,11 +352,15 @@ ProcessCode TrackFindingAlgorithm::execute(const AlgorithmContext& ctx) const { firstPropOptions.maxSteps = m_cfg.maxSteps; firstPropOptions.direction = m_cfg.reverseSearch ? Acts::Direction::Backward : Acts::Direction::Forward; + firstPropOptions.constrainToVolumeIds = m_cfg.constrainToVolumeIds; + firstPropOptions.endOfWorldVolumeIds = m_cfg.endOfWorldVolumeIds; Acts::PropagatorPlainOptions secondPropOptions(ctx.geoContext, ctx.magFieldContext); secondPropOptions.maxSteps = m_cfg.maxSteps; secondPropOptions.direction = firstPropOptions.direction.invert(); + secondPropOptions.constrainToVolumeIds = m_cfg.constrainToVolumeIds; + secondPropOptions.endOfWorldVolumeIds = m_cfg.endOfWorldVolumeIds; // Set the CombinatorialKalmanFilter options TrackFinderOptions firstOptions(ctx.geoContext, ctx.magFieldContext, @@ -379,6 +385,8 @@ ProcessCode TrackFindingAlgorithm::execute(const AlgorithmContext& ctx) const { logger().cloneWithSuffix("Propagator")); ExtrapolatorOptions extrapolationOptions(ctx.geoContext, ctx.magFieldContext); + extrapolationOptions.constrainToVolumeIds = m_cfg.constrainToVolumeIds; + extrapolationOptions.endOfWorldVolumeIds = m_cfg.endOfWorldVolumeIds; // Perform the track finding for all initial parameters ACTS_DEBUG("Invoke track finding with " << initialParameters.size() diff --git a/Examples/Python/python/acts/examples/reconstruction.py b/Examples/Python/python/acts/examples/reconstruction.py index e47b435e72b..ab12005af86 100644 --- a/Examples/Python/python/acts/examples/reconstruction.py +++ b/Examples/Python/python/acts/examples/reconstruction.py @@ -147,8 +147,24 @@ "maxPixelHoles", "maxStripHoles", "trimTracks", + "constrainToVolumes", + "endOfWorldVolumes", + ], + defaults=[ + 15.0, + 25.0, + 10, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, ], - defaults=[15.0, 25.0, 10, None, None, None, None, None, None, None, None], ) AmbiguityResolutionConfig = namedtuple( @@ -1531,11 +1547,13 @@ def addCKFTracks( reverseSearch=reverseSearch, seedDeduplication=ckfConfig.seedDeduplication, stayOnSeed=ckfConfig.stayOnSeed, - pixelVolumes=ckfConfig.pixelVolumes, - stripVolumes=ckfConfig.stripVolumes, + pixelVolumeIds=ckfConfig.pixelVolumes, + stripVolumeIds=ckfConfig.stripVolumes, maxPixelHoles=ckfConfig.maxPixelHoles, maxStripHoles=ckfConfig.maxStripHoles, trimTracks=ckfConfig.trimTracks, + constrainToVolumeIds=ckfConfig.constrainToVolumes, + endOfWorldVolumeIds=ckfConfig.endOfWorldVolumes, ), ) s.addAlgorithm(trackFinder) diff --git a/Examples/Python/src/TrackFinding.cpp b/Examples/Python/src/TrackFinding.cpp index 05ebcb3c7a7..4179d58d94a 100644 --- a/Examples/Python/src/TrackFinding.cpp +++ b/Examples/Python/src/TrackFinding.cpp @@ -337,11 +337,13 @@ void addTrackFinding(Context& ctx) { ACTS_PYTHON_MEMBER(reverseSearch); ACTS_PYTHON_MEMBER(seedDeduplication); ACTS_PYTHON_MEMBER(stayOnSeed); - ACTS_PYTHON_MEMBER(pixelVolumes); - ACTS_PYTHON_MEMBER(stripVolumes); + ACTS_PYTHON_MEMBER(pixelVolumeIds); + ACTS_PYTHON_MEMBER(stripVolumeIds); ACTS_PYTHON_MEMBER(maxPixelHoles); ACTS_PYTHON_MEMBER(maxStripHoles); ACTS_PYTHON_MEMBER(trimTracks); + ACTS_PYTHON_MEMBER(constrainToVolumeIds); + ACTS_PYTHON_MEMBER(endOfWorldVolumeIds); ACTS_PYTHON_STRUCT_END(); } diff --git a/Examples/Scripts/Python/full_chain_itk.py b/Examples/Scripts/Python/full_chain_itk.py index 6231127096c..5e268f87fcb 100755 --- a/Examples/Scripts/Python/full_chain_itk.py +++ b/Examples/Scripts/Python/full_chain_itk.py @@ -126,8 +126,8 @@ seedDeduplication=True, stayOnSeed=True, # ITk volumes from Noemi's plot - pixelVolumes={8, 9, 10, 13, 14, 15, 16, 18, 19, 20}, - stripVolumes={22, 23, 24}, + pixelVolumes=[8, 9, 10, 13, 14, 15, 16, 18, 19, 20], + stripVolumes=[22, 23, 24], maxPixelHoles=1, maxStripHoles=2, ), diff --git a/Examples/Scripts/Python/full_chain_odd.py b/Examples/Scripts/Python/full_chain_odd.py index e76111da7ac..e6da5eee4dd 100755 --- a/Examples/Scripts/Python/full_chain_odd.py +++ b/Examples/Scripts/Python/full_chain_odd.py @@ -363,10 +363,27 @@ numMeasurementsCutOff=10, seedDeduplication=True, stayOnSeed=True, - pixelVolumes={16, 17, 18}, - stripVolumes={23, 24, 25}, + pixelVolumes=[16, 17, 18], + stripVolumes=[23, 24, 25], maxPixelHoles=1, maxStripHoles=2, + constrainToVolumes=[ + 2, # beam pipe + 32, + 4, # beam pip gap + 16, + 17, + 18, # pixel + 20, # PST + 23, + 24, + 25, # short strip + 26, + 8, # long strip gap + 28, + 29, + 30, # long strip + ], ), outputDirRoot=outputDir if args.output_root else None, outputDirCsv=outputDir if args.output_csv else None, diff --git a/Tests/UnitTests/Fatras/Kernel/SimulationActorTests.cpp b/Tests/UnitTests/Fatras/Kernel/SimulationActorTests.cpp index de87bf883c1..e52d586c30b 100644 --- a/Tests/UnitTests/Fatras/Kernel/SimulationActorTests.cpp +++ b/Tests/UnitTests/Fatras/Kernel/SimulationActorTests.cpp @@ -12,6 +12,7 @@ #include "Acts/Definitions/PdgParticle.hpp" #include "Acts/Definitions/Units.hpp" #include "Acts/Geometry/GeometryContext.hpp" +#include "Acts/Geometry/TrackingVolume.hpp" #include "Acts/Material/HomogeneousSurfaceMaterial.hpp" #include "Acts/Material/MaterialSlab.hpp" #include "Acts/Propagator/ConstrainedStep.hpp" @@ -148,6 +149,11 @@ struct MockNavigator { return state.currentSurface; } + const Acts::TrackingVolume *currentVolume( + const MockNavigatorState & /*state*/) const { + return nullptr; + } + bool endOfWorldReached(const MockNavigatorState & /*state*/) const { return false; } @@ -158,6 +164,10 @@ struct MockPropagatorState { MockStepperState stepping; Acts::GeometryContext geoContext; Acts::PropagatorStage stage = Acts::PropagatorStage::invalid; + + struct { + std::vector constrainToVolumeIds; + } options; }; template From b0fcfb427f4b8a7132ee9be0b5d3f5ce911ce36e Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Thu, 3 Oct 2024 13:09:16 +0200 Subject: [PATCH 06/24] feat(geo): Extent designated initialization (#3680) This pull request introduces a new constructor for the `ExtentEnvelope` struct using a helper struct for designated initialization, and includes corresponding unit tests to ensure its functionality. The most important changes include the addition of the `Arguments` helper struct, the new constructor, and a new test case in the unit tests. Enhancements to `ExtentEnvelope` struct: * [`Core/include/Acts/Geometry/Extent.hpp`](diffhunk://#diff-82ced51a7d9c573938c161066e15e9d21194b2ca79b3033d8519b1317e6d18d6R77-R103): Added a new helper struct `Arguments` for designated initializer construction and a corresponding constructor in `ExtentEnvelope` to utilize this struct. Unit tests: * [`Tests/UnitTests/Core/Geometry/ExtentTests.cpp`](diffhunk://#diff-ec39444b2939b7caa485e011248f941d88a3d46a9e57f386c4c42c5550b23a4aR179-R189): Added a new test case `DesignatedInitializers` to verify the functionality of the new designated initializer constructor in `ExtentEnvelope`. --- Core/include/Acts/Geometry/Extent.hpp | 33 +++++++++++++++---- Tests/UnitTests/Core/Geometry/ExtentTests.cpp | 11 +++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/Core/include/Acts/Geometry/Extent.hpp b/Core/include/Acts/Geometry/Extent.hpp index 698477744c0..8728728996b 100644 --- a/Core/include/Acts/Geometry/Extent.hpp +++ b/Core/include/Acts/Geometry/Extent.hpp @@ -52,12 +52,6 @@ struct ExtentEnvelope { } } - /// Constructor from an array of envelopes - /// @param values the array of envelopes - constexpr explicit ExtentEnvelope( - const std::array& values) - : m_values(values) {} - /// Static factory for a zero envelope /// @return the zero envelope constexpr static ExtentEnvelope Zero() { @@ -74,6 +68,33 @@ struct ExtentEnvelope { }}; } + /// Helper struct for designated initializer construction + struct Arguments { + Envelope x = zeroEnvelope; + Envelope y = zeroEnvelope; + Envelope z = zeroEnvelope; + Envelope r = zeroEnvelope; + Envelope phi = zeroEnvelope; + Envelope rPhi = zeroEnvelope; + Envelope h = zeroEnvelope; + Envelope eta = zeroEnvelope; + Envelope mag = zeroEnvelope; + }; + + /// Constructor using a helper struct for designated initializaion + /// @param args the arguments + constexpr explicit ExtentEnvelope(Arguments&& args) { + using enum BinningValue; + m_values[toUnderlying(binX)] = args.x; + m_values[toUnderlying(binY)] = args.y; + m_values[toUnderlying(binZ)] = args.z; + m_values[toUnderlying(binR)] = args.r; + m_values[toUnderlying(binPhi)] = args.phi; + m_values[toUnderlying(binH)] = args.h; + m_values[toUnderlying(binEta)] = args.eta; + m_values[toUnderlying(binMag)] = args.mag; + } + /// Comparison operator between envelope sets /// @param lhs the left hand side /// @param rhs the right hand side diff --git a/Tests/UnitTests/Core/Geometry/ExtentTests.cpp b/Tests/UnitTests/Core/Geometry/ExtentTests.cpp index 80513a1d80b..e0913e273eb 100644 --- a/Tests/UnitTests/Core/Geometry/ExtentTests.cpp +++ b/Tests/UnitTests/Core/Geometry/ExtentTests.cpp @@ -176,6 +176,17 @@ BOOST_AUTO_TEST_CASE(ProtoSupportCaseTests) { BOOST_CHECK(volumeExtent.constrains(BinningValue::binR)); } +BOOST_AUTO_TEST_CASE(DesignatedInitializers) { + using enum BinningValue; + ExtentEnvelope exp; + exp[binX] = {1., 2.}; + exp[binEta] = {-1., 1.}; + + ExtentEnvelope act{{.x = {1., 2.}, .eta = {-1., 1.}}}; + + BOOST_CHECK(exp == act); +} + BOOST_AUTO_TEST_SUITE_END() } // namespace Acts::Test From 94e37c01391eb88b88203df4fa0f22b430edfb3d Mon Sep 17 00:00:00 2001 From: Stephen Nicholas Swatman Date: Thu, 3 Oct 2024 15:22:54 +0200 Subject: [PATCH 07/24] refactor: Unify proxy iterator types (#3664) This PR changed scope, and now unifies _all_ proxy iterators under a single template type. # Old PR description At this point in time, the iterator for `MeasurementContainer` does not match the `std::forward_iterator` concept which SonarCloud really wants me to use. In this commit, I make sure that the iterator satisfies the concept. Unfortunately, this requires a default-initializer, because `std::forward_iterator` requires `std::incrementable` which requires `std::regular`, which requires `std::semiregular` which in turn requires `std::default_initializable` which I think is silly. --- .../Acts/EventData/SpacePointContainer.hpp | 28 ++-- .../Acts/EventData/SpacePointContainer.ipp | 44 +++++- .../EventData/SpacePointProxyIterator.hpp | 58 -------- .../EventData/SpacePointProxyIterator.ipp | 100 -------------- .../include/Acts/EventData/TrackContainer.hpp | 47 ++++--- Core/include/Acts/EventData/TrackProxy.hpp | 114 +-------------- .../detail/CylindricalSpacePointGrid.ipp | 9 -- Core/include/Acts/Utilities/Iterator.hpp | 130 ++++++++++++++++++ Core/include/Acts/Utilities/TypeTraits.hpp | 16 +++ .../ActsExamples/EventData/Measurement.hpp | 65 +++------ .../Framework/src/EventData/Measurement.cpp | 10 ++ Tests/UnitTests/Core/EventData/TrackTests.cpp | 6 +- 12 files changed, 281 insertions(+), 346 deletions(-) delete mode 100644 Core/include/Acts/EventData/SpacePointProxyIterator.hpp delete mode 100644 Core/include/Acts/EventData/SpacePointProxyIterator.ipp create mode 100644 Core/include/Acts/Utilities/Iterator.hpp create mode 100644 Core/include/Acts/Utilities/TypeTraits.hpp diff --git a/Core/include/Acts/EventData/SpacePointContainer.hpp b/Core/include/Acts/EventData/SpacePointContainer.hpp index 32e43645d82..5a7be3b23f7 100644 --- a/Core/include/Acts/EventData/SpacePointContainer.hpp +++ b/Core/include/Acts/EventData/SpacePointContainer.hpp @@ -12,9 +12,9 @@ #include "Acts/Definitions/Units.hpp" #include "Acts/EventData/SpacePointData.hpp" #include "Acts/EventData/SpacePointProxy.hpp" -#include "Acts/EventData/SpacePointProxyIterator.hpp" #include "Acts/EventData/Utils.hpp" #include "Acts/Utilities/HashedString.hpp" +#include "Acts/Utilities/Iterator.hpp" #include #include @@ -67,19 +67,22 @@ class SpacePointContainer { public: friend class Acts::SpacePointProxy< Acts::SpacePointContainer>; - friend class Acts::SpacePointProxyIterator< - Acts::SpacePointContainer>; public: - using iterator = Acts::SpacePointProxyIterator< - Acts::SpacePointContainer>; - using const_iterator = iterator; - using SpacePointProxyType = Acts::SpacePointProxy>; + + using iterator = + ContainerIndexIterator, + SpacePointProxyType&, false>; + using const_iterator = + ContainerIndexIterator, + const SpacePointProxyType&, true>; + using ValueType = typename container_t::ValueType; using ProxyType = SpacePointProxyType; using value_type = ProxyType; + using size_type = std::size_t; public: // Constructors @@ -118,9 +121,15 @@ class SpacePointContainer { std::size_t size() const; - iterator begin() const; - iterator end() const; + iterator begin(); + iterator end(); + const_iterator cbegin() const; + const_iterator cend() const; + const_iterator begin() const; + const_iterator end() const; + ProxyType& at(const std::size_t n); + const ProxyType& at(const std::size_t n) const; const ValueType& sp(const std::size_t n) const; private: @@ -128,6 +137,7 @@ class SpacePointContainer { const container_t& container() const; const ProxyType& proxy(const std::size_t n) const; + std::vector& proxies(); const std::vector& proxies() const; float x(const std::size_t n) const; diff --git a/Core/include/Acts/EventData/SpacePointContainer.ipp b/Core/include/Acts/EventData/SpacePointContainer.ipp index abcab8ab5a2..01800be7cd5 100644 --- a/Core/include/Acts/EventData/SpacePointContainer.ipp +++ b/Core/include/Acts/EventData/SpacePointContainer.ipp @@ -105,22 +105,58 @@ std::size_t SpacePointContainer::size() const { template class holder_t> typename SpacePointContainer::iterator -SpacePointContainer::begin() const { +SpacePointContainer::begin() { return {*this, 0}; } template class holder_t> typename SpacePointContainer::iterator +SpacePointContainer::end() { + return {*this, size()}; +} + +template class holder_t> +typename SpacePointContainer::const_iterator +SpacePointContainer::begin() const { + return {*this, 0}; +} + +template class holder_t> +typename SpacePointContainer::const_iterator SpacePointContainer::end() const { return {*this, size()}; } +template class holder_t> +typename SpacePointContainer::const_iterator +SpacePointContainer::cbegin() const { + return {*this, 0}; +} + +template class holder_t> +typename SpacePointContainer::const_iterator +SpacePointContainer::cend() const { + return {*this, size()}; +} + template class holder_t> const container_t& SpacePointContainer::container() const { return *m_container; } +template class holder_t> +typename SpacePointContainer::ProxyType& +SpacePointContainer::at(const std::size_t n) { + return proxies().at(n); +} + +template class holder_t> +const typename SpacePointContainer::ProxyType& +SpacePointContainer::at(const std::size_t n) const { + return proxies().at(n); +} + template class holder_t> const typename SpacePointContainer::ValueType& SpacePointContainer::sp(const std::size_t n) const { @@ -173,6 +209,12 @@ SpacePointContainer::proxy(const std::size_t n) const { return proxies()[n]; } +template class holder_t> +std::vector::ProxyType>& +SpacePointContainer::proxies() { + return m_proxies; +} + template class holder_t> const std::vector< typename SpacePointContainer::ProxyType>& diff --git a/Core/include/Acts/EventData/SpacePointProxyIterator.hpp b/Core/include/Acts/EventData/SpacePointProxyIterator.hpp deleted file mode 100644 index c8b91d3c052..00000000000 --- a/Core/include/Acts/EventData/SpacePointProxyIterator.hpp +++ /dev/null @@ -1,58 +0,0 @@ -// This file is part of the ACTS project. -// -// Copyright (C) 2016 CERN for the benefit of the ACTS project -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -#pragma once - -#include "Acts/EventData/SpacePointProxy.hpp" -#include "Acts/Utilities/Holders.hpp" - -namespace Acts { - -template -class SpacePointProxyIterator { - public: - using ContainerType = container_t; - using ProxyType = typename container_t::SpacePointProxyType; - - using iterator_category = std::random_access_iterator_tag; - using value_type = ProxyType; - using difference_type = std::ptrdiff_t; - using pointer = value_type*; - using reference = value_type&; - - public: - // Constructors - SpacePointProxyIterator(container_t&& container, std::size_t index) = delete; - SpacePointProxyIterator(const container_t& container, std::size_t index); - - SpacePointProxyIterator& operator++(); - SpacePointProxyIterator& operator--(); - SpacePointProxyIterator operator++(int); - SpacePointProxyIterator operator--(int); - - bool operator==(const SpacePointProxyIterator& other) const; - auto operator<=>(const SpacePointProxyIterator& other) const; - - SpacePointProxyIterator& operator+=(const std::size_t offset); - SpacePointProxyIterator& operator-=(const std::size_t offset); - - SpacePointProxyIterator operator+(const std::size_t offset) const; - SpacePointProxyIterator operator-(const std::size_t offset) const; - - difference_type operator-(const SpacePointProxyIterator& other) const; - - const value_type& operator*() const; - - private: - const container_t* m_container{nullptr}; - std::size_t m_index{0ul}; -}; - -} // namespace Acts - -#include "Acts/EventData/SpacePointProxyIterator.ipp" diff --git a/Core/include/Acts/EventData/SpacePointProxyIterator.ipp b/Core/include/Acts/EventData/SpacePointProxyIterator.ipp deleted file mode 100644 index 5759e0416a1..00000000000 --- a/Core/include/Acts/EventData/SpacePointProxyIterator.ipp +++ /dev/null @@ -1,100 +0,0 @@ -// This file is part of the ACTS project. -// -// Copyright (C) 2016 CERN for the benefit of the ACTS project -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -namespace Acts { - -// Implementation -template -SpacePointProxyIterator::SpacePointProxyIterator( - const container_t& container, std::size_t index) - : m_container(&container), m_index(index) {} - -template -SpacePointProxyIterator& -SpacePointProxyIterator::operator++() { - ++m_index; - return *this; -} - -template -SpacePointProxyIterator& -SpacePointProxyIterator::operator--() { - --m_index; - return *this; -} - -template -SpacePointProxyIterator -SpacePointProxyIterator::operator++(int) { - SpacePointProxyIterator other(*this); - ++m_index; - return other; -} - -template -SpacePointProxyIterator -SpacePointProxyIterator::operator--(int) { - SpacePointProxyIterator other(*this); - --m_index; - return other; -} - -template -bool SpacePointProxyIterator::operator==( - const SpacePointProxyIterator& other) const { - return m_container == other.m_container && m_index == other.m_index; -} - -template -auto SpacePointProxyIterator::operator<=>( - const SpacePointProxyIterator& other) const { - return m_index <=> other.m_index; -} - -template -SpacePointProxyIterator& -SpacePointProxyIterator::operator+=(const std::size_t offset) { - m_index += offset; - return *this; -} - -template -SpacePointProxyIterator& -SpacePointProxyIterator::operator-=(const std::size_t offset) { - m_index -= offset; - return *this; -} - -template -SpacePointProxyIterator -SpacePointProxyIterator::operator+( - const std::size_t offset) const { - return SpacePointProxyIterator(*m_container, m_index + offset); -} - -template -SpacePointProxyIterator -SpacePointProxyIterator::operator-( - const std::size_t offset) const { - return SpacePointProxyIterator(*m_container, m_index - offset); -} - -template -typename SpacePointProxyIterator::difference_type -SpacePointProxyIterator::operator-( - const SpacePointProxyIterator& other) const { - return m_index - other.m_index; -} - -template -const typename SpacePointProxyIterator::value_type& -SpacePointProxyIterator::operator*() const { - return m_container->proxy(m_index); -} - -} // namespace Acts diff --git a/Core/include/Acts/EventData/TrackContainer.hpp b/Core/include/Acts/EventData/TrackContainer.hpp index 9591a66c6a4..f70c430f002 100644 --- a/Core/include/Acts/EventData/TrackContainer.hpp +++ b/Core/include/Acts/EventData/TrackContainer.hpp @@ -19,6 +19,8 @@ #include "Acts/EventData/Utils.hpp" #include "Acts/Utilities/HashedString.hpp" #include "Acts/Utilities/Holders.hpp" +#include "Acts/Utilities/Iterator.hpp" +#include "Acts/Utilities/TypeTraits.hpp" #include "Acts/Utilities/UnitVectors.hpp" #include @@ -78,6 +80,12 @@ class TrackContainer { using ConstTrackStateProxy = typename MultiTrajectory::ConstTrackStateProxy; + using size_type = IndexType; + using iterator = + Acts::ContainerIndexIterator; + using const_iterator = + Acts::ContainerIndexIterator; + #ifndef DOXYGEN friend TrackProxy; friend ConstTrackProxy; @@ -148,6 +156,21 @@ class TrackContainer { return {*this, itrack}; } + /// Get a const track proxy for a track index + /// @param itrack the track index in the container + /// @return A const track proxy for the index + ConstTrackProxy at(IndexType itrack) const { return getTrack(itrack); } + + /// Get a mutable track proxy for a track index + /// @note Only available if the track container is not read-only + /// @param itrack the track index in the container + /// @return A mutable track proxy for the index + TrackProxy at(IndexType itrack) + requires(!ReadOnly) + { + return {*this, itrack}; + } + /// Add a track to the container. Note this only creates the logical track and /// allocates memory. You can combine this with @c getTrack to obtain a track proxy /// @note Only available if the track container is not read-only @@ -184,36 +207,28 @@ class TrackContainer { /// Get a mutable iterator to the first track in the container /// @note Only available if the track container is not read-only /// @return a mutable iterator to the first track - auto begin() + iterator begin() requires(!ReadOnly) { - return detail_tc::TrackProxyIterator, - TrackProxy, false>{*this, 0}; + return iterator{*this, 0}; } /// Get a past-the-end iterator for this container /// @note Only available if the track container is not read-only /// @return a past-the-end iterator - auto end() + iterator end() requires(!ReadOnly) { - return detail_tc::TrackProxyIterator, - TrackProxy, false>{*this, size()}; + return iterator{*this, size()}; } /// Get an const iterator to the first track in the container /// @return a const iterator to the first track - auto begin() const { - return detail_tc::TrackProxyIterator, - ConstTrackProxy, true>{*this, 0}; - } + const_iterator begin() const { return const_iterator{*this, 0}; } /// Get a past-the-end iterator for this container /// @return a past-the-end iterator - auto end() const { - return detail_tc::TrackProxyIterator, - ConstTrackProxy, true>{*this, size()}; - } + const_iterator end() const { return const_iterator{*this, size()}; } /// @} @@ -411,8 +426,8 @@ class TrackContainer { } } - detail_tc::ConstIf, ReadOnly> m_container; - detail_tc::ConstIf, ReadOnly> m_traj; + const_if_t> m_container; + const_if_t> m_traj; }; template diff --git a/Core/include/Acts/EventData/TrackProxy.hpp b/Core/include/Acts/EventData/TrackProxy.hpp index f5a5aeca6d2..ce674e9cfa4 100644 --- a/Core/include/Acts/EventData/TrackProxy.hpp +++ b/Core/include/Acts/EventData/TrackProxy.hpp @@ -17,6 +17,7 @@ #include "Acts/EventData/TrackProxyConcept.hpp" #include "Acts/EventData/TrackStatePropMask.hpp" #include "Acts/Utilities/HashedString.hpp" +#include "Acts/Utilities/TypeTraits.hpp" #include "Acts/Utilities/UnitVectors.hpp" #include @@ -29,108 +30,6 @@ template class holder_t> class TrackContainer; -namespace detail_tc { -template -using ConstIf = std::conditional_t; - -/// Helper iterator to allow iteration over tracks via track proxies. -template -class TrackProxyIterator { - using ProxyType = proxy_t; - using IndexType = typename ProxyType::IndexType; - using ContainerType = container_t; - - public: - using iterator_category = std::random_access_iterator_tag; - using value_type = ProxyType; - using difference_type = std::ptrdiff_t; - using pointer = void; - using reference = void; - - TrackProxyIterator(container_t& container, IndexType itrack) - requires(!ReadOnly) - : m_container(&container), m_itrack(itrack) {} - - TrackProxyIterator(const container_t& container, IndexType itrack) - requires ReadOnly - : m_container(&container), m_itrack(itrack) {} - - TrackProxyIterator& operator++() { - m_itrack++; - return *this; - } - TrackProxyIterator& operator--() { - m_itrack--; - return *this; - } - - bool operator==(const TrackProxyIterator& other) const { - return m_container == other.m_container && m_itrack == other.m_itrack; - } - - auto operator<=>(const TrackProxyIterator& other) const { - return m_itrack <=> other.m_itrack; - } - - ProxyType operator*() const { return m_container->getTrack(m_itrack); } - - ProxyType operator*() - requires(!ReadOnly) - { - return m_container->getTrack(m_itrack); - } - - TrackProxyIterator operator[](difference_type n) const { - TrackProxyIterator copy = *this; - copy += n; - return copy; - }; - - TrackProxyIterator& operator+=(difference_type n) { - m_itrack += n; - return *this; - } - - TrackProxyIterator operator-=(difference_type n) { - m_itrack -= n; - return *this; - } - - friend difference_type operator-(const TrackProxyIterator& lhs, - const TrackProxyIterator& rhs) { - return lhs.m_itrack - rhs.m_itrack; - } - - friend TrackProxyIterator operator+(const TrackProxyIterator& lhs, - difference_type rhs) { - TrackProxyIterator copy = lhs; - copy += rhs; - return copy; - } - - friend TrackProxyIterator operator+(difference_type lhs, - const TrackProxyIterator& rhs) { - return rhs + lhs; - } - - friend TrackProxyIterator operator-(const TrackProxyIterator& lhs, - difference_type rhs) { - return lhs + (-rhs); - } - - friend TrackProxyIterator operator-(difference_type lhs, - const TrackProxyIterator& rhs) { - return rhs + (-lhs); - } - - private: - detail_lt::TransitiveConstPointer> - m_container; - IndexType m_itrack; -}; - -} // namespace detail_tc - /// Proxy class representing a single track. /// This class provides a **view** into an associated @ref TrackContainer, and /// has **reference semantics**. You can think of it as a pointer to a vector @@ -876,13 +775,14 @@ class TrackProxy { const auto& container() const { return *m_container; } private: - TrackProxy(detail_tc::ConstIf, - ReadOnly>& container, - IndexType itrack) + TrackProxy( + const_if_t>& + container, + IndexType itrack) : m_container{&container}, m_index{itrack} {} - detail_lt::TransitiveConstPointer, ReadOnly>> + detail_lt::TransitiveConstPointer< + const_if_t>> m_container; IndexType m_index; }; diff --git a/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.ipp b/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.ipp index ad23beeaf77..ea228510042 100644 --- a/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.ipp +++ b/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.ipp @@ -143,15 +143,6 @@ void Acts::CylindricalSpacePointGridCreator::fillGrid( Acts::CylindricalSpacePointGrid& grid, external_spacepoint_iterator_t spBegin, external_spacepoint_iterator_t spEnd, Acts::Extent& rRangeSPExtent) { - using iterated_value_t = - typename std::iter_value_t; - using iterated_t = typename std::remove_const_t< - typename std::remove_pointer_t>; - static_assert(!std::is_pointer_v, - "Iterator must contain pointers to space points"); - static_assert(std::same_as, - "Iterator does not contain type this class was templated with"); - if (!config.isInInternalUnits) { throw std::runtime_error( "SeedFinderConfig not in ACTS internal units in BinnedSPGroup"); diff --git a/Core/include/Acts/Utilities/Iterator.hpp b/Core/include/Acts/Utilities/Iterator.hpp new file mode 100644 index 00000000000..ccdff61c061 --- /dev/null +++ b/Core/include/Acts/Utilities/Iterator.hpp @@ -0,0 +1,130 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma once + +#include +#include +#include +#include + +namespace Acts { +template +class ContainerIndexIterator { + public: + using value_type = Value; + using iterator_category = std::random_access_iterator_tag; + using container_type = + std::conditional_t; + using difference_type = std::ptrdiff_t; + + ContainerIndexIterator() : m_container(nullptr), m_index(0) {} + + ContainerIndexIterator(container_type& container, std::size_t index) + : m_container(&container), m_index(index) {} + + template + explicit ContainerIndexIterator( + const ContainerIndexIterator<_Container, OtherValue, OtherConst>& o) + requires(!OtherConst || Const) + : m_container(o.m_container), m_index(o.m_index) {} + + value_type operator*() const { + assert(m_container != nullptr); + return m_container->at(m_index); + } + + template + ContainerIndexIterator& operator=( + const ContainerIndexIterator<_Container, OtherValue, OtherConst>& o) + requires(!OtherConst || Const) + { + m_container = o.m_container; + m_index = o.m_index; + return *this; + } + + ContainerIndexIterator& operator++() { + ++m_index; + return *this; + } + + ContainerIndexIterator operator++(int) { + auto copy = *this; + ++*this; + return copy; + } + + ContainerIndexIterator& operator+=(const difference_type& i) { + m_index += i; + return *this; + } + + friend ContainerIndexIterator operator+(const ContainerIndexIterator& t, + const difference_type& i) { + return ContainerIndexIterator(*t.m_container, t.m_index + i); + } + + friend ContainerIndexIterator operator+(const difference_type& i, + const ContainerIndexIterator& t) { + return t + i; + } + + ContainerIndexIterator& operator--() { + --m_index; + return *this; + } + + ContainerIndexIterator operator--(int) { + auto copy = *this; + --*this; + return copy; + } + + ContainerIndexIterator& operator-=(const difference_type& i) { + m_index -= i; + return *this; + } + + friend ContainerIndexIterator operator-(const ContainerIndexIterator& t, + const difference_type& i) { + return ContainerIndexIterator(*t.m_container, t.m_index - i); + } + + template + friend difference_type operator-( + const ContainerIndexIterator& t, + const ContainerIndexIterator<_Container, OtherValue, OtherConst>& o) { + assert(t.m_container == o.m_container); + return t.m_index - o.m_index; + } + + value_type operator[](const difference_type& i) const { return *(*this + i); } + + template + std::strong_ordering operator<=>( + const ContainerIndexIterator<_Container, OtherValue, OtherConst>& o) + const { + if (m_container == o.m_container) { + return m_index <=> o.m_index; + } else { + return m_container <=> o.m_container; + } + } + + template + bool operator==(const ContainerIndexIterator<_Container, OtherValue, + OtherConst>& other) const { + return m_container == other.m_container && m_index == other.m_index; + } + + private: + container_type* m_container; + typename container_type::size_type m_index; +}; +} // namespace Acts diff --git a/Core/include/Acts/Utilities/TypeTraits.hpp b/Core/include/Acts/Utilities/TypeTraits.hpp new file mode 100644 index 00000000000..a3c4aaccdcb --- /dev/null +++ b/Core/include/Acts/Utilities/TypeTraits.hpp @@ -0,0 +1,16 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma once + +#include + +namespace Acts { +template +using const_if_t = std::conditional_t; +} // namespace Acts diff --git a/Examples/Framework/include/ActsExamples/EventData/Measurement.hpp b/Examples/Framework/include/ActsExamples/EventData/Measurement.hpp index 5d57c82b531..8703806a164 100644 --- a/Examples/Framework/include/ActsExamples/EventData/Measurement.hpp +++ b/Examples/Framework/include/ActsExamples/EventData/Measurement.hpp @@ -17,12 +17,15 @@ #include "Acts/EventData/detail/CalculateResiduals.hpp" #include "Acts/EventData/detail/ParameterTraits.hpp" #include "Acts/EventData/detail/PrintParameters.hpp" +#include "Acts/Utilities/Iterator.hpp" #include "ActsExamples/EventData/MeasurementConcept.hpp" #include +#include #include #include #include +#include #include #include #include @@ -58,7 +61,8 @@ using ConstVariableBoundMeasurementProxy = /// provide access to the individual measurements. class MeasurementContainer { public: - using Index = std::size_t; + using size_type = std::size_t; + using Index = size_type; template using FixedProxy = FixedMeasurementProxy; template @@ -81,6 +85,15 @@ class MeasurementContainer { /// @return The index of the added measurement Index addMeasurement(std::uint8_t size); + /// @brief Get a variable-size measurement proxy + /// @param index The index of the measurement + /// @return The variable-size measurement proxy + VariableProxy at(Index index); + /// @brief Get a const variable-size measurement proxy + /// @param index The index of the measurement + /// @return The const variable-size measurement proxy + ConstVariableProxy at(Index index) const; + /// @brief Get a variable-size measurement proxy /// @param index The index of the measurement /// @return The variable-size measurement proxy @@ -125,48 +138,11 @@ class MeasurementContainer { template FixedProxy emplaceMeasurement(Args&&... args); - template - class IteratorImpl { - public: - using value_type = - std::conditional_t; - using reference = value_type; - using pointer = value_type*; - using difference_type = std::ptrdiff_t; - using iterator_category = std::forward_iterator_tag; - - using Container = std::conditional_t; - - IteratorImpl(Container& container, std::size_t index) - : m_container(container), m_index(index) {} - - reference operator*() const { return m_container.getMeasurement(m_index); } - - pointer operator->() const { return &operator*(); } - - IteratorImpl& operator++() { - ++m_index; - return *this; - } - - IteratorImpl operator++(int) { - auto copy = *this; - ++*this; - return copy; - } - - bool operator==(const IteratorImpl& other) const { - return m_index == other.m_index; - } - - private: - Container& m_container; - Index m_index; - }; - - using iterator = IteratorImpl; - using const_iterator = IteratorImpl; + using iterator = + Acts::ContainerIndexIterator; + using const_iterator = + Acts::ContainerIndexIterator; iterator begin(); iterator end(); @@ -535,4 +511,7 @@ MeasurementContainer::FixedProxy MeasurementContainer::emplaceMeasurement( return meas; } +static_assert( + std::random_access_iterator && + std::random_access_iterator); } // namespace ActsExamples diff --git a/Examples/Framework/src/EventData/Measurement.cpp b/Examples/Framework/src/EventData/Measurement.cpp index 09c1e0a257a..f37661948db 100644 --- a/Examples/Framework/src/EventData/Measurement.cpp +++ b/Examples/Framework/src/EventData/Measurement.cpp @@ -33,6 +33,16 @@ std::size_t MeasurementContainer::addMeasurement(std::uint8_t size) { return m_entries.size() - 1; } +MeasurementContainer::VariableProxy MeasurementContainer::at( + std::size_t index) { + return VariableProxy{*this, index}; +} + +MeasurementContainer::ConstVariableProxy MeasurementContainer::at( + std::size_t index) const { + return ConstVariableProxy{*this, index}; +} + MeasurementContainer::VariableProxy MeasurementContainer::getMeasurement( std::size_t index) { return VariableProxy{*this, index}; diff --git a/Tests/UnitTests/Core/EventData/TrackTests.cpp b/Tests/UnitTests/Core/EventData/TrackTests.cpp index 013132a60b9..432efad6447 100644 --- a/Tests/UnitTests/Core/EventData/TrackTests.cpp +++ b/Tests/UnitTests/Core/EventData/TrackTests.cpp @@ -228,9 +228,9 @@ BOOST_AUTO_TEST_CASE(IteratorConcept) { BOOST_CHECK(*it == tc.getTrack(0)); std::advance(it, 4); BOOST_CHECK(*it == tc.getTrack(4)); - BOOST_CHECK(*(it[-1]) == tc.getTrack(3)); - BOOST_CHECK(*(it[0]) == tc.getTrack(4)); - BOOST_CHECK(*(it[1]) == tc.getTrack(5)); + BOOST_CHECK(*(it + (-1)) == tc.getTrack(3)); + BOOST_CHECK(*(it + 0) == tc.getTrack(4)); + BOOST_CHECK(*(it + 1) == tc.getTrack(5)); BOOST_CHECK(*(it - 2) == tc.getTrack(2)); } From f5a34f31238f541d27b0f1238948818389e3455b Mon Sep 17 00:00:00 2001 From: Andreas Salzburger Date: Thu, 3 Oct 2024 18:17:50 +0200 Subject: [PATCH 08/24] feat!: (fix + chore) streamline nSegments usage (#3419) The generation of display vertices for segments was buggy and very unstable, I have put this onto more solid grounds, which helps displaying (not only) `AnnulusBounds` correctly. ITk Petal Before: ![Screenshot 2024-07-18 at 17 01 59](https://github.com/user-attachments/assets/930b93f2-8df7-4e80-a44b-3ec7a20a84ac) ITk Petal After: ![Screenshot 2024-07-19 at 12 07 32](https://github.com/user-attachments/assets/bc4edd4b-cc22-4b30-bad8-6032d42854da) When checking the code, I see that there was a pretty big inconsistency in how the nSegments have been handled, so I re-worked that: * `nSegments` (full 2 * PI segments) -> changed and renamed to `quaterSegments` This guarantees that the number of segments for a full circle is a multiplier of 4 and hence the phi extrema points at `(-pi,-0.5*pi,0,0.5*pi,pi)` are consistently added. This will lead to a correct `x` and `y` estimation for the `Extent` of the `Polyhedron`. This also guarantees that e.g. overlapping/attaching surfaces with the same number of segments will have vertices at the same positions, which will be better for the displaying. * inconsitent use of `unsigned int` (SurfaceBounds) and `std::size_t` -> changed to `unsigned int everywhere * Introduces tests for the vertex generation (which were missing) Co-authored-by: Paul Gessinger <1058585+paulgessinger@users.noreply.github.com> --- .../Acts/Detector/LayerStructureBuilder.hpp | 5 +- Core/include/Acts/Geometry/Polyhedron.hpp | 1 + Core/include/Acts/Surfaces/AnnulusBounds.hpp | 15 ++- Core/include/Acts/Surfaces/ConeSurface.hpp | 15 ++- .../Acts/Surfaces/ConvexPolygonBounds.hpp | 4 +- .../Acts/Surfaces/ConvexPolygonBounds.ipp | 2 +- Core/include/Acts/Surfaces/CylinderBounds.hpp | 16 ++- .../include/Acts/Surfaces/CylinderSurface.hpp | 16 ++- Core/include/Acts/Surfaces/DiamondBounds.hpp | 10 +- Core/include/Acts/Surfaces/DiscBounds.hpp | 11 +- Core/include/Acts/Surfaces/DiscSurface.hpp | 9 +- .../Acts/Surfaces/DiscTrapezoidBounds.hpp | 8 +- Core/include/Acts/Surfaces/EllipseBounds.hpp | 11 +- Core/include/Acts/Surfaces/PerigeeSurface.hpp | 4 +- Core/include/Acts/Surfaces/PlanarBounds.hpp | 11 +- Core/include/Acts/Surfaces/PlaneSurface.hpp | 12 +- .../include/Acts/Surfaces/RectangleBounds.hpp | 7 +- Core/include/Acts/Surfaces/StrawSurface.hpp | 8 +- Core/include/Acts/Surfaces/Surface.hpp | 15 ++- Core/include/Acts/Surfaces/SurfaceConcept.hpp | 2 +- .../include/Acts/Surfaces/TrapezoidBounds.hpp | 6 +- .../Acts/Surfaces/detail/FacesHelper.hpp | 8 +- .../Acts/Surfaces/detail/VerticesHelper.hpp | 71 +++++----- .../include/Acts/Visualization/ViewConfig.hpp | 4 +- .../detail/ObjVisualization3D.ipp | 4 +- Core/src/Detector/LayerStructureBuilder.cpp | 3 +- Core/src/Surfaces/AnnulusBounds.cpp | 49 ++++--- Core/src/Surfaces/CMakeLists.txt | 2 +- Core/src/Surfaces/ConeSurface.cpp | 62 ++++----- Core/src/Surfaces/CylinderBounds.cpp | 41 +++--- Core/src/Surfaces/CylinderSurface.cpp | 16 +-- Core/src/Surfaces/DiamondBounds.cpp | 2 +- Core/src/Surfaces/DiscSurface.cpp | 24 ++-- Core/src/Surfaces/DiscTrapezoidBounds.cpp | 2 +- Core/src/Surfaces/EllipseBounds.cpp | 4 +- Core/src/Surfaces/PerigeeSurface.cpp | 2 +- Core/src/Surfaces/PlaneSurface.cpp | 24 ++-- Core/src/Surfaces/StrawSurface.cpp | 19 ++- Core/src/Surfaces/Surface.cpp | 3 +- Core/src/Surfaces/TrapezoidBounds.cpp | 2 +- .../Surfaces/{ => detail}/VerticesHelper.cpp | 93 ++++++++------ Core/src/Visualization/EventDataView3D.cpp | 19 +-- Core/src/Visualization/GeometryView3D.cpp | 5 +- .../Geant4/src/SensitiveSurfaceMapper.cpp | 2 +- .../ActsExamples/Io/Svg/SvgDefaults.hpp | 6 +- Examples/Python/src/Geometry.cpp | 2 +- Examples/Python/src/Obj.cpp | 2 +- Examples/Python/src/Output.cpp | 2 +- Examples/Python/src/Svg.cpp | 2 +- .../include/Acts/Plugins/ActSVG/SvgUtils.hpp | 4 +- Plugins/ActSVG/src/SurfaceSvgConverter.cpp | 7 +- .../DD4hep/DD4hepDetectorSurfaceFactory.hpp | 4 +- .../Plugins/DD4hep/DD4hepLayerStructure.hpp | 4 +- .../src/DD4hepDetectorSurfaceFactory.cpp | 4 +- Plugins/DD4hep/src/DD4hepLayerStructure.cpp | 2 +- .../GeoModel/GeoModelBlueprintCreater.hpp | 4 +- .../GeoModel/src/GeoModelBlueprintCreater.cpp | 2 +- .../Tests/CommonHelpers/LineSurfaceStub.hpp | 4 +- .../Core/Surfaces/AnnulusBoundsTests.cpp | 27 +++- .../Surfaces/ConvexPolygonBoundsTests.cpp | 4 + .../Core/Surfaces/PolyhedronSurfacesTests.cpp | 46 ++++--- Tests/UnitTests/Core/Surfaces/SurfaceStub.hpp | 2 +- .../Core/Surfaces/VerticesHelperTests.cpp | 121 ++++++++++++++++++ .../UnitTests/Core/TrackFitting/Gx2fTests.cpp | 2 +- .../Visualization/EventDataView3DTests.cpp | 8 +- .../Core/Visualization/SurfaceView3DBase.hpp | 24 ++-- .../TrackingGeometryView3DBase.hpp | 28 ++-- .../Fatras/Digitization/SegmentizerTests.cpp | 4 +- .../IndexedSurfacesSvgConverterTests.cpp | 2 +- .../Plugins/ActSVG/LayerSvgConverterTests.cpp | 23 ++-- .../ActSVG/PortalSvgConverterTests.cpp | 4 +- .../ActSVG/SurfaceSvgConverterTests.cpp | 4 +- .../TrackingGeometrySvgConverterTests.cpp | 2 +- .../Plugins/TGeo/TGeoTubeConversionTests.cpp | 1 - 74 files changed, 577 insertions(+), 423 deletions(-) rename Core/src/Surfaces/{ => detail}/VerticesHelper.cpp (55%) diff --git a/Core/include/Acts/Detector/LayerStructureBuilder.hpp b/Core/include/Acts/Detector/LayerStructureBuilder.hpp index 9dd0e0a8e8e..02fd726824e 100644 --- a/Core/include/Acts/Detector/LayerStructureBuilder.hpp +++ b/Core/include/Acts/Detector/LayerStructureBuilder.hpp @@ -91,8 +91,9 @@ class LayerStructureBuilder : public IInternalStructureBuilder { /// Minimum number of surfaces to build an internal structure /// - otherwise the tryAll options is used unsigned int nMinimalSurfaces = 4u; - /// Polyhedron approximations - unsigned int nSegments = 1u; + /// Polyhedron approximations: number of segments to be used + /// to approximate a quarter of a circle + unsigned int quarterSegments = 1u; /// Extra information, mainly for screen output std::string auxiliary = ""; }; diff --git a/Core/include/Acts/Geometry/Polyhedron.hpp b/Core/include/Acts/Geometry/Polyhedron.hpp index 2a6eb47d765..00ccaaab4c5 100644 --- a/Core/include/Acts/Geometry/Polyhedron.hpp +++ b/Core/include/Acts/Geometry/Polyhedron.hpp @@ -39,6 +39,7 @@ struct Polyhedron { /// @param facesIn List of lists of indices for faces. /// @param triangularMeshIn List of lists of indices for a triangular mesh /// @param isExact A dedicated flag if this is exact or not + /// /// @note This creates copies of the input vectors Polyhedron(const std::vector& verticesIn, const std::vector& facesIn, diff --git a/Core/include/Acts/Surfaces/AnnulusBounds.hpp b/Core/include/Acts/Surfaces/AnnulusBounds.hpp index 17e64f2a505..f1283ec63b8 100644 --- a/Core/include/Acts/Surfaces/AnnulusBounds.hpp +++ b/Core/include/Acts/Surfaces/AnnulusBounds.hpp @@ -133,19 +133,20 @@ class AnnulusBounds : public DiscBounds { std::vector corners() const; /// This method returns the xy coordinates of the four corners of the - /// bounds in module coordinates (in x/y) + /// bounds in module coordinates (in x/y), and if quarterSegments is bigger or + /// equal to 0, the curved part of the segment is included and approximated + /// by the corresponding number of segments. + /// /// Starting from the upper right (max R, pos locX) and proceeding clock-wise /// i.e. (max R; pos locX), (min R; pos locX), (min R; neg loc X), (max R: neg /// locX) /// - /// @param lseg the number of segments used to approximate - /// and eventually curved line - /// - /// @note that that if @c lseg > 0, the extrema points are given, - /// which may slightly alter the number of segments returned + /// @param quarterSegments the number of segments used to approximate + /// a quarter of a circle /// /// @return vector for vertices in 2D - std::vector vertices(unsigned int lseg) const override; + std::vector vertices( + unsigned int quarterSegments = 2u) const override; /// This method returns inner radius double rMin() const final; diff --git a/Core/include/Acts/Surfaces/ConeSurface.hpp b/Core/include/Acts/Surfaces/ConeSurface.hpp index c15d84d1c23..a2b2572bba8 100644 --- a/Core/include/Acts/Surfaces/ConeSurface.hpp +++ b/Core/include/Acts/Surfaces/ConeSurface.hpp @@ -200,15 +200,16 @@ class ConeSurface : public RegularSurface { /// Return a Polyhedron for the surfaces /// /// @param gctx The current geometry context object, e.g. alignment - /// @param lseg Number of segments along curved lines, it represents - /// the full 2*M_PI coverange, if lseg is set to 1 only the extrema - /// are given - /// @note that a surface transform can invalidate the extrema - /// in the transformed space + /// @param quarterSegments Number of segments used to approximate a quarter + /// + /// @note The phi extrema points at (-pi, -1/2 pi, 0, 1/2 pi) that fall within + /// the surface will be inserted to guarantee an appropriate extent + /// measurement in x and y /// /// @return A list of vertices and a face/facett description of it - Polyhedron polyhedronRepresentation(const GeometryContext& gctx, - std::size_t lseg) const override; + Polyhedron polyhedronRepresentation( + const GeometryContext& gctx, + unsigned int quarterSegments = 2u) const override; /// Return properly formatted class name for screen output std::string name() const override; diff --git a/Core/include/Acts/Surfaces/ConvexPolygonBounds.hpp b/Core/include/Acts/Surfaces/ConvexPolygonBounds.hpp index 2f116ddcee5..e802056e7fd 100644 --- a/Core/include/Acts/Surfaces/ConvexPolygonBounds.hpp +++ b/Core/include/Acts/Surfaces/ConvexPolygonBounds.hpp @@ -114,13 +114,13 @@ class ConvexPolygonBounds : public ConvexPolygonBoundsBase { /// Return the vertices /// - /// @param lseg the number of segments used to approximate + /// @param ignoredSegments the number of segments used to approximate /// and eventually curved line /// /// @note the number of segments is ignored in this representation /// /// @return vector for vertices in 2D - std::vector vertices(unsigned int lseg = 1) const final; + std::vector vertices(unsigned int ignoredSegments = 0u) const final; /// Return a rectangle bounds object that encloses this polygon. /// @return The rectangular bounds diff --git a/Core/include/Acts/Surfaces/ConvexPolygonBounds.ipp b/Core/include/Acts/Surfaces/ConvexPolygonBounds.ipp index 505d9f95d82..9f1a61955ae 100644 --- a/Core/include/Acts/Surfaces/ConvexPolygonBounds.ipp +++ b/Core/include/Acts/Surfaces/ConvexPolygonBounds.ipp @@ -111,7 +111,7 @@ bool Acts::ConvexPolygonBounds::inside( template std::vector Acts::ConvexPolygonBounds::vertices( - unsigned int /*lseg*/) const { + unsigned int /*ignoredSegments*/) const { return {m_vertices.begin(), m_vertices.end()}; } diff --git a/Core/include/Acts/Surfaces/CylinderBounds.hpp b/Core/include/Acts/Surfaces/CylinderBounds.hpp index 7453029c213..51508055e06 100644 --- a/Core/include/Acts/Surfaces/CylinderBounds.hpp +++ b/Core/include/Acts/Surfaces/CylinderBounds.hpp @@ -112,12 +112,18 @@ class CylinderBounds : public SurfaceBounds { /// Returns true for full phi coverage bool coversFullAzimuth() const; - /// Create the bows/circles on either side of the cylinder + /// Create the bow/circle vertices on either side of the cylinder /// - /// @param trans is the global transform - /// @param lseg are the numbero if phi segments - std::vector createCircles(const Transform3 trans, - std::size_t lseg) const; + /// @param transform is the global transform + /// @param quarterSegments is the number of segments to approximate a quarter + /// of a circle. In order to symmetrize fully closed and sectoral cylinders, + /// also in the first case the two end points are given (albeit they overlap) + /// in -pi / pi + /// + /// @return a singlevector containing the vertices from one side and then + /// from the other side consecutively + std::vector circleVertices(const Transform3 transform, + unsigned int quarterSegments) const; /// Output Method for std::ostream std::ostream& toStream(std::ostream& sl) const final; diff --git a/Core/include/Acts/Surfaces/CylinderSurface.hpp b/Core/include/Acts/Surfaces/CylinderSurface.hpp index 82bc1c68bc2..09b953afce1 100644 --- a/Core/include/Acts/Surfaces/CylinderSurface.hpp +++ b/Core/include/Acts/Surfaces/CylinderSurface.hpp @@ -214,14 +214,20 @@ class CylinderSurface : public RegularSurface { /// Return a Polyhedron for a cylinder /// + /// This method represents the cylinder as a polyhedron with a given number + /// of segments to represent a quarter of a full circle. The polyedron will + /// consist of the vertices of the cylinder on both sides, and faces between + /// them, both as rectangular faces and as triangular faces. + /// /// @param gctx The current geometry context object, e.g. alignment - /// @param lseg Number of segments along curved lines, it represents - /// the full 2*M_PI coverange, if lseg is set to 1 only the extrema - /// are given + /// @param quarterSegments The number of segments to approximate a quarter of the + /// full circle; it's chosen to be 1, only the extrema points (-pi, -0.5pi, + /// 0., 0.5pi) are inserted to capture the correct extent in the x-y plane /// /// @return A list of vertices and a face/facett description of it - Polyhedron polyhedronRepresentation(const GeometryContext& gctx, - std::size_t lseg) const override; + Polyhedron polyhedronRepresentation( + const GeometryContext& gctx, + unsigned int quarterSegments = 2u) const override; /// Calculate the derivative of path length at the geometry constraint or /// point-of-closest-approach w.r.t. alignment parameters of the surface (i.e. diff --git a/Core/include/Acts/Surfaces/DiamondBounds.hpp b/Core/include/Acts/Surfaces/DiamondBounds.hpp index ddaf727a3e9..a23b8831125 100644 --- a/Core/include/Acts/Surfaces/DiamondBounds.hpp +++ b/Core/include/Acts/Surfaces/DiamondBounds.hpp @@ -90,15 +90,13 @@ class DiamondBounds : public PlanarBounds { bool inside(const Vector2& lposition, const BoundaryTolerance& boundaryTolerance) const final; - /// Return the vertices + /// Return the vertices that describe this shape /// - /// @param lseg the number of segments used to approximate - /// and eventually curved line - /// - /// @note the number of segments is ignored for this representation + /// @param ignoredSegments is an ignored parameter only used for + /// curved bound segments /// /// @return vector for vertices in 2D - std::vector vertices(unsigned int lseg = 1) const final; + std::vector vertices(unsigned int ignoredSegments = 0u) const final; // Bounding box representation const RectangleBounds& boundingBox() const final; diff --git a/Core/include/Acts/Surfaces/DiscBounds.hpp b/Core/include/Acts/Surfaces/DiscBounds.hpp index 22bcce3f960..626f8e2405c 100644 --- a/Core/include/Acts/Surfaces/DiscBounds.hpp +++ b/Core/include/Acts/Surfaces/DiscBounds.hpp @@ -29,14 +29,13 @@ class DiscBounds : public SurfaceBounds { /// Return the vertices /// - /// @param lseg the number of segments used to approximate - /// and eventually curved line, the number refers to full 2*PI - /// - /// @note that the extremas are given, which may slightly alter the - /// number of segments returned + /// @param quarterSegments The number of segments used to describe a quarter + /// of a circle, if it is 1, then only the extrema points in phi are inserted + /// next to the segment corners /// /// @return vector for vertices in 2D - virtual std::vector vertices(unsigned int lseg) const = 0; + virtual std::vector vertices( + unsigned int quarterSegments = 2u) const = 0; /// Returns a reference radius for binning virtual double binningValueR() const = 0; diff --git a/Core/include/Acts/Surfaces/DiscSurface.hpp b/Core/include/Acts/Surfaces/DiscSurface.hpp index 112ec0b30d2..e2c10df4023 100644 --- a/Core/include/Acts/Surfaces/DiscSurface.hpp +++ b/Core/include/Acts/Surfaces/DiscSurface.hpp @@ -312,13 +312,12 @@ class DiscSurface : public RegularSurface { /// Return a Polyhedron for the surfaces /// /// @param gctx The current geometry context object, e.g. alignment - /// @param lseg Number of segments along curved lines, it represents - /// the full 2*M_PI coverange, if lseg is set to 1 only the extrema - /// are given + /// @param quarterSegments Number of segments used to describe the + /// quarter of a full circle /// /// @return A list of vertices and a face/facett description of it - Polyhedron polyhedronRepresentation(const GeometryContext& gctx, - std::size_t lseg) const override; + Polyhedron polyhedronRepresentation( + const GeometryContext& gctx, unsigned int quarterSegments) const override; /// Calculate the derivative of bound track parameters local position w.r.t. /// position in local 3D Cartesian coordinates diff --git a/Core/include/Acts/Surfaces/DiscTrapezoidBounds.hpp b/Core/include/Acts/Surfaces/DiscTrapezoidBounds.hpp index 1e4d9d7706a..a3e4e5ea28f 100644 --- a/Core/include/Acts/Surfaces/DiscTrapezoidBounds.hpp +++ b/Core/include/Acts/Surfaces/DiscTrapezoidBounds.hpp @@ -123,13 +123,11 @@ class DiscTrapezoidBounds : public DiscBounds { /// This method returns the xy coordinates of the four corners of the /// bounds in module coorindates (in xy) /// - /// @param lseg the number of segments used to approximate - /// and eventually curved line - /// - /// @note that the number of segments are ignored for this surface + /// @param ignoredSegments is an ignored parameter only used for + /// curved bound segments /// /// @return vector for vertices in 2D - std::vector vertices(unsigned int lseg) const final; + std::vector vertices(unsigned int ignoredSegments = 0u) const final; private: std::array m_values; diff --git a/Core/include/Acts/Surfaces/EllipseBounds.hpp b/Core/include/Acts/Surfaces/EllipseBounds.hpp index dc6983b86bb..aec08d10381 100644 --- a/Core/include/Acts/Surfaces/EllipseBounds.hpp +++ b/Core/include/Acts/Surfaces/EllipseBounds.hpp @@ -92,14 +92,13 @@ class EllipseBounds : public PlanarBounds { /// Return the vertices /// - /// @param lseg the number of segments used to approximate - /// and eventually curved line, here it refers to the full 2PI Ellipse - /// - /// @note the number of segments to may be altered by also providing - /// the extremas in all direction + /// @param quarterSegments is the number of segments to approximate a quarter + /// of a circle. In order to symmetrize fully closed and sectoral cylinders, + /// also in the first case the two end points are given (albeit they overlap) + /// in -pi / pi /// /// @return vector for vertices in 2D - std::vector vertices(unsigned int lseg) const final; + std::vector vertices(unsigned int quarterSegments) const final; // Bounding box representation const RectangleBounds& boundingBox() const final; diff --git a/Core/include/Acts/Surfaces/PerigeeSurface.hpp b/Core/include/Acts/Surfaces/PerigeeSurface.hpp index 37b1044f0ce..1d913ecc43e 100644 --- a/Core/include/Acts/Surfaces/PerigeeSurface.hpp +++ b/Core/include/Acts/Surfaces/PerigeeSurface.hpp @@ -76,11 +76,11 @@ class PerigeeSurface : public LineSurface { /// Return a Polyhedron for the surfaces /// /// @param gctx The current geometry context object, e.g. alignment - /// @param lseg is ignored for a perigee @note ignored + /// @param ingoreSegments is an ignored parameter /// /// @return A list of vertices and a face/facett description of it Polyhedron polyhedronRepresentation(const GeometryContext& gctx, - std::size_t lseg) const final; + unsigned int ingoreSegments) const final; protected: /// Output Method for std::ostream diff --git a/Core/include/Acts/Surfaces/PlanarBounds.hpp b/Core/include/Acts/Surfaces/PlanarBounds.hpp index 8c4363dbbc3..28bc0e42b60 100644 --- a/Core/include/Acts/Surfaces/PlanarBounds.hpp +++ b/Core/include/Acts/Surfaces/PlanarBounds.hpp @@ -26,14 +26,15 @@ class PlanarBounds : public SurfaceBounds { public: /// Return the vertices /// - /// @param lseg the number of segments used to approximate - /// and eventually curved line + /// @param quarterSegments is the number of segments used to describe curved + /// segments in a quarter of the phi range. If it is 1, then only the extrema + /// points in phi are inserted next to the segment corners. /// - /// @note that the extremas are given, which may slightly alter the - /// number of segments returned + /// @note for planar bounds without curved segments @c quarterSegments is ignored /// /// @return vector for vertices in 2D - virtual std::vector vertices(unsigned int lseg = 1) const = 0; + virtual std::vector vertices( + unsigned int quarterSegments = 2u) const = 0; /// Bounding box parameters /// diff --git a/Core/include/Acts/Surfaces/PlaneSurface.hpp b/Core/include/Acts/Surfaces/PlaneSurface.hpp index 866cb4d4585..2bc65398f8d 100644 --- a/Core/include/Acts/Surfaces/PlaneSurface.hpp +++ b/Core/include/Acts/Surfaces/PlaneSurface.hpp @@ -198,13 +198,15 @@ class PlaneSurface : public RegularSurface { /// Return a Polyhedron for the surfaces /// /// @param gctx The current geometry context object, e.g. alignment - /// @param lseg Number of segments along curved lines, it represents - /// the full 2*M_PI coverange, if lseg is set to 1 only the extrema - /// are given + /// @param quarterSegments is the number of segments used to describe curved + /// segments in a quarter of the phi range. If it is 1, then only the extrema + /// points in phi are inserted next to the segment corners. + /// + /// @note for planar surfaces without curved segments @c quarterSegments is ignored /// /// @return A list of vertices and a face/facett description of it - Polyhedron polyhedronRepresentation(const GeometryContext& gctx, - std::size_t lseg) const override; + Polyhedron polyhedronRepresentation( + const GeometryContext& gctx, unsigned int quarterSegments) const override; /// Return properly formatted class name for screen output std::string name() const override; diff --git a/Core/include/Acts/Surfaces/RectangleBounds.hpp b/Core/include/Acts/Surfaces/RectangleBounds.hpp index 7ad67f3aafb..aba1743fa74 100644 --- a/Core/include/Acts/Surfaces/RectangleBounds.hpp +++ b/Core/include/Acts/Surfaces/RectangleBounds.hpp @@ -86,13 +86,12 @@ class RectangleBounds : public PlanarBounds { /// Return the vertices /// - /// @param lseg the number of segments used to approximate - /// and eventually curved line - /// + /// @param quarterSegments is the number of segments used to describe curved + /// segments in a quarter of the phi range. /// @note the number of segments is ignored in this representation /// /// @return vector for vertices in 2D - std::vector vertices(unsigned int lseg = 1) const final; + std::vector vertices(unsigned int quarterSegments = 0u) const final; // Bounding box representation const RectangleBounds& boundingBox() const final; diff --git a/Core/include/Acts/Surfaces/StrawSurface.hpp b/Core/include/Acts/Surfaces/StrawSurface.hpp index 88fd8423525..706bb7d9823 100644 --- a/Core/include/Acts/Surfaces/StrawSurface.hpp +++ b/Core/include/Acts/Surfaces/StrawSurface.hpp @@ -92,13 +92,13 @@ class StrawSurface : public LineSurface { /// Return a Polyhedron for the surfaces /// /// @param gctx The current geometry context object, e.g. alignment - /// @param lseg Number of segments along curved lines, it represents - /// the full 2*M_PI coverange, if lseg is set to 1 only the extrema - /// are given @note if lseg is set to 1 then only the straw is created + /// @param quarterSegments is the number of segments used to describe curved + /// segments in a quarter of the phi range. If it is 1, then only the extrema + /// points in phi are inserted next to the segment corners. /// /// @return A list of vertices and a face/facett description of it Polyhedron polyhedronRepresentation(const GeometryContext& gctx, - std::size_t lseg) const final; + unsigned int quarterSegments) const final; }; inline Surface::SurfaceType StrawSurface::type() const { diff --git a/Core/include/Acts/Surfaces/Surface.hpp b/Core/include/Acts/Surfaces/Surface.hpp index c93e7d3c9aa..3a823671823 100644 --- a/Core/include/Acts/Surfaces/Surface.hpp +++ b/Core/include/Acts/Surfaces/Surface.hpp @@ -418,20 +418,21 @@ class Surface : public virtual GeometryObject, /// Return properly formatted class name virtual std::string name() const = 0; - /// Return a Polyhedron for this object + /// Return a Polyhedron for surface objects /// /// @param gctx The current geometry context object, e.g. alignment - /// @param lseg Number of segments along curved lines, if the lseg - /// is set to one, only the corners and the extrema are given, - /// otherwise it represents the number of segments for a full 2*M_PI - /// circle and is scaled to the relevant sector + /// @param quarterSegments The number of segemtns to approximate a 0.5*pi sector, + /// which represents a quarter of the full circle + /// + /// @note In order to symmetrize the code between sectoral and closed cylinders + /// in case of closed cylinders, both (-pi, pi) are given as separate vertices /// /// @note An internal surface transform can invalidate the extrema /// in the transformed space /// /// @return A list of vertices and a face/facett description of it - virtual Polyhedron polyhedronRepresentation(const GeometryContext& gctx, - std::size_t lseg) const = 0; + virtual Polyhedron polyhedronRepresentation( + const GeometryContext& gctx, unsigned int quarterSegments = 2u) const = 0; /// The derivative of bound track parameters w.r.t. alignment /// parameters of its reference surface (i.e. local frame origin in diff --git a/Core/include/Acts/Surfaces/SurfaceConcept.hpp b/Core/include/Acts/Surfaces/SurfaceConcept.hpp index e09b420ee9f..2abcda1d5d7 100644 --- a/Core/include/Acts/Surfaces/SurfaceConcept.hpp +++ b/Core/include/Acts/Surfaces/SurfaceConcept.hpp @@ -87,7 +87,7 @@ concept SurfaceConcept = requires(S s, const S cs, S s2, const S cs2, { cs.name() } -> std::same_as; { - cs.polyhedronRepresentation(gctx, std::declval()) + cs.polyhedronRepresentation(gctx, std::declval()) } -> std::same_as; { diff --git a/Core/include/Acts/Surfaces/TrapezoidBounds.hpp b/Core/include/Acts/Surfaces/TrapezoidBounds.hpp index 3f116dc4110..8ccd3a5c928 100644 --- a/Core/include/Acts/Surfaces/TrapezoidBounds.hpp +++ b/Core/include/Acts/Surfaces/TrapezoidBounds.hpp @@ -108,13 +108,13 @@ class TrapezoidBounds : public PlanarBounds { /// Return the vertices /// - /// @param lseg the number of segments used to approximate - /// and eventually curved line + /// @param ignoredSegments is and ignored parameter used to describe + /// the number of segments to approximate curved sectors. /// /// @note the number of segments is ignored in this representation /// /// @return vector for vertices in 2D - std::vector vertices(unsigned int lseg = 1) const final; + std::vector vertices(unsigned int ignoredSegments = 0u) const final; // Bounding box representation const RectangleBounds& boundingBox() const final; diff --git a/Core/include/Acts/Surfaces/detail/FacesHelper.hpp b/Core/include/Acts/Surfaces/detail/FacesHelper.hpp index 260f7ce3b01..ca2922c325e 100644 --- a/Core/include/Acts/Surfaces/detail/FacesHelper.hpp +++ b/Core/include/Acts/Surfaces/detail/FacesHelper.hpp @@ -21,7 +21,7 @@ namespace Acts::detail { struct FacesHelper { using FaceVector = std::vector; - /// @brief This method words for all convex type surface setups + /// @brief This method works for all convex type surface setups /// It includes: /// /// Rectangle / Triangle / Polygon @@ -60,14 +60,12 @@ struct FacesHelper { /// vector is splittable in half into the two bows. /// /// @param vertices The vector of vertices - /// @param fullTwoPi The indicator if the concentric face is closed static std::pair cylindricalFaceMesh( - const std::vector& vertices, bool fullTwoPi = true) { + const std::vector& vertices) { FaceVector faces; FaceVector triangularMesh; std::size_t nqfaces = static_cast(0.5 * vertices.size()); - std::size_t reduce = (!fullTwoPi) ? 1 : 0; - for (std::size_t iface = 0; iface < nqfaces - reduce; ++iface) { + for (std::size_t iface = 0; iface < nqfaces - 1; ++iface) { std::size_t p2 = (iface + 1 == nqfaces) ? 0 : iface + 1; std::vector face = {iface, p2, p2 + nqfaces, nqfaces + iface}; diff --git a/Core/include/Acts/Surfaces/detail/VerticesHelper.hpp b/Core/include/Acts/Surfaces/detail/VerticesHelper.hpp index 71938626eaf..3e63c7189e6 100644 --- a/Core/include/Acts/Surfaces/detail/VerticesHelper.hpp +++ b/Core/include/Acts/Surfaces/detail/VerticesHelper.hpp @@ -22,51 +22,53 @@ namespace Acts::detail::VerticesHelper { /// A method that inserts the cartesian extrema points and segments /// a curved segment into sub segments /// -/// @param phiMin the minimum Phi of the bounds object -/// @param phiMax the maximum Phi of the bounds object +/// @param phiMin the minimum phi value +/// @param phiMax The second phi value /// @param phiRef is a vector of reference phi values to be included as well -/// @param phiTolerance is the tolerance for reference phi insertion -/// @return a vector +/// @param quarterSegments number of segments used to approximate a segment quarter +/// +/// @return a vector of generated phi values std::vector phiSegments(ActsScalar phiMin = -M_PI, ActsScalar phiMax = M_PI, const std::vector& phiRefs = {}, - ActsScalar phiTolerance = 1e-6); + unsigned int quarterSegments = 2u); /// Helper method to create a regular 2 or 3 D segment -/// between two phi values +/// between two phi values with a given number of segments +/// +/// It will insert the phi at extrema points and reference points, it uses +/// a minimum approximation of a circle with 8 segments /// /// @tparam vertex_t Type of vertex to be applied /// @tparam transform_t Optional transform /// -/// @param vertices [in,out] The 3D vertices to be filled -/// @param rxy The radius description if first +/= second: ellipse -/// @param phi1 The first phi value -/// @param phi2 The second phi value -/// @param lseg The number of segments for full 2*PI -/// @param addon The additional segments to be built +/// @param rXY The radius description if first +/= second: ellipse +/// @param phiMin the minimum phi value +/// @param phiMax the second phi value +/// @param phiRef is a vector of reference phi values to be included as well +/// @param quarterSegments number of segments used to approximate a segment quarter /// @param offset The out of plane offset position of the bow /// @param transform The transform applied (optional) +/// +/// @return a vector of vertices template -void createSegment(std::vector& vertices, - std::pair rxy, ActsScalar phi1, - ActsScalar phi2, unsigned int lseg, int addon = 0, - const vertex_t& offset = vertex_t::Zero(), - const transform_t& transform = transform_t::Identity()) { - // Calculate the number of segments - 1 is the minimum - unsigned int segs = - static_cast(std::abs(phi2 - phi1) / (2 * M_PI) * lseg); - segs = segs > 0 ? segs : 1; - ActsScalar phistep = (phi2 - phi1) / segs; - // Create the segments - for (unsigned int iphi = 0; iphi < segs + addon; ++iphi) { - ActsScalar phi = phi1 + iphi * phistep; +std::vector segmentVertices( + std::pair rXY, ActsScalar phiMin, ActsScalar phiMax, + const std::vector& phiRefs = {}, + unsigned int quarterSegments = 2u, + const vertex_t& offset = vertex_t::Zero(), + const transform_t& transform = transform_t::Identity()) { + std::vector vertices; + std::vector phis = + phiSegments(phiMin, phiMax, phiRefs, quarterSegments); + for (ActsScalar phi : phis) { vertex_t vertex = vertex_t::Zero(); - vertex(0) = rxy.first * std::cos(phi); - vertex(1) = rxy.second * std::sin(phi); - + vertex(0) = rXY.first * std::cos(phi); + vertex(1) = rXY.second * std::sin(phi); vertex = vertex + offset; vertices.push_back(transform * vertex); } + return vertices; } /// Construct vertices on an ellipse-like bound object. @@ -76,14 +78,15 @@ void createSegment(std::vector& vertices, /// @param outerRx The radius of the outer ellipse (in x) /// @param outerRy The radius of the outer ellipse (in y) /// @param avgPhi The phi direction of the center if sector -/// @param halfPhi The half phi sector if sector -/// @param lseg The number of segments for for a full 2*pi segment +/// @param halfPhi The half phi sector of the ellipse +/// @param quarterSegments number of segments used to approximate a segment quarter +/// /// @return a vector of 2d-vectors std::vector ellipsoidVertices(ActsScalar innerRx, ActsScalar innerRy, ActsScalar outerRx, ActsScalar outerRy, ActsScalar avgPhi = 0., ActsScalar halfPhi = M_PI, - unsigned int lseg = 1); + unsigned int quarterSegments = 2u); /// Construct vertices on an disc/wheel-like bound object. /// @@ -91,12 +94,14 @@ std::vector ellipsoidVertices(ActsScalar innerRx, ActsScalar innerRy, /// @param outerR The radius of the outer circle (sector) /// @param avgPhi The phi direction of the center if sector /// @param halfPhi The half phi sector if sector -/// @param lseg The number of segments for for a full 2*pi segment +/// @param quarterSegments number of segments used to approximate a segment quarter +/// /// @return a vector of 2d-vectors std::vector circularVertices(ActsScalar innerR, ActsScalar outerR, ActsScalar avgPhi = 0., ActsScalar halfPhi = M_PI, - unsigned int lseg = 1); + unsigned int quarterSegments = 2u); + /// Check if the point is inside the polygon w/o any tolerances. /// /// @tparam vertex_container_t is an iterable container diff --git a/Core/include/Acts/Visualization/ViewConfig.hpp b/Core/include/Acts/Visualization/ViewConfig.hpp index 6fcf1b47984..0414fa58aba 100644 --- a/Core/include/Acts/Visualization/ViewConfig.hpp +++ b/Core/include/Acts/Visualization/ViewConfig.hpp @@ -112,8 +112,8 @@ struct ViewConfig { double lineThickness = 0.15; /// The visual surface thickness for this object double surfaceThickness = 0.15; - /// The number of segments to approximate full 2pi - unsigned int nSegments = 72; + /// The number of segments to approximate a quarter of the circle + unsigned int quarterSegments = 72; /// Whether to triangulate or not bool triangulate = false; /// Write name - non-empty string indicates writing diff --git a/Core/include/Acts/Visualization/detail/ObjVisualization3D.ipp b/Core/include/Acts/Visualization/detail/ObjVisualization3D.ipp index 2419ef8c546..354ef518bef 100644 --- a/Core/include/Acts/Visualization/detail/ObjVisualization3D.ipp +++ b/Core/include/Acts/Visualization/detail/ObjVisualization3D.ipp @@ -160,8 +160,8 @@ void ObjVisualization3D::write(std::ostream& os, std::ostream& mos) const { } } os << "f"; - for (std::size_t i = 0; i < fc.size(); i++) { - os << " " << fc[i] + 1; + for (auto fi : fc) { + os << " " << fi + 1; } os << "\n"; ++is; diff --git a/Core/src/Detector/LayerStructureBuilder.cpp b/Core/src/Detector/LayerStructureBuilder.cpp index d936b27398a..94766a80d0d 100644 --- a/Core/src/Detector/LayerStructureBuilder.cpp +++ b/Core/src/Detector/LayerStructureBuilder.cpp @@ -256,7 +256,8 @@ Acts::Experimental::LayerStructureBuilder::construct( if (!support.internalConstraints.empty()) { // Estimate the extent from the surfaces for (const auto& s : internalSurfaces) { - auto sPolyhedron = s->polyhedronRepresentation(gctx, m_cfg.nSegments); + auto sPolyhedron = + s->polyhedronRepresentation(gctx, m_cfg.quarterSegments); supportExtent.extend(sPolyhedron.extent(), support.internalConstraints); } diff --git a/Core/src/Surfaces/AnnulusBounds.cpp b/Core/src/Surfaces/AnnulusBounds.cpp index d96b3bc7e80..34c7b5c468b 100644 --- a/Core/src/Surfaces/AnnulusBounds.cpp +++ b/Core/src/Surfaces/AnnulusBounds.cpp @@ -103,33 +103,30 @@ std::vector Acts::AnnulusBounds::corners() const { } std::vector Acts::AnnulusBounds::vertices( - unsigned int lseg) const { - if (lseg > 0) { - // List of vertices counter-clockwise starting with left inner - std::vector rvertices; - + unsigned int quarterSegments) const { + if (quarterSegments > 0u) { using VectorHelpers::phi; - auto phisInner = detail::VerticesHelper::phiSegments( - phi(m_inRightStripXY - m_moduleOrigin), - phi(m_inLeftStripXY - m_moduleOrigin)); - auto phisOuter = detail::VerticesHelper::phiSegments( - phi(m_outLeftStripXY - m_moduleOrigin), - phi(m_outRightStripXY - m_moduleOrigin)); - - // Inner bow from phi_min -> phi_max - for (unsigned int iseg = 0; iseg < phisInner.size() - 1; ++iseg) { - int addon = (iseg == phisInner.size() - 2) ? 1 : 0; - detail::VerticesHelper::createSegment( - rvertices, {get(eMinR), get(eMinR)}, phisInner[iseg], - phisInner[iseg + 1], lseg, addon); - } - // Upper bow from phi_max -> phi_min - for (unsigned int iseg = 0; iseg < phisOuter.size() - 1; ++iseg) { - int addon = (iseg == phisOuter.size() - 2) ? 1 : 0; - detail::VerticesHelper::createSegment( - rvertices, {get(eMaxR), get(eMaxR)}, phisOuter[iseg], - phisOuter[iseg + 1], lseg, addon); - } + + ActsScalar phiMinInner = phi(m_inRightStripXY - m_moduleOrigin); + ActsScalar phiMaxInner = phi(m_inLeftStripXY - m_moduleOrigin); + + ActsScalar phiMinOuter = phi(m_outRightStripXY - m_moduleOrigin); + ActsScalar phiMaxOuter = phi(m_outLeftStripXY - m_moduleOrigin); + + // Inner bow from phi_min -> phi_max (needs to be reversed) + std::vector rvertices = + detail::VerticesHelper::segmentVertices( + {get(eMinR), get(eMinR)}, phiMinInner, phiMaxInner, {}, + quarterSegments); + std::reverse(rvertices.begin(), rvertices.end()); + + // Outer bow from phi_min -> phi_max + auto overtices = + detail::VerticesHelper::segmentVertices( + {get(eMaxR), get(eMaxR)}, phiMinOuter, phiMaxOuter, {}, + quarterSegments); + rvertices.insert(rvertices.end(), overtices.begin(), overtices.end()); + std::for_each(rvertices.begin(), rvertices.end(), [&](Acts::Vector2& rv) { rv += m_moduleOrigin; }); return rvertices; diff --git a/Core/src/Surfaces/CMakeLists.txt b/Core/src/Surfaces/CMakeLists.txt index 2f704f595c6..07cc26cbfcf 100644 --- a/Core/src/Surfaces/CMakeLists.txt +++ b/Core/src/Surfaces/CMakeLists.txt @@ -24,7 +24,7 @@ target_sources( SurfaceArray.cpp SurfaceError.cpp TrapezoidBounds.cpp - VerticesHelper.cpp + detail/VerticesHelper.cpp RegularSurface.cpp CurvilinearSurface.cpp detail/AlignmentHelper.cpp diff --git a/Core/src/Surfaces/ConeSurface.cpp b/Core/src/Surfaces/ConeSurface.cpp index 98f1484609d..ce10c59815f 100644 --- a/Core/src/Surfaces/ConeSurface.cpp +++ b/Core/src/Surfaces/ConeSurface.cpp @@ -183,19 +183,18 @@ const Acts::ConeBounds& Acts::ConeSurface::bounds() const { } Acts::Polyhedron Acts::ConeSurface::polyhedronRepresentation( - const GeometryContext& gctx, std::size_t lseg) const { + const GeometryContext& gctx, unsigned int quarterSegments) const { // Prepare vertices and faces std::vector vertices; std::vector faces; std::vector triangularMesh; - - double minZ = bounds().get(ConeBounds::eMinZ); - double maxZ = bounds().get(ConeBounds::eMaxZ); + ActsScalar minZ = bounds().get(ConeBounds::eMinZ); + ActsScalar maxZ = bounds().get(ConeBounds::eMaxZ); if (minZ == -std::numeric_limits::infinity() || maxZ == std::numeric_limits::infinity()) { throw std::domain_error( - "Polyhedron repr of boundless surface not possible"); + "Polyhedron representation of boundless surface is not possible"); } auto ctransform = transform(gctx); @@ -208,64 +207,53 @@ Acts::Polyhedron Acts::ConeSurface::polyhedronRepresentation( } // Cone parameters - double hPhiSec = bounds().get(ConeBounds::eHalfPhiSector); - double avgPhi = bounds().get(ConeBounds::eAveragePhi); - bool fullCone = (hPhiSec == M_PI); - - // Get the phi segments from the helper - auto phiSegs = fullCone ? detail::VerticesHelper::phiSegments() - : detail::VerticesHelper::phiSegments( - avgPhi - hPhiSec, avgPhi + hPhiSec, - {static_cast(avgPhi)}); - - // Negative cone if exists - std::vector coneSides; + ActsScalar hPhiSec = bounds().get(ConeBounds::eHalfPhiSector); + ActsScalar avgPhi = bounds().get(ConeBounds::eAveragePhi); + std::vector refPhi = {}; + if (bool fullCone = (hPhiSec == M_PI); !fullCone) { + refPhi = {avgPhi}; + } + + // Add the cone sizes + std::vector coneSides; if (std::abs(minZ) > s_onSurfaceTolerance) { coneSides.push_back(minZ); } if (std::abs(maxZ) > s_onSurfaceTolerance) { coneSides.push_back(maxZ); } + for (auto& z : coneSides) { - // Remember the first vertex std::size_t firstIv = vertices.size(); // Radius and z offset double r = std::abs(z) * bounds().tanAlpha(); Vector3 zoffset(0., 0., z); - for (unsigned int iseg = 0; iseg < phiSegs.size() - 1; ++iseg) { - int addon = (iseg == phiSegs.size() - 2 && !fullCone) ? 1 : 0; - detail::VerticesHelper::createSegment(vertices, {r, r}, phiSegs[iseg], - phiSegs[iseg + 1], lseg, addon, - zoffset, ctransform); - } - // Create the faces + auto svertices = detail::VerticesHelper::segmentVertices( + {r, r}, avgPhi - hPhiSec, avgPhi + hPhiSec, refPhi, quarterSegments, + zoffset, ctransform); + vertices.insert(vertices.end(), svertices.begin(), svertices.end()); + // If the tip exists, the faces need to be triangular if (tipExists) { - for (std::size_t iv = firstIv + 2; iv < vertices.size() + 1; ++iv) { - std::size_t one = 0, two = iv - 1, three = iv - 2; + for (std::size_t iv = firstIv + 1; iv < svertices.size() + firstIv; + ++iv) { + std::size_t one = 0, two = iv, three = iv - 1; if (z < 0.) { std::swap(two, three); } faces.push_back({one, two, three}); } - // Complete cone if necessary - if (fullCone) { - if (z > 0.) { - faces.push_back({0, firstIv, vertices.size() - 1}); - } else { - faces.push_back({0, vertices.size() - 1, firstIv}); - } - } } } + // if no tip exists, connect the two bows if (tipExists) { triangularMesh = faces; } else { - auto facesMesh = - detail::FacesHelper::cylindricalFaceMesh(vertices, fullCone); + auto facesMesh = detail::FacesHelper::cylindricalFaceMesh(vertices); faces = facesMesh.first; triangularMesh = facesMesh.second; } + return Polyhedron(vertices, faces, triangularMesh, false); } diff --git a/Core/src/Surfaces/CylinderBounds.cpp b/Core/src/Surfaces/CylinderBounds.cpp index 76e3e902ea7..e7d5032c993 100644 --- a/Core/src/Surfaces/CylinderBounds.cpp +++ b/Core/src/Surfaces/CylinderBounds.cpp @@ -104,45 +104,40 @@ std::ostream& Acts::CylinderBounds::toStream(std::ostream& sl) const { return sl; } -std::vector Acts::CylinderBounds::createCircles( - const Transform3 ctrans, std::size_t lseg) const { +std::vector Acts::CylinderBounds::circleVertices( + const Transform3 transform, unsigned int quarterSegments) const { std::vector vertices; double avgPhi = get(eAveragePhi); double halfPhi = get(eHalfPhiSector); - bool fullCylinder = coversFullAzimuth(); - - // Get the phi segments from the helper - ensures extra points - auto phiSegs = fullCylinder ? detail::VerticesHelper::phiSegments() - : detail::VerticesHelper::phiSegments( - avgPhi - halfPhi, avgPhi + halfPhi, - {static_cast(avgPhi)}); + std::vector phiRef = {}; + if (bool fullCylinder = coversFullAzimuth(); fullCylinder) { + phiRef = {static_cast(avgPhi)}; + } // Write the two bows/circles on either side std::vector sides = {-1, 1}; for (auto& side : sides) { - for (std::size_t iseg = 0; iseg < phiSegs.size() - 1; ++iseg) { - int addon = (iseg == phiSegs.size() - 2 && !fullCylinder) ? 1 : 0; - /// Helper method to create the segment - detail::VerticesHelper::createSegment( - vertices, {get(eR), get(eR)}, phiSegs[iseg], phiSegs[iseg + 1], lseg, - addon, Vector3(0., 0., side * get(eHalfLengthZ)), ctrans); - } + /// Helper method to create the segment + auto svertices = detail::VerticesHelper::segmentVertices( + {get(eR), get(eR)}, avgPhi - halfPhi, avgPhi + halfPhi, phiRef, + quarterSegments, Vector3(0., 0., side * get(eHalfLengthZ)), transform); + vertices.insert(vertices.end(), svertices.begin(), svertices.end()); } - double bevelMinZ = get(eBevelMinZ); - double bevelMaxZ = get(eBevelMaxZ); + ActsScalar bevelMinZ = get(eBevelMinZ); + ActsScalar bevelMaxZ = get(eBevelMaxZ); // Modify the vertices position if bevel is defined if ((bevelMinZ != 0. || bevelMaxZ != 0.) && vertices.size() % 2 == 0) { auto halfWay = vertices.end() - vertices.size() / 2; - double mult{1}; - auto invCtrans = ctrans.inverse(); - auto func = [&mult, &ctrans, &invCtrans](Vector3& v) { - v = invCtrans * v; + ActsScalar mult{1}; + auto invTransform = transform.inverse(); + auto func = [&mult, &transform, &invTransform](Vector3& v) { + v = invTransform * v; v(2) += v(1) * mult; - v = ctrans * v; + v = transform * v; }; if (bevelMinZ != 0.) { mult = std::tan(-bevelMinZ); diff --git a/Core/src/Surfaces/CylinderSurface.cpp b/Core/src/Surfaces/CylinderSurface.cpp index 29ca1edf150..a39dce4a4fa 100644 --- a/Core/src/Surfaces/CylinderSurface.cpp +++ b/Core/src/Surfaces/CylinderSurface.cpp @@ -188,19 +188,15 @@ const Acts::CylinderBounds& Acts::CylinderSurface::bounds() const { } Acts::Polyhedron Acts::CylinderSurface::polyhedronRepresentation( - const GeometryContext& gctx, std::size_t lseg) const { + const GeometryContext& gctx, unsigned int quarterSegments) const { auto ctrans = transform(gctx); // Prepare vertices and faces - std::vector vertices = bounds().createCircles(ctrans, lseg); - std::vector faces; - std::vector triangularMesh; - - bool fullCylinder = bounds().coversFullAzimuth(); - - auto facesMesh = - detail::FacesHelper::cylindricalFaceMesh(vertices, fullCylinder); - return Polyhedron(vertices, facesMesh.first, facesMesh.second, false); + std::vector vertices = + bounds().circleVertices(ctrans, quarterSegments); + auto [faces, triangularMesh] = + detail::FacesHelper::cylindricalFaceMesh(vertices); + return Polyhedron(vertices, faces, triangularMesh, false); } Acts::Vector3 Acts::CylinderSurface::rotSymmetryAxis( diff --git a/Core/src/Surfaces/DiamondBounds.cpp b/Core/src/Surfaces/DiamondBounds.cpp index b0239eca3f3..7febf8e7f2d 100644 --- a/Core/src/Surfaces/DiamondBounds.cpp +++ b/Core/src/Surfaces/DiamondBounds.cpp @@ -27,7 +27,7 @@ bool Acts::DiamondBounds::inside( } std::vector Acts::DiamondBounds::vertices( - unsigned int /*lseg*/) const { + unsigned int /*ignoredSegments*/) const { // Vertices starting at lower left (min rel. phi) // counter-clockwise double x1 = get(DiamondBounds::eHalfLengthXnegY); diff --git a/Core/src/Surfaces/DiscSurface.cpp b/Core/src/Surfaces/DiscSurface.cpp index c46390ff1c9..aa2f3f7036f 100644 --- a/Core/src/Surfaces/DiscSurface.cpp +++ b/Core/src/Surfaces/DiscSurface.cpp @@ -154,12 +154,9 @@ const Acts::SurfaceBounds& Acts::DiscSurface::bounds() const { } Acts::Polyhedron Acts::DiscSurface::polyhedronRepresentation( - const GeometryContext& gctx, std::size_t lseg) const { + const GeometryContext& gctx, unsigned int quarterSegments) const { // Prepare vertices and faces std::vector vertices; - std::vector faces; - std::vector triangularMesh; - // Understand the disc bool fullDisc = m_bounds->coversFullAzimuth(); bool toCenter = m_bounds->rMin() < s_onSurfaceTolerance; @@ -167,7 +164,7 @@ Acts::Polyhedron Acts::DiscSurface::polyhedronRepresentation( bool exactPolyhedron = (m_bounds->type() == SurfaceBounds::eDiscTrapezoid); bool addCentreFromConvexFace = (m_bounds->type() != SurfaceBounds::eAnnulus); if (m_bounds) { - auto vertices2D = m_bounds->vertices(lseg); + auto vertices2D = m_bounds->vertices(quarterSegments); vertices.reserve(vertices2D.size() + 1); Vector3 wCenter(0., 0., 0); for (const auto& v2D : vertices2D) { @@ -182,22 +179,19 @@ Acts::Polyhedron Acts::DiscSurface::polyhedronRepresentation( if (addCentreFromConvexFace) { vertices.push_back(wCenter); } - auto facesMesh = detail::FacesHelper::convexFaceMesh(vertices, true); - faces = facesMesh.first; - triangularMesh = facesMesh.second; + auto [faces, triangularMesh] = + detail::FacesHelper::convexFaceMesh(vertices, true); + return Polyhedron(vertices, faces, triangularMesh, exactPolyhedron); } else { // Two concentric rings, we use the pure concentric method momentarily, // but that creates too many unneccesarry faces, when only two // are needed to describe the mesh, @todo investigate merging flag - auto facesMesh = detail::FacesHelper::cylindricalFaceMesh(vertices, true); - faces = facesMesh.first; - triangularMesh = facesMesh.second; + auto [faces, triangularMesh] = + detail::FacesHelper::cylindricalFaceMesh(vertices); + return Polyhedron(vertices, faces, triangularMesh, exactPolyhedron); } - } else { - throw std::domain_error( - "Polyhedron repr of boundless surface not possible."); } - return Polyhedron(vertices, faces, triangularMesh, exactPolyhedron); + throw std::domain_error("Polyhedron repr of boundless surface not possible."); } Acts::Vector2 Acts::DiscSurface::localPolarToCartesian( diff --git a/Core/src/Surfaces/DiscTrapezoidBounds.cpp b/Core/src/Surfaces/DiscTrapezoidBounds.cpp index 6cebe5433d5..ed02bac17de 100644 --- a/Core/src/Surfaces/DiscTrapezoidBounds.cpp +++ b/Core/src/Surfaces/DiscTrapezoidBounds.cpp @@ -64,7 +64,7 @@ bool Acts::DiscTrapezoidBounds::inside( } std::vector Acts::DiscTrapezoidBounds::vertices( - unsigned int /*lseg*/) const { + unsigned int /*ignoredSegments*/) const { Vector2 cAxis(std::cos(get(eAveragePhi)), std::sin(get(eAveragePhi))); Vector2 nAxis(cAxis.y(), -cAxis.x()); auto ymin = std::sqrt(get(eMinR) * get(eMinR) - diff --git a/Core/src/Surfaces/EllipseBounds.cpp b/Core/src/Surfaces/EllipseBounds.cpp index d6357eabcc3..485f36736d8 100644 --- a/Core/src/Surfaces/EllipseBounds.cpp +++ b/Core/src/Surfaces/EllipseBounds.cpp @@ -61,10 +61,10 @@ bool Acts::EllipseBounds::inside( } std::vector Acts::EllipseBounds::vertices( - unsigned int lseg) const { + unsigned int quarterSegments) const { return detail::VerticesHelper::ellipsoidVertices( get(eInnerRx), get(eInnerRy), get(eOuterRx), get(eOuterRy), - get(eAveragePhi), get(eHalfPhiSector), lseg); + get(eAveragePhi), get(eHalfPhiSector), quarterSegments); } const Acts::RectangleBounds& Acts::EllipseBounds::boundingBox() const { diff --git a/Core/src/Surfaces/PerigeeSurface.cpp b/Core/src/Surfaces/PerigeeSurface.cpp index 328db94478e..777d061b2b1 100644 --- a/Core/src/Surfaces/PerigeeSurface.cpp +++ b/Core/src/Surfaces/PerigeeSurface.cpp @@ -58,7 +58,7 @@ std::ostream& Acts::PerigeeSurface::toStreamImpl(const GeometryContext& gctx, } Acts::Polyhedron Acts::PerigeeSurface::polyhedronRepresentation( - const GeometryContext& gctx, std::size_t /*lseg*/) const { + const GeometryContext& gctx, unsigned int /*quarterSegments*/) const { // Prepare vertices and faces std::vector vertices; std::vector faces; diff --git a/Core/src/Surfaces/PlaneSurface.cpp b/Core/src/Surfaces/PlaneSurface.cpp index 414f2611eef..b33ef82eb46 100644 --- a/Core/src/Surfaces/PlaneSurface.cpp +++ b/Core/src/Surfaces/PlaneSurface.cpp @@ -88,16 +88,14 @@ const Acts::SurfaceBounds& Acts::PlaneSurface::bounds() const { } Acts::Polyhedron Acts::PlaneSurface::polyhedronRepresentation( - const GeometryContext& gctx, std::size_t lseg) const { + const GeometryContext& gctx, unsigned int quarterSegments) const { // Prepare vertices and faces std::vector vertices; - std::vector faces; - std::vector triangularMesh; bool exactPolyhedron = true; // If you have bounds you can create a polyhedron representation if (m_bounds) { - auto vertices2D = m_bounds->vertices(lseg); + auto vertices2D = m_bounds->vertices(quarterSegments); vertices.reserve(vertices2D.size() + 1); for (const auto& v2D : vertices2D) { vertices.push_back(transform(gctx) * Vector3(v2D.x(), v2D.y(), 0.)); @@ -116,22 +114,20 @@ Acts::Polyhedron Acts::PlaneSurface::polyhedronRepresentation( // @todo same as for Discs: coversFull is not the right criterium // for triangulation if (!isEllipse || !innerExists || !coversFull) { - auto facesMesh = detail::FacesHelper::convexFaceMesh(vertices); - faces = facesMesh.first; - triangularMesh = facesMesh.second; + auto [faces, triangularMesh] = + detail::FacesHelper::convexFaceMesh(vertices); + return Polyhedron(vertices, faces, triangularMesh, exactPolyhedron); } else { // Two concentric rings, we use the pure concentric method momentarily, // but that creates too many unneccesarry faces, when only two // are needed to describe the mesh, @todo investigate merging flag - auto facesMesh = detail::FacesHelper::cylindricalFaceMesh(vertices, true); - faces = facesMesh.first; - triangularMesh = facesMesh.second; + auto [faces, triangularMesh] = + detail::FacesHelper::cylindricalFaceMesh(vertices); + return Polyhedron(vertices, faces, triangularMesh, exactPolyhedron); } - } else { - throw std::domain_error( - "Polyhedron repr of boundless surface not possible."); } - return Polyhedron(vertices, faces, triangularMesh, exactPolyhedron); + throw std::domain_error( + "Polyhedron representation of boundless surface not possible."); } Acts::Vector3 Acts::PlaneSurface::normal(const GeometryContext& gctx, diff --git a/Core/src/Surfaces/StrawSurface.cpp b/Core/src/Surfaces/StrawSurface.cpp index 1ac8f8eb264..2ce1ef74b42 100644 --- a/Core/src/Surfaces/StrawSurface.cpp +++ b/Core/src/Surfaces/StrawSurface.cpp @@ -48,7 +48,7 @@ Acts::StrawSurface& Acts::StrawSurface::operator=(const StrawSurface& other) { } Acts::Polyhedron Acts::StrawSurface::polyhedronRepresentation( - const GeometryContext& gctx, std::size_t lseg) const { + const GeometryContext& gctx, unsigned int quarterSegments) const { // Prepare vertices and faces std::vector vertices; std::vector faces; @@ -56,20 +56,17 @@ Acts::Polyhedron Acts::StrawSurface::polyhedronRepresentation( const Transform3& ctransform = transform(gctx); // Draw the bounds if more than one segment are chosen - if (lseg > 1) { + if (quarterSegments > 0u) { double r = m_bounds->get(LineBounds::eR); - auto phiSegs = detail::VerticesHelper::phiSegments(); // Write the two bows/circles on either side std::vector sides = {-1, 1}; for (auto& side : sides) { - for (std::size_t iseg = 0; iseg < phiSegs.size() - 1; ++iseg) { - int addon = (iseg == phiSegs.size() - 2) ? 1 : 0; - /// Helper method to create the segment - detail::VerticesHelper::createSegment( - vertices, {r, r}, phiSegs[iseg], phiSegs[iseg + 1], lseg, addon, - Vector3(0., 0., side * m_bounds->get(LineBounds::eHalfLengthZ)), - ctransform); - } + /// Helper method to create the segment + auto svertices = detail::VerticesHelper::segmentVertices( + {r, r}, -M_PI, M_PI, {}, quarterSegments, + Vector3(0., 0., side * m_bounds->get(LineBounds::eHalfLengthZ)), + ctransform); + vertices.insert(vertices.end(), svertices.begin(), svertices.end()); } auto facesMesh = detail::FacesHelper::cylindricalFaceMesh(vertices); faces = facesMesh.first; diff --git a/Core/src/Surfaces/Surface.cpp b/Core/src/Surfaces/Surface.cpp index 3ebf11d8e92..cadef176cda 100644 --- a/Core/src/Surfaces/Surface.cpp +++ b/Core/src/Surfaces/Surface.cpp @@ -359,6 +359,7 @@ void Acts::Surface::associateLayer(const Acts::Layer& lay) { void Acts::Surface::visualize(IVisualization3D& helper, const GeometryContext& gctx, const ViewConfig& viewConfig) const { - Polyhedron polyhedron = polyhedronRepresentation(gctx, viewConfig.nSegments); + Polyhedron polyhedron = + polyhedronRepresentation(gctx, viewConfig.quarterSegments); polyhedron.visualize(helper, viewConfig); } diff --git a/Core/src/Surfaces/TrapezoidBounds.cpp b/Core/src/Surfaces/TrapezoidBounds.cpp index 06da2fa0fef..e74285e445e 100644 --- a/Core/src/Surfaces/TrapezoidBounds.cpp +++ b/Core/src/Surfaces/TrapezoidBounds.cpp @@ -96,7 +96,7 @@ bool Acts::TrapezoidBounds::inside( } std::vector Acts::TrapezoidBounds::vertices( - unsigned int /*lseg*/) const { + unsigned int /*ignoredSegments*/) const { const double hlXnY = get(TrapezoidBounds::eHalfLengthXnegY); const double hlXpY = get(TrapezoidBounds::eHalfLengthXposY); const double hlY = get(TrapezoidBounds::eHalfLengthY); diff --git a/Core/src/Surfaces/VerticesHelper.cpp b/Core/src/Surfaces/detail/VerticesHelper.cpp similarity index 55% rename from Core/src/Surfaces/VerticesHelper.cpp rename to Core/src/Surfaces/detail/VerticesHelper.cpp index 448ee97e8da..b8bed1fb50f 100644 --- a/Core/src/Surfaces/VerticesHelper.cpp +++ b/Core/src/Surfaces/detail/VerticesHelper.cpp @@ -14,42 +14,61 @@ std::vector Acts::detail::VerticesHelper::phiSegments( ActsScalar phiMin, ActsScalar phiMax, - const std::vector& phiRefs, ActsScalar phiTolerance) { - // This is to ensure that the extrema are built regardless of number - // of segments - std::vector phiSegments; - std::vector quarters = {-M_PI, -0.5 * M_PI, 0., 0.5 * M_PI, M_PI}; - // It does not cover the full azimuth - if (phiMin != -M_PI || phiMax != M_PI) { - phiSegments.push_back(phiMin); - for (unsigned int iq = 1; iq < 4; ++iq) { - if (phiMin < quarters[iq] && phiMax > quarters[iq]) { - phiSegments.push_back(quarters[iq]); - } + const std::vector& phiRefs, unsigned int quarterSegments) { + // Check that the phi range is valid + if (phiMin > phiMax) { + throw std::invalid_argument( + "VerticesHelper::phiSegments ... Minimum phi must be smaller than " + "maximum phi"); + } + + // First check that no reference phi is outside the range + for (ActsScalar phiRef : phiRefs) { + if (phiRef < phiMin || phiRef > phiMax) { + throw std::invalid_argument( + "VerticesHelper::phiSegments ... Reference phi is outside the range " + "of the segment"); } - phiSegments.push_back(phiMax); - } else { - phiSegments = quarters; } - // Insert the reference phis if - if (!phiRefs.empty()) { - for (const auto& phiRef : phiRefs) { - // Trying to find the right patch - if (std::ranges::none_of(phiSegments, [&](ActsScalar phiSeg) { - return std::abs(phiSeg - phiRef) < phiTolerance; + if (quarterSegments == 0u) { + throw std::invalid_argument( + "VerticesHelper::phiSegments ... Number of segments must be larger " + "than 0."); + } + std::vector phiSegments = {phiMin, phiMax}; + // Minimum approximation for a circle need + // - if the circle is closed the last point is given twice + for (unsigned int i = 0; i < 4 * quarterSegments + 1; ++i) { + ActsScalar phiExt = -M_PI + i * 2 * M_PI / (4 * quarterSegments); + if (phiExt > phiMin && phiExt < phiMax && + std::ranges::none_of(phiSegments, [&phiExt](ActsScalar phi) { + return std::abs(phi - phiExt) < + std::numeric_limits::epsilon(); + })) { + phiSegments.push_back(phiExt); + } + } + // Add the reference phis + for (const auto& phiRef : phiRefs) { + if (phiRef > phiMin && phiRef < phiMax) { + if (std::ranges::none_of(phiSegments, [&phiRef](ActsScalar phi) { + return std::abs(phi - phiRef) < + std::numeric_limits::epsilon(); })) { phiSegments.push_back(phiRef); } } - std::ranges::sort(phiSegments); } + + // Sort the phis + std::ranges::sort(phiSegments); return phiSegments; } std::vector Acts::detail::VerticesHelper::ellipsoidVertices( ActsScalar innerRx, ActsScalar innerRy, ActsScalar outerRx, ActsScalar outerRy, ActsScalar avgPhi, ActsScalar halfPhi, - unsigned int lseg) { + unsigned int quarterSegments) { // List of vertices counter-clockwise starting at smallest phi w.r.t center, // for both inner/outer ring/segment std::vector rvertices; // return vertices @@ -59,22 +78,20 @@ std::vector Acts::detail::VerticesHelper::ellipsoidVertices( bool innerExists = (innerRx > 0. && innerRy > 0.); bool closed = std::abs(halfPhi - M_PI) < s_onSurfaceTolerance; - // Get the phi segments from the helper method - auto phiSegs = detail::VerticesHelper::phiSegments( - avgPhi - halfPhi, avgPhi + halfPhi, {avgPhi}); + std::vector refPhi = {}; + if (avgPhi != 0.) { + refPhi.push_back(avgPhi); + } // The inner (if exists) and outer bow - for (unsigned int iseg = 0; iseg < phiSegs.size() - 1; ++iseg) { - int addon = (iseg == phiSegs.size() - 2 && !closed) ? 1 : 0; - if (innerExists) { - createSegment(ivertices, {innerRx, innerRy}, - phiSegs[iseg], phiSegs[iseg + 1], lseg, - addon); - } - createSegment(overtices, {outerRx, outerRy}, - phiSegs[iseg], phiSegs[iseg + 1], lseg, - addon); + if (innerExists) { + ivertices = segmentVertices( + {innerRx, innerRy}, avgPhi - halfPhi, avgPhi + halfPhi, refPhi, + quarterSegments); } + overtices = segmentVertices( + {outerRx, outerRy}, avgPhi - halfPhi, avgPhi + halfPhi, refPhi, + quarterSegments); // We want to keep the same counter-clockwise orientation for displaying if (!innerExists) { @@ -95,9 +112,9 @@ std::vector Acts::detail::VerticesHelper::ellipsoidVertices( std::vector Acts::detail::VerticesHelper::circularVertices( ActsScalar innerR, ActsScalar outerR, ActsScalar avgPhi, ActsScalar halfPhi, - unsigned int lseg) { + unsigned int quarterSegments) { return ellipsoidVertices(innerR, innerR, outerR, outerR, avgPhi, halfPhi, - lseg); + quarterSegments); } bool Acts::detail::VerticesHelper::onHyperPlane( diff --git a/Core/src/Visualization/EventDataView3D.cpp b/Core/src/Visualization/EventDataView3D.cpp index 63644d0204f..ac3a62a1e79 100644 --- a/Core/src/Visualization/EventDataView3D.cpp +++ b/Core/src/Visualization/EventDataView3D.cpp @@ -27,12 +27,13 @@ void Acts::EventDataView3D::drawCovarianceCartesian( std::vector ellipse = createEllipse( lambda0 * locErrorScale, lambda1 * locErrorScale, theta, - viewConfig.nSegments, viewConfig.offset, lposition, transform); + viewConfig.quarterSegments, viewConfig.offset, lposition, transform); ellipse.push_back(transform * Vector3(lposition.x(), lposition.y(), viewConfig.offset)); - auto faces = detail::FacesHelper::convexFaceMesh(ellipse, true); - Polyhedron ellipseHedron(ellipse, faces.first, faces.second); + auto [faces, triangularMesh] = + detail::FacesHelper::convexFaceMesh(ellipse, true); + Polyhedron ellipseHedron(ellipse, faces, triangularMesh); Acts::GeometryView3D::drawPolyhedron(helper, ellipseHedron, viewConfig); } @@ -56,12 +57,13 @@ void Acts::EventDataView3D::drawCovarianceAngular( std::vector ellipse = createEllipse(angularErrorScale * directionScale * lambda0 * sin(dtheta), angularErrorScale * directionScale * lambda1, theta, - viewConfig.nSegments, 0., {0., 0.}, eplane); + viewConfig.quarterSegments, 0., {0., 0.}, eplane); std::vector coneTop = ellipse; coneTop.push_back(anker); - auto coneTopFaces = detail::FacesHelper::convexFaceMesh(coneTop, true); - Polyhedron coneTopHedron(coneTop, coneTopFaces.first, coneTopFaces.second); + auto [faces, triangularMesh] = + detail::FacesHelper::convexFaceMesh(coneTop, true); + Polyhedron coneTopHedron(coneTop, faces, triangularMesh); GeometryView3D::drawPolyhedron(helper, coneTopHedron, viewConfig); std::vector cone = ellipse; @@ -69,7 +71,8 @@ void Acts::EventDataView3D::drawCovarianceAngular( // Force triangular ViewConfig coneViewConfig = viewConfig; coneViewConfig.triangulate = true; - auto coneFaces = detail::FacesHelper::convexFaceMesh(cone, true); - Polyhedron coneHedron(cone, coneFaces.first, coneFaces.second); + auto [facesCone, triangularMeshCone] = + detail::FacesHelper::convexFaceMesh(cone, true); + Polyhedron coneHedron(cone, facesCone, triangularMeshCone); GeometryView3D::drawPolyhedron(helper, coneHedron, coneViewConfig); } diff --git a/Core/src/Visualization/GeometryView3D.cpp b/Core/src/Visualization/GeometryView3D.cpp index e0a7c52a5a7..17b7b5c7df9 100644 --- a/Core/src/Visualization/GeometryView3D.cpp +++ b/Core/src/Visualization/GeometryView3D.cpp @@ -95,7 +95,6 @@ void Acts::GeometryView3D::drawSurfaceArray( auto phiValues = axes[0]->getBinEdges(); auto zValues = axes[1]->getBinEdges(); ViewConfig gridRadConfig = gridConfig; - gridRadConfig.nSegments = phiValues.size(); // Longitudinal lines for (auto phi : phiValues) { double cphi = std::cos(phi); @@ -120,7 +119,7 @@ void Acts::GeometryView3D::drawSurfaceArray( auto rValues = axes[0]->getBinEdges(); auto phiValues = axes[1]->getBinEdges(); ViewConfig gridRadConfig = gridConfig; - gridRadConfig.nSegments = phiValues.size(); + gridRadConfig.quarterSegments = phiValues.size(); for (auto r : rValues) { CylinderVolumeBounds cvb(r - 0.5 * thickness, r + 0.5 * thickness, 0.5 * thickness); @@ -244,7 +243,7 @@ void Acts::GeometryView3D::drawTrackingVolume( ViewConfig lConfig = layerView; ViewConfig sConfig = sensitiveView; ViewConfig gConfig = gridView; - gConfig.nSegments = 8; + gConfig.quarterSegments = 8; ViewConfig vcConfig = cConfig; std::string vname = tVolume.volumeName(); diff --git a/Examples/Algorithms/Geant4/src/SensitiveSurfaceMapper.cpp b/Examples/Algorithms/Geant4/src/SensitiveSurfaceMapper.cpp index ced44d89e94..2fd1a924d94 100644 --- a/Examples/Algorithms/Geant4/src/SensitiveSurfaceMapper.cpp +++ b/Examples/Algorithms/Geant4/src/SensitiveSurfaceMapper.cpp @@ -331,7 +331,7 @@ bool ActsExamples::SensitiveSurfaceMapper::checkMapping( if (writeMissingSurfacesAsObj) { Acts::ObjVisualization3D visualizer; Acts::ViewConfig vcfg; - vcfg.nSegments = 720; + vcfg.quarterSegments = 720; for (auto srf : missing) { Acts::GeometryView3D::drawSurface(visualizer, *srf, gctx, Acts::Transform3::Identity(), vcfg); diff --git a/Examples/Io/Svg/include/ActsExamples/Io/Svg/SvgDefaults.hpp b/Examples/Io/Svg/include/ActsExamples/Io/Svg/SvgDefaults.hpp index 14d0474776e..b4e6c4f8178 100644 --- a/Examples/Io/Svg/include/ActsExamples/Io/Svg/SvgDefaults.hpp +++ b/Examples/Io/Svg/include/ActsExamples/Io/Svg/SvgDefaults.hpp @@ -22,7 +22,7 @@ static inline Acts::Svg::Style layerStyle() { lStyle.highlights = {"mouseover", "mouseout"}; lStyle.strokeColor = {25, 25, 25}; lStyle.strokeWidth = 0.5; - lStyle.nSegments = 72u; + lStyle.quarterSegments = 72u; return lStyle; } @@ -43,7 +43,7 @@ static inline Acts::Svg::Style backgroundStyle() { bgStyle.highlights = {}; bgStyle.strokeColor = {25, 25, 25}; bgStyle.strokeWidth = 0.5; - bgStyle.nSegments = 72u; + bgStyle.quarterSegments = 72u; return bgStyle; } @@ -55,7 +55,7 @@ static inline Acts::Svg::Style pointStyle() { pStyle.highlights = {"mouseover", "mouseout"}; pStyle.strokeColor = {0, 0, 0}; pStyle.strokeWidth = 0.5; - pStyle.nSegments = 72u; + pStyle.quarterSegments = 72u; return pStyle; } diff --git a/Examples/Python/src/Geometry.cpp b/Examples/Python/src/Geometry.cpp index 4fb0f07d07a..51188ee93e4 100644 --- a/Examples/Python/src/Geometry.cpp +++ b/Examples/Python/src/Geometry.cpp @@ -333,7 +333,7 @@ void addExperimentalGeometry(Context& ctx) { ACTS_PYTHON_MEMBER(surfacesProvider); ACTS_PYTHON_MEMBER(supports); ACTS_PYTHON_MEMBER(binnings); - ACTS_PYTHON_MEMBER(nSegments); + ACTS_PYTHON_MEMBER(quarterSegments); ACTS_PYTHON_MEMBER(auxiliary); ACTS_PYTHON_STRUCT_END(); diff --git a/Examples/Python/src/Obj.cpp b/Examples/Python/src/Obj.cpp index ce03e60a3fe..5ca2df40033 100644 --- a/Examples/Python/src/Obj.cpp +++ b/Examples/Python/src/Obj.cpp @@ -35,7 +35,7 @@ void addObj(Context& ctx) { /// @param surfaces is the collection of surfaces /// @param viewContext is the geometry context /// @param viewRgb is the color of the surfaces - /// @param viewSegements is the number of segments to approximate a full circle + /// @param viewSegments is the number of segments to approximate a quarter of a circle /// @param fileName is the path to the output file /// mex.def("writeSurfacesObj", diff --git a/Examples/Python/src/Output.cpp b/Examples/Python/src/Output.cpp index 328ae9ecd92..b53ba905700 100644 --- a/Examples/Python/src/Output.cpp +++ b/Examples/Python/src/Output.cpp @@ -119,7 +119,7 @@ void addOutput(Context& ctx) { ACTS_PYTHON_MEMBER(offset); ACTS_PYTHON_MEMBER(lineThickness); ACTS_PYTHON_MEMBER(surfaceThickness); - ACTS_PYTHON_MEMBER(nSegments); + ACTS_PYTHON_MEMBER(quarterSegments); ACTS_PYTHON_MEMBER(triangulate); ACTS_PYTHON_MEMBER(outputName); ACTS_PYTHON_STRUCT_END(); diff --git a/Examples/Python/src/Svg.cpp b/Examples/Python/src/Svg.cpp index 2c0219d0466..f1d46cadcd8 100644 --- a/Examples/Python/src/Svg.cpp +++ b/Examples/Python/src/Svg.cpp @@ -225,7 +225,7 @@ void addSvg(Context& ctx) { ACTS_PYTHON_MEMBER(highlights); ACTS_PYTHON_MEMBER(strokeWidth); ACTS_PYTHON_MEMBER(strokeColor); - ACTS_PYTHON_MEMBER(nSegments); + ACTS_PYTHON_MEMBER(quarterSegments); ACTS_PYTHON_STRUCT_END(); } diff --git a/Plugins/ActSVG/include/Acts/Plugins/ActSVG/SvgUtils.hpp b/Plugins/ActSVG/include/Acts/Plugins/ActSVG/SvgUtils.hpp index a6faa5ed42c..0b36d7f82c9 100644 --- a/Plugins/ActSVG/include/Acts/Plugins/ActSVG/SvgUtils.hpp +++ b/Plugins/ActSVG/include/Acts/Plugins/ActSVG/SvgUtils.hpp @@ -19,6 +19,7 @@ namespace Acts::Svg { +/// @brief Style struct struct Style { // Fill parameters std::array fillColor = {255, 255, 255}; @@ -38,7 +39,8 @@ struct Style { unsigned int fontSize = 14u; - unsigned int nSegments = 72u; + /// Number of segments to approximate a quarter of a circle + unsigned int quarterSegments = 72u; /// Conversion to fill and stroke object from the base library /// @return a tuple of actsvg digestable objects diff --git a/Plugins/ActSVG/src/SurfaceSvgConverter.cpp b/Plugins/ActSVG/src/SurfaceSvgConverter.cpp index c5bf2bba429..d9fb864a1f4 100644 --- a/Plugins/ActSVG/src/SurfaceSvgConverter.cpp +++ b/Plugins/ActSVG/src/SurfaceSvgConverter.cpp @@ -21,7 +21,7 @@ Acts::Svg::ProtoSurface Acts::Svg::SurfaceConverter::convert( if (!cOptions.templateSurface) { // Polyhedron surface for vertices needed anyway Polyhedron surfaceHedron = - surface.polyhedronRepresentation(gctx, cOptions.style.nSegments); + surface.polyhedronRepresentation(gctx, cOptions.style.quarterSegments); auto vertices3D = surfaceHedron.vertices; pSurface._vertices = vertices3D; } else { @@ -30,7 +30,7 @@ Acts::Svg::ProtoSurface Acts::Svg::SurfaceConverter::convert( auto planarBounds = dynamic_cast(&(surface.bounds())); if (planarBounds != nullptr) { - auto vertices2D = planarBounds->vertices(cOptions.style.nSegments); + auto vertices2D = planarBounds->vertices(cOptions.style.quarterSegments); pSurface._vertices.reserve(vertices2D.size()); for (const auto& v2 : vertices2D) { pSurface._vertices.push_back({v2[0], v2[1], 0.}); @@ -40,7 +40,8 @@ Acts::Svg::ProtoSurface Acts::Svg::SurfaceConverter::convert( auto annulusBounds = dynamic_cast(&(surface.bounds())); if (annulusBounds != nullptr) { - auto vertices2D = annulusBounds->vertices(cOptions.style.nSegments); + auto vertices2D = + annulusBounds->vertices(cOptions.style.quarterSegments); pSurface._vertices.reserve(vertices2D.size()); for (const auto& v2 : vertices2D) { pSurface._vertices.push_back({v2[0], v2[1], 0.}); diff --git a/Plugins/DD4hep/include/Acts/Plugins/DD4hep/DD4hepDetectorSurfaceFactory.hpp b/Plugins/DD4hep/include/Acts/Plugins/DD4hep/DD4hepDetectorSurfaceFactory.hpp index 0a283433ab4..d6855443af2 100644 --- a/Plugins/DD4hep/include/Acts/Plugins/DD4hep/DD4hepDetectorSurfaceFactory.hpp +++ b/Plugins/DD4hep/include/Acts/Plugins/DD4hep/DD4hepDetectorSurfaceFactory.hpp @@ -71,8 +71,8 @@ class DD4hepDetectorSurfaceFactory { std::optional pExtent = std::nullopt; /// Optionally provide an Extent constraints to measure the layers std::vector extentConstraints = {}; - /// The approximination for extent measuring - std::size_t nExtentSegments = 1u; + /// The approximination of a circle quarter for extent measuring + std::size_t nExtentQSegments = 1u; }; /// Nested options struct to steer the conversion diff --git a/Plugins/DD4hep/include/Acts/Plugins/DD4hep/DD4hepLayerStructure.hpp b/Plugins/DD4hep/include/Acts/Plugins/DD4hep/DD4hepLayerStructure.hpp index bdf4780fb2f..a44e4341feb 100644 --- a/Plugins/DD4hep/include/Acts/Plugins/DD4hep/DD4hepLayerStructure.hpp +++ b/Plugins/DD4hep/include/Acts/Plugins/DD4hep/DD4hepLayerStructure.hpp @@ -65,8 +65,8 @@ class DD4hepLayerStructure { std::optional extent = std::nullopt; /// The extent constraints - optionally std::vector extentConstraints = {}; - /// Approximation for the polyhedron binning nSegments - unsigned int nSegments = 1u; + /// Approximation for the polyhedron binning + unsigned int quarterSegments = 1u; /// Patch the binning with the extent if possible bool patchBinningWithExtent = true; /// Conversion options diff --git a/Plugins/DD4hep/src/DD4hepDetectorSurfaceFactory.cpp b/Plugins/DD4hep/src/DD4hepDetectorSurfaceFactory.cpp index f3059fbeed5..d628ff76379 100644 --- a/Plugins/DD4hep/src/DD4hepDetectorSurfaceFactory.cpp +++ b/Plugins/DD4hep/src/DD4hepDetectorSurfaceFactory.cpp @@ -108,7 +108,7 @@ Acts::DD4hepDetectorSurfaceFactory::constructSensitiveComponents( // Measure if configured to do so if (cache.sExtent.has_value()) { auto sExtent = - sSurface->polyhedronRepresentation(gctx, cache.nExtentSegments) + sSurface->polyhedronRepresentation(gctx, cache.nExtentQSegments) .extent(); cache.sExtent.value().extend(sExtent, cache.extentConstraints); } @@ -137,7 +137,7 @@ Acts::DD4hepDetectorSurfaceFactory::constructPassiveComponents( // Measure if configured to do so if (cache.pExtent.has_value()) { auto sExtent = - pSurface->polyhedronRepresentation(gctx, cache.nExtentSegments) + pSurface->polyhedronRepresentation(gctx, cache.nExtentQSegments) .extent(); cache.pExtent.value().extend(sExtent, cache.extentConstraints); } diff --git a/Plugins/DD4hep/src/DD4hepLayerStructure.cpp b/Plugins/DD4hep/src/DD4hepLayerStructure.cpp index 64b6e8cf32e..10a4e6eb9f0 100644 --- a/Plugins/DD4hep/src/DD4hepLayerStructure.cpp +++ b/Plugins/DD4hep/src/DD4hepLayerStructure.cpp @@ -38,7 +38,7 @@ Acts::Experimental::DD4hepLayerStructure::builder( fCache.sExtent = options.extent; fCache.pExtent = options.extent; fCache.extentConstraints = options.extentConstraints; - fCache.nExtentSegments = options.nSegments; + fCache.nExtentQSegments = options.quarterSegments; m_surfaceFactory->construct(fCache, gctx, dd4hepElement, options.conversionOptions); diff --git a/Plugins/GeoModel/include/Acts/Plugins/GeoModel/GeoModelBlueprintCreater.hpp b/Plugins/GeoModel/include/Acts/Plugins/GeoModel/GeoModelBlueprintCreater.hpp index f382a49543a..df895eb0741 100644 --- a/Plugins/GeoModel/include/Acts/Plugins/GeoModel/GeoModelBlueprintCreater.hpp +++ b/Plugins/GeoModel/include/Acts/Plugins/GeoModel/GeoModelBlueprintCreater.hpp @@ -40,8 +40,8 @@ class GeoModelBlueprintCreater { std::vector> detectorSurfaces = {}; /// The binning values for the KDTree sorting std::vector kdtBinning = {}; - /// Polyhedron approximations - unsigned int nSegments = 1u; + /// Polyhedron approximation: number of segments per circlequarter + unsigned int quarterSegments = 1u; }; /// The cache struct diff --git a/Plugins/GeoModel/src/GeoModelBlueprintCreater.cpp b/Plugins/GeoModel/src/GeoModelBlueprintCreater.cpp index 9d31eeaaffc..93712d5d911 100644 --- a/Plugins/GeoModel/src/GeoModelBlueprintCreater.cpp +++ b/Plugins/GeoModel/src/GeoModelBlueprintCreater.cpp @@ -354,7 +354,7 @@ Acts::GeoModelBlueprintCreater::createInternalStructureBuilder( // Loop over surfaces and create an internal extent for (auto& sf : surfaces) { auto sfExtent = - sf->polyhedronRepresentation(gctx, m_cfg.nSegments).extent(); + sf->polyhedronRepresentation(gctx, m_cfg.quarterSegments).extent(); internalExtent.extend(sfExtent, internalConstraints); } ACTS_VERBOSE("Found " << surfaces.size() << " surfaces in range " diff --git a/Tests/CommonHelpers/Acts/Tests/CommonHelpers/LineSurfaceStub.hpp b/Tests/CommonHelpers/Acts/Tests/CommonHelpers/LineSurfaceStub.hpp index 848e02cab01..662969bc450 100644 --- a/Tests/CommonHelpers/Acts/Tests/CommonHelpers/LineSurfaceStub.hpp +++ b/Tests/CommonHelpers/Acts/Tests/CommonHelpers/LineSurfaceStub.hpp @@ -52,11 +52,11 @@ class LineSurfaceStub : public LineSurface { /// Return a Polyhedron for the surfaces /// /// @param gctx The current geometry context object, e.g. alignment - /// @param lseg is ignored for a perigee @note ignored + /// @param ingoredSegmeent is ignored for the srub /// /// @return A list of vertices and a face/facett description of it Polyhedron polyhedronRepresentation(const GeometryContext& /*gctx*/, - std::size_t /*lseg*/) const final { + unsigned int /*lseg*/) const final { return Polyhedron({}, {}, {}); } }; diff --git a/Tests/UnitTests/Core/Surfaces/AnnulusBoundsTests.cpp b/Tests/UnitTests/Core/Surfaces/AnnulusBoundsTests.cpp index c284d6d4796..9f09fb2c213 100644 --- a/Tests/UnitTests/Core/Surfaces/AnnulusBoundsTests.cpp +++ b/Tests/UnitTests/Core/Surfaces/AnnulusBoundsTests.cpp @@ -23,10 +23,10 @@ namespace Acts::Test { BOOST_AUTO_TEST_SUITE(Surfaces) -double minRadius = 7.2; -double maxRadius = 12.0; -double minPhi = 0.74195; -double maxPhi = 1.33970; +ActsScalar minRadius = 7.2; +ActsScalar maxRadius = 12.0; +ActsScalar minPhi = 0.74195; +ActsScalar maxPhi = 1.33970; Vector2 offset(-2., 2.); @@ -123,6 +123,25 @@ BOOST_AUTO_TEST_CASE(AnnulusBoundsProperties) { BOOST_CHECK_EQUAL(aBounds.get(AnnulusBounds::eMaxPhiRel), maxPhi); } +/// Unit tests for AnnulusBounds vertices +BOOST_AUTO_TEST_CASE(AnnulusBoundsVertices) { + /// Test construction with radii and default sector + AnnulusBounds aBounds(minRadius, maxRadius, minPhi, maxPhi, offset); + + // Retrieve the corners + auto corners = aBounds.corners(); + BOOST_CHECK_EQUAL(corners.size(), 4); + + // Retrieve the vertices + auto vertices = aBounds.vertices(0u); + BOOST_CHECK_EQUAL(vertices.size(), 4); + + // Now generate with more segments + unsigned int nQuarterSegments = 12; + vertices = aBounds.vertices(nQuarterSegments); + BOOST_CHECK_EQUAL(vertices.size(), 14u); +} + BOOST_AUTO_TEST_SUITE_END() } // namespace Acts::Test diff --git a/Tests/UnitTests/Core/Surfaces/ConvexPolygonBoundsTests.cpp b/Tests/UnitTests/Core/Surfaces/ConvexPolygonBoundsTests.cpp index bdb4c2948be..6b6712c230b 100644 --- a/Tests/UnitTests/Core/Surfaces/ConvexPolygonBoundsTests.cpp +++ b/Tests/UnitTests/Core/Surfaces/ConvexPolygonBoundsTests.cpp @@ -101,6 +101,10 @@ BOOST_AUTO_TEST_CASE(ConvexPolygonBoundsRecreation) { std::copy_n(valvector.begin(), poly<4>::eSize, values.begin()); poly<4> recreated(values); BOOST_CHECK_EQUAL(original, recreated); + + // Get the vertices back + auto rvertices = original.vertices(); + BOOST_CHECK_EQUAL(rvertices.size(), 4u); } BOOST_AUTO_TEST_CASE(ConvexPolygonBoundsDynamicTest) { diff --git a/Tests/UnitTests/Core/Surfaces/PolyhedronSurfacesTests.cpp b/Tests/UnitTests/Core/Surfaces/PolyhedronSurfacesTests.cpp index c858f10f6db..184d69e341f 100644 --- a/Tests/UnitTests/Core/Surfaces/PolyhedronSurfacesTests.cpp +++ b/Tests/UnitTests/Core/Surfaces/PolyhedronSurfacesTests.cpp @@ -42,7 +42,7 @@ namespace Acts::Test { const GeometryContext tgContext = GeometryContext(); const std::vector> testModes = { - {"Triangulate", 72}, {"Extrema", 1}}; + {"Triangulate", 18}, {"Extrema", 1}}; const Transform3 transform = Transform3::Identity(); const double epsAbs = 1e-12; @@ -61,9 +61,8 @@ BOOST_AUTO_TEST_CASE(ConeSurfacePolyhedrons) { const double rMax = hzPos * std::tan(alpha); - for (const auto& mode : testModes) { - ACTS_INFO("\tMode: " << std::get(mode)); - const unsigned int segments = std::get(mode); + for (const auto& [mode, segments] : testModes) { + ACTS_INFO("\tMode: " << mode); /// The full cone on one side { @@ -81,9 +80,10 @@ BOOST_AUTO_TEST_CASE(ConeSurfacePolyhedrons) { CHECK_CLOSE_ABS(extent.range(BinningValue::binZ).min(), 0_mm, epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binZ).max(), hzPos, epsAbs); - const unsigned int expectedFaces = segments < 4 ? 4 : segments; + const unsigned int expectedFaces = 4 * segments; BOOST_CHECK_EQUAL(oneConePh.faces.size(), expectedFaces); - BOOST_CHECK_EQUAL(oneConePh.vertices.size(), expectedFaces + 1); + // full segments + overlap at (pi/pi) + tip + BOOST_CHECK_EQUAL(oneConePh.vertices.size(), expectedFaces + 2); } /// The full cone on one side @@ -106,6 +106,11 @@ BOOST_AUTO_TEST_CASE(ConeSurfacePolyhedrons) { CHECK_CLOSE_ABS(extent.range(BinningValue::binR).max(), rMax, epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binZ).min(), hzpMin, epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binZ).max(), hzPos, epsAbs); + + const unsigned int expectedFaces = 4 * segments; + BOOST_CHECK_EQUAL(oneConePiecePh.faces.size(), expectedFaces); + BOOST_CHECK_EQUAL(oneConePiecePh.vertices.size(), + (expectedFaces + 1) * 2); } /// The full cone on both sides @@ -124,9 +129,11 @@ BOOST_AUTO_TEST_CASE(ConeSurfacePolyhedrons) { CHECK_CLOSE_ABS(extent.range(BinningValue::binZ).min(), hzNeg, epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binZ).max(), hzPos, epsAbs); - const unsigned int expectedFaces = segments < 4 ? 8 : 2 * segments; + const unsigned int expectedFaces = 2 * segments * 4; + const unsigned int expectedVertices = 2 * (4 * segments + 1) + 1; + BOOST_CHECK_EQUAL(twoConesPh.faces.size(), expectedFaces); - BOOST_CHECK_EQUAL(twoConesPh.vertices.size(), expectedFaces + 1); + BOOST_CHECK_EQUAL(twoConesPh.vertices.size(), expectedVertices); } /// A centered sectoral cone on both sides @@ -143,13 +150,16 @@ BOOST_AUTO_TEST_CASE(ConeSurfacePolyhedrons) { const auto extent = sectoralConesPh.extent(); CHECK_CLOSE_ABS(extent.range(BinningValue::binX).min(), 0, epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binX).max(), rMax, epsAbs); - // CHECK_CLOSE_ABS(extent.range(BinningValue::binY).min(), ???, - // epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binY).max(), - // ???, epsAbs); + CHECK_CLOSE_ABS(extent.range(BinningValue::binY).min(), + -rMax * std::sin(phiSector), epsAbs); + CHECK_CLOSE_ABS(extent.range(BinningValue::binY).max(), + rMax * std::sin(phiSector), epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binR).min(), 0_mm, epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binR).max(), rMax, epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binZ).min(), hzNeg, epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binZ).max(), hzPos, epsAbs); + + // Segment numbers are further checked with the VertexHelper checks } } } @@ -185,8 +195,8 @@ BOOST_AUTO_TEST_CASE(CylinderSurfacePolyhedrons) { CHECK_CLOSE_ABS(extent.range(BinningValue::binZ).min(), -hZ, epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binZ).max(), hZ, epsAbs); - const unsigned int expectedFaces = segments < 4 ? 4 : segments; - const unsigned int expectedVertices = segments < 4 ? 8 : 2 * segments; + const unsigned int expectedFaces = 4 * segments; + const unsigned int expectedVertices = (4 * segments + 1) * 2; BOOST_CHECK_EQUAL(fullCylinderPh.faces.size(), expectedFaces); BOOST_CHECK_EQUAL(fullCylinderPh.vertices.size(), expectedVertices); } @@ -248,7 +258,8 @@ BOOST_AUTO_TEST_CASE(DiscSurfacePolyhedrons) { CHECK_CLOSE_ABS(extent.range(BinningValue::binZ).max(), 0., epsAbs); const unsigned int expectedFaces = 1; - const unsigned int expectedVertices = segments > 4 ? segments + 1 : 4 + 1; + // Segments + overlap + center + const unsigned int expectedVertices = 4 * segments + 1 + 1; BOOST_CHECK_EQUAL(fullDiscPh.faces.size(), expectedFaces); BOOST_CHECK_EQUAL(fullDiscPh.vertices.size(), expectedVertices); } @@ -353,14 +364,7 @@ BOOST_AUTO_TEST_CASE(DiscSurfacePolyhedrons) { auto annulusDisc = Surface::makeShared(transform, annulus); auto annulusDiscPh = annulusDisc->polyhedronRepresentation(tgContext, segments); - const auto extent = annulusDiscPh.extent(); - // CHECK_CLOSE_ABS(extent.range(BinningValue::binX).min(), ???, - // epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binX).max(), - // ???, epsAbs); - // CHECK_CLOSE_ABS(extent.range(BinningValue::binY).min(), ???, - // epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binY).max(), - // ???, epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binR).min(), minRadius, epsAbs); CHECK_CLOSE_ABS(extent.range(BinningValue::binR).max(), maxRadius, diff --git a/Tests/UnitTests/Core/Surfaces/SurfaceStub.hpp b/Tests/UnitTests/Core/Surfaces/SurfaceStub.hpp index 573c9f55ce9..ce745bec85c 100644 --- a/Tests/UnitTests/Core/Surfaces/SurfaceStub.hpp +++ b/Tests/UnitTests/Core/Surfaces/SurfaceStub.hpp @@ -102,7 +102,7 @@ class SurfaceStub : public RegularSurface { /// Return a Polyhedron for the surfaces Polyhedron polyhedronRepresentation(const GeometryContext& /*gctx*/, - std::size_t /*lseg */) const final { + unsigned int /* ignored */) const final { std::vector vertices; std::vector> faces; std::vector> triangularMesh; diff --git a/Tests/UnitTests/Core/Surfaces/VerticesHelperTests.cpp b/Tests/UnitTests/Core/Surfaces/VerticesHelperTests.cpp index d2838760081..2b52064cb8d 100644 --- a/Tests/UnitTests/Core/Surfaces/VerticesHelperTests.cpp +++ b/Tests/UnitTests/Core/Surfaces/VerticesHelperTests.cpp @@ -92,6 +92,127 @@ BOOST_AUTO_TEST_CASE(VerticesHelperOnHyperPlane) { } } +BOOST_AUTO_TEST_CASE(GeneratePhiSegments) { + // Case (1): a small segment is given, no cartesian maximum vertex + ActsScalar minPhi = 0.1; + ActsScalar maxPhi = 0.3; + + auto phis = VerticesHelper::phiSegments(minPhi, maxPhi); + BOOST_CHECK_EQUAL(phis.size(), 2u); + BOOST_CHECK(phis[0] == minPhi); + BOOST_CHECK(phis[1] == maxPhi); + + // Case (2) a small segment is given, with one maximum vertex at phi = 0 + minPhi = -0.1; + phis = VerticesHelper::phiSegments(minPhi, maxPhi); + BOOST_CHECK_EQUAL(phis.size(), 3u); + BOOST_CHECK(phis[0] == minPhi); + BOOST_CHECK(phis[1] == 0.); + BOOST_CHECK(phis[2] == maxPhi); + + // Case (3) a small segment is given, with one maximum vertex at phi = 2pi, + // and one extra value + phis = VerticesHelper::phiSegments(minPhi, maxPhi, {0.25}); + BOOST_CHECK_EQUAL(phis.size(), 4u); + BOOST_CHECK(phis[0] == minPhi); + BOOST_CHECK(phis[1] == 0.); + BOOST_CHECK(phis[2] == 0.25); + BOOST_CHECK(phis[3] == maxPhi); + + // Case (4) a small segment is given, with one maximum vertex at phi = 2pi, + // and two extra values, one outside & hence throw an exception + BOOST_CHECK_THROW(VerticesHelper::phiSegments(minPhi, maxPhi, {0.25, 0.5}), + std::invalid_argument); + + // Case (5) an invalid phi range is given + BOOST_CHECK_THROW(VerticesHelper::phiSegments(0.8, 0.2, {0.25, 0.5}), + std::invalid_argument); + + // Case (6) a wrong number of minimum segments is given + BOOST_CHECK_THROW(VerticesHelper::phiSegments(0.1, 0.3, {0.25, 0.5}, 3), + std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(GenerateSegmentVertices) { + // Case (1): a small segment is given, no cartesian maximum vertex & 1 step + // segment + ActsScalar rx = 10.; + ActsScalar ry = 10.; + ActsScalar minPhi = 0.1; + ActsScalar maxPhi = 0.3; + + auto vertices = VerticesHelper::segmentVertices( + {rx, ry}, minPhi, maxPhi, {}, 1); + std::size_t expectedVertices = 2u; + BOOST_CHECK_EQUAL(vertices.size(), expectedVertices); + + // Now with a reference phi value + vertices = VerticesHelper::segmentVertices( + {rx, ry}, minPhi, maxPhi, {0.2}, 1); + expectedVertices = 3u; // the reference is inserted + BOOST_CHECK_EQUAL(vertices.size(), expectedVertices); + + // Now with more vertices - the the two corners and the ones from the + // reference + unsigned int quarterVertices = 36; + vertices = VerticesHelper::segmentVertices( + {rx, ry}, minPhi, maxPhi, {}, quarterVertices); + expectedVertices = + static_cast((maxPhi - minPhi) / M_PI_2 * quarterVertices) + + 2u; + BOOST_CHECK_EQUAL(vertices.size(), expectedVertices); + + // Case (2) a small segment is given, with one maximum vertex at phi = 0 + minPhi = -0.1; + vertices = VerticesHelper::segmentVertices( + {rx, ry}, minPhi, maxPhi, {}, 1); + expectedVertices = 3u; + BOOST_CHECK_EQUAL(vertices.size(), expectedVertices); + + // Same with more segments + quarterVertices = 12; + vertices = VerticesHelper::segmentVertices( + {rx, ry}, minPhi, maxPhi, {}, quarterVertices); + // Extrema will be covered by the segments + expectedVertices = + static_cast((maxPhi - minPhi) / M_PI_2 * quarterVertices) + + 2u; + BOOST_CHECK_EQUAL(vertices.size(), expectedVertices); +} + +BOOST_AUTO_TEST_CASE(GenerateCircleEllipseVertices) { + // Case (1): A full disc + ActsScalar ri = 0.; + ActsScalar ro = 10.; + + // Extreme points in phi - only outer radius + auto vertices = VerticesHelper::circularVertices(ri, ro, 0., M_PI, 1u); + unsigned int expectedVertices = 5u; + BOOST_CHECK_EQUAL(vertices.size(), expectedVertices); + + // Case (2): A ring + ri = 3.; + + // Extreme points in phi - only outer radius + vertices = VerticesHelper::circularVertices(ri, ro, 0., M_PI, 1u); + expectedVertices = 10u; + BOOST_CHECK_EQUAL(vertices.size(), expectedVertices); + + // Now with 10 bins per sector + ri = 0.; + + vertices = VerticesHelper::circularVertices(ri, ro, 0., M_PI, 10u); + expectedVertices = 41u; // 4 sectors + 1 overlap at (-pi/pi) + BOOST_CHECK_EQUAL(vertices.size(), expectedVertices); + + // Now ellipsiod + ActsScalar riy = 4.; + ActsScalar roy = 14.; + vertices = VerticesHelper::ellipsoidVertices(ri, riy, ro, roy, 0., M_PI, 10u); + expectedVertices = 41u; // 4 sectors + 1 overlap at (-pi/pi) + BOOST_CHECK_EQUAL(vertices.size(), expectedVertices); +} + BOOST_AUTO_TEST_SUITE_END() } // namespace Acts::detail::Test diff --git a/Tests/UnitTests/Core/TrackFitting/Gx2fTests.cpp b/Tests/UnitTests/Core/TrackFitting/Gx2fTests.cpp index 7ef5373f41b..bce69cba0b2 100644 --- a/Tests/UnitTests/Core/TrackFitting/Gx2fTests.cpp +++ b/Tests/UnitTests/Core/TrackFitting/Gx2fTests.cpp @@ -1054,7 +1054,7 @@ BOOST_AUTO_TEST_CASE(Material) { ViewConfig viewContainer = {.color = {220, 220, 0}}; viewContainer.triangulate = triangulate; ViewConfig viewGrid = {.color = {220, 0, 0}}; - viewGrid.nSegments = 8; + viewGrid.quarterSegments = 8; viewGrid.offset = 3.; viewGrid.triangulate = triangulate; diff --git a/Tests/UnitTests/Core/Visualization/EventDataView3DTests.cpp b/Tests/UnitTests/Core/Visualization/EventDataView3DTests.cpp index 36f823a02b6..bac538e4b58 100644 --- a/Tests/UnitTests/Core/Visualization/EventDataView3DTests.cpp +++ b/Tests/UnitTests/Core/Visualization/EventDataView3DTests.cpp @@ -29,7 +29,7 @@ BOOST_AUTO_TEST_CASE(BoundTrackParametersVisualizationObj) { for (const auto& objerr : objErrors) { std::cout << objerr << std::endl; } - BOOST_CHECK_EQUAL(std::count(objTest.begin(), objTest.end(), '\n'), 1458); + BOOST_CHECK_EQUAL(std::count(objTest.begin(), objTest.end(), '\n'), 4924); } BOOST_AUTO_TEST_CASE(BoundTrackParametersVisualizationPly) { @@ -40,7 +40,7 @@ BOOST_AUTO_TEST_CASE(BoundTrackParametersVisualizationPly) { for (const auto& plyerr : plyErrors) { std::cout << plyerr << std::endl; } - BOOST_CHECK_EQUAL(std::count(plyTest.begin(), plyTest.end(), '\n'), 973); + BOOST_CHECK_EQUAL(std::count(plyTest.begin(), plyTest.end(), '\n'), 3143); } BOOST_AUTO_TEST_CASE(MeasurementVisualizationObj) { @@ -85,7 +85,7 @@ BOOST_AUTO_TEST_CASE(MultiTrajectoryVisualizationObj) { for (const auto& objerr : objErrors) { std::cout << objerr << std::endl; } - BOOST_CHECK_EQUAL(std::count(objTest.begin(), objTest.end(), '\n'), 31010); + BOOST_CHECK_EQUAL(std::count(objTest.begin(), objTest.end(), '\n'), 103796); } BOOST_AUTO_TEST_CASE(MultiTrajectoryVisualizationPly) { @@ -96,7 +96,7 @@ BOOST_AUTO_TEST_CASE(MultiTrajectoryVisualizationPly) { for (const auto& plyerr : plyErrors) { std::cout << plyerr << std::endl; } - BOOST_CHECK_EQUAL(std::count(plyTest.begin(), plyTest.end(), '\n'), 20521); + BOOST_CHECK_EQUAL(std::count(plyTest.begin(), plyTest.end(), '\n'), 66091); } BOOST_AUTO_TEST_SUITE_END() diff --git a/Tests/UnitTests/Core/Visualization/SurfaceView3DBase.hpp b/Tests/UnitTests/Core/Visualization/SurfaceView3DBase.hpp index be83a27cb7b..146935279f0 100644 --- a/Tests/UnitTests/Core/Visualization/SurfaceView3DBase.hpp +++ b/Tests/UnitTests/Core/Visualization/SurfaceView3DBase.hpp @@ -68,7 +68,7 @@ static inline std::string run(IVisualization3D& helper, bool triangulate, coneSurfaces.push_back(cone); GeometryView3D::drawSurface(helper, *cone, gctx, Transform3::Identity(), sConfig); - ; + helper.write(std::string("Surfaces_ConeSurface") + tag); helper.write(cStream); helper.clear(); @@ -80,7 +80,7 @@ static inline std::string run(IVisualization3D& helper, bool triangulate, coneSurfaces.push_back(cone); GeometryView3D::drawSurface(helper, *cone, gctx, Transform3::Identity(), sConfig); - ; + helper.write(std::string("Surfaces_ConeSurfaceSector") + tag); helper.write(cStream); helper.clear(); @@ -92,7 +92,7 @@ static inline std::string run(IVisualization3D& helper, bool triangulate, coneSurfaces.push_back(cone); GeometryView3D::drawSurface(helper, *cone, gctx, Transform3::Identity(), sConfig); - ; + helper.write(std::string("Surfaces_ConeSurfaceSectorShifted") + tag); helper.write(cStream); helper.clear(); @@ -126,7 +126,7 @@ static inline std::string run(IVisualization3D& helper, bool triangulate, cylinderSurfaces.push_back(cylinder); GeometryView3D::drawSurface(helper, *cylinder, gctx, Transform3::Identity(), sConfig); - ; + helper.write(std::string("Surfaces_CylinderSurface") + tag); helper.write(cStream); helper.clear(); @@ -138,7 +138,7 @@ static inline std::string run(IVisualization3D& helper, bool triangulate, cylinderSurfaces.push_back(cylinder); GeometryView3D::drawSurface(helper, *cylinder, gctx, Transform3::Identity(), sConfig); - ; + helper.write(std::string("Surfaces_CylinderSurfaceSector") + tag); helper.write(cStream); helper.clear(); @@ -150,7 +150,7 @@ static inline std::string run(IVisualization3D& helper, bool triangulate, cylinderSurfaces.push_back(cylinder); GeometryView3D::drawSurface(helper, *cylinder, gctx, Transform3::Identity(), sConfig); - ; + helper.write(std::string("Surfaces_CylinderSurfaceSectorShifted") + tag); helper.write(cStream); helper.clear(); @@ -179,7 +179,7 @@ static inline std::string run(IVisualization3D& helper, bool triangulate, auto bbSurface = Surface::makeShared(identity, bbBounds); GeometryView3D::drawSurface(helper, *bbSurface, gctx, Transform3::Identity(), sConfig); - ; + helper.write(bbPath); helper.write(cStream); helper.clear(); @@ -199,7 +199,7 @@ static inline std::string run(IVisualization3D& helper, bool triangulate, radialSurfaces.push_back(disc); GeometryView3D::drawSurface(helper, *disc, gctx, Transform3::Identity(), sConfig); - ; + helper.write(std::string("Surfaces_DiscSurfaceFull") + tag); helper.write(cStream); helper.clear(); @@ -335,7 +335,7 @@ static inline std::string run(IVisualization3D& helper, bool triangulate, planarSurfaces.push_back(plane); GeometryView3D::drawSurface(helper, *plane, gctx, Transform3::Identity(), sConfig); - ; + helper.write(name + tag); helper.write(cStream); helper.clear(); @@ -349,7 +349,7 @@ static inline std::string run(IVisualization3D& helper, bool triangulate, planarSurfaces.push_back(plane); GeometryView3D::drawSurface(helper, *plane, gctx, Transform3::Identity(), sConfig); - ; + helper.write(name + tag); helper.write(cStream); helper.clear(); @@ -363,7 +363,7 @@ static inline std::string run(IVisualization3D& helper, bool triangulate, planarSurfaces.push_back(plane); GeometryView3D::drawSurface(helper, *plane, gctx, Transform3::Identity(), sConfig); - ; + helper.write(name + tag); helper.write(cStream); helper.clear(); @@ -377,7 +377,7 @@ static inline std::string run(IVisualization3D& helper, bool triangulate, planarSurfaces.push_back(plane); GeometryView3D::drawSurface(helper, *plane, gctx, Transform3::Identity(), sConfig); - ; + helper.write(name + tag); helper.write(cStream); helper.clear(); diff --git a/Tests/UnitTests/Core/Visualization/TrackingGeometryView3DBase.hpp b/Tests/UnitTests/Core/Visualization/TrackingGeometryView3DBase.hpp index b2fb89c0ee8..ea2e409222d 100644 --- a/Tests/UnitTests/Core/Visualization/TrackingGeometryView3DBase.hpp +++ b/Tests/UnitTests/Core/Visualization/TrackingGeometryView3DBase.hpp @@ -37,18 +37,22 @@ static inline std::string run(IVisualization3D& helper, bool triangulate, const std::string& tag) { std::stringstream cStream; - ViewConfig viewSensitive = {.color = {0, 180, 240}}; - viewSensitive.triangulate = triangulate; - ViewConfig viewPassive = {.color = {240, 280, 0}}; - viewPassive.triangulate = triangulate; - ViewConfig viewVolume = {.color = {220, 220, 0}}; - viewVolume.triangulate = triangulate; - ViewConfig viewContainer = {.color = {220, 220, 0}}; - viewContainer.triangulate = triangulate; - ViewConfig viewGrid = {.color = {220, 0, 0}}; - viewGrid.nSegments = 8; - viewGrid.offset = 3.; - viewGrid.triangulate = triangulate; + ViewConfig viewSensitive = {.color = {0, 180, 240}, + .quarterSegments = 72, + .triangulate = triangulate}; + ViewConfig viewPassive = {.color = {240, 280, 0}, + .quarterSegments = 72, + .triangulate = triangulate}; + ViewConfig viewVolume = {.color = {220, 220, 0}, + .quarterSegments = 72, + .triangulate = triangulate}; + ViewConfig viewContainer = {.color = {220, 220, 0}, + .quarterSegments = 72, + .triangulate = triangulate}; + ViewConfig viewGrid = {.color = {220, 0, 0}, + .offset = 3., + .quarterSegments = 8, + .triangulate = triangulate}; const Acts::TrackingVolume& tgVolume = *(tGeometry->highestTrackingVolume()); diff --git a/Tests/UnitTests/Fatras/Digitization/SegmentizerTests.cpp b/Tests/UnitTests/Fatras/Digitization/SegmentizerTests.cpp index 844122027d2..ad74a9024d1 100644 --- a/Tests/UnitTests/Fatras/Digitization/SegmentizerTests.cpp +++ b/Tests/UnitTests/Fatras/Digitization/SegmentizerTests.cpp @@ -217,9 +217,9 @@ BOOST_DATA_TEST_CASE( ".csv"); /// Run the Segmentizer - auto cSegement = cl.segments(geoCtx, *surface, segmentation, {start, end}); + auto cSegments = cl.segments(geoCtx, *surface, segmentation, {start, end}); - for (const auto& cs : cSegement) { + for (const auto& cs : cSegments) { csvHelper.writeLine(segments, cs.path2D[0], cs.path2D[1]); } diff --git a/Tests/UnitTests/Plugins/ActSVG/IndexedSurfacesSvgConverterTests.cpp b/Tests/UnitTests/Plugins/ActSVG/IndexedSurfacesSvgConverterTests.cpp index bc33298431a..6f2c78620e9 100644 --- a/Tests/UnitTests/Plugins/ActSVG/IndexedSurfacesSvgConverterTests.cpp +++ b/Tests/UnitTests/Plugins/ActSVG/IndexedSurfacesSvgConverterTests.cpp @@ -42,7 +42,7 @@ IndexedSurfacesConverter::Options generateDrawOptions() { sensitiveStyle.highlights = {"onmouseover", "onmouseout"}; sensitiveStyle.strokeWidth = 0.5; sensitiveStyle.strokeColor = {0, 0, 0}; - sensitiveStyle.nSegments = 72u; + sensitiveStyle.quarterSegments = 72u; std::pair allSensitives = {GeometryIdentifier(0u), sensitiveStyle}; diff --git a/Tests/UnitTests/Plugins/ActSVG/LayerSvgConverterTests.cpp b/Tests/UnitTests/Plugins/ActSVG/LayerSvgConverterTests.cpp index 7dd3a93fe95..ff4afc789b6 100644 --- a/Tests/UnitTests/Plugins/ActSVG/LayerSvgConverterTests.cpp +++ b/Tests/UnitTests/Plugins/ActSVG/LayerSvgConverterTests.cpp @@ -45,16 +45,17 @@ void setupTools() { std::shared_ptr generateDiscLayer(Acts::ActsScalar rInner, Acts::ActsScalar rOuter, - unsigned int nSegments, + unsigned int quarterSegments, unsigned int nRings, bool useTrapezoids = false) { // Some preparations setupTools(); + unsigned int fullSegments = 4 * quarterSegments; std::vector> moduleSurfaces; - Acts::ActsScalar phiStep = 2 * M_PI / nSegments; + Acts::ActsScalar phiStep = 2 * M_PI / fullSegments; Acts::ActsScalar rStep = (rOuter - rInner) / nRings; // Reserve & fill - moduleSurfaces.reserve(nSegments * nRings); + moduleSurfaces.reserve(fullSegments * nRings); // Radial disc if (!useTrapezoids) { for (unsigned int ir = 0; ir < nRings; ++ir) { @@ -62,7 +63,7 @@ std::shared_ptr generateDiscLayer(Acts::ActsScalar rInner, rBounds = std::make_shared( rInner + ir * rStep - 0.025 * rInner, rInner + (ir + 1u) * rStep + 0.025 * rInner, 0.55 * phiStep, 0.); - for (unsigned int is = 0; is < nSegments; ++is) { + for (unsigned int is = 0; is < fullSegments; ++is) { // Place the module auto placement = Acts::Transform3::Identity(); if ((is % 2) != 0u) { @@ -82,14 +83,14 @@ std::shared_ptr generateDiscLayer(Acts::ActsScalar rInner, Acts::ActsScalar yHalf = rStep * 0.5125; Acts::ActsScalar xHalfMin = - 1.15 * (rInner + ir * rStep) * M_PI / nSegments; + 1.15 * (rInner + ir * rStep) * M_PI / fullSegments; Acts::ActsScalar xHalfMax = - 1.15 * (rInner + (ir + 1) * rStep) * M_PI / nSegments; + 1.15 * (rInner + (ir + 1) * rStep) * M_PI / fullSegments; std::shared_ptr tBounds = std::make_shared(xHalfMin, xHalfMax, yHalf); - for (unsigned int is = 0; is < nSegments; ++is) { + for (unsigned int is = 0; is < fullSegments; ++is) { // Setting the phi Acts::ActsScalar cphi = -M_PI + is * phiStep; Acts::Vector3 center(radius * std::cos(cphi), radius * std::sin(cphi), @@ -111,7 +112,7 @@ std::shared_ptr generateDiscLayer(Acts::ActsScalar rInner, } } // Let's create the disc layer - return lCreator->discLayer(tgContext, moduleSurfaces, nRings, nSegments); + return lCreator->discLayer(tgContext, moduleSurfaces, nRings, fullSegments); } } // namespace @@ -125,7 +126,7 @@ BOOST_AUTO_TEST_CASE(DiscLayerRadialSvg) { discLayerStyle.highlights = {"mouseover", "mouseout"}; discLayerStyle.strokeColor = {25, 25, 25}; discLayerStyle.strokeWidth = 0.5; - discLayerStyle.nSegments = 72u; + discLayerStyle.quarterSegments = 72u; Acts::GeometryIdentifier geoID{0}; @@ -155,7 +156,7 @@ BOOST_AUTO_TEST_CASE(DiscLayerTrapezoidSvg) { discLayerStyle.highlights = {"mouseover", "mouseout"}; discLayerStyle.strokeColor = {25, 25, 25}; discLayerStyle.strokeWidth = 0.5; - discLayerStyle.nSegments = 72u; + discLayerStyle.quarterSegments = 72u; Acts::GeometryIdentifier geoID{0}; @@ -185,7 +186,7 @@ BOOST_AUTO_TEST_CASE(CylinderLayerSvg) { cylinderLayerStyle.highlights = {"mouseover", "mouseout"}; cylinderLayerStyle.strokeColor = {25, 25, 25}; cylinderLayerStyle.strokeWidth = 0.5; - cylinderLayerStyle.nSegments = 72u; + cylinderLayerStyle.quarterSegments = 72u; Acts::GeometryIdentifier geoID{0}; diff --git a/Tests/UnitTests/Plugins/ActSVG/PortalSvgConverterTests.cpp b/Tests/UnitTests/Plugins/ActSVG/PortalSvgConverterTests.cpp index 7f4cfa5d425..9fb1fdffa40 100644 --- a/Tests/UnitTests/Plugins/ActSVG/PortalSvgConverterTests.cpp +++ b/Tests/UnitTests/Plugins/ActSVG/PortalSvgConverterTests.cpp @@ -35,7 +35,7 @@ BOOST_AUTO_TEST_CASE(CylinderPortalsSvg) { portalStyle.highlights = {}; portalStyle.strokeColor = {25, 25, 25}; portalStyle.strokeWidth = 0.5; - portalStyle.nSegments = 72u; + portalStyle.quarterSegments = 72u; Acts::ActsScalar rInner = 10.; Acts::ActsScalar rOuter = 100.; @@ -94,7 +94,7 @@ BOOST_AUTO_TEST_CASE(CylinderContainerPortalsSvg) { portalStyle.highlights = {}; portalStyle.strokeColor = {25, 25, 25}; portalStyle.strokeWidth = 0.5; - portalStyle.nSegments = 72u; + portalStyle.quarterSegments = 72u; Acts::ActsScalar rInner = 10.; Acts::ActsScalar rMiddle = 100.; diff --git a/Tests/UnitTests/Plugins/ActSVG/SurfaceSvgConverterTests.cpp b/Tests/UnitTests/Plugins/ActSVG/SurfaceSvgConverterTests.cpp index 49c92f3aaf9..8450453720c 100644 --- a/Tests/UnitTests/Plugins/ActSVG/SurfaceSvgConverterTests.cpp +++ b/Tests/UnitTests/Plugins/ActSVG/SurfaceSvgConverterTests.cpp @@ -74,7 +74,7 @@ BOOST_AUTO_TEST_CASE(PlanarSurfaces) { planarStyle.highlightColor = {255, 153, 51}; planarStyle.highlights = {"mouseover", "mouseout"}; planarStyle.strokeWidth = 0.5; - planarStyle.nSegments = 0u; + planarStyle.quarterSegments = 0u; // Rectangle case auto rectangleBounds = std::make_shared(36., 64.); @@ -169,7 +169,7 @@ BOOST_AUTO_TEST_CASE(DiscSurfaces) { discStyle.highlightColor = {153, 204, 0}; discStyle.highlights = {"mouseover", "mouseout"}; discStyle.strokeWidth = 0.5; - discStyle.nSegments = 72u; + discStyle.quarterSegments = 72u; auto transform = Acts::Transform3::Identity(); transform.pretranslate(Acts::Vector3{20., 20., 100.}); diff --git a/Tests/UnitTests/Plugins/ActSVG/TrackingGeometrySvgConverterTests.cpp b/Tests/UnitTests/Plugins/ActSVG/TrackingGeometrySvgConverterTests.cpp index 7c869e90de7..3b7a8856875 100644 --- a/Tests/UnitTests/Plugins/ActSVG/TrackingGeometrySvgConverterTests.cpp +++ b/Tests/UnitTests/Plugins/ActSVG/TrackingGeometrySvgConverterTests.cpp @@ -32,7 +32,7 @@ BOOST_AUTO_TEST_CASE(CylindricalTrackingGeometrySvg) { cylinderLayerStyle.highlights = {"mouseover", "mouseout"}; cylinderLayerStyle.strokeColor = {25, 25, 25}; cylinderLayerStyle.strokeWidth = 0.5; - cylinderLayerStyle.nSegments = 72u; + cylinderLayerStyle.quarterSegments = 72u; Acts::GeometryIdentifier geoID{0}; diff --git a/Tests/UnitTests/Plugins/TGeo/TGeoTubeConversionTests.cpp b/Tests/UnitTests/Plugins/TGeo/TGeoTubeConversionTests.cpp index 3309895866b..2f18c391958 100644 --- a/Tests/UnitTests/Plugins/TGeo/TGeoTubeConversionTests.cpp +++ b/Tests/UnitTests/Plugins/TGeo/TGeoTubeConversionTests.cpp @@ -99,7 +99,6 @@ BOOST_AUTO_TEST_CASE(TGeoTube_to_CylinderSurface) { objVis, center, center + 1.2 * bR * rotation.col(1), 4., 2.5, green); GeometryView3D::drawArrowForward( objVis, center, center + 1.2 * bhZ * rotation.col(2), 4., 2.5, blue); - objVis.write("TGeoConversion_TGeoTube_CylinderSurface_" + std::to_string(icyl)); objVis.clear(); From dfc2aff43e3beecdbe62d521d114c47bb75336a7 Mon Sep 17 00:00:00 2001 From: "Alexander J. Pfleger" <70842573+AJPfleger@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:01:49 +0200 Subject: [PATCH 09/24] refactor: use `std::ranges::all_of`, `any_of`, `none_of` (#3593) I couldn't replace all instances, since https://github.com/acts-project/acts/blame/464675980c50a6070f5635127ee27fd29de5a679/Core/include/Acts/Propagator/MultiEigenStepperLoop.hpp#L458 seems not to return an iterator, that can be interpreted by ranges algorithms. --- Core/include/Acts/Surfaces/DiamondBounds.hpp | 3 +-- Core/src/Geometry/Extent.cpp | 6 ++---- .../Geant4/src/SensitiveSteppingAction.cpp | 10 +++++----- .../src/TrackFindingAlgorithmExaTrkX.cpp | 5 +++-- .../src/Utilities/EventDataTransforms.cpp | 2 +- Examples/Io/Root/src/RootMaterialDecorator.cpp | 4 ++-- Plugins/ExaTrkX/src/ExaTrkXPipeline.cpp | 7 ++++--- .../src/GeoModelDetectorObjectFactory.cpp | 17 +++++++++-------- Tests/Benchmarks/RayFrustumBenchmark.cpp | 16 ++++++++-------- .../Acts/Tests/CommonHelpers/Assertions.hpp | 18 +++++++++--------- .../Io/Csv/MeasurementReaderWriterTests.cpp | 3 ++- 11 files changed, 46 insertions(+), 45 deletions(-) diff --git a/Core/include/Acts/Surfaces/DiamondBounds.hpp b/Core/include/Acts/Surfaces/DiamondBounds.hpp index a23b8831125..7497bd22309 100644 --- a/Core/include/Acts/Surfaces/DiamondBounds.hpp +++ b/Core/include/Acts/Surfaces/DiamondBounds.hpp @@ -126,8 +126,7 @@ inline std::vector DiamondBounds::values() const { } inline void DiamondBounds::checkConsistency() noexcept(false) { - if (std::any_of(m_values.begin(), m_values.end(), - [](auto v) { return v <= 0.; })) { + if (std::ranges::any_of(m_values, [](auto v) { return v <= 0.; })) { throw std::invalid_argument("DiamondBounds: negative half length."); } if (get(eHalfLengthXnegY) > get(eHalfLengthXzeroY) || diff --git a/Core/src/Geometry/Extent.cpp b/Core/src/Geometry/Extent.cpp index 9e520d171a3..2f07f6358bc 100644 --- a/Core/src/Geometry/Extent.cpp +++ b/Core/src/Geometry/Extent.cpp @@ -144,8 +144,7 @@ bool Acts::Extent::contains(const Extent& rhs, // Check all if (!bValue.has_value()) { - return std::all_of(allBinningValues().begin(), allBinningValues().end(), - checkContainment); + return std::ranges::all_of(allBinningValues(), checkContainment); } // Check specific return checkContainment(bValue.value()); @@ -163,8 +162,7 @@ bool Acts::Extent::intersects(const Extent& rhs, // Check all if (!bValue.has_value()) { - return std::any_of(allBinningValues().begin(), allBinningValues().end(), - checkIntersect); + return std::ranges::any_of(allBinningValues(), checkIntersect); } // Check specific return checkIntersect(bValue.value()); diff --git a/Examples/Algorithms/Geant4/src/SensitiveSteppingAction.cpp b/Examples/Algorithms/Geant4/src/SensitiveSteppingAction.cpp index cb763ecc582..17df42bdb21 100644 --- a/Examples/Algorithms/Geant4/src/SensitiveSteppingAction.cpp +++ b/Examples/Algorithms/Geant4/src/SensitiveSteppingAction.cpp @@ -18,6 +18,7 @@ #include "ActsExamples/Geant4/SensitiveSurfaceMapper.hpp" #include "ActsFatras/EventData/Barcode.hpp" +#include #include #include #include @@ -254,11 +255,10 @@ void ActsExamples::SensitiveSteppingAction::UserSteppingAction( buffer.back().momentum4After(), eventStore().particleHitCount.at(particleId) - 1); - assert(std::all_of(buffer.begin(), buffer.end(), - [&](const auto& h) { return h.geometryId() == geoId; })); - assert(std::all_of(buffer.begin(), buffer.end(), [&](const auto& h) { - return h.particleId() == particleId; - })); + assert(std::ranges::all_of( + buffer, [&](const auto& h) { return h.geometryId() == geoId; })); + assert(std::ranges::all_of( + buffer, [&](const auto& h) { return h.particleId() == particleId; })); eventStore().numberGeantSteps += buffer.size(); eventStore().maxStepsForHit = diff --git a/Examples/Algorithms/TrackFindingExaTrkX/src/TrackFindingAlgorithmExaTrkX.cpp b/Examples/Algorithms/TrackFindingExaTrkX/src/TrackFindingAlgorithmExaTrkX.cpp index 06fcb1c9438..05fe35a6ea0 100644 --- a/Examples/Algorithms/TrackFindingExaTrkX/src/TrackFindingAlgorithmExaTrkX.cpp +++ b/Examples/Algorithms/TrackFindingExaTrkX/src/TrackFindingAlgorithmExaTrkX.cpp @@ -19,6 +19,7 @@ #include "ActsExamples/EventData/SimSpacePoint.hpp" #include "ActsExamples/Framework/WhiteBoard.hpp" +#include #include using namespace ActsExamples; @@ -97,8 +98,8 @@ ActsExamples::TrackFindingAlgorithmExaTrkX::TrackFindingAlgorithmExaTrkX( NodeFeature::eClusterX, NodeFeature::eClusterY, NodeFeature::eCellCount, NodeFeature::eCellSum, NodeFeature::eCluster1R, NodeFeature::eCluster2R}; - auto wantClFeatures = std::any_of( - m_cfg.nodeFeatures.begin(), m_cfg.nodeFeatures.end(), + auto wantClFeatures = std::ranges::any_of( + m_cfg.nodeFeatures, [&](const auto& f) { return Acts::rangeContainsValue(clFeatures, f); }); if (wantClFeatures && !m_inputClusters.isInitialized()) { diff --git a/Examples/Framework/src/Utilities/EventDataTransforms.cpp b/Examples/Framework/src/Utilities/EventDataTransforms.cpp index a82df0cf5a3..79d3d543a05 100644 --- a/Examples/Framework/src/Utilities/EventDataTransforms.cpp +++ b/Examples/Framework/src/Utilities/EventDataTransforms.cpp @@ -32,7 +32,7 @@ const ActsExamples::SimSpacePoint* ActsExamples::findSpacePointForIndex( ActsExamples::Index index, const SimSpacePointContainer& spacepoints) { auto match = [&](const SimSpacePoint& sp) { const auto& sls = sp.sourceLinks(); - return std::any_of(sls.begin(), sls.end(), [&](const auto& sl) { + return std::ranges::any_of(sls, [&](const auto& sl) { return sl.template get().index() == index; }); }; diff --git a/Examples/Io/Root/src/RootMaterialDecorator.cpp b/Examples/Io/Root/src/RootMaterialDecorator.cpp index b8f49c53ea1..37f396bb86c 100644 --- a/Examples/Io/Root/src/RootMaterialDecorator.cpp +++ b/Examples/Io/Root/src/RootMaterialDecorator.cpp @@ -148,8 +148,8 @@ ActsExamples::RootMaterialDecorator::RootMaterialDecorator( std::vector hists{n, v, o, min, max, t, x0, l0, A, Z, rho}; // Only go on when you have all histograms - if (std::all_of(hists.begin(), hists.end(), - [](const auto* hist) { return hist != nullptr; })) { + if (std::ranges::all_of( + hists, [](const auto* hist) { return hist != nullptr; })) { // Get the number of bins int nbins0 = t->GetNbinsX(); int nbins1 = t->GetNbinsY(); diff --git a/Plugins/ExaTrkX/src/ExaTrkXPipeline.cpp b/Plugins/ExaTrkX/src/ExaTrkXPipeline.cpp index a3d6dfe1148..d53de1d3071 100644 --- a/Plugins/ExaTrkX/src/ExaTrkXPipeline.cpp +++ b/Plugins/ExaTrkX/src/ExaTrkXPipeline.cpp @@ -8,6 +8,8 @@ #include "Acts/Plugins/ExaTrkX/ExaTrkXPipeline.hpp" +#include "Acts/Utilities/Helpers.hpp" + #include namespace Acts { @@ -27,9 +29,8 @@ ExaTrkXPipeline::ExaTrkXPipeline( if (!m_trackBuilder) { throw std::invalid_argument("Missing track building module"); } - if (m_edgeClassifiers.empty() or - not std::all_of(m_edgeClassifiers.begin(), m_edgeClassifiers.end(), - [](const auto &a) { return static_cast(a); })) { + if (m_edgeClassifiers.empty() || + rangeContainsValue(m_edgeClassifiers, nullptr)) { throw std::invalid_argument("Missing graph construction module"); } } diff --git a/Plugins/GeoModel/src/GeoModelDetectorObjectFactory.cpp b/Plugins/GeoModel/src/GeoModelDetectorObjectFactory.cpp index 91580a7bfda..a8844d9fa5d 100644 --- a/Plugins/GeoModel/src/GeoModelDetectorObjectFactory.cpp +++ b/Plugins/GeoModel/src/GeoModelDetectorObjectFactory.cpp @@ -19,6 +19,7 @@ #include "Acts/Plugins/GeoModel/GeoModelConverters.hpp" #include "Acts/Plugins/GeoModel/IGeoShapeConverter.hpp" +#include #include #include @@ -141,9 +142,9 @@ Acts::GeoModelDetectorObjectFactory::findAllSubVolumes(const PVConstLink &vol) { } bool Acts::GeoModelDetectorObjectFactory::convertBox(std::string name) { - auto convB = std::any_of( - m_cfg.convertBox.begin(), m_cfg.convertBox.end(), - [&](const auto &n) { return name.find(n) != std::string::npos; }); + auto convB = std::ranges::any_of(m_cfg.convertBox, [&](const auto &n) { + return name.find(n) != std::string::npos; + }); return convB; } @@ -192,14 +193,14 @@ bool Acts::GeoModelDetectorObjectFactory::matches(const std::string &name, return true; } - auto matchName = std::any_of( - m_cfg.nameList.begin(), m_cfg.nameList.end(), - [&](const auto &n) { return name.find(n) != std::string::npos; }); + auto matchName = std::ranges::any_of(m_cfg.nameList, [&](const auto &n) { + return name.find(n) != std::string::npos; + }); std::string matStr = physvol->getLogVol()->getMaterial()->getName(); - auto matchMaterial = std::any_of( - m_cfg.materialList.begin(), m_cfg.materialList.end(), + auto matchMaterial = std::ranges::any_of( + m_cfg.materialList, [&](const auto &m) { return matStr.find(m) != std::string::npos; }); bool match = matchMaterial && matchName; diff --git a/Tests/Benchmarks/RayFrustumBenchmark.cpp b/Tests/Benchmarks/RayFrustumBenchmark.cpp index 2d0069da1b1..e4f11e60991 100644 --- a/Tests/Benchmarks/RayFrustumBenchmark.cpp +++ b/Tests/Benchmarks/RayFrustumBenchmark.cpp @@ -187,10 +187,10 @@ int main(int /*argc*/, char** /*argv[]*/) { return {name, func(testBox, ray)}; }); - bool all = std::all_of(results.begin(), results.end(), - [](const auto& r) { return r.second; }); - bool none = std::none_of(results.begin(), results.end(), - [](const auto& r) { return r.second; }); + bool all = + std::ranges::all_of(results, [](const auto& r) { return r.second; }); + bool none = + std::ranges::none_of(results, [](const auto& r) { return r.second; }); if (!all && !none) { std::cerr << "Discrepancy: " << std::endl; @@ -369,10 +369,10 @@ int main(int /*argc*/, char** /*argv[]*/) { return {name, func(testBox, fr)}; }); - bool all = std::all_of(results.begin(), results.end(), - [](const auto& r) { return r.second; }); - bool none = std::none_of(results.begin(), results.end(), - [](const auto& r) { return r.second; }); + bool all = + std::ranges::all_of(results, [](const auto& r) { return r.second; }); + bool none = + std::ranges::none_of(results, [](const auto& r) { return r.second; }); if (!all && !none) { std::cerr << "Discrepancy: " << std::endl; diff --git a/Tests/CommonHelpers/Acts/Tests/CommonHelpers/Assertions.hpp b/Tests/CommonHelpers/Acts/Tests/CommonHelpers/Assertions.hpp index 6bc19dc0431..3fee5af18ae 100644 --- a/Tests/CommonHelpers/Acts/Tests/CommonHelpers/Assertions.hpp +++ b/Tests/CommonHelpers/Acts/Tests/CommonHelpers/Assertions.hpp @@ -10,15 +10,15 @@ #include +#include #include -#define CHECK_NE_COLLECTIONS(col1, col2) \ - do { \ - BOOST_CHECK_EQUAL(col1.size(), col2.size()); \ - std::vector result; \ - for (std::size_t i = 0; i < col1.size(); i++) { \ - result.push_back(col1[i] == col2[i]); \ - } \ - BOOST_CHECK( \ - !std::all_of(result.begin(), result.end(), [](bool r) { return r; })); \ +#define CHECK_NE_COLLECTIONS(col1, col2) \ + do { \ + BOOST_CHECK_EQUAL(col1.size(), col2.size()); \ + std::vector result; \ + for (std::size_t i = 0; i < col1.size(); i++) { \ + result.push_back(col1[i] == col2[i]); \ + } \ + BOOST_CHECK(!std::ranges::all_of(result, [](bool r) { return r; })); \ } while (0) diff --git a/Tests/UnitTests/Examples/Io/Csv/MeasurementReaderWriterTests.cpp b/Tests/UnitTests/Examples/Io/Csv/MeasurementReaderWriterTests.cpp index 1a3d9662a2a..7786a5ec8a1 100644 --- a/Tests/UnitTests/Examples/Io/Csv/MeasurementReaderWriterTests.cpp +++ b/Tests/UnitTests/Examples/Io/Csv/MeasurementReaderWriterTests.cpp @@ -20,6 +20,7 @@ #include "ActsExamples/Io/Csv/CsvMeasurementReader.hpp" #include "ActsExamples/Io/Csv/CsvMeasurementWriter.hpp" +#include #include #include #include @@ -152,7 +153,7 @@ BOOST_AUTO_TEST_CASE(CsvMeasurementRoundTrip) { std::abs(ca.activation - cb.activation) < 1.e-4; }; - BOOST_CHECK(std::any_of(b.channels.begin(), b.channels.end(), match)); + BOOST_CHECK(std::ranges::any_of(b.channels, match)); } } From a42ce7c116cea43d0f43b385171c14b5c63f698c Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Thu, 3 Oct 2024 21:46:30 +0200 Subject: [PATCH 10/24] fix: Correct initial `q/p` covariance term in `TrackParamsEstimationAlgorithm` in Examples (#3665) I seem to have made a mistake calculating the covariance term on the first try in https://github.com/acts-project/acts/pull/3422. This is corrected here --- .../performance_ckf.root | Bin 31176 -> 31236 bytes .../performance_ckf_ambi.root | Bin 27720 -> 27753 bytes ...ance_vertexing_amvf_gauss_notime_hist.root | Bin 48479 -> 48551 bytes ...ormance_vertexing_amvf_grid_time_hist.root | Bin 48977 -> 48768 bytes .../src/TrackParamsEstimationAlgorithm.cpp | 15 ++++---- .../TruthTracking/ParticleSmearing.cpp | 2 +- Examples/Python/tests/root_file_hashes.txt | 32 +++++++++--------- 7 files changed, 24 insertions(+), 25 deletions(-) diff --git a/CI/physmon/reference/trackfinding_ttbar_pu200/performance_ckf.root b/CI/physmon/reference/trackfinding_ttbar_pu200/performance_ckf.root index f7fb66f24ab29839e053027bc7629112e88fbdcb..b02aa3773acb66183310e6abacdafc30284ce3e8 100644 GIT binary patch delta 29195 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 RzWsXn7URwV?^T2S{{UHY1L*(& delta 29094 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 diff --git a/CI/physmon/reference/trackfinding_ttbar_pu200/performance_ckf_ambi.root b/CI/physmon/reference/trackfinding_ttbar_pu200/performance_ckf_ambi.root index 8fbd66c0abb73f0c6e3246214e1e439dab5565d0..35c6a2c8519e1b829695dc2bb8045d47472b6b75 100644 GIT binary patch delta 25521 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 delta 26523 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 rQ(I?2$(_naYRk|Rd}LEw4xb^AD{jBpF zJ^^r$U*?eO4FG`20sy=Ss6Gg2!9Ga2kGp@9VMh)I)-#|s0UmqVx(AP{#5udkK>mBY z1ORZ-9!G$DAR|Bi6e~Xt&~EWKK*h@M0e;{4_iy0y$D!agAD^#c1+oEQA1{nyp8dXv zg=Ge!GC;eiK6U|j!BGHD!JBXapvngTO<)^PafL46xZb!pM6IL@?iKo&Vo8Yc!`qU; zS7@eYuo%>LOzn7Nciak5#` zVhCEssz4y;+^{T40LMTq!m2>qliX8Fr1zl!X{5G36ad$psK#%Kf0GPm1(JhN5Ktef z1{=eWVE_O?ZYa4rP;z5oDIc3yADd9%piO$uiNVnb&!A5PmJow~BGiKJTfR=Wj}9)w zqdrtO6Pscxz6UY~k3LxdG#JJP;y=+8Z9}`@{P5iP_W7;UO3ZT^31coe1TQ2krYa=9 zO!d3q0eTvdk#k(<#e!Q)B(!cU0ATc|L@5+N$cvfiA72a*1pt4;3V02#sc7Zmil*V} zY;Iy_?p%e5B*z4uc)jP;KvWJW1IoXtEhT}|RYbZ6UvpD|`#Ft4MS*RU%fRF=DPDH= zwIDkJ>B@SX*PK09ow`WpLOnP)m=riXj&1fP_=7BPp_izL7~yXN65j&ThP*BtUfRpV zc07wqG&`pfN2KctSU2I4e1U^J!O+u&g2qH)9D4Iq4#*z#bj;XS+ytn>qqCL0vCxiV zpSw@N24}pD4&X+5A_);Q-~^OHODJ1Bk8E+3fLpm}K=D9;B|N|rSfeha04s)-t}n

KqXvy2PW!9ORJcEL*`kKyZN2? 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 delta 27411 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 Date: Fri, 4 Oct 2024 00:46:56 +0200 Subject: [PATCH 11/24] fix: Fit iterator traits for ContainerIndexIterator (#3689) It looks like that `std:: iterator_traits< Acts::ContainerIndexIterator< whatever ...> >::iterator_category ` was evaluated to `std::output_iterator_tag` instead of `Iter::iterator_category` (i.e. `std::random_access_iterator_tag`). This made it impossible to use `std::distance` with this iterator class. See https://en.cppreference.com/w/cpp/iterator/iterator_traits to see why. This PR fixes it. --- Core/include/Acts/Utilities/Iterator.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Core/include/Acts/Utilities/Iterator.hpp b/Core/include/Acts/Utilities/Iterator.hpp index ccdff61c061..a69e6c77faa 100644 --- a/Core/include/Acts/Utilities/Iterator.hpp +++ b/Core/include/Acts/Utilities/Iterator.hpp @@ -22,6 +22,8 @@ class ContainerIndexIterator { using container_type = std::conditional_t; using difference_type = std::ptrdiff_t; + using pointer = void; + using reference = void; ContainerIndexIterator() : m_container(nullptr), m_index(0) {} From bcf1a194db044a45b7e025a72c7180ac10e04d50 Mon Sep 17 00:00:00 2001 From: Andreas Salzburger Date: Fri, 4 Oct 2024 02:02:54 +0200 Subject: [PATCH 12/24] feat: Allow Style setting for IndexedSurfaceGrid in Svg (#3686) This PR allows to define the style of the grid when displaying the `IndexedSurfaceGrid` in SVG. --- Examples/Python/src/Svg.cpp | 28 +++++++++++++++++++ .../ActSVG/DetectorVolumeSvgConverter.hpp | 2 ++ .../ActSVG/src/DetectorVolumeSvgConverter.cpp | 3 +- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Examples/Python/src/Svg.cpp b/Examples/Python/src/Svg.cpp index f1d46cadcd8..ee011920947 100644 --- a/Examples/Python/src/Svg.cpp +++ b/Examples/Python/src/Svg.cpp @@ -328,6 +328,21 @@ void addSvg(Context& ctx) { }); } + { + auto gco = py::class_(svg, "GridOptions") + .def(py::init<>()); + ACTS_PYTHON_STRUCT_BEGIN(gco, Svg::GridConverter::Options); + ACTS_PYTHON_MEMBER(style); + ACTS_PYTHON_STRUCT_END(); + + auto isco = py::class_( + svg, "IndexedSurfacesOptions") + .def(py::init<>()); + ACTS_PYTHON_STRUCT_BEGIN(isco, Svg::IndexedSurfacesConverter::Options); + ACTS_PYTHON_MEMBER(gridOptions); + ACTS_PYTHON_STRUCT_END(); + } + // How detector volumes are drawn: Svg DetectorVolume options & drawning { auto c = py::class_( @@ -338,12 +353,16 @@ void addSvg(Context& ctx) { ACTS_PYTHON_MEMBER(portalIndices); ACTS_PYTHON_MEMBER(portalOptions); ACTS_PYTHON_MEMBER(surfaceOptions); + ACTS_PYTHON_MEMBER(indexedSurfacesOptions); ACTS_PYTHON_STRUCT_END(); // Define the proto volume & indexed surface grid py::class_(svg, "ProtoVolume"); py::class_(svg, "ProtoIndexedSurfaceGrid"); + // Define the proto grid + py::class_(svg, "ProtoGrid"); + // Convert an Acts::Experimental::DetectorVolume object into an // acts::svg::proto::volume svg.def("convertDetectorVolume", &Svg::DetectorVolumeConverter::convert); @@ -352,6 +371,15 @@ void addSvg(Context& ctx) { svg.def("drawDetectorVolume", &drawDetectorVolume); } + // Draw the ProtoIndexedSurfaceGrid + { + svg.def("drawIndexedSurfaces", + [](const Svg::ProtoIndexedSurfaceGrid& pIndexedSurfaceGrid, + const std::string& identification) { + return Svg::View::xy(pIndexedSurfaceGrid, identification); + }); + } + // How a detector is drawn: Svg Detector options & drawning { svg.def("drawDetector", &drawDetector); } diff --git a/Plugins/ActSVG/include/Acts/Plugins/ActSVG/DetectorVolumeSvgConverter.hpp b/Plugins/ActSVG/include/Acts/Plugins/ActSVG/DetectorVolumeSvgConverter.hpp index 6ac84da77ee..f27db472439 100644 --- a/Plugins/ActSVG/include/Acts/Plugins/ActSVG/DetectorVolumeSvgConverter.hpp +++ b/Plugins/ActSVG/include/Acts/Plugins/ActSVG/DetectorVolumeSvgConverter.hpp @@ -41,6 +41,8 @@ struct Options { PortalConverter::Options portalOptions; /// The Surface converter options SurfaceConverter::Options surfaceOptions; + /// The Grid converter options + IndexedSurfacesConverter::Options indexedSurfacesOptions; }; /// Write/create the detector volume diff --git a/Plugins/ActSVG/src/DetectorVolumeSvgConverter.cpp b/Plugins/ActSVG/src/DetectorVolumeSvgConverter.cpp index c187e69569b..93527488538 100644 --- a/Plugins/ActSVG/src/DetectorVolumeSvgConverter.cpp +++ b/Plugins/ActSVG/src/DetectorVolumeSvgConverter.cpp @@ -68,7 +68,8 @@ Acts::Svg::DetectorVolumeConverter::convert( // Make dedicated surface grid sheets const auto& internalNavigationDelegate = dVolume.internalNavigation(); - IndexedSurfacesConverter::Options isOptions; + IndexedSurfacesConverter::Options isOptions = + volumeOptions.indexedSurfacesOptions; // Use or transfer the surface style if (isOptions.surfaceStyles.empty()) { std::pair style{ From 68cf91d23e5ce79e5060aaa8c9b110618cae73bc Mon Sep 17 00:00:00 2001 From: Carlo Varni <75478407+CarloVarni@users.noreply.github.com> Date: Fri, 4 Oct 2024 11:39:51 +0200 Subject: [PATCH 13/24] feat: Add test for space point edm (#3690) Adds some unit test for the space point edm, with also checks on the types Requires https://github.com/acts-project/acts/pull/3689 to compile (already merged) --- Tests/UnitTests/Core/EventData/CMakeLists.txt | 1 + .../EventData/SpacePointContainerEdmTests.cpp | 203 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 Tests/UnitTests/Core/EventData/SpacePointContainerEdmTests.cpp diff --git a/Tests/UnitTests/Core/EventData/CMakeLists.txt b/Tests/UnitTests/Core/EventData/CMakeLists.txt index 5699dc4527f..61b5079da8e 100644 --- a/Tests/UnitTests/Core/EventData/CMakeLists.txt +++ b/Tests/UnitTests/Core/EventData/CMakeLists.txt @@ -15,5 +15,6 @@ add_unittest(ParticleHypothesis ParticleHypothesisTests.cpp) add_unittest(MultiTrajectoryHelpers MultiTrajectoryHelpersTests.cpp) add_unittest(SubspaceHelpers SubspaceHelpersTests.cpp) add_unittest(SeedEdm SeedEdmTests.cpp) +add_unittest(SpacePointContainerEdm SpacePointContainerEdmTests.cpp) add_non_compile_test(MultiTrajectory TrackContainerComplianceTests.cpp) diff --git a/Tests/UnitTests/Core/EventData/SpacePointContainerEdmTests.cpp b/Tests/UnitTests/Core/EventData/SpacePointContainerEdmTests.cpp new file mode 100644 index 00000000000..7ca20985499 --- /dev/null +++ b/Tests/UnitTests/Core/EventData/SpacePointContainerEdmTests.cpp @@ -0,0 +1,203 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include + +#include "Acts/Definitions/Algebra.hpp" +#include "Acts/EventData/SpacePointContainer.hpp" +#include "Acts/Utilities/HashedString.hpp" +#include "Acts/Utilities/Holders.hpp" + +#include +#include +#include +#include +#include + +namespace Acts::Test { + +struct SpacePoint { + SpacePoint() = default; + SpacePoint(float ix, float iy, float iz, float ivarR, float ivarZ) + : x(ix), y(iy), z(iz), varR(ivarR), varZ(ivarZ) {} + float x{}; + float y{}; + float z{}; + float varR{}; + float varZ{}; +}; +using SpacePointCollection = std::vector; + +class Adapter { + public: + friend Acts::SpacePointContainer; + friend Acts::SpacePointContainer; + using value_type = SpacePoint; + using ValueType = value_type; + + Adapter(SpacePointCollection&&) = delete; + Adapter(const SpacePointCollection& externalCollection) + : m_storage(&externalCollection) {} + + private: + std::size_t size_impl() const { return storage().size(); } + + float x_impl(std::size_t idx) const { return storage()[idx].x; }; + float y_impl(std::size_t idx) const { return storage()[idx].y; }; + float z_impl(std::size_t idx) const { return storage()[idx].z; }; + float varianceR_impl(std::size_t idx) const { return storage()[idx].varR; } + float varianceZ_impl(std::size_t idx) const { return storage()[idx].varZ; } + + const SpacePoint& get_impl(std::size_t idx) const { return storage()[idx]; } + + std::any component_impl(Acts::HashedString key, std::size_t /*n*/) const { + using namespace Acts::HashedStringLiteral; + switch (key) { + case "TopStripVector"_hash: + case "BottomStripVector"_hash: + case "StripCenterDistance"_hash: + case "TopStripCenterPosition"_hash: + return Acts::Vector3(0., 0., 0.); + default: + throw std::runtime_error("no such component " + std::to_string(key)); + } + } + + const SpacePointCollection& storage() const { return *m_storage; } + + private: + const SpacePointCollection* m_storage{}; +}; + +BOOST_AUTO_TEST_CASE(spacepoint_container_edm_traits) { + using adapter_t = Acts::Test::Adapter; + using container_t = + Acts::SpacePointContainer; + using proxy_t = Acts::SpacePointProxy; + using iterator_t = Acts::ContainerIndexIterator; + + static_assert(std::ranges::range); + static_assert(std::same_as); + static_assert( + std::same_as::iterator_category, + std::random_access_iterator_tag>); +} + +BOOST_AUTO_TEST_CASE(spacepoint_container_edm_constructors) { + std::size_t nExternalPoints = 10; + SpacePointCollection externalCollection(nExternalPoints); + + Acts::SpacePointContainerConfig spConfig; + Acts::SpacePointContainerOptions spOptions; + + Acts::Test::Adapter adapterForRef(externalCollection); + Acts::SpacePointContainer + spContainerRef(spConfig, spOptions, adapterForRef); + + Acts::SpacePointContainer + spContainerVal(spConfig, spOptions, + Acts::Test::Adapter(externalCollection)); + + BOOST_CHECK_EQUAL(spContainerRef.size(), nExternalPoints); + BOOST_CHECK_EQUAL(spContainerVal.size(), nExternalPoints); +} + +BOOST_AUTO_TEST_CASE(spacepoint_container_edm_functionalities) { + std::size_t nExternalPoints = 100; + SpacePointCollection externalCollection; + externalCollection.reserve(nExternalPoints); + for (std::size_t i = 0; i < nExternalPoints; ++i) { + externalCollection.emplace_back(1.f * i, 1.5f * i, 2.f * i, 2.5f * i, + 3.f * i); + } + + Acts::SpacePointContainerConfig spConfig; + spConfig.useDetailedDoubleMeasurementInfo = true; + Acts::SpacePointContainerOptions spOptions; + + Acts::Test::Adapter adapter(externalCollection); + Acts::SpacePointContainer + spContainer(spConfig, spOptions, adapter); + + BOOST_CHECK_EQUAL(spContainer.size(), nExternalPoints); + BOOST_CHECK_EQUAL(spContainer.size(), externalCollection.size()); + BOOST_CHECK_EQUAL(spContainer.end() - spContainer.begin(), nExternalPoints); + BOOST_CHECK_EQUAL(std::distance(spContainer.begin(), spContainer.end()), + nExternalPoints); + BOOST_CHECK_EQUAL(std::distance(std::ranges::begin(spContainer), + std::ranges::end(spContainer)), + nExternalPoints); + + using proxy_t = Acts::SpacePointProxy< + Acts::SpacePointContainer>; + static_assert(std::same_as); + static_assert( + std::same_as); + static_assert( + std::same_as); + static_assert( + std::same_as); + static_assert( + std::same_as); + + using iterator_t = + Acts::ContainerIndexIterator; + using const_iterator_t = + Acts::ContainerIndexIterator; + static_assert( + std::same_as); + static_assert(std::same_as); + static_assert( + std::same_as); + static_assert(std::same_as); + + std::size_t n = 0ul; + for (const proxy_t& proxy : spContainer) { + float refX = 1.f * n; + float refY = 1.5f * n; + float refZ = 2.f * n; + float refCovR = 2.5f * n; + float refCovZ = 3.f * n; + float refRadius = std::hypot(refX, refY); + float refPhi = std::atan2(refY, refX); + + BOOST_CHECK_EQUAL(proxy.index(), n); + BOOST_CHECK_EQUAL(proxy.x(), refX); + BOOST_CHECK_EQUAL(proxy.y(), refY); + BOOST_CHECK_EQUAL(proxy.z(), refZ); + BOOST_CHECK_EQUAL(proxy.radius(), refRadius); + BOOST_CHECK_EQUAL(proxy.phi(), refPhi); + BOOST_CHECK_EQUAL(proxy.varianceR(), refCovR); + BOOST_CHECK_EQUAL(proxy.varianceZ(), refCovZ); + + const Acts::Vector3& topStripVector = proxy.topStripVector(); + const Acts::Vector3& bottomStripVector = proxy.bottomStripVector(); + const Acts::Vector3& stripCenterDistance = proxy.stripCenterDistance(); + const Acts::Vector3& topStripCenterPosition = + proxy.topStripCenterPosition(); + + for (std::size_t i = 0; i < 3; ++i) { + BOOST_CHECK_EQUAL(topStripVector[i], 0.); + BOOST_CHECK_EQUAL(bottomStripVector[i], 0.); + BOOST_CHECK_EQUAL(stripCenterDistance[i], 0.); + BOOST_CHECK_EQUAL(topStripCenterPosition[i], 0.); + } + + const Acts::Test::SpacePoint& sp = proxy.externalSpacePoint(); + BOOST_CHECK_EQUAL(&sp, &externalCollection[n]); + + ++n; + } + BOOST_CHECK_EQUAL(n, nExternalPoints); +} + +} // namespace Acts::Test From 5c5d085b8bd7e5ab0cdc536aaf16d7c41b0b7699 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Fri, 4 Oct 2024 13:16:08 +0200 Subject: [PATCH 14/24] fix: BinUtility was auto-convertible from Transform3 (#3691) This was visible when trying to do ```cpp std::cout << Transform3{}; ``` which would invoke BinUtility's output stream operator, because that wasn't a hidden `friend`, and BinUtility was auto-convertible. --- Core/include/Acts/Utilities/BinUtility.hpp | 10 ++++++---- Core/src/Utilities/BinUtility.cpp | 15 --------------- Core/src/Utilities/CMakeLists.txt | 1 - 3 files changed, 6 insertions(+), 20 deletions(-) delete mode 100644 Core/src/Utilities/BinUtility.cpp diff --git a/Core/include/Acts/Utilities/BinUtility.hpp b/Core/include/Acts/Utilities/BinUtility.hpp index b01c8d27158..a1213ddd8ec 100644 --- a/Core/include/Acts/Utilities/BinUtility.hpp +++ b/Core/include/Acts/Utilities/BinUtility.hpp @@ -47,7 +47,7 @@ class BinUtility { /// Constructor with only a Transform3 /// /// @param tForm is the local to global transform - BinUtility(const Transform3& tForm) + explicit BinUtility(const Transform3& tForm) : m_binningData(), m_transform(tForm), m_itransform(tForm.inverse()) { m_binningData.reserve(3); } @@ -317,13 +317,15 @@ class BinUtility { return ss.str(); } + /// Overload of << operator for std::ostream for debug output + friend std::ostream& operator<<(std::ostream& sl, const BinUtility& bgen) { + return bgen.toStream(sl); + } + private: std::vector m_binningData; /// vector of BinningData Transform3 m_transform; /// shared transform Transform3 m_itransform; /// unique inverse transform }; -/// Overload of << operator for std::ostream for debug output -std::ostream& operator<<(std::ostream& sl, const BinUtility& bgen); - } // namespace Acts diff --git a/Core/src/Utilities/BinUtility.cpp b/Core/src/Utilities/BinUtility.cpp deleted file mode 100644 index 707f6dce1a0..00000000000 --- a/Core/src/Utilities/BinUtility.cpp +++ /dev/null @@ -1,15 +0,0 @@ -// This file is part of the ACTS project. -// -// Copyright (C) 2016 CERN for the benefit of the ACTS project -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -#include "Acts/Utilities/BinUtility.hpp" - -#include - -std::ostream& Acts::operator<<(std::ostream& sl, const BinUtility& bgen) { - return bgen.toStream(sl); -} diff --git a/Core/src/Utilities/CMakeLists.txt b/Core/src/Utilities/CMakeLists.txt index 6c5553387a0..1fb61813cd3 100644 --- a/Core/src/Utilities/CMakeLists.txt +++ b/Core/src/Utilities/CMakeLists.txt @@ -2,7 +2,6 @@ target_sources( ActsCore PRIVATE AnnealingUtility.cpp - BinUtility.cpp Logger.cpp SpacePointUtility.cpp TrackHelpers.cpp From 81e129bc52d420a85b7e8f381c865affd951024b Mon Sep 17 00:00:00 2001 From: Carlo Varni <75478407+CarloVarni@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:02:25 +0200 Subject: [PATCH 15/24] refactor!: Do not use geometry extent during seeding (#3688) In the seeding we only need the radius range of the space points (and only when the range is computed by the code and not provided by the user). As of now, when filling the grid we waste a lot of time to fill the `Acts::Extent` object (for all the possible `Acts::BinningValue` even if we don't need them all). I have refactored the code so that we do not do this expensive operation. The user will have to do it by them selves or provide a validity search window via JO. No changes expected from this PR --- .../detail/CylindricalSpacePointGrid.hpp | 3 +- .../detail/CylindricalSpacePointGrid.ipp | 5 +--- .../TrackFinding/src/SeedingAlgorithm.cpp | 30 +++++++++++-------- .../src/SeedingAlgorithmHashing.cpp | 29 +++++++++++------- .../UnitTests/Core/Seeding/SeedFinderTest.cpp | 7 +---- 5 files changed, 39 insertions(+), 35 deletions(-) diff --git a/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.hpp b/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.hpp index b6902ecd35a..3acdc5a4c05 100644 --- a/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.hpp +++ b/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.hpp @@ -8,7 +8,6 @@ #pragma once -#include "Acts/Geometry/Extent.hpp" #include "Acts/Seeding/BinnedGroup.hpp" #include "Acts/Seeding/SeedFinderConfig.hpp" #include "Acts/Utilities/Grid.hpp" @@ -121,7 +120,7 @@ class CylindricalSpacePointGridCreator { const Acts::SeedFinderOptions& options, Acts::CylindricalSpacePointGrid& grid, external_spacepoint_iterator_t spBegin, - external_spacepoint_iterator_t spEnd, Acts::Extent& rRangeSPExtent); + external_spacepoint_iterator_t spEnd); }; } // namespace Acts diff --git a/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.ipp b/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.ipp index ea228510042..a80d3105e82 100644 --- a/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.ipp +++ b/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.ipp @@ -142,7 +142,7 @@ void Acts::CylindricalSpacePointGridCreator::fillGrid( const Acts::SeedFinderOptions& options, Acts::CylindricalSpacePointGrid& grid, external_spacepoint_iterator_t spBegin, - external_spacepoint_iterator_t spEnd, Acts::Extent& rRangeSPExtent) { + external_spacepoint_iterator_t spEnd) { if (!config.isInInternalUnits) { throw std::runtime_error( "SeedFinderConfig not in ACTS internal units in BinnedSPGroup"); @@ -176,9 +176,6 @@ void Acts::CylindricalSpacePointGridCreator::fillGrid( float spY = sp.y(); float spZ = sp.z(); - // store x,y,z values in extent - rRangeSPExtent.extend({spX, spY, spZ}); - // remove SPs according to experiment specific cuts if (!config.spacePointSelector(sp)) { continue; diff --git a/Examples/Algorithms/TrackFinding/src/SeedingAlgorithm.cpp b/Examples/Algorithms/TrackFinding/src/SeedingAlgorithm.cpp index 40111376a9c..b60acc13e85 100644 --- a/Examples/Algorithms/TrackFinding/src/SeedingAlgorithm.cpp +++ b/Examples/Algorithms/TrackFinding/src/SeedingAlgorithm.cpp @@ -11,7 +11,6 @@ #include "Acts/Definitions/Algebra.hpp" #include "Acts/EventData/Seed.hpp" #include "Acts/EventData/SpacePointData.hpp" -#include "Acts/Geometry/Extent.hpp" #include "Acts/Seeding/BinnedGroup.hpp" #include "Acts/Seeding/SeedFilter.hpp" #include "Acts/Utilities/BinningType.hpp" @@ -243,16 +242,28 @@ ActsExamples::ProcessCode ActsExamples::SeedingAlgorithm::execute( using value_type = typename decltype(spContainer)::SpacePointProxyType; using seed_type = Acts::Seed; - // extent used to store r range for middle spacepoint - Acts::Extent rRangeSPExtent; - Acts::CylindricalSpacePointGrid grid = Acts::CylindricalSpacePointGridCreator::createGrid( m_cfg.gridConfig, m_cfg.gridOptions); Acts::CylindricalSpacePointGridCreator::fillGrid( m_cfg.seedFinderConfig, m_cfg.seedFinderOptions, grid, - spContainer.begin(), spContainer.end(), rRangeSPExtent); + spContainer.begin(), spContainer.end()); + + // Compute radius Range + // we rely on the fact the grid is storing the proxies + // with a sorting in the radius + float minRange = std::numeric_limits::max(); + float maxRange = std::numeric_limits::lowest(); + for (const auto& coll : grid) { + if (coll.empty()) { + continue; + } + const auto* firstEl = coll.front(); + const auto* lastEl = coll.back(); + minRange = std::min(firstEl->radius(), minRange); + maxRange = std::max(lastEl->radius(), maxRange); + } std::array, 2ul> navigation; navigation[1ul] = m_cfg.seedFinderConfig.zBinsCustomLooping; @@ -261,15 +272,10 @@ ActsExamples::ProcessCode ActsExamples::SeedingAlgorithm::execute( std::move(grid), *m_bottomBinFinder, *m_topBinFinder, std::move(navigation)); - // safely clamp double to float - float up = Acts::clampValue( - std::floor(rRangeSPExtent.max(Acts::BinningValue::binR) / 2) * 2); - /// variable middle SP radial region of interest const Acts::Range1D rMiddleSPRange( - std::floor(rRangeSPExtent.min(Acts::BinningValue::binR) / 2) * 2 + - m_cfg.seedFinderConfig.deltaRMiddleMinSPRange, - up - m_cfg.seedFinderConfig.deltaRMiddleMaxSPRange); + minRange + m_cfg.seedFinderConfig.deltaRMiddleMinSPRange, + maxRange - m_cfg.seedFinderConfig.deltaRMiddleMaxSPRange); // run the seeding static thread_local std::vector seeds; diff --git a/Examples/Algorithms/TrackFinding/src/SeedingAlgorithmHashing.cpp b/Examples/Algorithms/TrackFinding/src/SeedingAlgorithmHashing.cpp index ec664aef437..84011207f19 100644 --- a/Examples/Algorithms/TrackFinding/src/SeedingAlgorithmHashing.cpp +++ b/Examples/Algorithms/TrackFinding/src/SeedingAlgorithmHashing.cpp @@ -11,7 +11,6 @@ #include "Acts/Definitions/Algebra.hpp" #include "Acts/EventData/Seed.hpp" #include "Acts/EventData/SpacePointData.hpp" -#include "Acts/Geometry/Extent.hpp" #include "Acts/Plugins/Hashing/HashingAlgorithm.hpp" #include "Acts/Plugins/Hashing/HashingTraining.hpp" #include "Acts/Seeding/BinnedGroup.hpp" @@ -269,15 +268,28 @@ ActsExamples::ProcessCode ActsExamples::SeedingAlgorithmHashing::execute( Acts::SpacePointContainer spContainer(spConfig, spOptions, container); - // extent used to store r range for middle spacepoint - Acts::Extent rRangeSPExtent; // construct the seeding tools Acts::CylindricalSpacePointGrid grid = Acts::CylindricalSpacePointGridCreator::createGrid( m_cfg.gridConfig, m_cfg.gridOptions); Acts::CylindricalSpacePointGridCreator::fillGrid( m_cfg.seedFinderConfig, m_cfg.seedFinderOptions, grid, - spContainer.begin(), spContainer.end(), rRangeSPExtent); + spContainer.begin(), spContainer.end()); + + // Compute radius Range + // we rely on the fact the grid is storing the proxies + // with a sorting in the radius + float minRange = std::numeric_limits::max(); + float maxRange = std::numeric_limits::lowest(); + for (const auto& coll : grid) { + if (coll.empty()) { + continue; + } + const auto* firstEl = coll.front(); + const auto* lastEl = coll.back(); + minRange = std::min(firstEl->radius(), minRange); + maxRange = std::max(lastEl->radius(), maxRange); + } std::array, 2ul> navigation; navigation[1ul] = m_cfg.seedFinderConfig.zBinsCustomLooping; @@ -287,15 +299,10 @@ ActsExamples::ProcessCode ActsExamples::SeedingAlgorithmHashing::execute( std::move(grid), *m_bottomBinFinder, *m_topBinFinder, std::move(navigation)); - // safely clamp double to float - float up = Acts::clampValue( - std::floor(rRangeSPExtent.max(Acts::BinningValue::binR) / 2) * 2); - /// variable middle SP radial region of interest const Acts::Range1D rMiddleSPRange( - std::floor(rRangeSPExtent.min(Acts::BinningValue::binR) / 2) * 2 + - m_cfg.seedFinderConfig.deltaRMiddleMinSPRange, - up - m_cfg.seedFinderConfig.deltaRMiddleMaxSPRange); + minRange + m_cfg.seedFinderConfig.deltaRMiddleMinSPRange, + maxRange - m_cfg.seedFinderConfig.deltaRMiddleMaxSPRange); // this creates seeds of proxy, we need to convert it to seed of space // points diff --git a/Tests/UnitTests/Core/Seeding/SeedFinderTest.cpp b/Tests/UnitTests/Core/Seeding/SeedFinderTest.cpp index 4f263e693f7..08bec8a4d92 100644 --- a/Tests/UnitTests/Core/Seeding/SeedFinderTest.cpp +++ b/Tests/UnitTests/Core/Seeding/SeedFinderTest.cpp @@ -10,7 +10,6 @@ #include "Acts/Definitions/Units.hpp" #include "Acts/EventData/Seed.hpp" #include "Acts/EventData/SpacePointContainer.hpp" -#include "Acts/Geometry/Extent.hpp" #include "Acts/Seeding/BinnedGroup.hpp" #include "Acts/Seeding/SeedFilter.hpp" #include "Acts/Seeding/SeedFilterConfig.hpp" @@ -173,9 +172,6 @@ int main(int argc, char** argv) { int numPhiNeighbors = 1; - // extent used to store r range for middle spacepoint - Acts::Extent rRangeSPExtent; - config.useVariableMiddleSPRange = false; const Acts::Range1D rMiddleSPRange; @@ -213,8 +209,7 @@ int main(int argc, char** argv) { Acts::CylindricalSpacePointGridCreator::createGrid(gridConf, gridOpts); Acts::CylindricalSpacePointGridCreator::fillGrid( - config, options, grid, spContainer.begin(), spContainer.end(), - rRangeSPExtent); + config, options, grid, spContainer.begin(), spContainer.end()); auto spGroup = Acts::CylindricalBinnedGroup( std::move(grid), *bottomBinFinder, *topBinFinder); From 68d0709b40cc7a8dd41f09beda2534d333586efb Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Fri, 4 Oct 2024 17:11:58 +0200 Subject: [PATCH 16/24] feat: Gen 3 portal shells (#3564) Part of #3502 Blocked by: - https://github.com/acts-project/acts/pull/3673 --- This pull request introduces the new `PortalShell` classes to the Acts geometry module, along with corresponding unit tests. The changes are primarily focused on adding new functionality for handling portal shells in tracking volumes. ### New Functionality: * [`Core/include/Acts/Geometry/PortalShell.hpp`](diffhunk://#diff-80595cf723b4c4b0a2cf3de28ac0da38793f2955a2e0ce1dc4fd87381fac79aeR1-R115): Introduced new `PortalShell` classes, including `PortalShellBase`, `CylinderPortalShell`, `SingleCylinderPortalShell`, and `CylinderStackPortalShell`. These classes provide interfaces and implementations for managing portals within cylindrical tracking volumes. * [`Core/src/Geometry/PortalShell.cpp`](diffhunk://#diff-4d9e1b25351b7d20e1dbdc440060aa02eaab832aabfde0b621c44bf8241ca7b8R1-R387): Implemented the methods for the new `PortalShell` classes, including portal management and validation logic. ### Build System Updates: * [`Core/src/Geometry/CMakeLists.txt`](diffhunk://#diff-5d46d063bba89d4f5042c2ba4cdfbdcb77335367cb0ce9800dd5d036011e2c56R45): Added the new `PortalShell.cpp` source file to the build system. * [`Tests/UnitTests/Core/Geometry/CMakeLists.txt`](diffhunk://#diff-569c5da4fca89fb082a2e207221e8d00cde5d6352c26878d8df0e11ef9f148eeR37): Added a new unit test for `PortalShell` to ensure the new functionality is properly tested. --- Core/include/Acts/Geometry/PortalShell.hpp | 225 +++++ Core/src/Geometry/CMakeLists.txt | 1 + Core/src/Geometry/PortalShell.cpp | 388 +++++++++ Tests/UnitTests/Core/Geometry/CMakeLists.txt | 1 + .../Core/Geometry/PortalShellTests.cpp | 768 ++++++++++++++++++ 5 files changed, 1383 insertions(+) create mode 100644 Core/include/Acts/Geometry/PortalShell.hpp create mode 100644 Core/src/Geometry/PortalShell.cpp create mode 100644 Tests/UnitTests/Core/Geometry/PortalShellTests.cpp diff --git a/Core/include/Acts/Geometry/PortalShell.hpp b/Core/include/Acts/Geometry/PortalShell.hpp new file mode 100644 index 00000000000..39e60b8774b --- /dev/null +++ b/Core/include/Acts/Geometry/PortalShell.hpp @@ -0,0 +1,225 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma once + +#include "Acts/Geometry/BoundarySurfaceFace.hpp" +#include "Acts/Utilities/BinningType.hpp" +#include "Acts/Utilities/Logger.hpp" + +#include +#include + +namespace Acts { + +class Portal; +class TrackingVolume; +class GeometryContext; + +/// @class PortalShellBase +/// The portal shell of a volume is the set of portals that describes +/// connections "outside" of the volume. Which boundary surfaces of a volume are +/// included in the shell depends on the volume. Portal shells are only used +/// during geometry construction, and are not part of the final geometry +/// description. +/// +/// This class is the base class for all portal shells +class PortalShellBase { + public: + /// Virtusl destructor + virtual ~PortalShellBase() = default; + + /// Connect a volume to the outer side of all portal shells. Which "side" is + /// "outer" depends on the volume type. + /// This method essentially creates a @c TrivialPortalLink on the unconnected + /// side of each portal that is part of the chell + /// @param volume The volume to connect + virtual void connectOuter(TrackingVolume& volume) = 0; + + /// Get the number of portals in the shell. This number depends on the volume + /// type + /// @return The number of portals in the shell + virtual std::size_t size() const = 0; + + /// Instruct the shell to register the portals with the volume, handing over + /// shared ownership in the process. + /// @note The target volume depends on the shell type, e.g. composite shells + /// like the @c CylinerStackPortalShell register portals to the *correct* + /// volumes. + virtual void applyToVolume() = 0; + + /// Check if a portal is *valid*, e.g. if non of the portals has two + /// unconnected sides. + /// @return True if the shell is valid, false otherwise + virtual bool isValid() const = 0; + + /// Get a label for the portal shell for debugging purposes + /// @return A label for the portal shell + virtual std::string label() const = 0; +}; + +/// @class CylinderPortalShell +/// Base class for cylinder shaped portal shells, e.g. shells for cylinder +/// volumes +class CylinderPortalShell : public PortalShellBase { + public: + /// Enum describing the possible faces of a cylinder portal shell + /// @note These values are synchronized with the BoundarySurfaceFace enum. + /// Once Gen1 is removed, this can be changed. + enum class Face : unsigned int { + PositiveDisc = BoundarySurfaceFace::positiveFaceXY, + NegativeDisc = BoundarySurfaceFace::negativeFaceXY, + OuterCylinder = BoundarySurfaceFace::tubeOuterCover, + InnerCylinder = BoundarySurfaceFace::tubeInnerCover, + NegativePhiPlane = BoundarySurfaceFace::tubeSectorNegativePhi, + PositivePhiPlane = BoundarySurfaceFace::tubeSectorPositivePhi + }; + + using enum Face; + + /// Retrieve the portal associated to the given face. Can be nullptr if unset. + /// @param face The face to retrieve the portal for + /// @return The portal associated to the face + virtual Portal* portal(Face face) = 0; + + /// Retrieve a shared_ptr for the portal associated to the given face. Can be + /// nullptr if unset. + /// @param face The face to retrieve the portal for + /// @return The portal associated to the face + virtual std::shared_ptr portalPtr(Face face) = 0; + + /// Set the portal associated to the given face. + /// @param portal The portal to set + /// @param face The face to set the portal + virtual void setPortal(std::shared_ptr portal, Face face) = 0; + + /// @copydoc PortalShellBase::connectOuter + void connectOuter(TrackingVolume& volume) override; +}; + +/// Output stream operator for the CylinderPortalShell::Face enum +/// @param os The output stream +/// @param face The face to output +/// @return The output stream +std::ostream& operator<<(std::ostream& os, CylinderPortalShell::Face face); + +/// @class SingleCylinderPortalShell +/// This class describes a cylinder shell containing a single volume. The +/// available faces depend on the configuration of the cylinder volume bounds. +/// If a phi sector is configured, the shell will have corresponding portal +/// slots. If the inner radius is non-zero, the shell will have an inner +/// cylinder portal slot. +class SingleCylinderPortalShell : public CylinderPortalShell { + public: + /// Construct a single cylinder portal shell for the given volume + /// @param volume The volume to create the shell for + explicit SingleCylinderPortalShell(TrackingVolume& volume); + + /// @copydoc PortalShellBase::size + std::size_t size() const final; + + /// @copydoc CylinderPortalShell::portal + Portal* portal(Face face) final; + + /// @copydoc CylinderPortalShell::portalPtr + std::shared_ptr portalPtr(Face face) final; + + /// @copydoc CylinderPortalShell::setPortal + void setPortal(std::shared_ptr portal, Face face) final; + + /// @copydoc PortalShellBase::applyToVolume + void applyToVolume() override; + + /// @copydoc PortalShellBase::isValid + bool isValid() const override; + + /// @copydoc PortalShellBase::label + std::string label() const override; + + private: + std::array, 6> m_portals{}; + + TrackingVolume* m_volume; +}; + +/// @class CylinderStackPortalShell +/// This class describes a cylinder shell containing multiple volumes. The +/// available faces depend on the configuration of the cylinder volume bounds. +/// @note The stack shell currently does not support phi sectors +/// The stack can be oriented along the (local) z or r direction, which drives +/// the stacking. Depending on the direction, portals on the shells of children +/// are merged or fused. Subsequently, portal access respects shared portals +/// between shells. Below is an illustration of a stack in the r direction: +/// +/// Fused +-----------------+ +/// portals ----+ | | +/// | | v OuterCylinder +/// | +------+------+ +/// | | | | +/// | | | |<--+ +/// +--+---+ v | | +/// +---+---------+ | +/// | | | | Shared portal +/// | | |<--+--- (grid) +/// | v | | PositiveDisc +/// +-------------+ | +/// r ^ | | | +/// | | |<--+ +/// | | | +/// | +-------------+ InnerCylinder +/// +-----> ^ (if rMin>0) +/// z | | +/// +-----------------+ +/// +/// @note The shells must be ordered in the given direction +/// Depending on the stack direction, the portal lookup will return different +/// portals. In the illustration above, the `PositiveDisc` portal is shared +/// among all shells, while the `OuterCylinder` and `InnerCylinder` portals are +/// looked up from the innermost and outermost shell in the r direction. +class CylinderStackPortalShell : public CylinderPortalShell { + public: + /// Construct the portal shell stack from the given shells + /// @param gctx The geometry context + /// @param shells The shells to stack + /// @note The shells must be ordered in the given direction + /// @param direction The stacking direction + /// @param logger A logging instance for debugging + CylinderStackPortalShell(const GeometryContext& gctx, + std::vector shells, + BinningValue direction, + const Logger& logger = getDummyLogger()); + + /// @copydoc PortalShellBase::size + std::size_t size() const final; + + /// @copydoc CylinderPortalShell::portal + Portal* portal(Face face) final; + + /// @copydoc CylinderPortalShell::portalPtr + std::shared_ptr portalPtr(Face face) final; + + /// @copydoc CylinderPortalShell::setPortal + void setPortal(std::shared_ptr portal, Face face) final; + + void applyToVolume() override { + // No-op, because it's a composite portal shell + } + + /// @copydoc PortalShellBase::isValid + bool isValid() const override; + + /// @copydoc PortalShellBase::label + std::string label() const override; + + private: + BinningValue m_direction; + std::vector m_shells; + bool m_hasInnerCylinder{true}; +}; + +} // namespace Acts diff --git a/Core/src/Geometry/CMakeLists.txt b/Core/src/Geometry/CMakeLists.txt index fa5d70c1c02..74f6e0e126d 100644 --- a/Core/src/Geometry/CMakeLists.txt +++ b/Core/src/Geometry/CMakeLists.txt @@ -42,4 +42,5 @@ target_sources( CompositePortalLink.cpp PortalLinkBase.cpp PortalError.cpp + PortalShell.cpp ) diff --git a/Core/src/Geometry/PortalShell.cpp b/Core/src/Geometry/PortalShell.cpp new file mode 100644 index 00000000000..59c83c1529f --- /dev/null +++ b/Core/src/Geometry/PortalShell.cpp @@ -0,0 +1,388 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "Acts/Geometry/PortalShell.hpp" + +#include "Acts/Geometry/BoundarySurfaceFace.hpp" +#include "Acts/Geometry/CylinderVolumeBounds.hpp" +#include "Acts/Geometry/Portal.hpp" +#include "Acts/Geometry/PortalLinkBase.hpp" +#include "Acts/Geometry/TrackingVolume.hpp" + +#include +#include +#include + +#include + +namespace Acts { + +void CylinderPortalShell::connectOuter(TrackingVolume& volume) { + for (Face face : {PositiveDisc, NegativeDisc, OuterCylinder, InnerCylinder, + NegativePhiPlane, PositivePhiPlane}) { + const auto& portalAtFace = portalPtr(face); + if (portalAtFace != nullptr) { + portalAtFace->fill(volume); + volume.addPortal(portalAtFace); + } + } +} + +SingleCylinderPortalShell::SingleCylinderPortalShell(TrackingVolume& volume) + : m_volume{&volume} { + if (m_volume->volumeBounds().type() != VolumeBounds::BoundsType::eCylinder) { + throw std::invalid_argument("Invalid volume bounds type"); + } + + const auto& bounds = + dynamic_cast(m_volume->volumeBounds()); + + std::vector orientedSurfaces = + bounds.orientedSurfaces(m_volume->transform()); + + auto handle = [&](Face face, std::size_t from) { + const auto& source = orientedSurfaces.at(from); + m_portals.at(toUnderlying(face)) = + std::make_shared(source.direction, source.surface, *m_volume); + }; + + if (orientedSurfaces.size() == 6) { + // Fully equipped cylinder + handle(PositiveDisc, positiveFaceXY); + handle(NegativeDisc, negativeFaceXY); + handle(OuterCylinder, tubeOuterCover); + handle(InnerCylinder, tubeInnerCover); + handle(NegativePhiPlane, tubeSectorNegativePhi); + handle(PositivePhiPlane, tubeSectorPositivePhi); + } else if (orientedSurfaces.size() == 5) { + // Phi sector but no inner cylinder (rMin == 0) + handle(PositiveDisc, positiveFaceXY); + handle(NegativeDisc, negativeFaceXY); + handle(OuterCylinder, tubeOuterCover); + // Skip inner tube cover, requires offsetting + handle(NegativePhiPlane, tubeSectorNegativePhi - 1); + handle(PositivePhiPlane, tubeSectorPositivePhi - 1); + } else if (orientedSurfaces.size() == 4) { + // No phi sector but rMin > 0 + handle(PositiveDisc, positiveFaceXY); + handle(NegativeDisc, negativeFaceXY); + handle(OuterCylinder, tubeOuterCover); + handle(InnerCylinder, tubeInnerCover); + } else if (orientedSurfaces.size() == 3) { + // No phi sector and rMin == 0 + handle(PositiveDisc, positiveFaceXY); + handle(NegativeDisc, negativeFaceXY); + handle(OuterCylinder, tubeOuterCover); + } else { + throw std::invalid_argument("Invalid number of oriented surfaces"); + } +} + +Portal* SingleCylinderPortalShell::portal(Face face) { + return portalPtr(face).get(); +} + +std::shared_ptr SingleCylinderPortalShell::portalPtr(Face face) { + return m_portals.at(toUnderlying(face)); +} + +void SingleCylinderPortalShell::setPortal(std::shared_ptr portal, + Face face) { + assert(portal != nullptr); + assert(portal->isValid()); + m_portals.at(toUnderlying(face)) = std::move(portal); +} + +std::size_t SingleCylinderPortalShell::size() const { + std::size_t count = 0; + std::ranges::for_each( + m_portals, [&count](const auto& portal) { count += portal ? 1 : 0; }); + return count; +} + +void SingleCylinderPortalShell::applyToVolume() { + for (std::size_t i = 0; i < m_portals.size(); i++) { + const auto& portal = m_portals.at(i); + if (portal != nullptr) { + if (!portal->isValid()) { + std::stringstream ss; + ss << static_cast(i); + throw std::runtime_error{"Invalid portal found in shell at " + + ss.str()}; + } + m_volume->addPortal(portal); + } + } +} + +bool SingleCylinderPortalShell::isValid() const { + return std::ranges::all_of(m_portals, [](const auto& portal) { + return portal == nullptr || portal->isValid(); + }); +}; + +std::string SingleCylinderPortalShell::label() const { + std::stringstream ss; + ss << "CylinderShell(vol=" << m_volume->volumeName() << ")"; + return ss.str(); +} + +CylinderStackPortalShell::CylinderStackPortalShell( + const GeometryContext& gctx, std::vector shells, + BinningValue direction, const Logger& logger) + : m_direction{direction}, m_shells{std::move(shells)} { + ACTS_VERBOSE("Making cylinder stack shell in " << m_direction + << " direction"); + if (std::ranges::any_of(m_shells, + [](const auto* shell) { return shell == nullptr; })) { + ACTS_ERROR("Invalid shell pointer"); + throw std::invalid_argument("Invalid shell pointer"); + } + + ACTS_VERBOSE(" ~> " << label()); + + if (!std::ranges::all_of( + m_shells, [](const auto* shell) { return shell->isValid(); })) { + ACTS_ERROR("Invalid shell"); + throw std::invalid_argument("Invalid shell"); + } + + auto merge = [&](Face face) { + std::vector> portals; + std::ranges::transform( + m_shells, std::back_inserter(portals), + [face](auto* shell) { return shell->portalPtr(face); }); + + auto merged = std::accumulate( + std::next(portals.begin()), portals.end(), portals.front(), + [&](const auto& aPortal, + const auto& bPortal) -> std::shared_ptr { + assert(aPortal != nullptr); + assert(bPortal != nullptr); + + return std::make_shared( + Portal::merge(gctx, *aPortal, *bPortal, direction, logger)); + }); + + assert(merged != nullptr); + assert(merged->isValid()); + + // reset merged portal on all shells + for (auto& shell : m_shells) { + shell->setPortal(merged, face); + } + }; + + auto fuse = [&](Face faceA, Face faceB) { + for (std::size_t i = 1; i < m_shells.size(); i++) { + auto& shellA = m_shells.at(i - 1); + auto& shellB = m_shells.at(i); + ACTS_VERBOSE("Fusing " << shellA->label() << " and " << shellB->label()); + + auto fused = std::make_shared(Portal::fuse( + gctx, *shellA->portalPtr(faceA), *shellB->portalPtr(faceB), logger)); + + assert(fused != nullptr && "Invalid fused portal"); + assert(fused->isValid() && "Fused portal is invalid"); + + shellA->setPortal(fused, faceA); + shellB->setPortal(fused, faceB); + + assert(shellA->isValid() && "Shell A is not valid after fusing"); + assert(shellB->isValid() && "Shell B is not valid after fusing"); + } + }; + + if (direction == BinningValue::binR) { + ACTS_VERBOSE("Merging portals at positive and negative discs"); + merge(PositiveDisc); + merge(NegativeDisc); + + ACTS_VERBOSE("Fusing portals at outer and inner cylinders"); + fuse(OuterCylinder, InnerCylinder); + + } else if (direction == BinningValue::binZ) { + bool allHaveInnerCylinders = std::ranges::all_of( + m_shells, [](const auto* shell) { return shell->size() == 4; }); + + bool noneHaveInnerCylinders = std::ranges::all_of( + m_shells, [](const auto* shell) { return shell->size() == 3; }); + + if (!allHaveInnerCylinders && !noneHaveInnerCylinders) { + ACTS_ERROR("Invalid inner cylinder configuration"); + throw std::invalid_argument("Invalid inner cylinder configuration"); + } + + m_hasInnerCylinder = allHaveInnerCylinders; + + ACTS_VERBOSE("Merging portals at outer cylinders"); + merge(OuterCylinder); + assert(isValid() && "Shell is not valid after outer merging"); + + if (m_hasInnerCylinder) { + ACTS_VERBOSE("Merging portals at inner cylinders"); + merge(InnerCylinder); + assert(isValid() && "Shell is not valid after inner merging"); + } + + ACTS_VERBOSE("Fusing portals at positive and negative discs"); + fuse(PositiveDisc, NegativeDisc); + assert(isValid() && "Shell is not valid after disc fusing"); + + } else { + throw std::invalid_argument("Invalid direction"); + } + + assert(isValid() && "Shell is not valid after construction"); +} + +std::size_t CylinderStackPortalShell::size() const { + return m_hasInnerCylinder ? 4 : 3; +} + +Portal* CylinderStackPortalShell::portal(Face face) { + return portalPtr(face).get(); +} + +std::shared_ptr CylinderStackPortalShell::portalPtr(Face face) { + if (m_direction == BinningValue::binR) { + switch (face) { + case NegativeDisc: + return m_shells.front()->portalPtr(NegativeDisc); + case PositiveDisc: + return m_shells.front()->portalPtr(PositiveDisc); + case OuterCylinder: + return m_shells.back()->portalPtr(OuterCylinder); + case InnerCylinder: + return m_shells.front()->portalPtr(InnerCylinder); + case NegativePhiPlane: + [[fallthrough]]; + case PositivePhiPlane: + return nullptr; + default: + std::stringstream ss; + ss << "Invalid face: " << face; + throw std::invalid_argument(ss.str()); + } + + } else { + switch (face) { + case NegativeDisc: + return m_shells.front()->portalPtr(NegativeDisc); + case PositiveDisc: + return m_shells.back()->portalPtr(PositiveDisc); + case OuterCylinder: + [[fallthrough]]; + case InnerCylinder: + return m_shells.front()->portalPtr(face); + case NegativePhiPlane: + [[fallthrough]]; + case PositivePhiPlane: + return nullptr; + default: + std::stringstream ss; + ss << "Invalid face: " << face; + throw std::invalid_argument(ss.str()); + } + } +} + +void CylinderStackPortalShell::setPortal(std::shared_ptr portal, + Face face) { + assert(portal != nullptr); + + if (m_direction == BinningValue::binR) { + switch (face) { + case NegativeDisc: + [[fallthrough]]; + case PositiveDisc: + for (auto* shell : m_shells) { + shell->setPortal(portal, face); + } + break; + case OuterCylinder: + m_shells.back()->setPortal(std::move(portal), OuterCylinder); + break; + case InnerCylinder: + if (!m_hasInnerCylinder) { + throw std::invalid_argument("Inner cylinder not available"); + } + m_shells.front()->setPortal(std::move(portal), InnerCylinder); + break; + default: + std::stringstream ss; + ss << "Invalid face: " << face; + throw std::invalid_argument(ss.str()); + } + + } else { + switch (face) { + case NegativeDisc: + m_shells.front()->setPortal(std::move(portal), NegativeDisc); + break; + case PositiveDisc: + m_shells.back()->setPortal(std::move(portal), PositiveDisc); + break; + case InnerCylinder: + if (!m_hasInnerCylinder) { + throw std::invalid_argument("Inner cylinder not available"); + } + [[fallthrough]]; + case OuterCylinder: + for (auto* shell : m_shells) { + shell->setPortal(portal, face); + } + break; + default: + std::stringstream ss; + ss << "Invalid face: " << face; + throw std::invalid_argument(ss.str()); + } + } +} + +bool CylinderStackPortalShell::isValid() const { + return std::ranges::all_of(m_shells, [](const auto* shell) { + assert(shell != nullptr); + return shell->isValid(); + }); +} + +std::string CylinderStackPortalShell::label() const { + std::stringstream ss; + ss << "CylinderStackShell(dir=" << m_direction << ", children="; + + std::vector labels; + std::ranges::transform(m_shells, std::back_inserter(labels), + [](const auto* shell) { return shell->label(); }); + ss << boost::algorithm::join(labels, "|"); + ss << ")"; + return ss.str(); +} + +std::ostream& operator<<(std::ostream& os, CylinderPortalShell::Face face) { + switch (face) { + using enum CylinderPortalShell::Face; + case PositiveDisc: + return os << "PositiveDisc"; + case NegativeDisc: + return os << "NegativeDisc"; + case OuterCylinder: + return os << "OuterCylinder"; + case InnerCylinder: + return os << "InnerCylinder"; + case NegativePhiPlane: + return os << "NegativePhiPlane"; + case PositivePhiPlane: + return os << "PositivePhiPlane"; + default: + return os << "Invalid face"; + } +} + +} // namespace Acts diff --git a/Tests/UnitTests/Core/Geometry/CMakeLists.txt b/Tests/UnitTests/Core/Geometry/CMakeLists.txt index 1fa422bb0a6..0226e46d7d0 100644 --- a/Tests/UnitTests/Core/Geometry/CMakeLists.txt +++ b/Tests/UnitTests/Core/Geometry/CMakeLists.txt @@ -34,3 +34,4 @@ add_unittest(Volume VolumeTests.cpp) add_unittest(CylinderVolumeStack CylinderVolumeStackTests.cpp) add_unittest(PortalLink PortalLinkTests.cpp) add_unittest(Portal PortalTests.cpp) +add_unittest(PortalShell PortalShellTests.cpp) diff --git a/Tests/UnitTests/Core/Geometry/PortalShellTests.cpp b/Tests/UnitTests/Core/Geometry/PortalShellTests.cpp new file mode 100644 index 00000000000..4c44617524a --- /dev/null +++ b/Tests/UnitTests/Core/Geometry/PortalShellTests.cpp @@ -0,0 +1,768 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include + +#include "Acts/Definitions/Units.hpp" +#include "Acts/Geometry/CuboidVolumeBounds.hpp" +#include "Acts/Geometry/CutoutCylinderVolumeBounds.hpp" +#include "Acts/Geometry/CylinderVolumeBounds.hpp" +#include "Acts/Geometry/GridPortalLink.hpp" +#include "Acts/Geometry/Portal.hpp" +#include "Acts/Geometry/PortalShell.hpp" +#include "Acts/Geometry/TrackingVolume.hpp" +#include "Acts/Geometry/TrivialPortalLink.hpp" +#include "Acts/Surfaces/SurfaceMergingException.hpp" + +#include + +using namespace Acts::UnitLiterals; + +namespace Acts::Test { +GeometryContext gctx; + +std::size_t getVolumeIndex() { + static std::size_t i = 1; + return i++; +} + +auto makeVolume(auto&&... pars) { + TrackingVolume vol(Transform3::Identity(), + std::make_shared( + std::forward(pars)...)); + vol.setVolumeName("cyl" + std::to_string(getVolumeIndex())); + return vol; +}; + +auto logger = Acts::getDefaultLogger("UnitTests", Acts::Logging::VERBOSE); + +BOOST_AUTO_TEST_SUITE(PortalShellTests) + +BOOST_AUTO_TEST_CASE(ConstructionFromVolume) { + // - Cylinder + // | | no phi | phi | + // | --------- | ------ | --- | + // | rMin > 0 | 1 | 3 | + // | rMin == 0 | 2 | 4 | + + auto cyl1 = makeVolume(30_mm, 40_mm, 100_mm); + auto cyl2 = makeVolume(0_mm, 40_mm, 100_mm); + auto cyl3 = makeVolume(30_mm, 40_mm, 100_mm, 45_degree); + auto cyl4 = makeVolume(0_mm, 40_mm, 100_mm, 45_degree); + + TrackingVolume boxVolume( + Transform3::Identity(), + std::make_shared(10_mm, 10_mm, 10_mm)); + + BOOST_CHECK_THROW(SingleCylinderPortalShell{boxVolume}, + std::invalid_argument); + + SingleCylinderPortalShell shell1{cyl1}; + BOOST_CHECK_EQUAL(shell1.size(), 4); + + using enum CylinderPortalShell::Face; + + const auto* pDisc = shell1.portal(PositiveDisc); + BOOST_REQUIRE_NE(pDisc, nullptr); + BOOST_CHECK_EQUAL( + pDisc + ->resolveVolume(gctx, Vector3{35_mm, 0_mm, 100_mm}, -Vector3::UnitZ()) + .value(), + &cyl1); + BOOST_CHECK_EQUAL( + pDisc->resolveVolume(gctx, Vector3{35_mm, 0_mm, 100_mm}, Vector3::UnitZ()) + .value(), + nullptr); + + const auto* nDisc = shell1.portal(NegativeDisc); + BOOST_REQUIRE_NE(nDisc, nullptr); + BOOST_CHECK_EQUAL(nDisc + ->resolveVolume(gctx, Vector3{35_mm, 0_mm, -100_mm}, + -Vector3::UnitZ()) + .value(), + nullptr); + BOOST_CHECK_EQUAL( + nDisc + ->resolveVolume(gctx, Vector3{35_mm, 0_mm, -100_mm}, Vector3::UnitZ()) + .value(), + &cyl1); + + const auto* oCyl = shell1.portal(OuterCylinder); + BOOST_REQUIRE_NE(oCyl, nullptr); + BOOST_CHECK_EQUAL( + oCyl->resolveVolume(gctx, Vector3{40_mm, 0_mm, 10_mm}, Vector3::UnitX()) + .value(), + nullptr); + BOOST_CHECK_EQUAL( + oCyl->resolveVolume(gctx, Vector3{40_mm, 0_mm, 10_mm}, -Vector3::UnitX()) + .value(), + &cyl1); + + const auto* iCyl = shell1.portal(InnerCylinder); + BOOST_REQUIRE_NE(iCyl, nullptr); + BOOST_CHECK_EQUAL( + iCyl->resolveVolume(gctx, Vector3{30_mm, 0_mm, 10_mm}, Vector3::UnitX()) + .value(), + &cyl1); + BOOST_CHECK_EQUAL( + iCyl->resolveVolume(gctx, Vector3{30_mm, 0_mm, 10_mm}, -Vector3::UnitX()) + .value(), + nullptr); + + SingleCylinderPortalShell shell2{cyl2}; + BOOST_CHECK_EQUAL(shell2.size(), 3); + + pDisc = shell2.portal(PositiveDisc); + BOOST_REQUIRE_NE(pDisc, nullptr); + BOOST_CHECK_EQUAL( + pDisc + ->resolveVolume(gctx, Vector3{35_mm, 0_mm, 100_mm}, -Vector3::UnitZ()) + .value(), + &cyl2); + BOOST_CHECK_EQUAL( + pDisc->resolveVolume(gctx, Vector3{35_mm, 0_mm, 100_mm}, Vector3::UnitZ()) + .value(), + nullptr); + + nDisc = shell2.portal(NegativeDisc); + BOOST_REQUIRE_NE(nDisc, nullptr); + BOOST_CHECK_EQUAL(nDisc + ->resolveVolume(gctx, Vector3{35_mm, 0_mm, -100_mm}, + -Vector3::UnitZ()) + .value(), + nullptr); + BOOST_CHECK_EQUAL( + nDisc + ->resolveVolume(gctx, Vector3{35_mm, 0_mm, -100_mm}, Vector3::UnitZ()) + .value(), + &cyl2); + + oCyl = shell2.portal(OuterCylinder); + BOOST_REQUIRE_NE(oCyl, nullptr); + BOOST_CHECK_EQUAL( + oCyl->resolveVolume(gctx, Vector3{40_mm, 0_mm, 10_mm}, Vector3::UnitX()) + .value(), + nullptr); + BOOST_CHECK_EQUAL( + oCyl->resolveVolume(gctx, Vector3{40_mm, 0_mm, 10_mm}, -Vector3::UnitX()) + .value(), + &cyl2); + + iCyl = shell2.portal(InnerCylinder); + BOOST_CHECK_EQUAL(iCyl, nullptr); + + SingleCylinderPortalShell shell3{cyl3}; + BOOST_CHECK_EQUAL(shell3.size(), 6); + + pDisc = shell3.portal(PositiveDisc); + BOOST_REQUIRE_NE(pDisc, nullptr); + BOOST_CHECK_EQUAL( + pDisc + ->resolveVolume(gctx, Vector3{35_mm, 0_mm, 100_mm}, -Vector3::UnitZ()) + .value(), + &cyl3); + BOOST_CHECK_EQUAL( + pDisc->resolveVolume(gctx, Vector3{35_mm, 0_mm, 100_mm}, Vector3::UnitZ()) + .value(), + nullptr); + + nDisc = shell3.portal(NegativeDisc); + BOOST_REQUIRE_NE(nDisc, nullptr); + BOOST_CHECK_EQUAL(nDisc + ->resolveVolume(gctx, Vector3{35_mm, 0_mm, -100_mm}, + -Vector3::UnitZ()) + .value(), + nullptr); + BOOST_CHECK_EQUAL( + nDisc + ->resolveVolume(gctx, Vector3{35_mm, 0_mm, -100_mm}, Vector3::UnitZ()) + .value(), + &cyl3); + + oCyl = shell3.portal(OuterCylinder); + BOOST_REQUIRE_NE(oCyl, nullptr); + BOOST_CHECK_EQUAL( + oCyl->resolveVolume(gctx, Vector3{40_mm, 0_mm, 10_mm}, Vector3::UnitX()) + .value(), + nullptr); + BOOST_CHECK_EQUAL( + oCyl->resolveVolume(gctx, Vector3{40_mm, 0_mm, 10_mm}, -Vector3::UnitX()) + .value(), + &cyl3); + + iCyl = shell3.portal(InnerCylinder); + BOOST_REQUIRE_NE(iCyl, nullptr); + BOOST_CHECK_EQUAL( + iCyl->resolveVolume(gctx, Vector3{30_mm, 0_mm, 10_mm}, Vector3::UnitX()) + .value(), + &cyl3); + BOOST_CHECK_EQUAL( + iCyl->resolveVolume(gctx, Vector3{30_mm, 0_mm, 10_mm}, -Vector3::UnitX()) + .value(), + nullptr); + + auto anglePoint = [](double angle, double r, double z) { + return Vector3{r * std::cos(angle), r * std::sin(angle), z}; + }; + + const auto* nPhi = shell3.portal(NegativePhiPlane); + BOOST_REQUIRE_NE(nPhi, nullptr); + Vector3 point = anglePoint(-45_degree, 35_mm, 10_mm); + Vector3 dir = Vector3::UnitZ().cross(point).normalized(); + Vector3 idir = (-Vector3::UnitZ()).cross(point).normalized(); + BOOST_CHECK_EQUAL(nPhi->resolveVolume(gctx, point, dir).value(), nullptr); + BOOST_CHECK_EQUAL(nPhi->resolveVolume(gctx, point, idir).value(), &cyl3); + + const auto* pPhi = shell3.portal(PositivePhiPlane); + BOOST_REQUIRE_NE(pPhi, nullptr); + point = anglePoint(45_degree, 35_mm, 10_mm); + dir = Vector3::UnitZ().cross(point).normalized(); + idir = (-Vector3::UnitZ()).cross(point).normalized(); + BOOST_CHECK_EQUAL(pPhi->resolveVolume(gctx, point, dir).value(), nullptr); + BOOST_CHECK_EQUAL(pPhi->resolveVolume(gctx, point, idir).value(), &cyl3); + + SingleCylinderPortalShell shell4{cyl4}; + BOOST_CHECK_EQUAL(shell4.size(), 5); + + pDisc = shell4.portal(PositiveDisc); + BOOST_REQUIRE_NE(pDisc, nullptr); + BOOST_CHECK_EQUAL( + pDisc + ->resolveVolume(gctx, Vector3{35_mm, 0_mm, 100_mm}, -Vector3::UnitZ()) + .value(), + &cyl4); + BOOST_CHECK_EQUAL( + pDisc->resolveVolume(gctx, Vector3{35_mm, 0_mm, 100_mm}, Vector3::UnitZ()) + .value(), + nullptr); + + nDisc = shell4.portal(NegativeDisc); + BOOST_REQUIRE_NE(nDisc, nullptr); + BOOST_CHECK_EQUAL(nDisc + ->resolveVolume(gctx, Vector3{35_mm, 0_mm, -100_mm}, + -Vector3::UnitZ()) + .value(), + nullptr); + BOOST_CHECK_EQUAL( + nDisc + ->resolveVolume(gctx, Vector3{35_mm, 0_mm, -100_mm}, Vector3::UnitZ()) + .value(), + &cyl4); + + oCyl = shell4.portal(OuterCylinder); + BOOST_REQUIRE_NE(oCyl, nullptr); + BOOST_CHECK_EQUAL( + oCyl->resolveVolume(gctx, Vector3{40_mm, 0_mm, 10_mm}, Vector3::UnitX()) + .value(), + nullptr); + BOOST_CHECK_EQUAL( + oCyl->resolveVolume(gctx, Vector3{40_mm, 0_mm, 10_mm}, -Vector3::UnitX()) + .value(), + &cyl4); + + iCyl = shell4.portal(InnerCylinder); + BOOST_CHECK_EQUAL(iCyl, nullptr); + + nPhi = shell4.portal(NegativePhiPlane); + BOOST_REQUIRE_NE(nPhi, nullptr); + point = anglePoint(-45_degree, 35_mm, 10_mm); + dir = Vector3::UnitZ().cross(point).normalized(); + idir = (-Vector3::UnitZ()).cross(point).normalized(); + BOOST_CHECK_EQUAL(nPhi->resolveVolume(gctx, point, dir).value(), nullptr); + BOOST_CHECK_EQUAL(nPhi->resolveVolume(gctx, point, idir).value(), &cyl4); + + pPhi = shell4.portal(PositivePhiPlane); + BOOST_REQUIRE_NE(pPhi, nullptr); + point = anglePoint(45_degree, 35_mm, 10_mm); + dir = Vector3::UnitZ().cross(point).normalized(); + idir = (-Vector3::UnitZ()).cross(point).normalized(); + BOOST_CHECK_EQUAL(pPhi->resolveVolume(gctx, point, dir).value(), nullptr); + BOOST_CHECK_EQUAL(pPhi->resolveVolume(gctx, point, idir).value(), &cyl4); +} + +// outer cylinder +// +----------+----------+ +// | | | +// negative | v | positive +// disc +--> <--+ disc +// | ^ | +// | | | +// +----------+----------+ +// inner cylinder + +BOOST_AUTO_TEST_CASE(PortalAssignment) { + using enum CylinderPortalShell::Face; + TrackingVolume vol( + Transform3::Identity(), + std::make_shared(30_mm, 100_mm, 100_mm)); + + SingleCylinderPortalShell shell{vol}; + + const auto* iCyl = shell.portal(InnerCylinder); + const auto* pDisc = shell.portal(PositiveDisc); + auto* oCyl = shell.portal(OuterCylinder); + auto* nDisc = shell.portal(NegativeDisc); + + // Setting new outer cylinder + BOOST_REQUIRE_NE(oCyl, nullptr); + auto* oCylLink = dynamic_cast( + oCyl->getLink(Direction::OppositeNormal)); + BOOST_REQUIRE_NE(oCylLink, nullptr); + + auto grid = oCylLink->makeGrid(BinningValue::binZ); + + auto portal2 = + std::make_shared(Direction::OppositeNormal, std::move(grid)); + shell.setPortal(portal2, OuterCylinder); + BOOST_CHECK_EQUAL(shell.portal(OuterCylinder), portal2.get()); + + // Other portals should stay the same + BOOST_CHECK_EQUAL(shell.portal(InnerCylinder), iCyl); + BOOST_CHECK_EQUAL(shell.portal(PositiveDisc), pDisc); + BOOST_CHECK_EQUAL(shell.portal(NegativeDisc), nDisc); + + // Setting new negative disc + BOOST_REQUIRE_NE(nDisc, nullptr); + auto* nDiscLink = dynamic_cast( + nDisc->getLink(Direction::AlongNormal)); + BOOST_REQUIRE_NE(nDiscLink, nullptr); + + grid = nDiscLink->makeGrid(BinningValue::binR); + + auto portal3 = + std::make_shared(Direction::AlongNormal, std::move(grid)); + shell.setPortal(portal3, NegativeDisc); + BOOST_CHECK_EQUAL(shell.portal(NegativeDisc), portal3.get()); + + // Other portals should stay the same + BOOST_CHECK_EQUAL(shell.portal(OuterCylinder), portal2.get()); + BOOST_CHECK_EQUAL(shell.portal(InnerCylinder), iCyl); + BOOST_CHECK_EQUAL(shell.portal(PositiveDisc), pDisc); +} + +BOOST_AUTO_TEST_SUITE(CylinderStack) +BOOST_AUTO_TEST_CASE(ZDirection) { + using enum CylinderPortalShell::Face; + BOOST_TEST_CONTEXT("rMin>0") { + TrackingVolume vol1( + Transform3{Translation3{Vector3::UnitZ() * -100_mm}}, + std::make_shared(30_mm, 100_mm, 100_mm)); + + TrackingVolume vol2( + Transform3{Translation3{Vector3::UnitZ() * 100_mm}}, + std::make_shared(30_mm, 100_mm, 100_mm)); + + SingleCylinderPortalShell shell1{vol1}; + SingleCylinderPortalShell shell2{vol2}; + + BOOST_CHECK_NE(shell1.portal(PositiveDisc), shell2.portal(NegativeDisc)); + + CylinderStackPortalShell stack{ + gctx, {&shell1, &shell2}, BinningValue::binZ}; + BOOST_CHECK_EQUAL(stack.size(), 4); + + const auto* iCyl = stack.portal(InnerCylinder); + BOOST_CHECK_EQUAL(shell1.portal(InnerCylinder), iCyl); + BOOST_CHECK_EQUAL(shell2.portal(InnerCylinder), iCyl); + + const auto* oCyl = stack.portal(OuterCylinder); + BOOST_CHECK_EQUAL(shell1.portal(OuterCylinder), oCyl); + BOOST_CHECK_EQUAL(shell2.portal(OuterCylinder), oCyl); + + BOOST_CHECK_EQUAL(stack.portal(PositiveDisc), shell2.portal(PositiveDisc)); + BOOST_CHECK_EQUAL(stack.portal(NegativeDisc), shell1.portal(NegativeDisc)); + + BOOST_CHECK_EQUAL(stack.portal(NegativePhiPlane), nullptr); + BOOST_CHECK_EQUAL(stack.portalPtr(NegativePhiPlane), nullptr); + + BOOST_CHECK_EQUAL(stack.portal(PositivePhiPlane), nullptr); + BOOST_CHECK_EQUAL(stack.portalPtr(PositivePhiPlane), nullptr); + + // Disc portals have been fused + BOOST_CHECK_EQUAL(shell1.portal(PositiveDisc), shell2.portal(NegativeDisc)); + + shell1 = SingleCylinderPortalShell{vol1}; + shell2 = SingleCylinderPortalShell{vol2}; + + BOOST_CHECK_THROW( + CylinderStackPortalShell(gctx, {&shell1, &shell2}, BinningValue::binR), + SurfaceMergingException); + } + + BOOST_TEST_CONTEXT("rMin==0") { + TrackingVolume vol1( + Transform3{Translation3{Vector3::UnitZ() * -100_mm}}, + std::make_shared(0_mm, 100_mm, 100_mm)); + + TrackingVolume vol2( + Transform3{Translation3{Vector3::UnitZ() * 100_mm}}, + std::make_shared(0_mm, 100_mm, 100_mm)); + + SingleCylinderPortalShell shell1{vol1}; + SingleCylinderPortalShell shell2{vol2}; + + BOOST_CHECK_EQUAL(shell1.portal(InnerCylinder), nullptr); + BOOST_CHECK_EQUAL(shell2.portal(InnerCylinder), nullptr); + + BOOST_CHECK_NE(shell1.portal(PositiveDisc), shell2.portal(NegativeDisc)); + + CylinderStackPortalShell stack{ + gctx, {&shell1, &shell2}, BinningValue::binZ}; + BOOST_CHECK_EQUAL(stack.size(), 3); + + // Disc portals have been fused + BOOST_CHECK_EQUAL(shell1.portal(PositiveDisc), shell2.portal(NegativeDisc)); + + const auto* iCyl = stack.portal(InnerCylinder); + BOOST_CHECK_EQUAL(iCyl, nullptr); + + const auto* oCyl = stack.portal(OuterCylinder); + BOOST_CHECK_EQUAL(shell1.portal(OuterCylinder), oCyl); + BOOST_CHECK_EQUAL(shell2.portal(OuterCylinder), oCyl); + + BOOST_CHECK_EQUAL(stack.portal(PositiveDisc), shell2.portal(PositiveDisc)); + BOOST_CHECK_EQUAL(stack.portal(NegativeDisc), shell1.portal(NegativeDisc)); + + BOOST_CHECK_EQUAL(stack.portal(NegativePhiPlane), nullptr); + BOOST_CHECK_EQUAL(stack.portalPtr(NegativePhiPlane), nullptr); + + BOOST_CHECK_EQUAL(stack.portal(PositivePhiPlane), nullptr); + BOOST_CHECK_EQUAL(stack.portalPtr(PositivePhiPlane), nullptr); + + shell1 = SingleCylinderPortalShell{vol1}; + shell2 = SingleCylinderPortalShell{vol2}; + + BOOST_CHECK_THROW( + CylinderStackPortalShell(gctx, {&shell1, &shell2}, BinningValue::binR), + SurfaceMergingException); + } +} + +BOOST_AUTO_TEST_CASE(RDirection) { + using enum CylinderPortalShell::Face; + BOOST_TEST_CONTEXT("rMin>0") { + TrackingVolume vol1( + Transform3::Identity(), + std::make_shared(30_mm, 100_mm, 100_mm)); + + TrackingVolume vol2( + Transform3::Identity(), + std::make_shared(100_mm, 150_mm, 100_mm)); + + SingleCylinderPortalShell shell1{vol1}; + SingleCylinderPortalShell shell2{vol2}; + + BOOST_CHECK_NE(shell1.portal(OuterCylinder), shell2.portal(InnerCylinder)); + + CylinderStackPortalShell stack{ + gctx, {&shell1, &shell2}, BinningValue::binR}; + BOOST_CHECK_EQUAL(stack.size(), 4); + + // Internal cylinder portals have been fused + BOOST_CHECK_EQUAL(shell1.portal(OuterCylinder), + shell2.portal(InnerCylinder)); + + const auto* nDisc = stack.portal(NegativeDisc); + const auto* pDisc = stack.portal(PositiveDisc); + + BOOST_CHECK_EQUAL(shell1.portal(NegativeDisc), nDisc); + BOOST_CHECK_EQUAL(shell2.portal(NegativeDisc), nDisc); + BOOST_CHECK_EQUAL(shell1.portal(PositiveDisc), pDisc); + BOOST_CHECK_EQUAL(shell2.portal(PositiveDisc), pDisc); + + BOOST_CHECK_EQUAL(stack.portal(InnerCylinder), + shell1.portal(InnerCylinder)); + BOOST_CHECK_EQUAL(stack.portal(OuterCylinder), + shell2.portal(OuterCylinder)); + + BOOST_CHECK_EQUAL(stack.portal(NegativePhiPlane), nullptr); + BOOST_CHECK_EQUAL(stack.portalPtr(NegativePhiPlane), nullptr); + + BOOST_CHECK_EQUAL(stack.portal(PositivePhiPlane), nullptr); + BOOST_CHECK_EQUAL(stack.portalPtr(PositivePhiPlane), nullptr); + + shell1 = SingleCylinderPortalShell{vol1}; + shell2 = SingleCylinderPortalShell{vol2}; + + BOOST_CHECK_THROW( + CylinderStackPortalShell(gctx, {&shell1, &shell2}, BinningValue::binZ), + SurfaceMergingException); + } + + BOOST_TEST_CONTEXT("rMin==0") { + TrackingVolume vol1( + Transform3::Identity(), + std::make_shared(0_mm, 100_mm, 100_mm)); + + TrackingVolume vol2( + Transform3::Identity(), + std::make_shared(100_mm, 150_mm, 100_mm)); + + SingleCylinderPortalShell shell1{vol1}; + SingleCylinderPortalShell shell2{vol2}; + + BOOST_CHECK_EQUAL(shell1.portal(InnerCylinder), nullptr); + BOOST_CHECK_NE(shell1.portal(OuterCylinder), shell2.portal(InnerCylinder)); + + CylinderStackPortalShell stack{ + gctx, {&shell1, &shell2}, BinningValue::binR}; + BOOST_CHECK_EQUAL(stack.size(), 4); + + // Internal cylinder portals have been fused + BOOST_CHECK_EQUAL(shell1.portal(OuterCylinder), + shell2.portal(InnerCylinder)); + + const auto* nDisc = stack.portal(NegativeDisc); + const auto* pDisc = stack.portal(PositiveDisc); + + BOOST_CHECK_EQUAL(shell1.portal(NegativeDisc), nDisc); + BOOST_CHECK_EQUAL(shell2.portal(NegativeDisc), nDisc); + BOOST_CHECK_EQUAL(shell1.portal(PositiveDisc), pDisc); + BOOST_CHECK_EQUAL(shell2.portal(PositiveDisc), pDisc); + + BOOST_CHECK_EQUAL(stack.portal(InnerCylinder), nullptr); + BOOST_CHECK_EQUAL(stack.portal(OuterCylinder), + shell2.portal(OuterCylinder)); + + BOOST_CHECK_EQUAL(stack.portal(NegativePhiPlane), nullptr); + BOOST_CHECK_EQUAL(stack.portalPtr(NegativePhiPlane), nullptr); + + BOOST_CHECK_EQUAL(stack.portal(PositivePhiPlane), nullptr); + BOOST_CHECK_EQUAL(stack.portalPtr(PositivePhiPlane), nullptr); + + shell1 = SingleCylinderPortalShell{vol1}; + shell2 = SingleCylinderPortalShell{vol2}; + + BOOST_CHECK_THROW( + CylinderStackPortalShell(gctx, {&shell1, &shell2}, BinningValue::binZ), + std::invalid_argument); + } +} + +BOOST_AUTO_TEST_CASE(NestedStacks) { + // ^ + // r | +---------------------------------+---------+ + // | | | | + // | | | | + // | | vol2 | | + // | | | | + // | | | | + // | +---------------------------------+ | + // | | | | + // | | | | + // | | gap | vol3 | + // | | | | + // | | | | + // | +---------------------------------+ | + // | | | | + // | | | | + // | | vol1 | | + // | | | | + // | | | | + // | +---------------------------------+---------+ + // | + // +--------------------------------------------------> + // z + + Transform3 base = Transform3::Identity(); + + TrackingVolume vol1( + base, std::make_shared(23_mm, 48_mm, 200_mm), + "PixelLayer0"); + + TrackingVolume gap( + base, std::make_shared(48_mm, 250_mm, 200_mm), + "Gap"); + + TrackingVolume vol2( + base, std::make_shared(250_mm, 400_mm, 200_mm), + "PixelLayer3"); + + TrackingVolume vol3( + base * Translation3{Vector3::UnitZ() * 300_mm}, + std::make_shared(23_mm, 400_mm, 100_mm), + "PixelEcPos"); + + SingleCylinderPortalShell shell1{vol1}; + BOOST_CHECK(shell1.isValid()); + SingleCylinderPortalShell gapShell{gap}; + BOOST_CHECK(gapShell.isValid()); + SingleCylinderPortalShell shell2{vol2}; + BOOST_CHECK(shell2.isValid()); + + CylinderStackPortalShell stack{ + gctx, {&shell1, &gapShell, &shell2}, BinningValue::binR}; + + BOOST_CHECK(stack.isValid()); + + SingleCylinderPortalShell shell3{vol3}; + BOOST_CHECK(shell3.isValid()); + + CylinderStackPortalShell stack2{ + gctx, {&stack, &shell3}, BinningValue::binZ, *logger}; + BOOST_CHECK(stack2.isValid()); + + using enum CylinderPortalShell::Face; + + auto lookup = [](auto& shell, CylinderPortalShell::Face face, + Vector3 position, + Vector3 direction) -> const TrackingVolume* { + const auto* portal = shell.portal(face); + BOOST_REQUIRE_NE(portal, nullptr); + return portal->resolveVolume(gctx, position, direction).value(); + }; + + // Interconnection in the r direction + + BOOST_CHECK_EQUAL(lookup(shell1, InnerCylinder, 23_mm * Vector3::UnitX(), + -Vector3::UnitX()), + nullptr); + BOOST_CHECK_EQUAL( + lookup(shell1, InnerCylinder, 23_mm * Vector3::UnitX(), Vector3::UnitX()), + &vol1); + + BOOST_CHECK_EQUAL( + lookup(shell1, OuterCylinder, 48_mm * Vector3::UnitX(), Vector3::UnitX()), + &gap); + + BOOST_CHECK_EQUAL(lookup(gapShell, InnerCylinder, 48_mm * Vector3::UnitX(), + -Vector3::UnitX()), + &vol1); + + BOOST_CHECK_EQUAL(lookup(gapShell, OuterCylinder, 250_mm * Vector3::UnitX(), + Vector3::UnitX()), + &vol2); + + BOOST_CHECK_EQUAL(lookup(shell2, InnerCylinder, 250_mm * Vector3::UnitX(), + -Vector3::UnitX()), + &gap); + + BOOST_CHECK_EQUAL(lookup(shell2, OuterCylinder, 400_mm * Vector3::UnitX(), + Vector3::UnitX()), + nullptr); + + BOOST_CHECK_EQUAL(lookup(shell2, OuterCylinder, 400_mm * Vector3::UnitX(), + -Vector3::UnitX()), + &vol2); + + BOOST_CHECK_EQUAL(lookup(shell2, OuterCylinder, 400_mm * Vector3::UnitX(), + -Vector3::UnitX()), + &vol2); + + // Left connection + + BOOST_CHECK_EQUAL(lookup(shell1, NegativeDisc, Vector3(30_mm, 0, -200_mm), + -Vector3::UnitZ()), + nullptr); + + BOOST_CHECK_EQUAL(lookup(shell1, NegativeDisc, Vector3(30_mm, 0, -200_mm), + Vector3::UnitZ()), + &vol1); + + BOOST_CHECK_EQUAL(lookup(gapShell, NegativeDisc, Vector3(60_mm, 0, -200_mm), + -Vector3::UnitZ()), + nullptr); + + BOOST_CHECK_EQUAL(lookup(gapShell, NegativeDisc, Vector3(60_mm, 0, -200_mm), + Vector3::UnitZ()), + &gap); + + BOOST_CHECK_EQUAL(lookup(shell2, NegativeDisc, Vector3(300_mm, 0, -200_mm), + -Vector3::UnitZ()), + nullptr); + + BOOST_CHECK_EQUAL(lookup(shell2, NegativeDisc, Vector3(300_mm, 0, -200_mm), + Vector3::UnitZ()), + &vol2); + + // Right connection + + BOOST_CHECK_EQUAL(lookup(shell1, PositiveDisc, Vector3(30_mm, 0, 200_mm), + -Vector3::UnitZ()), + &vol1); + + BOOST_CHECK_EQUAL( + lookup(shell1, PositiveDisc, Vector3(30_mm, 0, 200_mm), Vector3::UnitZ()), + &vol3); + + BOOST_CHECK_EQUAL(lookup(gapShell, PositiveDisc, Vector3(60_mm, 0, 200_mm), + -Vector3::UnitZ()), + &gap); + + BOOST_CHECK_EQUAL(lookup(gapShell, PositiveDisc, Vector3(60_mm, 0, 200_mm), + Vector3::UnitZ()), + &vol3); + + BOOST_CHECK_EQUAL(lookup(shell2, PositiveDisc, Vector3(300_mm, 0, 200_mm), + -Vector3::UnitZ()), + &vol2); + + BOOST_CHECK_EQUAL(lookup(shell2, PositiveDisc, Vector3(300_mm, 0, 200_mm), + Vector3::UnitZ()), + &vol3); + + // Right outer connection + + BOOST_CHECK_EQUAL(lookup(shell3, PositiveDisc, Vector3(300_mm, 0, 400_mm), + -Vector3::UnitZ()), + &vol3); + + BOOST_CHECK_EQUAL(lookup(shell3, PositiveDisc, Vector3(300_mm, 0, 400_mm), + Vector3::UnitZ()), + nullptr); +} + +BOOST_AUTO_TEST_CASE(ConnectOuter) { + auto cyl1 = makeVolume(30_mm, 40_mm, 100_mm); + auto cyl2 = makeVolume(0_mm, 50_mm, 110_mm); + + SingleCylinderPortalShell shell{cyl1}; + + using enum CylinderPortalShell::Face; + BOOST_CHECK_EQUAL( + shell.portal(OuterCylinder)->getLink(Direction::AlongNormal), nullptr); + BOOST_CHECK_EQUAL( + shell.portal(InnerCylinder)->getLink(Direction::OppositeNormal), nullptr); + BOOST_CHECK_EQUAL(shell.portal(PositiveDisc)->getLink(Direction::AlongNormal), + nullptr); + BOOST_CHECK_EQUAL( + shell.portal(NegativeDisc)->getLink(Direction::OppositeNormal), nullptr); + + shell.connectOuter(cyl2); + + BOOST_CHECK_NE(shell.portal(OuterCylinder)->getLink(Direction::AlongNormal), + nullptr); + BOOST_CHECK_NE( + shell.portal(InnerCylinder)->getLink(Direction::OppositeNormal), nullptr); + BOOST_CHECK_NE(shell.portal(PositiveDisc)->getLink(Direction::AlongNormal), + nullptr); + BOOST_CHECK_NE(shell.portal(NegativeDisc)->getLink(Direction::OppositeNormal), + nullptr); +} + +BOOST_AUTO_TEST_CASE(RegisterInto) { + using enum CylinderPortalShell::Face; + TrackingVolume vol1( + Transform3::Identity(), + std::make_shared(0_mm, 100_mm, 100_mm)); + + SingleCylinderPortalShell shell{vol1}; + + BOOST_CHECK_EQUAL(vol1.portals().size(), 0); + + shell.applyToVolume(); + BOOST_CHECK_EQUAL(vol1.portals().size(), 3); +} + +BOOST_AUTO_TEST_SUITE_END() // CylinderStack +BOOST_AUTO_TEST_SUITE_END() + +} // namespace Acts::Test From 74244eb053e501a65e2f16c03fc74bae91f864d3 Mon Sep 17 00:00:00 2001 From: Carlo Varni <75478407+CarloVarni@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:22:01 +0200 Subject: [PATCH 17/24] feat!: Add a radius bin to the grid (#3662) This adds a radius bin to the grid. The binning can be provided by the user with a vector of values. If none is provided, the range `rMin -> rMax` will be used. That means that now the grids is 3D : `(phi, z, radius)` By default `rMin = 0_mm`, while `rMax = 320_mm` --- .../detail/CylindricalSpacePointGrid.hpp | 20 ++++--- .../detail/CylindricalSpacePointGrid.ipp | 54 ++++++++----------- .../TrackFinding/SeedingAlgorithm.hpp | 4 +- .../TrackFinding/SeedingAlgorithmHashing.hpp | 4 +- .../TrackFinding/src/SeedingAlgorithm.cpp | 10 ++-- .../src/SeedingAlgorithmHashing.cpp | 10 ++-- .../UnitTests/Core/Seeding/SeedFinderTest.cpp | 9 ++-- 7 files changed, 53 insertions(+), 58 deletions(-) diff --git a/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.hpp b/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.hpp index 3acdc5a4c05..42d60158dd0 100644 --- a/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.hpp +++ b/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.hpp @@ -22,6 +22,7 @@ template using CylindricalSpacePointGrid = Acts::Grid< std::vector, Acts::Axis, + Acts::Axis, Acts::Axis>; /// Cylindrical Binned Group @@ -35,22 +36,25 @@ using CylindricalBinnedGroupIterator = Acts::BinnedGroupIterator< struct CylindricalSpacePointGridConfig { // minimum pT to be found by seedFinder - float minPt = 0; + float minPt = 0 * Acts::UnitConstants::MeV; // maximum extension of sensitive detector layer relevant for seeding as // distance from x=y=0 (i.e. in r) - float rMax = 0; + float rMax = 320 * Acts::UnitConstants::mm; + // maximum extension of sensitive detector layer relevant for seeding as + // distance from x=y=0 (i.e. in r) + float rMin = 0 * Acts::UnitConstants::mm; // maximum extension of sensitive detector layer relevant for seeding in // positive direction in z - float zMax = 0; + float zMax = 0 * Acts::UnitConstants::mm; // maximum extension of sensitive detector layer relevant for seeding in // negative direction in z - float zMin = 0; + float zMin = 0 * Acts::UnitConstants::mm; // maximum distance in r from middle space point to bottom or top spacepoint - float deltaRMax = 0; + float deltaRMax = 0 * Acts::UnitConstants::mm; // maximum forward direction expressed as cot(theta) float cotThetaMax = 0; // maximum impact parameter in mm - float impactMax = 0; + float impactMax = 0 * Acts::UnitConstants::mm; // minimum phi value for phiAxis construction float phiMin = -M_PI; // maximum phi value for phiAxis construction @@ -65,7 +69,8 @@ struct CylindricalSpacePointGridConfig { // maximum number of phi bins int maxPhiBins = 10000; // enable non equidistant binning in z - std::vector zBinEdges; + std::vector zBinEdges{}; + std::vector rBinEdges{}; bool isInInternalUnits = false; CylindricalSpacePointGridConfig toInternalUnits() const { if (isInInternalUnits) { @@ -77,6 +82,7 @@ struct CylindricalSpacePointGridConfig { CylindricalSpacePointGridConfig config = *this; config.isInInternalUnits = true; config.minPt /= 1_MeV; + config.rMin /= 1_mm; config.rMax /= 1_mm; config.zMax /= 1_mm; config.zMin /= 1_mm; diff --git a/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.ipp b/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.ipp index a80d3105e82..5d8c886f980 100644 --- a/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.ipp +++ b/Core/include/Acts/Seeding/detail/CylindricalSpacePointGrid.ipp @@ -103,7 +103,7 @@ Acts::CylindricalSpacePointGridCreator::createGrid( config.phiMin, config.phiMax, phiBins); // vector that will store the edges of the bins of z - std::vector zValues; + std::vector zValues{}; // If zBinEdges is not defined, calculate the edges as zMin + bin * zBinSize if (config.zBinEdges.empty()) { @@ -130,9 +130,19 @@ Acts::CylindricalSpacePointGridCreator::createGrid( } } + std::vector rValues{}; + rValues.reserve(std::max(2ul, config.rBinEdges.size())); + if (config.rBinEdges.empty()) { + rValues = {config.rMin, config.rMax}; + } else { + rValues.insert(rValues.end(), config.rBinEdges.begin(), + config.rBinEdges.end()); + } + Axis zAxis(std::move(zValues)); + Axis rAxis(std::move(rValues)); return Acts::CylindricalSpacePointGrid( - std::make_tuple(std::move(phiAxis), std::move(zAxis))); + std::make_tuple(std::move(phiAxis), std::move(zAxis), std::move(rAxis))); } template ( - (config.rMax + options.beamPos.norm()) / config.binSizeR); + // Space points are assumed to be ALREADY CORRECTED for beamspot position + // phi, z and r space point selection comes naturally from the + // grid axis definition. No need to explicitly cut on those values. + // If a space point is outside the validity range of these quantities + // it goes in an over- or under-flow bin. + // Additional cuts can be applied by customizing the space point selector + // in the config object. // keep track of changed bins while sorting std::vector usedBinIndex(grid.size(), false); @@ -172,37 +182,15 @@ void Acts::CylindricalSpacePointGridCreator::fillGrid( for (external_spacepoint_iterator_t it = spBegin; it != spEnd; it++, ++counter) { const external_spacepoint_t& sp = *it; - float spX = sp.x(); - float spY = sp.y(); - float spZ = sp.z(); // remove SPs according to experiment specific cuts if (!config.spacePointSelector(sp)) { continue; } - // remove SPs outside z and phi region - if (spZ > config.zMax || spZ < config.zMin) { - continue; - } - - float spPhi = std::atan2(spY, spX); - if (spPhi > config.phiMax || spPhi < config.phiMin) { - continue; - } - - // calculate r-Bin index and protect against overflow (underflow not - // possible) - std::size_t rIndex = - static_cast(sp.radius() / config.binSizeR); - // if index out of bounds, the SP is outside the region of interest - if (rIndex >= numRBins) { - continue; - } - // fill rbins into grid - std::size_t globIndex = - grid.globalBinFromPosition(Acts::Vector2{sp.phi(), sp.z()}); + std::size_t globIndex = grid.globalBinFromPosition( + Acts::Vector3{sp.phi(), sp.z(), sp.radius()}); auto& rbin = grid.at(globIndex); rbin.push_back(&sp); diff --git a/Examples/Algorithms/TrackFinding/include/ActsExamples/TrackFinding/SeedingAlgorithm.hpp b/Examples/Algorithms/TrackFinding/include/ActsExamples/TrackFinding/SeedingAlgorithm.hpp index 61042cd5d09..73bb81d5d7f 100644 --- a/Examples/Algorithms/TrackFinding/include/ActsExamples/TrackFinding/SeedingAlgorithm.hpp +++ b/Examples/Algorithms/TrackFinding/include/ActsExamples/TrackFinding/SeedingAlgorithm.hpp @@ -100,8 +100,8 @@ class SeedingAlgorithm final : public IAlgorithm { Acts::SeedFinder> m_seedFinder; - std::unique_ptr> m_bottomBinFinder; - std::unique_ptr> m_topBinFinder; + std::unique_ptr> m_bottomBinFinder{nullptr}; + std::unique_ptr> m_topBinFinder{nullptr}; Config m_cfg; diff --git a/Examples/Algorithms/TrackFinding/include/ActsExamples/TrackFinding/SeedingAlgorithmHashing.hpp b/Examples/Algorithms/TrackFinding/include/ActsExamples/TrackFinding/SeedingAlgorithmHashing.hpp index 194ef4944a1..78a0ef2ae89 100644 --- a/Examples/Algorithms/TrackFinding/include/ActsExamples/TrackFinding/SeedingAlgorithmHashing.hpp +++ b/Examples/Algorithms/TrackFinding/include/ActsExamples/TrackFinding/SeedingAlgorithmHashing.hpp @@ -135,8 +135,8 @@ class SeedingAlgorithmHashing final : public IAlgorithm { Acts::SeedFinder> m_seedFinder; - std::unique_ptr> m_bottomBinFinder; - std::unique_ptr> m_topBinFinder; + std::unique_ptr> m_bottomBinFinder{nullptr}; + std::unique_ptr> m_topBinFinder{nullptr}; Config m_cfg; diff --git a/Examples/Algorithms/TrackFinding/src/SeedingAlgorithm.cpp b/Examples/Algorithms/TrackFinding/src/SeedingAlgorithm.cpp index b60acc13e85..14b89d848ca 100644 --- a/Examples/Algorithms/TrackFinding/src/SeedingAlgorithm.cpp +++ b/Examples/Algorithms/TrackFinding/src/SeedingAlgorithm.cpp @@ -190,10 +190,10 @@ ActsExamples::SeedingAlgorithm::SeedingAlgorithm( ActsExamples::SpacePointContainer>, Acts::detail::RefHolder>::SpacePointProxyType; - m_bottomBinFinder = std::make_unique>( - m_cfg.numPhiNeighbors, cfg.zBinNeighborsBottom); - m_topBinFinder = std::make_unique>( - m_cfg.numPhiNeighbors, m_cfg.zBinNeighborsTop); + m_bottomBinFinder = std::make_unique>( + m_cfg.numPhiNeighbors, cfg.zBinNeighborsBottom, 0); + m_topBinFinder = std::make_unique>( + m_cfg.numPhiNeighbors, m_cfg.zBinNeighborsTop, 0); m_cfg.seedFinderConfig.seedFilter = std::make_unique>( @@ -265,7 +265,7 @@ ActsExamples::ProcessCode ActsExamples::SeedingAlgorithm::execute( maxRange = std::max(lastEl->radius(), maxRange); } - std::array, 2ul> navigation; + std::array, 3ul> navigation; navigation[1ul] = m_cfg.seedFinderConfig.zBinsCustomLooping; auto spacePointsGrouping = Acts::CylindricalBinnedGroup( diff --git a/Examples/Algorithms/TrackFinding/src/SeedingAlgorithmHashing.cpp b/Examples/Algorithms/TrackFinding/src/SeedingAlgorithmHashing.cpp index 84011207f19..c722a9e5a2b 100644 --- a/Examples/Algorithms/TrackFinding/src/SeedingAlgorithmHashing.cpp +++ b/Examples/Algorithms/TrackFinding/src/SeedingAlgorithmHashing.cpp @@ -174,10 +174,10 @@ ActsExamples::SeedingAlgorithmHashing::SeedingAlgorithmHashing( m_cfg.seedFinderConfig.experimentCuts.connect(); } - m_bottomBinFinder = std::make_unique>( - m_cfg.numPhiNeighbors, m_cfg.zBinNeighborsBottom); - m_topBinFinder = std::make_unique>( - m_cfg.numPhiNeighbors, m_cfg.zBinNeighborsTop); + m_bottomBinFinder = std::make_unique>( + m_cfg.numPhiNeighbors, m_cfg.zBinNeighborsBottom, 0); + m_topBinFinder = std::make_unique>( + m_cfg.numPhiNeighbors, m_cfg.zBinNeighborsTop, 0); m_cfg.seedFinderConfig.seedFilter = std::make_unique>( @@ -291,7 +291,7 @@ ActsExamples::ProcessCode ActsExamples::SeedingAlgorithmHashing::execute( maxRange = std::max(lastEl->radius(), maxRange); } - std::array, 2ul> navigation; + std::array, 3ul> navigation; navigation[1ul] = m_cfg.seedFinderConfig.zBinsCustomLooping; // groups spacepoints diff --git a/Tests/UnitTests/Core/Seeding/SeedFinderTest.cpp b/Tests/UnitTests/Core/Seeding/SeedFinderTest.cpp index 08bec8a4d92..9d89c1c96c6 100644 --- a/Tests/UnitTests/Core/Seeding/SeedFinderTest.cpp +++ b/Tests/UnitTests/Core/Seeding/SeedFinderTest.cpp @@ -145,6 +145,7 @@ int main(int argc, char** argv) { Acts::SeedFinderConfig config; // silicon detector max config.rMax = 160._mm; + config.rMin = 0._mm; config.deltaRMin = 5._mm; config.deltaRMax = 160._mm; config.deltaRMinTopSP = config.deltaRMin; @@ -178,10 +179,10 @@ int main(int argc, char** argv) { std::vector> zBinNeighborsTop; std::vector> zBinNeighborsBottom; - auto bottomBinFinder = std::make_unique>( - Acts::GridBinFinder<2ul>(numPhiNeighbors, zBinNeighborsBottom)); - auto topBinFinder = std::make_unique>( - Acts::GridBinFinder<2ul>(numPhiNeighbors, zBinNeighborsTop)); + auto bottomBinFinder = std::make_unique>( + numPhiNeighbors, zBinNeighborsBottom, 0); + auto topBinFinder = std::make_unique>( + numPhiNeighbors, zBinNeighborsTop, 0); Acts::SeedFilterConfig sfconf; Acts::ATLASCuts atlasCuts = Acts::ATLASCuts(); From 6cea6b2ddf6fb6c47d2fc9495463643fbbee7890 Mon Sep 17 00:00:00 2001 From: Carlo Varni <75478407+CarloVarni@users.noreply.github.com> Date: Fri, 4 Oct 2024 19:32:11 +0200 Subject: [PATCH 18/24] feat: Add support for Timed Clusterization (#3654) This add possibility of doing clusterization using as well time information --- .../Acts/Clusterization/Clusterization.hpp | 50 +++- .../Acts/Clusterization/Clusterization.ipp | 100 ++----- .../Clusterization/TimedClusterization.hpp | 38 +++ .../Clusterization/TimedClusterization.ipp | 36 +++ .../Core/Clusterization/CMakeLists.txt | 1 + .../TimedClusterizationTests.cpp | 260 ++++++++++++++++++ 6 files changed, 401 insertions(+), 84 deletions(-) create mode 100644 Core/include/Acts/Clusterization/TimedClusterization.hpp create mode 100644 Core/include/Acts/Clusterization/TimedClusterization.ipp create mode 100644 Tests/UnitTests/Core/Clusterization/TimedClusterizationTests.cpp diff --git a/Core/include/Acts/Clusterization/Clusterization.hpp b/Core/include/Acts/Clusterization/Clusterization.hpp index 3ebfd8d562d..a7b4d853b20 100644 --- a/Core/include/Acts/Clusterization/Clusterization.hpp +++ b/Core/include/Acts/Clusterization/Clusterization.hpp @@ -13,6 +13,26 @@ namespace Acts::Ccl { +template +concept HasRetrievableColumnInfo = requires(Cell cell) { + { getCellColumn(cell) } -> std::same_as; +}; + +template +concept HasRetrievableRowInfo = requires(Cell cell) { + { getCellRow(cell) } -> std::same_as; +}; + +template +concept HasRetrievableLabelInfo = requires(Cell cell) { + { getCellLabel(cell) } -> std::same_as; +}; + +template +concept CanAcceptCell = requires(Cell cell, Cluster cluster) { + { clusterAddCell(cluster, cell) } -> std::same_as; +}; + using Label = int; constexpr Label NO_LABEL = 0; @@ -28,17 +48,21 @@ enum class ConnectResult { // Default connection type for 2-D grids: 4- or 8-cell connectivity template + requires(Acts::Ccl::HasRetrievableColumnInfo && + Acts::Ccl::HasRetrievableRowInfo) struct Connect2D { - bool conn8; - Connect2D() : conn8{true} {} + bool conn8{true}; + Connect2D() = default; explicit Connect2D(bool commonCorner) : conn8{commonCorner} {} - ConnectResult operator()(const Cell& ref, const Cell& iter) const; + virtual ConnectResult operator()(const Cell& ref, const Cell& iter) const; + virtual ~Connect2D() = default; }; // Default connection type for 1-D grids: 2-cell connectivity -template +template struct Connect1D { - ConnectResult operator()(const Cell& ref, const Cell& iter) const; + virtual ConnectResult operator()(const Cell& ref, const Cell& iter) const; + virtual ~Connect1D() = default; }; // Default connection type based on GridDim @@ -49,13 +73,16 @@ struct DefaultConnect { }; template -struct DefaultConnect : public Connect2D { - explicit DefaultConnect(bool commonCorner) : Connect2D(commonCorner) {} - DefaultConnect() : DefaultConnect(true) {} +struct DefaultConnect : public Connect1D { + ~DefaultConnect() override = default; }; template -struct DefaultConnect : public Connect1D {}; +struct DefaultConnect : public Connect2D { + explicit DefaultConnect(bool commonCorner) : Connect2D(commonCorner) {} + DefaultConnect() = default; + ~DefaultConnect() override = default; +}; /// @brief labelClusters /// @@ -70,6 +97,8 @@ struct DefaultConnect : public Connect1D {}; template > + requires( + Acts::Ccl::HasRetrievableLabelInfo) void labelClusters(CellCollection& cells, Connect connect = Connect()); /// @brief mergeClusters @@ -82,6 +111,9 @@ void labelClusters(CellCollection& cells, Connect connect = Connect()); /// @return nothing template + requires(GridDim == 1 || GridDim == 2) && + Acts::Ccl::HasRetrievableLabelInfo< + typename CellCollection::value_type> ClusterCollection mergeClusters(CellCollection& /*cells*/); /// @brief createClusters diff --git a/Core/include/Acts/Clusterization/Clusterization.ipp b/Core/include/Acts/Clusterization/Clusterization.ipp index 0aa79b53981..90a69930ce1 100644 --- a/Core/include/Acts/Clusterization/Clusterization.ipp +++ b/Core/include/Acts/Clusterization/Clusterization.ipp @@ -14,60 +14,6 @@ namespace Acts::Ccl::internal { -// Machinery for validating generic Cell/Cluster types at compile-time - -template -struct cellTypeHasRequiredFunctions : std::false_type {}; - -template -struct cellTypeHasRequiredFunctions< - T, 2, - std::void_t())), - decltype(getCellColumn(std::declval())), - decltype(getCellLabel(std::declval()))>> : std::true_type { -}; - -template -struct cellTypeHasRequiredFunctions< - T, 1, - std::void_t())), - decltype(getCellLabel(std::declval()))>> : std::true_type { -}; - -template -struct clusterTypeHasRequiredFunctions : std::false_type {}; - -template -struct clusterTypeHasRequiredFunctions< - T, U, - std::void_t(), std::declval()))>> - : std::true_type {}; - -template -constexpr void staticCheckGridDim() { - static_assert( - GridDim == 1 || GridDim == 2, - "mergeClusters is only defined for grid dimensions of 1 or 2. "); -} - -template -constexpr void staticCheckCellType() { - constexpr bool hasFns = cellTypeHasRequiredFunctions(); - static_assert(hasFns, - "Cell type should have the following functions: " - "'int getCellRow(const Cell&)', " - "'int getCellColumn(const Cell&)', " - "'Label& getCellLabel(Cell&)'"); -} - -template -constexpr void staticCheckClusterType() { - constexpr bool hasFns = clusterTypeHasRequiredFunctions(); - static_assert(hasFns, - "Cluster type should have the following function: " - "'void clusterAddCell(Cluster&, const Cell&)'"); -} - template struct Compare { static_assert(GridDim != 1 && GridDim != 2, @@ -75,25 +21,27 @@ struct Compare { }; // Comparator function object for cells, column-wise ordering -// Specialization for 2-D grid -template -struct Compare { +// Specialization for 1-D grids +template +struct Compare { bool operator()(const Cell& c0, const Cell& c1) const { - int row0 = getCellRow(c0); - int row1 = getCellRow(c1); int col0 = getCellColumn(c0); int col1 = getCellColumn(c1); - return (col0 == col1) ? row0 < row1 : col0 < col1; + return col0 < col1; } }; -// Specialization for 1-D grids +// Specialization for 2-D grid template -struct Compare { + requires(Acts::Ccl::HasRetrievableColumnInfo && + Acts::Ccl::HasRetrievableRowInfo) +struct Compare { bool operator()(const Cell& c0, const Cell& c1) const { + int row0 = getCellRow(c0); + int row1 = getCellRow(c1); int col0 = getCellColumn(c0); int col1 = getCellColumn(c1); - return col0 < col1; + return (col0 == col1) ? row0 < row1 : col0 < col1; } }; @@ -184,6 +132,10 @@ Connections getConnections(typename std::vector::iterator it, } template + requires( + Acts::Ccl::HasRetrievableLabelInfo && + Acts::Ccl::CanAcceptCell) ClusterCollection mergeClustersImpl(CellCollection& cells) { using Cluster = typename ClusterCollection::value_type; @@ -215,6 +167,8 @@ ClusterCollection mergeClustersImpl(CellCollection& cells) { namespace Acts::Ccl { template + requires(Acts::Ccl::HasRetrievableColumnInfo && + Acts::Ccl::HasRetrievableRowInfo) ConnectResult Connect2D::operator()(const Cell& ref, const Cell& iter) const { int deltaRow = std::abs(getCellRow(ref) - getCellRow(iter)); @@ -237,7 +191,7 @@ ConnectResult Connect2D::operator()(const Cell& ref, return ConnectResult::eNoConn; } -template +template ConnectResult Connect1D::operator()(const Cell& ref, const Cell& iter) const { int deltaCol = std::abs(getCellColumn(ref) - getCellColumn(iter)); @@ -267,9 +221,10 @@ void recordEquivalences(const internal::Connections seen, } template + requires( + Acts::Ccl::HasRetrievableLabelInfo) void labelClusters(CellCollection& cells, Connect connect) { using Cell = typename CellCollection::value_type; - internal::staticCheckCellType(); internal::DisjointSets ds{}; @@ -277,7 +232,8 @@ void labelClusters(CellCollection& cells, Connect connect) { std::ranges::sort(cells, internal::Compare()); // First pass: Allocate labels and record equivalences - for (auto it = cells.begin(); it != cells.end(); ++it) { + for (auto it = std::ranges::begin(cells); it != std::ranges::end(cells); + ++it) { const internal::Connections seen = internal::getConnections(it, cells, connect); if (seen.nconn == 0) { @@ -299,13 +255,11 @@ void labelClusters(CellCollection& cells, Connect connect) { template + requires(GridDim == 1 || GridDim == 2) && + Acts::Ccl::HasRetrievableLabelInfo< + typename CellCollection::value_type> ClusterCollection mergeClusters(CellCollection& cells) { using Cell = typename CellCollection::value_type; - using Cluster = typename ClusterCollection::value_type; - internal::staticCheckGridDim(); - internal::staticCheckCellType(); - internal::staticCheckClusterType(); - if constexpr (GridDim > 1) { // Sort the cells by their cluster label, only needed if more than // one spatial dimension @@ -318,10 +272,6 @@ ClusterCollection mergeClusters(CellCollection& cells) { template ClusterCollection createClusters(CellCollection& cells, Connect connect) { - using Cell = typename CellCollection::value_type; - using Cluster = typename ClusterCollection::value_type; - internal::staticCheckCellType(); - internal::staticCheckClusterType(); labelClusters(cells, connect); return mergeClusters(cells); } diff --git a/Core/include/Acts/Clusterization/TimedClusterization.hpp b/Core/include/Acts/Clusterization/TimedClusterization.hpp new file mode 100644 index 00000000000..e92ebce44e2 --- /dev/null +++ b/Core/include/Acts/Clusterization/TimedClusterization.hpp @@ -0,0 +1,38 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma once + +#include "Acts/Clusterization/Clusterization.hpp" +#include "Acts/Definitions/Algebra.hpp" + +#include + +namespace Acts::Ccl { + +template +concept HasRetrievableTimeInfo = requires(Cell cell) { + { getCellTime(cell) } -> std::same_as; +}; + +template +struct TimedConnect : public Acts::Ccl::DefaultConnect { + Acts::ActsScalar timeTolerance{std::numeric_limits::max()}; + + TimedConnect() = default; + TimedConnect(Acts::ActsScalar time); + TimedConnect(Acts::ActsScalar time, bool commonCorner) + requires(N == 2); + ~TimedConnect() override = default; + + ConnectResult operator()(const Cell& ref, const Cell& iter) const override; +}; + +} // namespace Acts::Ccl + +#include "Acts/Clusterization/TimedClusterization.ipp" diff --git a/Core/include/Acts/Clusterization/TimedClusterization.ipp b/Core/include/Acts/Clusterization/TimedClusterization.ipp new file mode 100644 index 00000000000..0e7b3e5bda8 --- /dev/null +++ b/Core/include/Acts/Clusterization/TimedClusterization.ipp @@ -0,0 +1,36 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace Acts::Ccl { + +template +TimedConnect::TimedConnect(Acts::ActsScalar time) + : timeTolerance(time) {} + +template +TimedConnect::TimedConnect(Acts::ActsScalar time, bool commonCorner) + requires(N == 2) + : Acts::Ccl::DefaultConnect(commonCorner), timeTolerance(time) {} + +template +Acts::Ccl::ConnectResult TimedConnect::operator()( + const Cell& ref, const Cell& iter) const { + Acts::Ccl::ConnectResult spaceCompatibility = + Acts::Ccl::DefaultConnect::operator()(ref, iter); + if (spaceCompatibility != Acts::Ccl::ConnectResult::eConn) { + return spaceCompatibility; + } + + if (std::abs(getCellTime(ref) - getCellTime(iter)) < timeTolerance) { + return Acts::Ccl::ConnectResult::eConn; + } + + return Acts::Ccl::ConnectResult::eNoConn; +} + +} // namespace Acts::Ccl diff --git a/Tests/UnitTests/Core/Clusterization/CMakeLists.txt b/Tests/UnitTests/Core/Clusterization/CMakeLists.txt index 38dffefd462..a6c4844bdb9 100644 --- a/Tests/UnitTests/Core/Clusterization/CMakeLists.txt +++ b/Tests/UnitTests/Core/Clusterization/CMakeLists.txt @@ -1,2 +1,3 @@ add_unittest(Clusterization1D ClusterizationTests1D.cpp) add_unittest(Clusterization2D ClusterizationTests2D.cpp) +add_unittest(TimedClusterization TimedClusterizationTests.cpp) diff --git a/Tests/UnitTests/Core/Clusterization/TimedClusterizationTests.cpp b/Tests/UnitTests/Core/Clusterization/TimedClusterizationTests.cpp new file mode 100644 index 00000000000..ac60393d9b6 --- /dev/null +++ b/Tests/UnitTests/Core/Clusterization/TimedClusterizationTests.cpp @@ -0,0 +1,260 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include + +#include "Acts/Clusterization/TimedClusterization.hpp" + +namespace Acts::Test { + +// Define objects +using Identifier = std::size_t; +struct Cell { + Cell(Identifier identifier, int c, int r, double t) + : id(identifier), column(c), row(r), time(t) {} + + Identifier id{}; + int column{0}; + int row{0}; + int label{-1}; + double time{0.}; +}; + +struct Cluster { + std::vector ids{}; +}; + +using CellCollection = std::vector; +using ClusterCollection = std::vector; + +// Define functions +static inline int getCellRow(const Cell& cell) { + return cell.row; +} + +static inline int getCellColumn(const Cell& cell) { + return cell.column; +} + +static inline int& getCellLabel(Cell& cell) { + return cell.label; +} + +static inline double getCellTime(const Cell& cell) { + return cell.time; +} + +static void clusterAddCell(Cluster& cl, const Cell& cell) { + cl.ids.push_back(cell.id); +} + +BOOST_AUTO_TEST_CASE(TimedGrid_1D_withtime) { + // 1x10 matrix + /* + X X X Y O X Y Y X X + */ + // 6 + 3 cells -> 3 + 2 clusters in total + + std::vector cells; + // X + cells.emplace_back(0ul, 0, -1, 0); + cells.emplace_back(1ul, 1, -1, 0); + cells.emplace_back(2ul, 2, -1, 0); + cells.emplace_back(3ul, 5, -1, 0); + cells.emplace_back(4ul, 8, -1, 0); + cells.emplace_back(5ul, 9, -1, 0); + // Y + cells.emplace_back(6ul, 3, 0, 1); + cells.emplace_back(7ul, 6, 1, 1); + cells.emplace_back(8ul, 7, 1, 1); + + std::vector> expectedResults; + expectedResults.push_back({0ul, 1ul, 2ul}); + expectedResults.push_back({6ul}); + expectedResults.push_back({3ul}); + expectedResults.push_back({7ul, 8ul}); + expectedResults.push_back({4ul, 5ul}); + + ClusterCollection clusters = + Acts::Ccl::createClusters( + cells, Acts::Ccl::TimedConnect(0.5)); + + BOOST_CHECK_EQUAL(5ul, clusters.size()); + + for (std::size_t i(0); i < clusters.size(); ++i) { + std::vector& timedIds = clusters[i].ids; + const std::vector& expected = expectedResults[i]; + std::sort(timedIds.begin(), timedIds.end()); + BOOST_CHECK_EQUAL(timedIds.size(), expected.size()); + + for (std::size_t j(0); j < timedIds.size(); ++j) { + BOOST_CHECK_EQUAL(timedIds[j], expected[j]); + } + } +} + +BOOST_AUTO_TEST_CASE(TimedGrid_2D_notime) { + // 4x4 matrix + /* + X O O X + O O O X + X X O O + X O O X + */ + // 7 cells -> 4 clusters in total + + std::vector cells; + cells.emplace_back(0ul, 0, 0, 0); + cells.emplace_back(1ul, 3, 0, 0); + cells.emplace_back(2ul, 3, 1, 0); + cells.emplace_back(3ul, 0, 2, 0); + cells.emplace_back(4ul, 1, 2, 0); + cells.emplace_back(5ul, 0, 3, 0); + cells.emplace_back(6ul, 3, 3, 0); + + std::vector> expectedResults; + expectedResults.push_back({0ul}); + expectedResults.push_back({3ul, 4ul, 5ul}); + expectedResults.push_back({1ul, 2ul}); + expectedResults.push_back({6ul}); + + ClusterCollection clusters = + Acts::Ccl::createClusters( + cells, + Acts::Ccl::TimedConnect(std::numeric_limits::max())); + + BOOST_CHECK_EQUAL(4ul, clusters.size()); + + // Compare against default connect (only space) + ClusterCollection defaultClusters = + Acts::Ccl::createClusters( + cells, Acts::Ccl::DefaultConnect()); + + BOOST_CHECK_EQUAL(4ul, defaultClusters.size()); + BOOST_CHECK_EQUAL(defaultClusters.size(), expectedResults.size()); + + std::vector sizes{1, 3, 2, 1}; + for (std::size_t i(0); i < clusters.size(); ++i) { + std::vector& timedIds = clusters[i].ids; + std::vector& defaultIds = defaultClusters[i].ids; + const std::vector& expected = expectedResults[i]; + BOOST_CHECK_EQUAL(timedIds.size(), defaultIds.size()); + BOOST_CHECK_EQUAL(timedIds.size(), sizes[i]); + BOOST_CHECK_EQUAL(timedIds.size(), expected.size()); + + std::sort(timedIds.begin(), timedIds.end()); + std::sort(defaultIds.begin(), defaultIds.end()); + for (std::size_t j(0); j < timedIds.size(); ++j) { + BOOST_CHECK_EQUAL(timedIds[j], defaultIds[j]); + BOOST_CHECK_EQUAL(timedIds[j], expected[j]); + } + } +} + +BOOST_AUTO_TEST_CASE(TimedGrid_2D_withtime) { + // 4x4 matrix + /* + X Y O X + O Y Y X + X X Z Z + X O O X + */ + // 7 + 3 + 2 cells -> 4 + 1 + 1 clusters in total + + std::vector cells; + // X + cells.emplace_back(0ul, 0, 0, 0); + cells.emplace_back(1ul, 3, 0, 0); + cells.emplace_back(2ul, 3, 1, 0); + cells.emplace_back(3ul, 0, 2, 0); + cells.emplace_back(4ul, 1, 2, 0); + cells.emplace_back(5ul, 0, 3, 0); + cells.emplace_back(6ul, 3, 3, 0); + // Y + cells.emplace_back(7ul, 1, 0, 1); + cells.emplace_back(8ul, 1, 1, 1); + cells.emplace_back(9ul, 2, 1, 1); + // Z + cells.emplace_back(10ul, 2, 2, 2); + cells.emplace_back(11ul, 3, 2, 2); + + std::vector> expectedResults; + expectedResults.push_back({0ul}); + expectedResults.push_back({3ul, 4ul, 5ul}); + expectedResults.push_back({7ul, 8ul, 9ul}); + expectedResults.push_back({10ul, 11ul}); + expectedResults.push_back({1ul, 2ul}); + expectedResults.push_back({6ul}); + + ClusterCollection clusters = + Acts::Ccl::createClusters( + cells, Acts::Ccl::TimedConnect(0.5)); + + BOOST_CHECK_EQUAL(6ul, clusters.size()); + + std::vector sizes{1, 3, 3, 2, 2, 1}; + for (std::size_t i(0); i < clusters.size(); ++i) { + std::vector& timedIds = clusters[i].ids; + BOOST_CHECK_EQUAL(timedIds.size(), sizes[i]); + std::sort(timedIds.begin(), timedIds.end()); + + const std::vector& expected = expectedResults[i]; + BOOST_CHECK_EQUAL(timedIds.size(), expected.size()); + + for (std::size_t j(0); j < timedIds.size(); ++j) { + BOOST_CHECK_EQUAL(timedIds[j], expected[j]); + } + } +} + +BOOST_AUTO_TEST_CASE(TimedGrid_2D_noTollerance) { + // 4x4 matrix + /* + X O O X + O O O X + X X O O + X O O X + */ + // 7 cells -> 7 clusters in total + // since time requirement will never be satisfied + + std::vector cells; + cells.emplace_back(0ul, 0, 0, 0); + cells.emplace_back(1ul, 3, 0, 0); + cells.emplace_back(2ul, 3, 1, 0); + cells.emplace_back(3ul, 0, 2, 0); + cells.emplace_back(4ul, 1, 2, 0); + cells.emplace_back(5ul, 0, 3, 0); + cells.emplace_back(6ul, 3, 3, 0); + + std::vector> expectedResults; + expectedResults.push_back({0ul}); + expectedResults.push_back({3ul}); + expectedResults.push_back({5ul}); + expectedResults.push_back({4ul}); + expectedResults.push_back({1ul}); + expectedResults.push_back({2ul}); + expectedResults.push_back({6ul}); + + ClusterCollection clusters = + Acts::Ccl::createClusters( + cells, Acts::Ccl::TimedConnect(0.)); + + BOOST_CHECK_EQUAL(7ul, clusters.size()); + + for (std::size_t i(0); i < clusters.size(); ++i) { + std::vector& timedIds = clusters[i].ids; + const std::vector& expected = expectedResults[i]; + + BOOST_CHECK_EQUAL(timedIds.size(), 1); + BOOST_CHECK_EQUAL(timedIds.size(), expected.size()); + BOOST_CHECK_EQUAL(timedIds[0], expected[0]); + } +} + +} // namespace Acts::Test From 238639ba370cc6ed1723a8cf95a7b0d140f3e365 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Fri, 4 Oct 2024 20:53:42 +0200 Subject: [PATCH 19/24] refactor(geo): Add portals + surfaces to closeGeometry and visitSurfaces (#3678) Also includes some cleanup. Related to #3502 Blocked by: - https://github.com/acts-project/acts/pull/3675 --- Core/include/Acts/Geometry/TrackingVolume.hpp | 13 ++++++ Core/src/Geometry/TrackingVolume.cpp | 45 +++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/Core/include/Acts/Geometry/TrackingVolume.hpp b/Core/include/Acts/Geometry/TrackingVolume.hpp index b208eefa54b..1d1a5b6dc3c 100644 --- a/Core/include/Acts/Geometry/TrackingVolume.hpp +++ b/Core/include/Acts/Geometry/TrackingVolume.hpp @@ -15,6 +15,7 @@ #include "Acts/Geometry/GeometryIdentifier.hpp" #include "Acts/Geometry/GlueVolumesDescriptor.hpp" #include "Acts/Geometry/Layer.hpp" +#include "Acts/Geometry/Portal.hpp" #include "Acts/Geometry/TrackingVolumeVisitorConcept.hpp" #include "Acts/Geometry/Volume.hpp" #include "Acts/Material/IVolumeMaterial.hpp" @@ -181,6 +182,10 @@ class TrackingVolume : public Volume { for (const auto& bs : m_boundarySurfaces) { visitor(&(bs->surfaceRepresentation())); } + + for (const auto& portal : portals()) { + visitor(&portal.surface()); + } } // Internal structure @@ -214,6 +219,14 @@ class TrackingVolume : public Volume { volume->visitSurfaces(visitor, restrictToSensitives); } } + + for (const auto& surface : surfaces()) { + visitor(&surface); + } + + for (const auto& volume : volumes()) { + volume.visitSurfaces(visitor, restrictToSensitives); + } } /// @brief Visit all sensitive surfaces diff --git a/Core/src/Geometry/TrackingVolume.cpp b/Core/src/Geometry/TrackingVolume.cpp index 79ffdc9d180..9bcf9cc969f 100644 --- a/Core/src/Geometry/TrackingVolume.cpp +++ b/Core/src/Geometry/TrackingVolume.cpp @@ -347,6 +347,31 @@ void TrackingVolume::closeGeometry( std::unordered_map& volumeMap, std::size_t& vol, const GeometryIdentifierHook& hook, const Logger& logger) { + if (!boundarySurfaces().empty() && !portals().empty()) { + ACTS_ERROR( + "TrackingVolume::closeGeometry: Volume " + << volumeName() + << " has both boundary surfaces and portals. This is not supported."); + throw std::invalid_argument( + "Volume has both boundary surfaces and portals"); + } + + if (m_confinedVolumes && !volumes().empty()) { + ACTS_ERROR( + "TrackingVolume::closeGeometry: Volume " + << volumeName() + << " has both confined volumes and volumes. This is not supported."); + throw std::invalid_argument("Volume has both confined volumes and volumes"); + } + + if (m_confinedLayers && !surfaces().empty()) { + ACTS_ERROR( + "TrackingVolume::closeGeometry: Volume " + << volumeName() + << " has both confined layers and surfaces. This is not supported."); + throw std::invalid_argument("Volume has both confined layers and surfaces"); + } + // we can construct the volume ID from this auto volumeID = GeometryIdentifier().setVolume(++vol); // assign the Volume ID to the volume itself @@ -379,7 +404,8 @@ void TrackingVolume::closeGeometry( // get the intersection solution auto& bSurface = bSurfIter->surfaceRepresentation(); // create the boundary surface id - auto boundaryID = GeometryIdentifier(volumeID).setBoundary(++iboundary); + iboundary++; + auto boundaryID = GeometryIdentifier(volumeID).setBoundary(iboundary); // now assign to the boundary surface auto& mutableBSurface = *(const_cast(&bSurface)); mutableBSurface.assignGeometryId(boundaryID); @@ -397,7 +423,8 @@ void TrackingVolume::closeGeometry( // loop over the layers for (auto& layerPtr : m_confinedLayers->arrayObjects()) { // create the layer identification - auto layerID = GeometryIdentifier(volumeID).setLayer(++ilayer); + ilayer++; + auto layerID = GeometryIdentifier(volumeID).setLayer(ilayer); // now close the geometry auto mutableLayerPtr = std::const_pointer_cast(layerPtr); mutableLayerPtr->closeGeometry(materialDecorator, layerID, hook, @@ -428,12 +455,24 @@ void TrackingVolume::closeGeometry( GeometryIdentifier::Value iportal = 0; for (auto& portal : portals()) { - auto portalId = GeometryIdentifier(volumeID).setBoundary(++iportal); + iportal++; + auto portalId = GeometryIdentifier(volumeID).setBoundary(iportal); assert(portal.isValid() && "Invalid portal encountered during closing"); portal.surface().assignGeometryId(portalId); } + GeometryIdentifier::Value isensitive = 0; + + for (auto& surface : surfaces()) { + if (surface.associatedDetectorElement() == nullptr) { + continue; + } + isensitive++; + auto sensitiveId = GeometryIdentifier(volumeID).setSensitive(isensitive); + surface.assignGeometryId(sensitiveId); + } + for (auto& volume : volumes()) { volume.closeGeometry(materialDecorator, volumeMap, vol, hook, logger); } From c96fe3835e20aa2ff2d116ed6ac405581cbf27d5 Mon Sep 17 00:00:00 2001 From: Carlo Varni <75478407+CarloVarni@users.noreply.github.com> Date: Sun, 6 Oct 2024 22:09:13 +0200 Subject: [PATCH 20/24] refactor: use std::atan2 instead of atan2f (#3695) --- Core/include/Acts/EventData/SpacePointContainer.hpp | 2 -- Core/include/Acts/EventData/SpacePointContainer.ipp | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Core/include/Acts/EventData/SpacePointContainer.hpp b/Core/include/Acts/EventData/SpacePointContainer.hpp index 5a7be3b23f7..619a1393ecc 100644 --- a/Core/include/Acts/EventData/SpacePointContainer.hpp +++ b/Core/include/Acts/EventData/SpacePointContainer.hpp @@ -19,8 +19,6 @@ #include #include -#include - namespace Acts { struct SpacePointContainerConfig { diff --git a/Core/include/Acts/EventData/SpacePointContainer.ipp b/Core/include/Acts/EventData/SpacePointContainer.ipp index 01800be7cd5..e0c381e4a50 100644 --- a/Core/include/Acts/EventData/SpacePointContainer.ipp +++ b/Core/include/Acts/EventData/SpacePointContainer.ipp @@ -6,7 +6,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -#include +#include namespace Acts { @@ -44,7 +44,7 @@ void SpacePointContainer::initialize() { m_data.setZ(i, external_container.z_impl(i)); m_data.setRadius( i, std::sqrt(m_data.x(i) * m_data.x(i) + m_data.y(i) * m_data.y(i))); - m_data.setPhi(i, atan2f(m_data.y(i), m_data.x(i))); + m_data.setPhi(i, std::atan2(m_data.y(i), m_data.x(i))); m_data.setVarianceR(i, external_container.varianceR_impl(i)); m_data.setVarianceZ(i, external_container.varianceZ_impl(i)); From 26a4a8ff14813b43f3f36fceebee5fc8fdabe8c9 Mon Sep 17 00:00:00 2001 From: Giacomo Alocco <40990466+galocco@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:50:59 +0200 Subject: [PATCH 21/24] refactor: set convertMaterial as configurable (#3604) The default value of convertMaterial is false to allow the conversion of all materials (not only primitives). convertMaterial can now be set in the createSurface python binding. --- Examples/Python/src/Geant4Component.cpp | 4 +++- .../Acts/Plugins/Geant4/Geant4DetectorSurfaceFactory.hpp | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Examples/Python/src/Geant4Component.cpp b/Examples/Python/src/Geant4Component.cpp index 14e7b138888..ff34da94cc0 100644 --- a/Examples/Python/src/Geant4Component.cpp +++ b/Examples/Python/src/Geant4Component.cpp @@ -334,7 +334,8 @@ PYBIND11_MODULE(ActsPythonBindingsGeant4, mod) { const std::vector& sensitiveMatches, const std::vector& - passiveMatches) { + passiveMatches, + bool convertMaterial) { // Initiate the detector construction & retrieve world ActsExamples::GdmlDetectorConstruction gdmlContruction(gdmlFileName); const auto* world = gdmlContruction.Construct(); @@ -351,6 +352,7 @@ PYBIND11_MODULE(ActsPythonBindingsGeant4, mod) { Acts::Geant4DetectorSurfaceFactory::Options options; options.sensitiveSurfaceSelector = sensitiveSelectors; options.passiveSurfaceSelector = passiveSelectors; + options.convertMaterial = convertMaterial; G4Transform3D nominal; Acts::Geant4DetectorSurfaceFactory factory; diff --git a/Plugins/Geant4/include/Acts/Plugins/Geant4/Geant4DetectorSurfaceFactory.hpp b/Plugins/Geant4/include/Acts/Plugins/Geant4/Geant4DetectorSurfaceFactory.hpp index fae8fc20fea..d06c77412e4 100644 --- a/Plugins/Geant4/include/Acts/Plugins/Geant4/Geant4DetectorSurfaceFactory.hpp +++ b/Plugins/Geant4/include/Acts/Plugins/Geant4/Geant4DetectorSurfaceFactory.hpp @@ -67,7 +67,7 @@ class Geant4DetectorSurfaceFactory { /// Convert the length scale ActsScalar scaleConversion = 1.; /// Convert the material - bool convertMaterial = true; + bool convertMaterial = false; /// Converted material thickness (< 0 indicates keeping original thickness) ActsScalar convertedMaterialThickness = -1; /// A selector for sensitive surfaces From 8f0ae3cbbecafb1a295719fc25949f8e1c6d2589 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Mon, 7 Oct 2024 12:05:10 +0200 Subject: [PATCH 22/24] refactor(geo): Geometry visualization update (#3681) Blocked by: - https://github.com/acts-project/acts/pull/3675 Related to #3502 --- The most significant changes include adding visualization methods to `TrackingGeometry` and `TrackingVolume`, updating the `Surface` class, and enhancing the `IVisualization3D` interface. Additionally, the `ObjVisualization3D` class has been refactored, and new default view configurations have been defined. ### Visualization Enhancements: * [`TrackingGeometry.hpp`](diffhunk://#diff-cc497a4ee5032615db90ef065e9466279a0fde38fbd201840a32d783d3d6ec4aR148-R158): Added a `visualize` method to enable visualization of tracking geometry, including substructures. (`[Core/include/Acts/Geometry/TrackingGeometry.hppR148-R158](diffhunk://#diff-cc497a4ee5032615db90ef065e9466279a0fde38fbd201840a32d783d3d6ec4aR148-R158)`) * [`TrackingVolume.hpp`](diffhunk://#diff-835fb549fb77cdaa632a4e2131c8dc17ad4973ab3f1600a4608582b706a3f68eR326-R344): Added methods to visualize tracking volumes and manage surfaces, including `visualize`, `surfaces`, and `addSurface`. (`[[1]](diffhunk://#diff-835fb549fb77cdaa632a4e2131c8dc17ad4973ab3f1600a4608582b706a3f68eR326-R344)`, `[[2]](diffhunk://#diff-835fb549fb77cdaa632a4e2131c8dc17ad4973ab3f1600a4608582b706a3f68eR477-R487)`, `[[3]](diffhunk://#diff-835fb549fb77cdaa632a4e2131c8dc17ad4973ab3f1600a4608582b706a3f68eR550)`) ### Interface Updates: * [`IVisualization3D.hpp`](diffhunk://#diff-c56ccee98d351daf6ac595cd265d30716a594ceb74030138411595aca66fdc39R75-R80): Introduced a destructor and a new `object` method to start a new object context. (`[Core/include/Acts/Visualization/IVisualization3D.hppR75-R80](diffhunk://#diff-c56ccee98d351daf6ac595cd265d30716a594ceb74030138411595aca66fdc39R75-R80)`) * [`ObjVisualization3D.hpp`](diffhunk://#diff-fab69ace861a3e5d8fc173e8627732777a5fdd52703e9e162a14db563ea56a2aL29-R32): Refactored to use `double` as the value type and added support for object contexts. (`[[1]](diffhunk://#diff-fab69ace861a3e5d8fc173e8627732777a5fdd52703e9e162a14db563ea56a2aL29-R32)`, `[[2]](diffhunk://#diff-fab69ace861a3e5d8fc173e8627732777a5fdd52703e9e162a14db563ea56a2aR76-R102)`) ### Configuration and Defaults: * [`ViewConfig.hpp`](diffhunk://#diff-e403e98244e75d6b7d32864417c37619de6becee73f53415b733073f655f0929R114-R117): Defined new default view configurations for surfaces, portals, volumes, and other elements. (`[[1]](diffhunk://#diff-e403e98244e75d6b7d32864417c37619de6becee73f53415b733073f655f0929R114-R117)`, `[[2]](diffhunk://#diff-e403e98244e75d6b7d32864417c37619de6becee73f53415b733073f655f0929R139-R146)`) ### Codebase Cleanup: * Removed obsolete and redundant code from `ObjVisualization3D.ipp`. (`[Core/include/Acts/Visualization/detail/ObjVisualization3D.ippL1-L179](diffhunk://#diff-29bbb6e8cfcb388c51d1e88237dd0c7d582762d7e05a0ec12278bdc336b0aaa1L1-L179)`) --- .../Acts/Geometry/TrackingGeometry.hpp | 11 + Core/include/Acts/Geometry/TrackingVolume.hpp | 12 + Core/include/Acts/Geometry/Volume.hpp | 2 +- Core/include/Acts/Surfaces/Surface.hpp | 2 +- .../Acts/Visualization/GeometryView3D.hpp | 6 - .../Acts/Visualization/IVisualization3D.hpp | 6 + .../Acts/Visualization/ObjVisualization3D.hpp | 44 ++-- .../Acts/Visualization/PlyVisualization3D.hpp | 4 + .../include/Acts/Visualization/ViewConfig.hpp | 45 +++- .../detail/ObjVisualization3D.ipp | 179 --------------- Core/src/Geometry/TrackingGeometry.cpp | 8 + Core/src/Geometry/TrackingVolume.cpp | 21 ++ Core/src/Visualization/CMakeLists.txt | 5 +- Core/src/Visualization/GeometryView3D.cpp | 10 - Core/src/Visualization/ObjVisualization3D.cpp | 211 ++++++++++++++++++ Examples/Python/python/acts/examples/odd.py | 3 +- Examples/Python/src/Geometry.cpp | 20 +- Examples/Python/src/ModuleEntry.cpp | 2 +- Examples/Python/src/Obj.cpp | 5 + Examples/Python/src/Output.cpp | 8 +- .../Visualization/Visualization3DTests.cpp | 2 + 21 files changed, 366 insertions(+), 240 deletions(-) delete mode 100644 Core/include/Acts/Visualization/detail/ObjVisualization3D.ipp create mode 100644 Core/src/Visualization/ObjVisualization3D.cpp diff --git a/Core/include/Acts/Geometry/TrackingGeometry.hpp b/Core/include/Acts/Geometry/TrackingGeometry.hpp index b0ac658eecd..e8d48c5ae5e 100644 --- a/Core/include/Acts/Geometry/TrackingGeometry.hpp +++ b/Core/include/Acts/Geometry/TrackingGeometry.hpp @@ -145,6 +145,17 @@ class TrackingGeometry { const std::unordered_map& geoIdSurfaceMap() const; + /// Visualize a tracking geometry including substructure + /// @param helper The visualization helper that implement the output + /// @param gctx The geometry context + /// @param viewConfig Global view config + /// @param portalViewConfig View config for portals + /// @param sensitiveViewConfig View configuration for sensitive surfaces + void visualize(IVisualization3D& helper, const GeometryContext& gctx, + const ViewConfig& viewConfig = s_viewVolume, + const ViewConfig& portalViewConfig = s_viewPortal, + const ViewConfig& sensitiveViewConfig = s_viewSensitive) const; + private: // the known world std::shared_ptr m_world; diff --git a/Core/include/Acts/Geometry/TrackingVolume.hpp b/Core/include/Acts/Geometry/TrackingVolume.hpp index 1d1a5b6dc3c..c980314fa6d 100644 --- a/Core/include/Acts/Geometry/TrackingVolume.hpp +++ b/Core/include/Acts/Geometry/TrackingVolume.hpp @@ -26,6 +26,7 @@ #include "Acts/Utilities/BinnedArray.hpp" #include "Acts/Utilities/Logger.hpp" #include "Acts/Utilities/TransformRange.hpp" +#include "Acts/Visualization/ViewConfig.hpp" #include #include @@ -486,6 +487,17 @@ class TrackingVolume : public Volume { /// - positiveFaceXY GlueVolumesDescriptor& glueVolumesDescriptor(); + /// Produces a 3D visualization of this tracking volume + /// @param helper The visualization helper describing the output format + /// @param gctx The geometry context + /// @param viewConfig The view configuration + /// @param portalViewConfig View configuration for portals + /// @param sensitiveViewConfig View configuration for sensitive surfaces + void visualize(IVisualization3D& helper, const GeometryContext& gctx, + const ViewConfig& viewConfig, + const ViewConfig& portalViewConfig, + const ViewConfig& sensitiveViewConfig) const; + private: void connectDenseBoundarySurfaces( MutableTrackingVolumeVector& confinedDenseVolumes); diff --git a/Core/include/Acts/Geometry/Volume.hpp b/Core/include/Acts/Geometry/Volume.hpp index a59451efe09..9217e758444 100644 --- a/Core/include/Acts/Geometry/Volume.hpp +++ b/Core/include/Acts/Geometry/Volume.hpp @@ -122,7 +122,7 @@ class Volume : public GeometryObject { /// @param gctx The geometry context /// @param viewConfig The view configuration void visualize(IVisualization3D& helper, const GeometryContext& gctx, - const ViewConfig& viewConfig = {}) const; + const ViewConfig& viewConfig) const; protected: Transform3 m_transform; diff --git a/Core/include/Acts/Surfaces/Surface.hpp b/Core/include/Acts/Surfaces/Surface.hpp index 3a823671823..79153e8c98e 100644 --- a/Core/include/Acts/Surfaces/Surface.hpp +++ b/Core/include/Acts/Surfaces/Surface.hpp @@ -482,7 +482,7 @@ class Surface : public virtual GeometryObject, const GeometryContext& gctx, const Vector3& position) const = 0; void visualize(IVisualization3D& helper, const GeometryContext& gctx, - const ViewConfig& viewConfig = {}) const; + const ViewConfig& viewConfig = s_viewSurface) const; protected: /// Output Method for std::ostream, to be overloaded by child classes diff --git a/Core/include/Acts/Visualization/GeometryView3D.hpp b/Core/include/Acts/Visualization/GeometryView3D.hpp index 05053de31ef..fed07697a87 100644 --- a/Core/include/Acts/Visualization/GeometryView3D.hpp +++ b/Core/include/Acts/Visualization/GeometryView3D.hpp @@ -31,12 +31,6 @@ class DetectorVolume; class Portal; } // namespace Experimental -static const ViewConfig s_viewSensitive; -static const ViewConfig s_viewPassive; -static const ViewConfig s_viewVolume; -static const ViewConfig s_viewGrid; -static const ViewConfig s_viewLine; - struct GeometryView3D { /// Helper method to draw Polyhedron objects /// diff --git a/Core/include/Acts/Visualization/IVisualization3D.hpp b/Core/include/Acts/Visualization/IVisualization3D.hpp index 734ab3fe129..be8c5b96226 100644 --- a/Core/include/Acts/Visualization/IVisualization3D.hpp +++ b/Core/include/Acts/Visualization/IVisualization3D.hpp @@ -72,6 +72,12 @@ class IVisualization3D { /// Remove all contents of this helper /// virtual void clear() = 0; + + virtual ~IVisualization3D() = default; + + /// Start a new object context + /// @param name The name of the object + virtual void object(const std::string& name) = 0; }; /// Overload of the << operator to facilitate writing to streams. diff --git a/Core/include/Acts/Visualization/ObjVisualization3D.hpp b/Core/include/Acts/Visualization/ObjVisualization3D.hpp index 941e719f870..a9f496ce304 100644 --- a/Core/include/Acts/Visualization/ObjVisualization3D.hpp +++ b/Core/include/Acts/Visualization/ObjVisualization3D.hpp @@ -12,12 +12,8 @@ #include "Acts/Visualization/IVisualization3D.hpp" #include "Acts/Visualization/ViewConfig.hpp" -#include #include -#include -#include #include -#include #include #include @@ -26,14 +22,10 @@ namespace Acts { /// This helper produces output in the OBJ format. Note that colors are not /// supported in this implementation. /// -template class ObjVisualization3D : public IVisualization3D { public: - static_assert(std::is_same_v || std::is_same_v, - "Use either double or float"); - /// Stored value type, should be double or float - using ValueType = T; + using ValueType = double; /// Type of a vertex based on the value type using VertexType = Eigen::Matrix; @@ -77,22 +69,32 @@ class ObjVisualization3D : public IVisualization3D { /// @copydoc Acts::IVisualization3D::clear() void clear() final; + /// Start a new object context with a name + /// @param name The name of the object + void object(const std::string& name) final; + private: + struct Object { + std::string name; + std::vector vertices{}; + std::vector faces{}; + std::vector lines{}; + + /// The object data to be written + /// Map of colors to be written at given index position + std::map lineColors{}; + std::map vertexColors{}; + std::map faceColors{}; + }; + + Object& object(); + const Object& object() const; + /// The output parameters unsigned int m_outputPrecision = 4; double m_outputScalor = 1.; - /// The object data to be written - std::vector m_vertices; - std::vector m_faces; - std::vector m_lines; - /// Map of colors to be written at given index position - std::map m_lineColors; - std::map m_vertexColors; - std::map m_faceColors; -}; -#ifndef DOXYGEN -#include "detail/ObjVisualization3D.ipp" -#endif + std::vector m_objects; +}; } // namespace Acts diff --git a/Core/include/Acts/Visualization/PlyVisualization3D.hpp b/Core/include/Acts/Visualization/PlyVisualization3D.hpp index e2c94481b9c..a4367dc7e02 100644 --- a/Core/include/Acts/Visualization/PlyVisualization3D.hpp +++ b/Core/include/Acts/Visualization/PlyVisualization3D.hpp @@ -60,6 +60,10 @@ class PlyVisualization3D : public IVisualization3D { /// @copydoc Acts::IVisualization3D::clear() void clear() final; + void object(const std::string& /*name*/) final { + // Unimplemented + } + private: std::vector> m_vertices; std::vector m_faces; diff --git a/Core/include/Acts/Visualization/ViewConfig.hpp b/Core/include/Acts/Visualization/ViewConfig.hpp index 0414fa58aba..e2cf7b80618 100644 --- a/Core/include/Acts/Visualization/ViewConfig.hpp +++ b/Core/include/Acts/Visualization/ViewConfig.hpp @@ -10,7 +10,7 @@ #include #include -#include +#include namespace Acts { @@ -49,17 +49,31 @@ struct Color { constexpr Color(double r, double g, double b) : Color{std::array{r, g, b}} {} + private: + constexpr static int hexToInt(std::string_view hex) { + constexpr auto hexCharToInt = [](char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } else if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } else if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } else { + throw std::invalid_argument("Invalid hex character"); + } + }; + + int value = 0; + for (char c : hex) { + value = (value << 4) + hexCharToInt(c); + } + return value; + }; + + public: /// Constructor from hex string. The expected format is `#RRGGBB` /// @param hex The hex string constexpr explicit Color(std::string_view hex) { - auto hexToInt = [](std::string_view hexStr) { - int value = 0; - std::stringstream ss; - ss << std::hex << hexStr; - ss >> value; - return value; - }; - if (hex[0] == '#' && hex.size() == 7) { rgb[0] = hexToInt(hex.substr(1, 2)); // Extract R component rgb[1] = hexToInt(hex.substr(3, 2)); // Extract G component @@ -85,7 +99,6 @@ struct Color { /// @param rhs The second color /// @return True if the colors are equal friend bool operator==(const Color& lhs, const Color& rhs) = default; - /// Output stream operator /// @param os The output stream /// @param color The color to be printed @@ -99,6 +112,10 @@ struct Color { std::array rgb{}; }; +constexpr Color s_defaultSurfaceColor{"#0000aa"}; +constexpr Color s_defaultPortalColor{"#308c48"}; +constexpr Color s_defaultVolumColor{"#ffaa00"}; + /// @brief Struct to concentrate all visualization configurations /// in order to harmonize visualization interfaces struct ViewConfig { @@ -120,4 +137,12 @@ struct ViewConfig { std::filesystem::path outputName = std::filesystem::path(""); }; +static const ViewConfig s_viewSurface = {.color = {170, 170, 170}}; +static const ViewConfig s_viewPortal = {.color = Color{"#308c48"}}; +static const ViewConfig s_viewSensitive = {.color = {0, 180, 240}}; +static const ViewConfig s_viewPassive = {.color = {240, 280, 0}}; +static const ViewConfig s_viewVolume = {.color = {220, 220, 0}}; +static const ViewConfig s_viewGrid = {.color = {220, 0, 0}}; +static const ViewConfig s_viewLine = {.color = {0, 0, 220}}; + } // namespace Acts diff --git a/Core/include/Acts/Visualization/detail/ObjVisualization3D.ipp b/Core/include/Acts/Visualization/detail/ObjVisualization3D.ipp deleted file mode 100644 index 354ef518bef..00000000000 --- a/Core/include/Acts/Visualization/detail/ObjVisualization3D.ipp +++ /dev/null @@ -1,179 +0,0 @@ -// This file is part of the ACTS project. -// -// Copyright (C) 2016 CERN for the benefit of the ACTS project -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -template -void ObjVisualization3D::vertex(const Vector3& vtx, Color color) { - m_vertexColors[m_vertices.size()] = color; - m_vertices.push_back(vtx.template cast()); -} - -template -void ObjVisualization3D::line(const Vector3& a, const Vector3& b, - Color color) { - if (color != Color{0, 0, 0}) { - m_lineColors[m_lines.size()] = color; - } - // not implemented - vertex(a, color); - vertex(b, color); - m_lines.push_back({m_vertices.size() - 2, m_vertices.size() - 1}); -} - -template -void ObjVisualization3D::face(const std::vector& vtxs, - Color color) { - if (color != Color{0, 0, 0}) { - m_faceColors[m_faces.size()] = color; - } - FaceType idxs; - idxs.reserve(vtxs.size()); - for (const auto& vtx : vtxs) { - vertex(vtx, color); - idxs.push_back(m_vertices.size() - 1); - } - m_faces.push_back(std::move(idxs)); -} - -template -void ObjVisualization3D::faces(const std::vector& vtxs, - const std::vector& faces, - Color color) { - // No faces given - call the face() method - if (faces.empty()) { - face(vtxs, color); - } else { - if (color != Color{0, 0, 0}) { - m_faceColors[m_faces.size()] = color; - } - auto vtxoffs = m_vertices.size(); - if (color != Color{0, 0, 0}) { - m_vertexColors[m_vertices.size()] = color; - } - m_vertices.insert(m_vertices.end(), vtxs.begin(), vtxs.end()); - for (const auto& face : faces) { - if (face.size() == 2) { - m_lines.push_back({face[0] + vtxoffs, face[2] + vtxoffs}); - } else { - FaceType rawFace = face; - std::transform(rawFace.begin(), rawFace.end(), rawFace.begin(), - [&](std::size_t& iv) { return (iv + vtxoffs); }); - m_faces.push_back(rawFace); - } - } - } -} - -template -void ObjVisualization3D::write(const std::filesystem::path& path) const { - std::ofstream os; - std::filesystem::path objectpath = path; - if (!objectpath.has_extension()) { - objectpath.replace_extension(std::filesystem::path("obj")); - } - os.open(std::filesystem::absolute(objectpath).string()); - std::filesystem::path mtlpath = objectpath; - mtlpath.replace_extension(std::filesystem::path("mtl")); - - const std::string mtlpathString = std::filesystem::absolute(mtlpath).string(); - os << "mtllib " << mtlpathString << "\n"; - std::ofstream mtlos; - mtlos.open(mtlpathString); - - write(os, mtlos); - os.close(); - mtlos.close(); -} - -template -void ObjVisualization3D::write(std::ostream& os) const { - std::stringstream sterile; - write(os, sterile); -} - -template -void ObjVisualization3D::write(std::ostream& os, std::ostream& mos) const { - std::map materials; - - auto mixColor = [&](const Color& color) -> std::string { - std::string materialName; - materialName = "material_"; - materialName += std::to_string(color[0]) + std::string("_"); - materialName += std::to_string(color[1]) + std::string("_"); - materialName += std::to_string(color[2]); - - if (!materials.contains(materialName)) { - mos << "newmtl " << materialName << "\n"; - std::vector shadings = {"Ka", "Kd", "Ks"}; - for (const auto& shd : shadings) { - mos << shd << " " << std::to_string(color[0] / 256.) << " "; - mos << std::to_string(color[1] / 256.) << " "; - mos << std::to_string(color[2] / 256.) << " " - << "\n"; - } - mos << "\n"; - } - return std::string("usemtl ") + materialName; - }; - - std::size_t iv = 0; - Color lastVertexColor = {0, 0, 0}; - for (const VertexType& vtx : m_vertices) { - if (m_vertexColors.contains(iv)) { - auto color = m_vertexColors.find(iv)->second; - if (color != lastVertexColor) { - os << mixColor(color) << "\n"; - lastVertexColor = color; - } - } - - os << "v " << std::setprecision(m_outputPrecision) - << m_outputScalor * vtx.x() << " " << m_outputScalor * vtx.y() << " " - << m_outputScalor * vtx.z() << "\n"; - ++iv; - } - std::size_t il = 0; - Color lastLineColor = {0, 0, 0}; - for (const LineType& ln : m_lines) { - if (m_lineColors.contains(il)) { - auto color = m_lineColors.find(il)->second; - if (color != lastLineColor) { - os << mixColor(color) << "\n"; - lastLineColor = color; - } - } - os << "l " << ln.first + 1 << " " << ln.second + 1 << "\n"; - ++il; - } - std::size_t is = 0; - Color lastFaceColor = {0, 0, 0}; - for (const FaceType& fc : m_faces) { - if (m_faceColors.contains(is)) { - auto color = m_faceColors.find(is)->second; - if (color != lastFaceColor) { - os << mixColor(color) << "\n"; - lastFaceColor = color; - } - } - os << "f"; - for (auto fi : fc) { - os << " " << fi + 1; - } - os << "\n"; - ++is; - } -} - -template -void ObjVisualization3D::clear() { - m_vertices.clear(); - m_faces.clear(); - m_lines.clear(); - m_lineColors.clear(); - m_vertexColors.clear(); - m_faceColors.clear(); -} diff --git a/Core/src/Geometry/TrackingGeometry.cpp b/Core/src/Geometry/TrackingGeometry.cpp index b68d788d08b..f9207afbd18 100644 --- a/Core/src/Geometry/TrackingGeometry.cpp +++ b/Core/src/Geometry/TrackingGeometry.cpp @@ -85,3 +85,11 @@ const std::unordered_map& Acts::TrackingGeometry::geoIdSurfaceMap() const { return m_surfacesById; } + +void Acts::TrackingGeometry::visualize( + IVisualization3D& helper, const GeometryContext& gctx, + const ViewConfig& viewConfig, const ViewConfig& portalViewConfig, + const ViewConfig& sensitiveViewConfig) const { + highestTrackingVolume()->visualize(helper, gctx, viewConfig, portalViewConfig, + sensitiveViewConfig); +} diff --git a/Core/src/Geometry/TrackingVolume.cpp b/Core/src/Geometry/TrackingVolume.cpp index 9bcf9cc969f..ad2c939fcbd 100644 --- a/Core/src/Geometry/TrackingVolume.cpp +++ b/Core/src/Geometry/TrackingVolume.cpp @@ -723,4 +723,25 @@ void TrackingVolume::addSurface(std::shared_ptr surface) { m_surfaces.push_back(std::move(surface)); } +void TrackingVolume::visualize(IVisualization3D& helper, + const GeometryContext& gctx, + const ViewConfig& viewConfig, + const ViewConfig& portalViewConfig, + const ViewConfig& sensitiveViewConfig) const { + helper.object(volumeName()); + Volume::visualize(helper, gctx, viewConfig); + + if (!surfaces().empty()) { + helper.object(volumeName() + "_sensitives"); + } + for (const auto& surface : surfaces()) { + surface.visualize(helper, gctx, sensitiveViewConfig); + } + + for (const auto& child : volumes()) { + child.visualize(helper, gctx, viewConfig, portalViewConfig, + sensitiveViewConfig); + } +} + } // namespace Acts diff --git a/Core/src/Visualization/CMakeLists.txt b/Core/src/Visualization/CMakeLists.txt index 5d4f916af1c..567b1e1e236 100644 --- a/Core/src/Visualization/CMakeLists.txt +++ b/Core/src/Visualization/CMakeLists.txt @@ -1 +1,4 @@ -target_sources(ActsCore PRIVATE GeometryView3D.cpp EventDataView3D.cpp) +target_sources( + ActsCore + PRIVATE GeometryView3D.cpp EventDataView3D.cpp ObjVisualization3D.cpp +) diff --git a/Core/src/Visualization/GeometryView3D.cpp b/Core/src/Visualization/GeometryView3D.cpp index 17b7b5c7df9..f16d9e809cc 100644 --- a/Core/src/Visualization/GeometryView3D.cpp +++ b/Core/src/Visualization/GeometryView3D.cpp @@ -26,7 +26,6 @@ #include "Acts/Surfaces/RadialBounds.hpp" #include "Acts/Surfaces/Surface.hpp" #include "Acts/Surfaces/SurfaceArray.hpp" -#include "Acts/Utilities/BinnedArray.hpp" #include "Acts/Utilities/BinningType.hpp" #include "Acts/Utilities/IAxis.hpp" #include "Acts/Utilities/UnitVectors.hpp" @@ -36,18 +35,9 @@ #include #include #include -#include #include #include -namespace Acts::Experimental { -ViewConfig s_viewSensitive = {.color = {0, 180, 240}}; -ViewConfig s_viewPassive = {.color = {240, 280, 0}}; -ViewConfig s_viewVolume = {.color = {220, 220, 0}}; -ViewConfig s_viewGrid = {.color = {220, 0, 0}}; -ViewConfig s_viewLine = {.color = {0, 0, 220}}; -} // namespace Acts::Experimental - void Acts::GeometryView3D::drawPolyhedron(IVisualization3D& helper, const Polyhedron& polyhedron, const ViewConfig& viewConfig) { diff --git a/Core/src/Visualization/ObjVisualization3D.cpp b/Core/src/Visualization/ObjVisualization3D.cpp new file mode 100644 index 00000000000..777e883b3d3 --- /dev/null +++ b/Core/src/Visualization/ObjVisualization3D.cpp @@ -0,0 +1,211 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "Acts/Visualization/ObjVisualization3D.hpp" + +#include +#include +#include + +namespace Acts { + +void ObjVisualization3D::vertex(const Vector3& vtx, Color color) { + auto& o = object(); + o.vertexColors[o.vertices.size()] = color; + o.vertices.push_back(vtx.template cast()); +} + +void ObjVisualization3D::line(const Vector3& a, const Vector3& b, Color color) { + auto& o = object(); + if (color != Color{0, 0, 0}) { + o.lineColors[o.lines.size()] = color; + } + // not implemented + vertex(a, color); + vertex(b, color); + o.lines.push_back({o.vertices.size() - 2, o.vertices.size() - 1}); +} + +void ObjVisualization3D::face(const std::vector& vtxs, Color color) { + auto& o = object(); + if (color != Color{0, 0, 0}) { + o.faceColors[o.faces.size()] = color; + } + FaceType idxs; + idxs.reserve(vtxs.size()); + for (const auto& vtx : vtxs) { + vertex(vtx, color); + idxs.push_back(o.vertices.size() - 1); + } + o.faces.push_back(std::move(idxs)); +} + +void ObjVisualization3D::faces(const std::vector& vtxs, + const std::vector& faces, + Color color) { + auto& o = object(); + // No faces given - call the face() method + if (faces.empty()) { + face(vtxs, color); + } else { + if (color != Color{0, 0, 0}) { + o.faceColors[o.faces.size()] = color; + } + auto vtxoffs = o.vertices.size(); + if (color != Color{0, 0, 0}) { + o.vertexColors[o.vertices.size()] = color; + } + o.vertices.insert(o.vertices.end(), vtxs.begin(), vtxs.end()); + for (const auto& face : faces) { + if (face.size() == 2) { + o.lines.push_back({face[0] + vtxoffs, face[2] + vtxoffs}); + } else { + FaceType rawFace; + std::ranges::transform( + face, std::back_inserter(rawFace), + [&](unsigned long iv) { return (iv + vtxoffs); }); + o.faces.push_back(rawFace); + } + } + } +} + +void ObjVisualization3D::write(const std::filesystem::path& path) const { + std::ofstream os; + std::filesystem::path objectpath = path; + if (!objectpath.has_extension()) { + objectpath.replace_extension(std::filesystem::path("obj")); + } + os.open(std::filesystem::absolute(objectpath).string()); + std::filesystem::path mtlpath = objectpath; + mtlpath.replace_extension(std::filesystem::path("mtl")); + + const std::string mtlpathString = std::filesystem::absolute(mtlpath).string(); + os << "mtllib " << mtlpathString << "\n"; + std::ofstream mtlos; + mtlos.open(mtlpathString); + + write(os, mtlos); + os.close(); + mtlos.close(); +} + +void ObjVisualization3D::write(std::ostream& os) const { + std::stringstream sterile; + write(os, sterile); +} + +void ObjVisualization3D::write(std::ostream& os, std::ostream& mos) const { + std::map> materials; + + auto mixColor = [&](const Color& color) { + std::string materialName; + materialName = "material_"; + materialName += std::to_string(color[0]) + std::string("_"); + materialName += std::to_string(color[1]) + std::string("_"); + materialName += std::to_string(color[2]); + + if (!materials.contains(materialName)) { + mos << "newmtl " << materialName << "\n"; + std::vector shadings = {"Ka", "Kd", "Ks"}; + for (const auto& shd : shadings) { + mos << shd << " " << std::to_string(color[0] / 256.) << " "; + mos << std::to_string(color[1] / 256.) << " "; + mos << std::to_string(color[2] / 256.) << " " << "\n"; + } + mos << "\n"; + } + return std::string("usemtl ") + materialName; + }; + + std::size_t vertexOffset = 0; + for (const auto& o : m_objects) { + if (!o.name.empty()) { + os << "o " << o.name << "\n"; + } + + std::size_t iv = 0; + Color lastVertexColor = {0, 0, 0}; + for (const VertexType& vtx : o.vertices) { + if (o.vertexColors.contains(iv)) { + auto color = o.vertexColors.find(iv)->second; + if (color != lastVertexColor) { + os << mixColor(color) << "\n"; + lastVertexColor = color; + } + } + + os << "v " << std::setprecision(m_outputPrecision) + << m_outputScalor * vtx.x() << " " << m_outputScalor * vtx.y() << " " + << m_outputScalor * vtx.z() << "\n"; + ++iv; + } + std::size_t il = 0; + Color lastLineColor = {0, 0, 0}; + for (const auto& [start, end] : o.lines) { + if (o.lineColors.contains(il)) { + auto color = o.lineColors.find(il)->second; + if (color != lastLineColor) { + os << mixColor(color) << "\n"; + lastLineColor = color; + } + } + os << "l " << vertexOffset + start + 1 << " " << vertexOffset + end + 1 + << "\n"; + ++il; + } + std::size_t is = 0; + Color lastFaceColor = {0, 0, 0}; + for (const FaceType& fc : o.faces) { + if (o.faceColors.contains(is)) { + auto color = o.faceColors.find(is)->second; + if (color != lastFaceColor) { + os << mixColor(color) << "\n"; + lastFaceColor = color; + } + } + os << "f"; + for (std::size_t fi : fc) { + os << " " << vertexOffset + fi + 1; + } + os << "\n"; + ++is; + } + + vertexOffset += iv; + } +} + +void ObjVisualization3D::clear() { + m_objects.clear(); +} + +void ObjVisualization3D::object(const std::string& name) { + if (name.empty()) { + throw std::invalid_argument{"Object name can not be empty"}; + } + m_objects.push_back(Object{.name = name}); +} + +ObjVisualization3D::Object& ObjVisualization3D::object() { + if (m_objects.empty()) { + m_objects.push_back(Object{.name = ""}); + } + + return m_objects.back(); +} + +const ObjVisualization3D::Object& ObjVisualization3D::object() const { + if (m_objects.empty()) { + throw std::runtime_error{"No objects present"}; + } + + return m_objects.back(); +} + +} // namespace Acts diff --git a/Examples/Python/python/acts/examples/odd.py b/Examples/Python/python/acts/examples/odd.py index 4e1efb1c783..a28058af78b 100644 --- a/Examples/Python/python/acts/examples/odd.py +++ b/Examples/Python/python/acts/examples/odd.py @@ -76,8 +76,9 @@ def getOpenDataDetector( } def geoid_hook(geoid, surface): + gctx = acts.GeometryContext() if geoid.volume() in volumeRadiusCutsMap: - r = math.sqrt(surface.center()[0] ** 2 + surface.center()[1] ** 2) + r = math.sqrt(surface.center(gctx)[0] ** 2 + surface.center(gctx)[1] ** 2) geoid.setExtra(1) for cut in volumeRadiusCutsMap[geoid.volume()]: diff --git a/Examples/Python/src/Geometry.cpp b/Examples/Python/src/Geometry.cpp index 51188ee93e4..05484845465 100644 --- a/Examples/Python/src/Geometry.cpp +++ b/Examples/Python/src/Geometry.cpp @@ -37,6 +37,7 @@ #include "Acts/Surfaces/SurfaceArray.hpp" #include "Acts/Utilities/Helpers.hpp" #include "Acts/Utilities/RangeXD.hpp" +#include "Acts/Visualization/ViewConfig.hpp" #include "ActsExamples/Geometry/VolumeAssociationTest.hpp" #include @@ -109,13 +110,12 @@ void addGeometry(Context& ctx) { { py::class_>(m, "Surface") + // Can't bind directly because GeometryObject is virtual base of Surface .def("geometryId", - [](Acts::Surface& self) { return self.geometryId(); }) - .def("center", - [](Acts::Surface& self) { - return self.center(Acts::GeometryContext{}); - }) - .def("type", [](Acts::Surface& self) { return self.type(); }); + [](const Surface& self) { return self.geometryId(); }) + .def("center", &Surface::center) + .def("type", &Surface::type) + .def("visualize", &Surface::visualize); } { @@ -167,7 +167,11 @@ void addGeometry(Context& ctx) { }) .def_property_readonly( "highestTrackingVolume", - &Acts::TrackingGeometry::highestTrackingVolumePtr); + &Acts::TrackingGeometry::highestTrackingVolumePtr) + .def("visualize", &Acts::TrackingGeometry::visualize, py::arg("helper"), + py::arg("gctx"), py::arg("viewConfig") = s_viewVolume, + py::arg("portalViewConfig") = s_viewPortal, + py::arg("sensitiveViewConfig") = s_viewSensitive); } { @@ -287,7 +291,7 @@ void addExperimentalGeometry(Context& ctx) { for (const auto& surface : smap) { auto gid = surface->geometryId(); // Exclusion criteria - if (sensitiveOnly and gid.sensitive() == 0) { + if (sensitiveOnly && gid.sensitive() == 0) { continue; }; surfaceVolumeLayerMap[gid.volume()][gid.layer()].push_back(surface); diff --git a/Examples/Python/src/ModuleEntry.cpp b/Examples/Python/src/ModuleEntry.cpp index 77348edaf1c..1a0e27db907 100644 --- a/Examples/Python/src/ModuleEntry.cpp +++ b/Examples/Python/src/ModuleEntry.cpp @@ -120,6 +120,7 @@ PYBIND11_MODULE(ActsPythonBindings, m) { addAlgebra(ctx); addBinning(ctx); addEventData(ctx); + addOutput(ctx); addPropagation(ctx); addGeometryBuildingGen1(ctx); @@ -128,7 +129,6 @@ PYBIND11_MODULE(ActsPythonBindings, m) { addMagneticField(ctx); addMaterial(ctx); - addOutput(ctx); addDetector(ctx); addExampleAlgorithms(ctx); addInput(ctx); diff --git a/Examples/Python/src/Obj.cpp b/Examples/Python/src/Obj.cpp index 5ca2df40033..09acb0c53b6 100644 --- a/Examples/Python/src/Obj.cpp +++ b/Examples/Python/src/Obj.cpp @@ -6,6 +6,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +#include "Acts/Visualization/IVisualization3D.hpp" #include #include #include @@ -19,6 +20,7 @@ #include #include +#include namespace py = pybind11; using namespace pybind11::literals; @@ -87,5 +89,8 @@ void addObj(Context& ctx) { obj.write(fileName); }); } + + py::class_(m, "ObjVisualization3D") + .def(py::init<>()); } } // namespace Acts::Python diff --git a/Examples/Python/src/Output.cpp b/Examples/Python/src/Output.cpp index b53ba905700..11472d8e203 100644 --- a/Examples/Python/src/Output.cpp +++ b/Examples/Python/src/Output.cpp @@ -10,6 +10,7 @@ #include "Acts/Geometry/GeometryHierarchyMap.hpp" #include "Acts/Plugins/Python/Utilities.hpp" #include "Acts/Utilities/Logger.hpp" +#include "Acts/Visualization/IVisualization3D.hpp" #include "Acts/Visualization/ViewConfig.hpp" #include "ActsExamples/Digitization/DigitizationConfig.hpp" #include "ActsExamples/Framework/ProcessCode.hpp" @@ -57,6 +58,7 @@ #include #include +#include namespace Acts { class TrackingGeometry; @@ -130,10 +132,14 @@ void addOutput(Context& ctx) { .def(py::init<>()) .def(py::init()) .def(py::init()) - .def(py::init()) + .def(py::init()) .def_readonly("rgb", &Color::rgb); } + py::class_(m, "IVisualization3D") + .def("write", py::overload_cast( + &IVisualization3D::write, py::const_)); + { using Writer = ActsExamples::ObjTrackingGeometryWriter; auto w = py::class_>( diff --git a/Tests/UnitTests/Core/Visualization/Visualization3DTests.cpp b/Tests/UnitTests/Core/Visualization/Visualization3DTests.cpp index 58958bbbe6c..ea3e810d119 100644 --- a/Tests/UnitTests/Core/Visualization/Visualization3DTests.cpp +++ b/Tests/UnitTests/Core/Visualization/Visualization3DTests.cpp @@ -335,6 +335,8 @@ BOOST_AUTO_TEST_CASE(ColorTests) { BOOST_CHECK_EQUAL(grey, Color(std::array{128 / 255.0, 128 / 255.0, 128 / 255.0})); BOOST_CHECK_EQUAL(grey, Color(128 / 255.0, 128 / 255.0, 128 / 255.0)); + + static_assert(Color{"#0000ff"} == Color(0, 0, 255)); } BOOST_AUTO_TEST_SUITE_END() From 39c6acac6dee2504fa8213c2d37a3ad42aecb3d4 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Mon, 7 Oct 2024 20:21:21 +0200 Subject: [PATCH 23/24] refactor(geo): Teach ProtoLayer to respect local coordinate system (#3697) Needed for https://github.com/acts-project/acts/issues/3502 --- This pull request introduces several enhancements and new features to the `ProtoLayer` struct and its associated tests in the `Core` and `Tests` directories. The key changes include the addition of a local transform, multiple constructors, and new test cases to validate the functionality. ### Enhancements to `ProtoLayer` struct: * Added a new member `transform` to the `ProtoLayer` struct and updated its constructors to accept a `Transform3` parameter with a default value of `Transform3::Identity()`. (`Core/include/Acts/Geometry/ProtoLayer.hpp`, `Core/src/Geometry/ProtoLayer.cpp`) [[1]](diffhunk://#diff-1451530599ae50cdf187f035045a620fe857ff4abcb25c7a0eefc33189ef3260R36-R38) [[2]](diffhunk://#diff-1451530599ae50cdf187f035045a620fe857ff4abcb25c7a0eefc33189ef3260R47-R50) [[3]](diffhunk://#diff-1451530599ae50cdf187f035045a620fe857ff4abcb25c7a0eefc33189ef3260R60-R76) [[4]](diffhunk://#diff-4d780d37fe19501d1f964a2d67f4a654c4365f31f736d0c9653a169f1637f29eL16-R43) [[5]](diffhunk://#diff-4d780d37fe19501d1f964a2d67f4a654c4365f31f736d0c9653a169f1637f29eL81-R95) * Implemented a friend function for the output stream operator to facilitate easy printing of `ProtoLayer` objects. (`Core/include/Acts/Geometry/ProtoLayer.hpp`) ### Codebase simplification: * Removed unused `#include` directives from `ProtoLayer.cpp` to clean up the code. (`Core/src/Geometry/ProtoLayer.cpp`) ### Enhancements to unit tests: * Added new test cases in `ProtoLayerTests.cpp` to validate the behavior of `ProtoLayer` with different transformations. These changes enhance the flexibility and functionality of the `ProtoLayer` struct and ensure that the new features are thoroughly tested. --- Core/include/Acts/Geometry/ProtoLayer.hpp | 32 +++- Core/src/Geometry/ProtoLayer.cpp | 34 ++-- .../Core/Geometry/ProtoLayerTests.cpp | 161 +++++++++++++++--- 3 files changed, 184 insertions(+), 43 deletions(-) diff --git a/Core/include/Acts/Geometry/ProtoLayer.hpp b/Core/include/Acts/Geometry/ProtoLayer.hpp index d12e674918d..1e5d95dc025 100644 --- a/Core/include/Acts/Geometry/ProtoLayer.hpp +++ b/Core/include/Acts/Geometry/ProtoLayer.hpp @@ -33,6 +33,9 @@ struct ProtoLayer { /// The envelope parameters ExtentEnvelope envelope = ExtentEnvelope::Zero(); + /// The local transform + Transform3 transform = Transform3::Identity(); + /// Constructor /// /// Loops over a provided vector of surface and calculates the various @@ -41,8 +44,10 @@ struct ProtoLayer { /// /// @param gctx The current geometry context object, e.g. alignment /// @param surfaces The vector of surfaces to consider + /// @param transformIn The local transform to evaluate the sizing in ProtoLayer(const GeometryContext& gctx, - const std::vector& surfaces); + const std::vector& surfaces, + const Transform3& transformIn = Transform3::Identity()); /// Constructor /// @@ -52,8 +57,23 @@ struct ProtoLayer { /// /// @param gctx The current geometry context object, e.g. alignment /// @param surfaces The vector of surfaces to consider + /// @param transformIn The local transform to evaluate the sizing in ProtoLayer(const GeometryContext& gctx, - const std::vector>& surfaces); + const std::vector>& surfaces, + const Transform3& transformIn = Transform3::Identity()); + + /// Constructor + /// + /// Loops over a provided vector of surface and calculates the various + /// min/max values in one go. Also takes into account the thickness + /// of an associated DetectorElement, if it exists. + /// + /// @param gctx The current geometry context object, e.g. alignment + /// @param surfaces The vector of surfaces to consider + /// @param transformIn The local transform to evaluate the sizing in + ProtoLayer(const GeometryContext& gctx, + const std::vector>& surfaces, + const Transform3& transformIn = Transform3::Identity()); ProtoLayer() = default; @@ -81,6 +101,14 @@ struct ProtoLayer { /// @param sl the input ostream std::ostream& toStream(std::ostream& sl) const; + /// Output stream operator + /// @param sl the input ostream + /// @param pl the ProtoLayer to be printed + /// @return the output ostream + friend std::ostream& operator<<(std::ostream& sl, const ProtoLayer& pl) { + return pl.toStream(sl); + } + /// Give access to the surfaces used/assigned to the ProtoLayer const std::vector& surfaces() const; diff --git a/Core/src/Geometry/ProtoLayer.cpp b/Core/src/Geometry/ProtoLayer.cpp index 659b34b1168..c8f190d4293 100644 --- a/Core/src/Geometry/ProtoLayer.cpp +++ b/Core/src/Geometry/ProtoLayer.cpp @@ -13,26 +13,34 @@ #include "Acts/Surfaces/RegularSurface.hpp" #include "Acts/Utilities/Helpers.hpp" -#include -#include -#include -#include - using Acts::VectorHelpers::perp; using Acts::VectorHelpers::phi; namespace Acts { ProtoLayer::ProtoLayer(const GeometryContext& gctx, - const std::vector& surfaces) - : m_surfaces(surfaces) { + const std::vector& surfaces, + const Transform3& transformIn) + : transform(transformIn), m_surfaces(surfaces) { measure(gctx, surfaces); } ProtoLayer::ProtoLayer( const GeometryContext& gctx, - const std::vector>& surfaces) - : m_surfaces(unpack_shared_vector(surfaces)) { + const std::vector>& surfaces, + const Transform3& transformIn) + : transform(transformIn), m_surfaces(unpack_shared_vector(surfaces)) { + measure(gctx, m_surfaces); +} + +ProtoLayer::ProtoLayer(const GeometryContext& gctx, + const std::vector>& surfaces, + const Transform3& transformIn) + : transform(transformIn) { + m_surfaces.reserve(surfaces.size()); + for (const auto& sf : surfaces) { + m_surfaces.push_back(sf.get()); + } measure(gctx, m_surfaces); } @@ -78,15 +86,13 @@ void ProtoLayer::measure(const GeometryContext& gctx, double thickness = element->thickness(); // We need a translation along and opposite half thickness Vector3 sfNormal = regSurface->normal(gctx, sf->center(gctx)); - std::vector deltaT = {-0.5 * thickness, 0.5 * thickness}; - for (const auto& dT : deltaT) { - Transform3 dtransform = Transform3::Identity(); - dtransform.pretranslate(dT * sfNormal); + for (const auto& dT : {-0.5 * thickness, 0.5 * thickness}) { + Transform3 dtransform = transform * Translation3{dT * sfNormal}; extent.extend(sfPolyhedron.extent(dtransform)); } continue; } - extent.extend(sfPolyhedron.extent()); + extent.extend(sfPolyhedron.extent(transform)); } } diff --git a/Tests/UnitTests/Core/Geometry/ProtoLayerTests.cpp b/Tests/UnitTests/Core/Geometry/ProtoLayerTests.cpp index cbace6a6559..efde97d9b41 100644 --- a/Tests/UnitTests/Core/Geometry/ProtoLayerTests.cpp +++ b/Tests/UnitTests/Core/Geometry/ProtoLayerTests.cpp @@ -9,30 +9,33 @@ #include #include "Acts/Definitions/Algebra.hpp" +#include "Acts/Definitions/Units.hpp" +#include "Acts/Geometry/DetectorElementBase.hpp" #include "Acts/Geometry/Extent.hpp" #include "Acts/Geometry/GeometryContext.hpp" #include "Acts/Geometry/ProtoLayer.hpp" #include "Acts/Surfaces/PlaneSurface.hpp" #include "Acts/Surfaces/RectangleBounds.hpp" #include "Acts/Surfaces/Surface.hpp" +#include "Acts/Tests/CommonHelpers/DetectorElementStub.hpp" #include "Acts/Tests/CommonHelpers/FloatComparisons.hpp" #include "Acts/Utilities/BinningType.hpp" #include "Acts/Utilities/RangeXD.hpp" -#include #include #include #include #include -#include #include namespace Acts::Test::Layers { +GeometryContext tgContext = GeometryContext(); + BOOST_AUTO_TEST_SUITE(Geometry) BOOST_AUTO_TEST_CASE(ProtoLayerTests) { - GeometryContext tgContext = GeometryContext(); + using enum BinningValue; // Create a proto layer with 4 surfaces on the x/y grid auto recBounds = std::make_shared(3., 6.); @@ -105,20 +108,20 @@ BOOST_AUTO_TEST_CASE(ProtoLayerTests) { // Test 1 - identity transform auto protoLayer = createProtoLayer(Transform3::Identity()); - CHECK_CLOSE_ABS(protoLayer.range(BinningValue::binX), 12., 1e-8); - CHECK_CLOSE_ABS(protoLayer.medium(BinningValue::binX), 0., 1e-8); - CHECK_CLOSE_ABS(protoLayer.min(BinningValue::binX), -6., 1e-8); - CHECK_CLOSE_ABS(protoLayer.max(BinningValue::binX), 6., 1e-8); - CHECK_CLOSE_ABS(protoLayer.range(BinningValue::binY), 6., 1e-8); - CHECK_CLOSE_ABS(protoLayer.medium(BinningValue::binY), 0., 1e-8); - CHECK_CLOSE_ABS(protoLayer.min(BinningValue::binY), -3., 1e-8); - CHECK_CLOSE_ABS(protoLayer.max(BinningValue::binY), 3., 1e-8); - CHECK_CLOSE_ABS(protoLayer.range(BinningValue::binZ), 12., 1e-8); - CHECK_CLOSE_ABS(protoLayer.medium(BinningValue::binZ), 0., 1e-8); - CHECK_CLOSE_ABS(protoLayer.min(BinningValue::binZ), -6., 1e-8); - CHECK_CLOSE_ABS(protoLayer.max(BinningValue::binZ), 6., 1e-8); - CHECK_CLOSE_ABS(protoLayer.max(BinningValue::binR), std::hypot(3, 6), 1e-8); - CHECK_CLOSE_ABS(protoLayer.min(BinningValue::binR), 3., 1e-8); + CHECK_CLOSE_ABS(protoLayer.range(binX), 12., 1e-8); + CHECK_CLOSE_ABS(protoLayer.medium(binX), 0., 1e-8); + CHECK_CLOSE_ABS(protoLayer.min(binX), -6., 1e-8); + CHECK_CLOSE_ABS(protoLayer.max(binX), 6., 1e-8); + CHECK_CLOSE_ABS(protoLayer.range(binY), 6., 1e-8); + CHECK_CLOSE_ABS(protoLayer.medium(binY), 0., 1e-8); + CHECK_CLOSE_ABS(protoLayer.min(binY), -3., 1e-8); + CHECK_CLOSE_ABS(protoLayer.max(binY), 3., 1e-8); + CHECK_CLOSE_ABS(protoLayer.range(binZ), 12., 1e-8); + CHECK_CLOSE_ABS(protoLayer.medium(binZ), 0., 1e-8); + CHECK_CLOSE_ABS(protoLayer.min(binZ), -6., 1e-8); + CHECK_CLOSE_ABS(protoLayer.max(binZ), 6., 1e-8); + CHECK_CLOSE_ABS(protoLayer.max(binR), std::hypot(3, 6), 1e-8); + CHECK_CLOSE_ABS(protoLayer.min(binR), 3., 1e-8); // Test 1a @@ -127,16 +130,15 @@ BOOST_AUTO_TEST_CASE(ProtoLayerTests) { auto protoLayerRot = createProtoLayer(AngleAxis3(-0.345, Vector3::UnitZ()) * Transform3::Identity()); - BOOST_CHECK_NE(protoLayer.min(BinningValue::binX), -6.); - CHECK_CLOSE_ABS(protoLayerRot.medium(BinningValue::binX), 0., 1e-8); - CHECK_CLOSE_ABS(protoLayerRot.medium(BinningValue::binY), 0., 1e-8); - CHECK_CLOSE_ABS(protoLayerRot.range(BinningValue::binZ), 12., 1e-8); - CHECK_CLOSE_ABS(protoLayerRot.medium(BinningValue::binZ), 0., 1e-8); - CHECK_CLOSE_ABS(protoLayerRot.min(BinningValue::binZ), -6., 1e-8); - CHECK_CLOSE_ABS(protoLayerRot.max(BinningValue::binZ), 6., 1e-8); - CHECK_CLOSE_ABS(protoLayerRot.min(BinningValue::binR), 3., 1e-8); - CHECK_CLOSE_ABS(protoLayerRot.max(BinningValue::binR), std::hypot(3, 6), - 1e-8); + BOOST_CHECK_NE(protoLayer.min(binX), -6.); + CHECK_CLOSE_ABS(protoLayerRot.medium(binX), 0., 1e-8); + CHECK_CLOSE_ABS(protoLayerRot.medium(binY), 0., 1e-8); + CHECK_CLOSE_ABS(protoLayerRot.range(binZ), 12., 1e-8); + CHECK_CLOSE_ABS(protoLayerRot.medium(binZ), 0., 1e-8); + CHECK_CLOSE_ABS(protoLayerRot.min(binZ), -6., 1e-8); + CHECK_CLOSE_ABS(protoLayerRot.max(binZ), 6., 1e-8); + CHECK_CLOSE_ABS(protoLayerRot.min(binR), 3., 1e-8); + CHECK_CLOSE_ABS(protoLayerRot.max(binR), std::hypot(3, 6), 1e-8); std::stringstream sstream; protoLayerRot.toStream(sstream); @@ -155,6 +157,111 @@ Extent in space : BOOST_CHECK_EQUAL(sstream.str(), oString); } +BOOST_AUTO_TEST_CASE(OrientedLayer) { + using enum BinningValue; + using namespace Acts::UnitLiterals; + + Transform3 base = Transform3::Identity(); + + auto recBounds = std::make_shared(3_mm, 6_mm); + + std::vector> detectorElements; + + auto makeFan = [&](double yrot, double thickness = 0) { + detectorElements.clear(); + + std::size_t nSensors = 8; + double deltaPhi = 2 * M_PI / nSensors; + double r = 20_mm; + std::vector> surfaces; + for (std::size_t i = 0; i < nSensors; i++) { + // Create a fan of sensors + + Transform3 trf = base * AngleAxis3{yrot, Vector3::UnitY()} * + AngleAxis3{deltaPhi * i, Vector3::UnitZ()} * + Translation3(Vector3::UnitX() * r); + + auto& element = detectorElements.emplace_back( + std::make_unique(trf, recBounds, thickness)); + + surfaces.push_back(element->surface().getSharedPtr()); + } + return surfaces; + }; + + std::vector> surfaces = makeFan(0_degree); + + ProtoLayer protoLayer(tgContext, surfaces); + + BOOST_CHECK_EQUAL(protoLayer.surfaces().size(), 8); + BOOST_CHECK_CLOSE(protoLayer.min(binX), -23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.max(binX), 23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.min(binY), -23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.max(binY), 23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.min(binZ), 0_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.max(binZ), 0_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.min(binR), 17_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.max(binR), 23.769728648_mm, 1e-8); + + surfaces = makeFan(45_degree); + + // Do NOT provide rotation matrix: sizing will be affected + protoLayer = {tgContext, surfaces}; + + BOOST_CHECK_EQUAL(protoLayer.surfaces().size(), 8); + BOOST_CHECK_CLOSE(protoLayer.min(binX), -16.26345596_mm, 1e-4); + BOOST_CHECK_CLOSE(protoLayer.max(binX), 16.26345596_mm, 1e-4); + BOOST_CHECK_CLOSE(protoLayer.min(binY), -23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.max(binY), 23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.min(binZ), -16.26345596_mm, 1e-4); + BOOST_CHECK_CLOSE(protoLayer.max(binZ), 16.26345596_mm, 1e-4); + + protoLayer = {tgContext, surfaces, + Transform3{AngleAxis3{45_degree, Vector3::UnitY()}}.inverse()}; + + BOOST_CHECK_EQUAL(protoLayer.surfaces().size(), 8); + BOOST_CHECK_CLOSE(protoLayer.range(binX), 46_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.min(binX), -23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.max(binX), 23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.range(binY), 46_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.min(binY), -23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.max(binY), 23_mm, 1e-8); + CHECK_SMALL(protoLayer.range(binZ), 1e-14); + CHECK_SMALL(protoLayer.min(binZ), 1e-14); + CHECK_SMALL(protoLayer.max(binZ), 1e-14); + + surfaces = makeFan(0_degree, 10_mm); + + protoLayer = {tgContext, surfaces}; + + BOOST_CHECK_EQUAL(protoLayer.surfaces().size(), 8); + BOOST_CHECK_CLOSE(protoLayer.range(binX), 46_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.min(binX), -23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.max(binX), 23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.range(binY), 46_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.min(binY), -23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.max(binY), 23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.range(binZ), 10_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.min(binZ), -5_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.max(binZ), 5_mm, 1e-8); + + surfaces = makeFan(45_degree, 10_mm); + + protoLayer = {tgContext, surfaces, + Transform3{AngleAxis3{45_degree, Vector3::UnitY()}}.inverse()}; + + BOOST_CHECK_EQUAL(protoLayer.surfaces().size(), 8); + BOOST_CHECK_CLOSE(protoLayer.range(binX), 46_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.min(binX), -23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.max(binX), 23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.range(binY), 46_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.min(binY), -23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.max(binY), 23_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.range(binZ), 10_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.min(binZ), -5_mm, 1e-8); + BOOST_CHECK_CLOSE(protoLayer.max(binZ), 5_mm, 1e-8); +} + BOOST_AUTO_TEST_SUITE_END() } // namespace Acts::Test::Layers From 550c0d6cb82ec82e37f65f236ba099606c8e9619 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 8 Oct 2024 11:12:18 +0200 Subject: [PATCH 24/24] build: Use correct GM variables in cmake config (#3699) The generated CMake config was using the wrong version variables from GeoModel. --- cmake/ActsConfig.cmake.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/ActsConfig.cmake.in b/cmake/ActsConfig.cmake.in index b66c5ae3544..7ed4b3884a5 100644 --- a/cmake/ActsConfig.cmake.in +++ b/cmake/ActsConfig.cmake.in @@ -77,8 +77,8 @@ if(PluginPodio IN_LIST Acts_COMPONENTS) find_dependency(podio @podio_VERSION@ CONFIG EXACT) endif() if(PluginGeoModel IN_LIST Acts_COMPONENTS) - find_dependency(GeoModelCore @GeoModel_VERSION@ CONFIG EXACT) - find_dependency(GeoModelIO @GeoModel_VERSION@ CONFIG EXACT) + find_dependency(GeoModelCore @GeoModelCore_VERSION@ CONFIG EXACT) + find_dependency(GeoModelIO @GeoModelIO_VERSION@ CONFIG EXACT) endif() if (PluginHashing IN_LIST Acts_COMPONENTS) find_dependency(Annoy @ANNOY_VERSION@ CONFIG EXACT)