From 1d1d4860513f963f718f2c1e122a37fb1d2e1faf Mon Sep 17 00:00:00 2001 From: Maximiliano Puccio Date: Tue, 8 Sep 2026 13:03:08 +0200 Subject: [PATCH 1/2] ITSMFT: unify cellular automaton tracking for ITS and MFT Introduce shared tracking, propagation, material handling and runtime ROF tables with compatibility wrappers for legacy ITS callers. Add ITS and MFT CA workflows with checked configuration, reusable workflow sessions and workflow-owned publication. Include tracking and workflow tests. --- Detectors/ITSMFT/ITS/CMakeLists.txt | 1 + .../ITSMFT/ITS/workflow-ca/CMakeLists.txt | 48 + .../include/ITSCAWorkflow/CATrackerSpec.h | 92 ++ .../include/ITSCAWorkflow/ConfigPreflight.h | 60 + .../ITSCAWorkflow/PublicationAdapter.h | 175 +++ .../include/ITSCAWorkflow/TruthSeeding.h | 49 + .../ITS/workflow-ca/src/CATrackerSpec.cxx | 403 +++++ .../ITS/workflow-ca/src/ConfigPreflight.cxx | 113 ++ .../src/its-ca-tracker-workflow.cxx | 69 + .../test/testITSCAConfigPreflight.cxx | 201 +++ .../test/testITSCATrackerDPLContract.cxx | 121 ++ .../test/testITSCATruthSeeding.cxx | 75 + Detectors/ITSMFT/MFT/workflow/CMakeLists.txt | 37 +- .../include/MFTWorkflow/CARecoWorkflow.h | 27 + .../include/MFTWorkflow/CATrackerSpec.h | 87 ++ .../include/MFTWorkflow/CAWorkflowOptions.h | 93 ++ .../include/MFTWorkflow/TrackerSpec.h | 2 +- .../MFT/workflow/src/CARecoWorkflow.cxx | 64 + .../ITSMFT/MFT/workflow/src/CATrackerSpec.cxx | 336 ++++ .../MFT/workflow/src/CAWorkflowOptions.cxx | 131 ++ .../ITSMFT/MFT/workflow/src/RecoWorkflow.cxx | 2 +- .../ITSMFT/MFT/workflow/src/TrackerSpec.cxx | 10 +- .../MFT/workflow/src/TracksToRecordsSpec.cxx | 1 - .../MFT/workflow/src/mft-ca-reco-workflow.cxx | 70 + .../workflow/src/mft-ca-tracker-workflow.cxx | 63 + .../src/mft-cluster-writer-workflow.cxx | 1 - .../MFT/workflow/src/mft-reco-workflow.cxx | 3 - .../test/testCATrackerPublicationDecision.cxx | 42 + .../workflow/test/testMFTCARecoWorkflow.cxx | 189 +++ .../test/testMFTCATrackerDPLContract.cxx | 71 + Detectors/ITSMFT/common/CMakeLists.txt | 1 + .../ITSMFT/common/tracking/CMakeLists.txt | 58 +- .../ITSMFTTracking/CapacityEstimator.h | 6 + .../tracking/include/ITSMFTTracking/Cell.h | 235 +++ .../include/ITSMFTTracking/ClusterDecoding.h | 239 +++ .../include/ITSMFTTracking/Configuration.h | 341 ++++ .../include/ITSMFTTracking/DetectorLayout.h | 115 ++ .../include/ITSMFTTracking/GenericTrack.h | 99 ++ .../GenericTrackOutputAdapter.h | 463 ++++++ .../ITSMFTTracking/GlobalMeasurement.h | 90 ++ .../tracking/include/ITSMFTTracking/IOUtils.h | 333 ++++ .../ITSMFTDetectorDefinitions.h | 112 ++ .../tracking/include/ITSMFTTracking/IdTypes.h | 82 + .../ITSMFTTracking/IndexTableConfiguration.h | 96 ++ .../IndexTableConfigurationSet.h | 68 + .../include/ITSMFTTracking/IndexTableUtils.h | 221 +++ .../ITSMFTTracking/IterationConfiguration.h | 79 + .../include/ITSMFTTracking/LayerMask.h | 116 ++ .../include/ITSMFTTracking/MaterialPhysics.h | 152 ++ .../include/ITSMFTTracking/Propagator.h | 85 + .../include/ITSMFTTracking/ROFLookupTables.h | 630 ++------ .../include/ITSMFTTracking/ROFViews.h | 382 +++++ .../include/ITSMFTTracking/RefitDriver.h | 283 ++++ .../ITSMFTTracking/SurfaceDescriptor.h | 93 ++ .../ITSMFTTracking/SurfaceMeasurement.h | 57 + .../include/ITSMFTTracking/SurfaceSpec.h | 234 +++ .../SurfaceStateOperationResult.h | 58 + .../include/ITSMFTTracking/SurfaceTiming.h | 229 +++ .../ITSMFTTracking/SurfaceTrackState.h | 127 ++ .../include/ITSMFTTracking/TimeFrame.h | 222 +++ .../tracking/include/ITSMFTTracking/Tracker.h | 119 ++ .../include/ITSMFTTracking/TrackerTraits.h | 162 ++ .../ITSMFTTracking/TrackingConfigParam.h | 145 ++ .../ITSMFTTracking/TrackingPrimitives.h | 57 + .../ITSMFTTracking/TraversalTopology.h | 145 ++ .../include/ITSMFTTracking/TripletFitting.h | 76 + .../include/ITSMFTTracking/WorkflowSession.h | 284 ++++ .../ITSMFTTracking/detail/CandidateFinding.h | 79 + .../detail/ITSSharedClusterCompatibility.h | 147 ++ .../detail/MFTFwdTrackHelpers.h | 130 ++ .../detail/SurfaceStateOperations.h | 81 + .../detail/SurfaceTrackStateLegacyAdapters.h | 149 ++ .../ITSMFTTracking/detail/TimeFrameScratch.h | 114 ++ .../detail/TrackerTraversalPreparation.h | 53 + .../detail/TrackingKernelParameters.h | 59 + .../common/tracking/src/CandidateFinding.cxx | 98 ++ .../common/tracking/src/Configuration.cxx | 429 ++++++ .../tracking/src/FamilyMaterialOperations.cxx | 356 +++++ .../ITSMFT/common/tracking/src/IOUtils.cxx | 646 ++++++++ .../tracking/src/ITSMFTTrackingLinkDef.h | 26 + .../tracking/src/IndexTableConfiguration.cxx | 74 + .../common/tracking/src/MaterialPhysics.cxx | 177 +++ .../ITSMFT/common/tracking/src/Propagator.cxx | 462 ++++++ .../src/PropagatorBarrelOperations.cxx | 727 +++++++++ .../src/PropagatorForwardOperations.cxx | 614 ++++++++ .../ITSMFT/common/tracking/src/TimeFrame.cxx | 482 ++++++ .../common/tracking/src/TimeFrameScratch.cxx | 125 ++ .../ITSMFT/common/tracking/src/Tracker.cxx | 620 ++++++++ .../common/tracking/src/TrackerTraits.cxx | 1177 ++++++++++++++ .../src/TrackerTraversalPreparation.cxx | 68 + .../tracking/src/TrackingConfigParam.cxx | 23 + .../common/tracking/src/TraversalTopology.cxx | 168 ++ .../common/tracking/src/TripletFitting.cxx | 433 ++++++ .../common/tracking/test/CMakeLists.txt | 37 + .../test/CombinedTrackingTestSupport.h | 265 ++++ .../test/TrackingParameterTestSupport.h | 93 ++ .../tracking/test/TraversalTestSupport.h | 86 ++ .../common/tracking/test/testCellFinding.cxx | 495 ++++++ .../test/testCombinedTrackingComposition.cxx | 1365 +++++++++++++++++ .../testComputeLayerCellsOrchestration.cxx | 1301 ++++++++++++++++ ...testComputeLayerTrackletsOrchestration.cxx | 782 ++++++++++ .../test/testCovarianceSanitization.cxx | 666 ++++++++ .../tracking/test/testDetectorLayout.cxx | 141 ++ .../common/tracking/test/testGenericTrack.cxx | 1184 ++++++++++++++ ...stITSCommonCATrackingModeConfiguration.cxx | 231 +++ .../test/testITSMFTSurfaceSpecProjection.cxx | 192 +++ .../test/testMFTCATrackingConfiguration.cxx | 193 +++ .../tracking/test/testMFTNormalizedRefit.cxx | 539 +++++++ .../tracking/test/testMaterialPhysics.cxx | 626 ++++++++ .../tracking/test/testMultiSourceLoading.cxx | 1316 ++++++++++++++++ .../common/tracking/test/testPropagator.cxx | 965 ++++++++++++ .../tracking/test/testROFLookupTables.cxx | 120 ++ .../tracking/test/testSlabBumpAllocator.cxx | 25 +- .../tracking/test/testSurfaceTiming.cxx | 261 ++++ .../tracking/test/testTimeFrameLifecycle.cxx | 411 +++++ .../test/testTimeFrameLoadFailure.cxx | 122 ++ .../test/testTrackerFailureContract.cxx | 1001 ++++++++++++ .../tracking/test/testTrackletFinding.cxx | 835 ++++++++++ .../tracking/test/testTraversalTopology.cxx | 224 +++ .../tracking/test/testTripletFitting.cxx | 327 ++++ .../tracking/test/testWorkflowSession.cxx | 485 ++++++ .../common/workflow-ca-writer/CMakeLists.txt | 26 + .../ITSMFTCAWriter/ITSCATrackWriterSpec.h | 29 + .../ITSMFTCAWriter/MFTCATrackWriterSpec.h} | 23 +- .../src/ITSCATrackWriterSpec.cxx | 62 + .../src/MFTCATrackWriterSpec.cxx} | 10 +- .../test/testITSMFTCAWriterContract.cxx | 106 ++ 127 files changed, 30447 insertions(+), 544 deletions(-) create mode 100644 Detectors/ITSMFT/ITS/workflow-ca/CMakeLists.txt create mode 100644 Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/CATrackerSpec.h create mode 100644 Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/ConfigPreflight.h create mode 100644 Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/PublicationAdapter.h create mode 100644 Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/TruthSeeding.h create mode 100644 Detectors/ITSMFT/ITS/workflow-ca/src/CATrackerSpec.cxx create mode 100644 Detectors/ITSMFT/ITS/workflow-ca/src/ConfigPreflight.cxx create mode 100644 Detectors/ITSMFT/ITS/workflow-ca/src/its-ca-tracker-workflow.cxx create mode 100644 Detectors/ITSMFT/ITS/workflow-ca/test/testITSCAConfigPreflight.cxx create mode 100644 Detectors/ITSMFT/ITS/workflow-ca/test/testITSCATrackerDPLContract.cxx create mode 100644 Detectors/ITSMFT/ITS/workflow-ca/test/testITSCATruthSeeding.cxx create mode 100644 Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CARecoWorkflow.h create mode 100644 Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CATrackerSpec.h create mode 100644 Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CAWorkflowOptions.h create mode 100644 Detectors/ITSMFT/MFT/workflow/src/CARecoWorkflow.cxx create mode 100644 Detectors/ITSMFT/MFT/workflow/src/CATrackerSpec.cxx create mode 100644 Detectors/ITSMFT/MFT/workflow/src/CAWorkflowOptions.cxx create mode 100644 Detectors/ITSMFT/MFT/workflow/src/mft-ca-reco-workflow.cxx create mode 100644 Detectors/ITSMFT/MFT/workflow/src/mft-ca-tracker-workflow.cxx create mode 100644 Detectors/ITSMFT/MFT/workflow/test/testCATrackerPublicationDecision.cxx create mode 100644 Detectors/ITSMFT/MFT/workflow/test/testMFTCARecoWorkflow.cxx create mode 100644 Detectors/ITSMFT/MFT/workflow/test/testMFTCATrackerDPLContract.cxx create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Cell.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ClusterDecoding.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Configuration.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/DetectorLayout.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/GenericTrack.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/GenericTrackOutputAdapter.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/GlobalMeasurement.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IOUtils.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ITSMFTDetectorDefinitions.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IdTypes.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IndexTableConfiguration.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IndexTableConfigurationSet.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IndexTableUtils.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IterationConfiguration.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/LayerMask.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/MaterialPhysics.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Propagator.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ROFViews.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/RefitDriver.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceDescriptor.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceMeasurement.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceSpec.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceStateOperationResult.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceTiming.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceTrackState.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TimeFrame.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Tracker.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackerTraits.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackingConfigParam.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackingPrimitives.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TraversalTopology.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TripletFitting.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/WorkflowSession.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/CandidateFinding.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/ITSSharedClusterCompatibility.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/MFTFwdTrackHelpers.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/SurfaceStateOperations.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/SurfaceTrackStateLegacyAdapters.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/TimeFrameScratch.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/TrackerTraversalPreparation.h create mode 100644 Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/TrackingKernelParameters.h create mode 100644 Detectors/ITSMFT/common/tracking/src/CandidateFinding.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/Configuration.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/FamilyMaterialOperations.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/IOUtils.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/ITSMFTTrackingLinkDef.h create mode 100644 Detectors/ITSMFT/common/tracking/src/IndexTableConfiguration.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/MaterialPhysics.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/Propagator.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/PropagatorBarrelOperations.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/PropagatorForwardOperations.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/TimeFrame.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/TimeFrameScratch.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/Tracker.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/TrackerTraits.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/TrackerTraversalPreparation.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/TrackingConfigParam.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/TraversalTopology.cxx create mode 100644 Detectors/ITSMFT/common/tracking/src/TripletFitting.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/CombinedTrackingTestSupport.h create mode 100644 Detectors/ITSMFT/common/tracking/test/TrackingParameterTestSupport.h create mode 100644 Detectors/ITSMFT/common/tracking/test/TraversalTestSupport.h create mode 100644 Detectors/ITSMFT/common/tracking/test/testCellFinding.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testCombinedTrackingComposition.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testComputeLayerCellsOrchestration.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testComputeLayerTrackletsOrchestration.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testCovarianceSanitization.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testDetectorLayout.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testGenericTrack.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testITSCommonCATrackingModeConfiguration.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testITSMFTSurfaceSpecProjection.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testMFTCATrackingConfiguration.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testMFTNormalizedRefit.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testMaterialPhysics.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testMultiSourceLoading.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testPropagator.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testSurfaceTiming.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testTimeFrameLifecycle.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testTimeFrameLoadFailure.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testTrackerFailureContract.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testTrackletFinding.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testTraversalTopology.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testTripletFitting.cxx create mode 100644 Detectors/ITSMFT/common/tracking/test/testWorkflowSession.cxx create mode 100644 Detectors/ITSMFT/common/workflow-ca-writer/CMakeLists.txt create mode 100644 Detectors/ITSMFT/common/workflow-ca-writer/include/ITSMFTCAWriter/ITSCATrackWriterSpec.h rename Detectors/ITSMFT/{MFT/workflow/include/MFTWorkflow/TrackWriterSpec.h => common/workflow-ca-writer/include/ITSMFTCAWriter/MFTCATrackWriterSpec.h} (61%) create mode 100644 Detectors/ITSMFT/common/workflow-ca-writer/src/ITSCATrackWriterSpec.cxx rename Detectors/ITSMFT/{MFT/workflow/src/TrackWriterSpec.cxx => common/workflow-ca-writer/src/MFTCATrackWriterSpec.cxx} (85%) create mode 100644 Detectors/ITSMFT/common/workflow-ca-writer/test/testITSMFTCAWriterContract.cxx diff --git a/Detectors/ITSMFT/ITS/CMakeLists.txt b/Detectors/ITSMFT/ITS/CMakeLists.txt index 708556ec8b7ec..43ddf49d4660a 100644 --- a/Detectors/ITSMFT/ITS/CMakeLists.txt +++ b/Detectors/ITSMFT/ITS/CMakeLists.txt @@ -15,6 +15,7 @@ add_subdirectory(simulation) add_subdirectory(reconstruction) add_subdirectory(tracking) add_subdirectory(workflow) +add_subdirectory(workflow-ca) add_subdirectory(postprocessing) add_subdirectory(macros) add_subdirectory(QC) diff --git a/Detectors/ITSMFT/ITS/workflow-ca/CMakeLists.txt b/Detectors/ITSMFT/ITS/workflow-ca/CMakeLists.txt new file mode 100644 index 0000000000000..f41dc3fcb1fd9 --- /dev/null +++ b/Detectors/ITSMFT/ITS/workflow-ca/CMakeLists.txt @@ -0,0 +1,48 @@ +# Copyright 2019-2020 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. + +o2_add_library(ITSCAWorkflow + TARGETVARNAME targetName + SOURCES src/ConfigPreflight.cxx + src/CATrackerSpec.cxx + PUBLIC_LINK_LIBRARIES O2::Framework + O2::SimulationDataFormat + O2::DataFormatsITS + O2::DataFormatsITSMFT + O2::ITSBase + O2::ITSMFTTracking + O2::ITSMFTCAWriter + O2::MFTTracking + O2::Steer + O2::CCDB) + +o2_add_executable(ca-tracker-workflow + SOURCES src/its-ca-tracker-workflow.cxx + COMPONENT_NAME its + PUBLIC_LINK_LIBRARIES O2::ITSCAWorkflow) + +o2_add_test(its-ca-config-preflight + COMPONENT_NAME its + LABELS "its;workflow;itsmft" + SOURCES test/testITSCAConfigPreflight.cxx + PUBLIC_LINK_LIBRARIES O2::ITSCAWorkflow) + +o2_add_test(its-ca-tracker-dpl-contract + COMPONENT_NAME its + LABELS "its;workflow;itsmft" + SOURCES test/testITSCATrackerDPLContract.cxx + PUBLIC_LINK_LIBRARIES O2::ITSCAWorkflow) + +o2_add_test(its-ca-truth-seeding + COMPONENT_NAME its + LABELS "its;workflow;itsmft" + SOURCES test/testITSCATruthSeeding.cxx + PUBLIC_LINK_LIBRARIES O2::ITSCAWorkflow) diff --git a/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/CATrackerSpec.h b/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/CATrackerSpec.h new file mode 100644 index 0000000000000..7fde173014826 --- /dev/null +++ b/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/CATrackerSpec.h @@ -0,0 +1,92 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file CATrackerSpec.h +/// \brief ITS common-CA tracker DPL device with tracker-only outputs. + +#ifndef O2_ITS_CA_WORKFLOW_CATRACKERSPEC_H_ +#define O2_ITS_CA_WORKFLOW_CATRACKERSPEC_H_ + +#include +#include +#include +#include + +#include + +#include "DataFormatsITS/TrackITS.h" +#include "DataFormatsITSMFT/ROFRecord.h" +#include "DetectorsBase/GRPGeomHelper.h" +#include "Framework/DataProcessorSpec.h" +#include "Framework/Task.h" +#include "ITSMFTTracking/GenericTrackOutputAdapter.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSCAWorkflow/ConfigPreflight.h" +#include "ITSMFTTracking/ClusterDecoding.h" +#include "ITSCAWorkflow/PublicationAdapter.h" +#include "ITSMFTTracking/Tracker.h" +#include "ITSMFTTracking/TrackerTraits.h" +#include "ITSMFTTracking/WorkflowSession.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "ITSMFTTracking/ROFViews.h" +#include "ITSMFTTracking/ROFLookupTables.h" +#include "DataFormatsITSMFT/TopologyDictionary.h" +#include "SimulationDataFormat/MCCompLabel.h" + +namespace o2::its::ca +{ + +using o2::itsmft::tracking::CATrackerPublicationAction; +using o2::itsmft::tracking::decideCATrackerPublicationAction; + +/// ITS common-CA tracker DPL task. Owns the TimeFrame and composes the +/// workflow input/timing/publication edge with Tracker. +class CATrackerDPL : public o2::framework::Task +{ + public: + CATrackerDPL(std::shared_ptr gr, WorkflowOptions options); + ~CATrackerDPL() override = default; + + void init(framework::InitContext& ic) final; + void run(framework::ProcessingContext& pc) final; + void finaliseCCDB(framework::ConcreteDataMatcher& matcher, void* obj) final; + + private: + void updateTimeDependentParams(framework::ProcessingContext& pc); + void addTruthSeedingVertices(const o2::InteractionRecord& origin, gsl::span rofs); + void configureROFViews(gsl::span rofs); + void initialiseTracking(); + o2::itsmft::tracking::TrackingOutcome processTimeFrame( + gsl::span rofs, + gsl::span clusters, + gsl::span patterns, + const o2::dataformats::MCTruthContainer* labels); + bool isActive() const noexcept { return mTracker != nullptr && mTracker->isConfiguredFor(mSession.frame); } + + std::shared_ptr mGGCCDBRequest; + bool mUseMC = false; + bool mTrackingInitialised = false; + WorkflowOptions mOptions; + o2::itsmft::tracking::WorkflowSession mSession{"ITS", o2::itsmft::tracking::ITSNLayers}; + std::unique_ptr mTrackerTraits; + std::unique_ptr mTracker; + std::unique_ptr mClusterDecoder; + const o2::itsmft::TopologyDictionary* mDictionary = nullptr; + o2::itsmft::tracking::ITSSharedClusterCompatibility mCompatibility; + PublicationAdapter mPublication; +}; + +o2::framework::DataProcessorSpec getCATrackerSpec(const WorkflowOptions& options); + +} // namespace o2::its::ca + +#endif // O2_ITS_CA_WORKFLOW_CATRACKERSPEC_H_ diff --git a/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/ConfigPreflight.h b/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/ConfigPreflight.h new file mode 100644 index 0000000000000..694d7eca4669f --- /dev/null +++ b/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/ConfigPreflight.h @@ -0,0 +1,60 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file ConfigPreflight.h +/// \brief Driver-level configuration and vertex-constraint preflight for the +/// ITS common-CA tracker workflow. +/// +/// Resolve driver options before constructing any DPL device. + +#ifndef ALICEO2_ITS_CA_WORKFLOW_CONFIGPREFLIGHT_H_ +#define ALICEO2_ITS_CA_WORKFLOW_CONFIGPREFLIGHT_H_ + +#include + +#include "ITSMFTTracking/Configuration.h" + +namespace o2::framework +{ +class ConfigContext; +} + +namespace o2::its::ca +{ + +/// Rejects a raw --configKeyValues string carrying an ITSCATrackerParam.* +/// override before applying the accepted string to ConfigurableParam. +void applyConfigKeyValuesOrFatal(const std::string& configKeyValues); + +/// Fatals unless mode is Sync or Async, naming the rejected mode explicitly, +/// before device construction. +void requireSupportedTrackingModeOrFatal(o2::itsmft::TrackingMode::Type mode); + +enum class VertexSource { Diamond, + Truth }; +struct WorkflowOptions { + bool useMC = true; + bool useFullGeometry = false; + bool writeRootOutput = true; + o2::itsmft::TrackingMode::Type mode = o2::itsmft::TrackingMode::Sync; + int nThreads = 1; + VertexSource vertexSource = VertexSource::Diamond; + std::string truthContext = "collisioncontext.root"; +}; + +// An empty explicit source requires exactly one legacy alias. No physics +// constraint is enabled by default, and MC output labels are independent. +VertexSource resolveVertexSource(const std::string& explicitSource, bool useDiamond, bool useTruth); +WorkflowOptions readWorkflowOptions(const o2::framework::ConfigContext&); + +} // namespace o2::its::ca + +#endif // ALICEO2_ITS_CA_WORKFLOW_CONFIGPREFLIGHT_H_ diff --git a/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/PublicationAdapter.h b/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/PublicationAdapter.h new file mode 100644 index 0000000000000..0aaefda032a40 --- /dev/null +++ b/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/PublicationAdapter.h @@ -0,0 +1,175 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITS_CA_PUBLICATIONADAPTER_H_ +#define ALICEO2_ITS_CA_PUBLICATIONADAPTER_H_ + +#ifndef GPUCA_GPUCODE + +#include +#include +#include +#include +#include +#include + +#include "DetectorsCommonDataFormats/DetID.h" +#include "GPUCommonMath.h" +#include "ITSMFTTracking/detail/ITSSharedClusterCompatibility.h" +#include "ITSMFTTracking/GenericTrack.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/MathUtils.h" + +namespace o2::its::ca +{ + +// Workflow-owned ITS compatibility for generic tracking results. +class PublicationAdapter +{ + public: + void adoptITSSharedClusterCompatibility(o2::itsmft::tracking::ITSSharedClusterCompatibility* sidecar) noexcept { mSidecar = sidecar; } + o2::itsmft::tracking::ITSSharedClusterCompatibility* getITSSharedClusterCompatibility() const noexcept { return mSidecar; } + + bool completeAccepted(gsl::span trackIndices, + const o2::itsmft::IterationParameters& params, + const o2::itsmft::tracking::TimeFrame& frame, + bool final) + { + if (mSidecar == nullptr) { + return true; + } + if (!stageSharedClusterFlags(trackIndices, params, frame)) { + return false; + } + return !final || mSidecar->replaceFromAcceptedTrackIndices(mAcceptedTrackIndices, mSharedClusterFlags); + } + + void reset() noexcept + { + mSharedClusterFlags.clear(); + mAcceptedTrackIndices.clear(); + if (mSidecar != nullptr) { + mSidecar->clear(); + } + } + + class Cleanup + { + public: + explicit Cleanup(PublicationAdapter& adapter) : mAdapter(adapter) { mAdapter.reset(); } + Cleanup(const Cleanup&) = delete; + Cleanup& operator=(const Cleanup&) = delete; + ~Cleanup() noexcept { mAdapter.reset(); } + + private: + PublicationAdapter& mAdapter; + }; + Cleanup cleanupOnExit() { return Cleanup{*this}; } + + private: + struct SharedClusterTrackInfo { + int layer{-1}; + uint32_t clusterId{std::numeric_limits::max()}; + int rof{-1}; + float phi{0.f}; + float eta{0.f}; + int charge{0}; + }; + + static std::optional makeSharedClusterTrackInfo(const o2::itsmft::tracking::GenericTrack& track, + const o2::itsmft::tracking::TimeFrame& frame) + { + const int layer = track.hitLayers.first(); + const auto& references = frame.getTrackClusterIndices(); + if (layer < 0 || !isValidTrackRange(track, static_cast(references.size())) || + track.firstClusterRef == track.clusterRefEnd || + static_cast(layer) >= frame.getLayout().size()) { + return std::nullopt; + } + const auto& reference = references[track.firstClusterRef]; + if (reference.layer != o2::itsmft::tracking::LayerId{static_cast(layer)} || !reference.isValid()) { + return std::nullopt; + } + const auto& state = track.innerState; + if (!state.hasRecognizedKind() || !o2::gpu::GPUCommonMath::Finite(state.parameters[3]) || + !o2::gpu::GPUCommonMath::Finite(state.parameters[4])) { + return std::nullopt; + } + const float phi = state.kind == o2::itsmft::tracking::SurfaceKind::Cylinder ? std::asin(state.parameters[2]) + state.alpha : state.parameters[2]; + const float eta = std::asinh(state.parameters[3]); + if (!o2::gpu::GPUCommonMath::Finite(phi) || !o2::gpu::GPUCommonMath::Finite(eta)) { + return std::nullopt; + } + return SharedClusterTrackInfo{layer, reference.clusterId, frame.getClusterROF(layer, static_cast(reference.clusterId)), + phi, eta, state.parameters[4] < 0.f ? -1 : 1}; + } + + bool stageSharedClusterFlags(gsl::span trackIndices, + const o2::itsmft::IterationParameters& params, + const o2::itsmft::tracking::TimeFrame& frame) + { + mAcceptedTrackIndices.reserve(mAcceptedTrackIndices.size() + trackIndices.size()); + for (const auto index : trackIndices) { + if (index >= frame.getGenericTracks().size() || + (!mAcceptedTrackIndices.empty() && mAcceptedTrackIndices.back() >= index)) { + return false; + } + mAcceptedTrackIndices.push_back(index); + } + if (!trackIndices.empty() && mSharedClusterFlags.size() <= trackIndices.back()) { + mSharedClusterFlags.resize(static_cast(trackIndices.back()) + 1, 0); + } + if (!params.AllowSharingFirstCluster) { + return true; + } + std::vector trackInfo; + trackInfo.reserve(trackIndices.size()); + for (const auto index : trackIndices) { + const auto info = makeSharedClusterTrackInfo(frame.getGenericTracks()[index], frame); + if (!info) { + return false; + } + trackInfo.push_back(*info); + } + for (size_t first = 0; first < trackInfo.size(); ++first) { + for (size_t second = first + 1; second < trackInfo.size(); ++second) { + if (trackInfo[second].layer != trackInfo[first].layer || trackInfo[second].clusterId != trackInfo[first].clusterId) { + continue; + } + if (trackInfo[first].rof != trackInfo[second].rof) { + continue; + } + if (!o2::its::math_utils::isPhiDifferenceBelow(trackInfo[first].phi, trackInfo[second].phi, params.SharedClusterMaxDeltaPhi)) { + continue; + } + if (std::abs(trackInfo[first].eta - trackInfo[second].eta) > params.SharedClusterMaxDeltaEta) { + continue; + } + if (params.SharedClusterOppositeSign && trackInfo[first].charge == trackInfo[second].charge) { + continue; + } + mSharedClusterFlags[trackIndices[first]] = 1; + mSharedClusterFlags[trackIndices[second]] = 1; + } + } + return true; + } + + o2::itsmft::tracking::ITSSharedClusterCompatibility* mSidecar = nullptr; + std::vector mSharedClusterFlags; + std::vector mAcceptedTrackIndices; +}; + +} // namespace o2::its::ca + +#endif // !GPUCA_GPUCODE + +#endif // ALICEO2_ITS_CA_PUBLICATIONADAPTER_H_ diff --git a/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/TruthSeeding.h b/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/TruthSeeding.h new file mode 100644 index 0000000000000..78e57cd9b87c2 --- /dev/null +++ b/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/TruthSeeding.h @@ -0,0 +1,49 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef O2_ITS_CA_TRUTH_SEEDING_H_ +#define O2_ITS_CA_TRUTH_SEEDING_H_ + +#include +#include +#include + +#include "CommonDataFormat/InteractionRecord.h" +#include "DataFormatsITS/TimeEstBC.h" +#include "ITSMFTTracking/SurfaceTiming.h" + +namespace o2::its::ca +{ +// Use the same origin as cluster loading. ROF delay/bias belong to the +// readout window, not to the collision timestamp. Preserve the existing +// forward uncertainty interval and select only collisions overlapping this TF. +inline std::optional truthSeedingTime( + const o2::InteractionRecord& collision, const o2::InteractionRecord& origin, + const o2::itsmft::tracking::ROFIntervalBC& window, uint32_t duration) noexcept +{ + if (collision.isDummy() || !window.isValid() || duration == 0) { + return std::nullopt; + } + const auto begin = collision.differenceInBC(origin); + const auto end = begin + duration; + if (end <= window.begin || begin >= window.end || end <= 0) { + return std::nullopt; + } + // TimeEstBC has unsigned bounds; clip only the part preceding this origin. + const auto clippedBegin = std::max(int64_t{0}, begin); + if (end > std::numeric_limits::max()) { + return std::nullopt; + } + return o2::its::TimeEstBC{static_cast(clippedBegin), static_cast(end - clippedBegin)}; +} +} // namespace o2::its::ca + +#endif diff --git a/Detectors/ITSMFT/ITS/workflow-ca/src/CATrackerSpec.cxx b/Detectors/ITSMFT/ITS/workflow-ca/src/CATrackerSpec.cxx new file mode 100644 index 0000000000000..a7d01372c8f2b --- /dev/null +++ b/Detectors/ITSMFT/ITS/workflow-ca/src/CATrackerSpec.cxx @@ -0,0 +1,403 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// @file CATrackerSpec.cxx + +#include "ITSCAWorkflow/CATrackerSpec.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "DataFormatsITSMFT/CompCluster.h" +#include "DataFormatsITSMFT/DPLAlpideParam.h" +#include "DataFormatsITSMFT/ROFRecord.h" +#include "DataFormatsITSMFT/TopologyDictionary.h" +#include "DetectorsBase/GeometryManager.h" +#include "Framework/CCDBParamSpec.h" +#include "Framework/DataProcessorSpec.h" +#include "Framework/Logger.h" +#include "ITSBase/GeometryTGeo.h" +#include "ITSMFTTracking/Tracker.h" +#include "ITSMFTTracking/GenericTrackOutputAdapter.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/SurfaceTiming.h" +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "ITSMFTTracking/BoundedAllocator.h" +#include "CommonConstants/LHCConstants.h" +#include "DetectorsBase/Propagator.h" +#include +#include "SimulationDataFormat/MCCompLabel.h" +#include "SimulationDataFormat/MCTruthContainer.h" +#include "SimulationDataFormat/DigitizationContext.h" +#include "SimulationDataFormat/O2DatabasePDG.h" +#include "Steer/MCKinematicsReader.h" +#include "ITSCAWorkflow/TruthSeeding.h" + +using namespace o2::framework; + +namespace o2::its::ca +{ + +namespace +{ +using namespace o2::itsmft::tracking; + +template +constexpr std::array detectorLocalToLayoutLayers() +{ + std::array order{}; + for (int i = 0; i < NLayers; ++i) { + order[i] = LayerId{static_cast(i)}; + } + return order; +} + +inline constexpr auto kLayerToLayout = detectorLocalToLayoutLayers(); + +bool completePublication(PublicationAdapter& publication, + const TimeFrame& frame, + const Tracker& tracker, + const TrackingResult& result) +{ + const auto configurations = tracker.getIterationConfigurations(); + std::size_t firstTrack = 0; + for (std::size_t iteration = 0; iteration < configurations.size(); ++iteration) { + if (iteration >= result.acceptedTrackCounts.size() || + result.acceptedTrackCounts[iteration] > frame.getGenericTracks().size() - firstTrack) { + return false; + } + std::vector trackIndices(result.acceptedTrackCounts[iteration]); + std::iota(trackIndices.begin(), trackIndices.end(), static_cast(firstTrack)); + if (!publication.completeAccepted(trackIndices, configurations[iteration].parameters, frame, iteration + 1 == configurations.size())) { + return false; + } + firstTrack += result.acceptedTrackCounts[iteration]; + } + return firstTrack == frame.getGenericTracks().size(); +} + +} // namespace + +CATrackerDPL::CATrackerDPL(std::shared_ptr gr, WorkflowOptions options) + : mGGCCDBRequest(std::move(gr)), mUseMC(options.useMC), mOptions(std::move(options)) +{ + mClusterDecoder = std::make_unique(); + mPublication.adoptITSSharedClusterCompatibility(&mCompatibility); +} + +void CATrackerDPL::addTruthSeedingVertices(const o2::InteractionRecord& origin, gsl::span rofs) +{ + if (rofs.empty()) { + return; + } + LOGP(info, "ITS CA using truth seeds as vertices"); + const auto& clock = mSession.frame.getROFViews().overlap.getLayer(0); + const o2::itsmft::tracking::ROFTimingConfig timing{clock.mROFLength, clock.mROFDelay, clock.mROFBias, clock.mROFAddTimeErr}; + const auto first = o2::itsmft::tracking::computeROFIntervalBC(rofs.front().getBCData(), origin, timing, 0); + const auto last = o2::itsmft::tracking::computeROFIntervalBC(rofs.back().getBCData(), origin, timing, rofs.size() - 1); + const auto firstWindow = o2::itsmft::tracking::widen(first.interval, timing.rofAddTimeErr); + const auto lastWindow = o2::itsmft::tracking::widen(last.interval, timing.rofAddTimeErr); + if (!first.ok() || !last.ok() || !firstWindow.ok() || !lastWindow.ok()) { + throw std::runtime_error("ITS CA truth seeding received invalid ROF timing"); + } + const o2::itsmft::tracking::ROFIntervalBC window{std::max(int64_t{0}, firstWindow.interval.begin), lastWindow.interval.end, 0, 0}; + const std::unique_ptr dc{o2::steer::DigitizationContext::loadFromFile(mOptions.truthContext.c_str())}; + if (!dc) { + throw std::runtime_error("ITS CA truth seeding could not load " + mOptions.truthContext); + } + const auto& irs = dc->getEventRecords(); + o2::steer::MCKinematicsReader mcReader(dc.get()); + constexpr int iSrc = 0; + const auto eveId2colId = dc->getCollisionIndicesForSource(iSrc); + std::vector> selected; + for (int iEve = 0; iEve < mcReader.getNEvents(iSrc); ++iEve) { + const auto collision = eveId2colId.find(iEve); + if (collision == eveId2colId.end()) { + continue; + } + const auto timestamp = truthSeedingTime(irs.at(collision->second), origin, window, clock.mROFLength / 2); + if (timestamp) { + selected.emplace_back(*timestamp, iEve); + } + } + // The ROF vertex lookup performs a binary search by lower timestamp. + std::sort(selected.begin(), selected.end(), [](const auto& a, const auto& b) { + return std::pair{a.first.lower(), a.second} < std::pair{b.first.lower(), b.second}; + }); + for (const auto& [timestamp, iEve] : selected) { + const auto& event = mcReader.getMCEventHeader(iSrc, iEve); + o2::itsmft::tracking::Vertex vertex; + vertex.getTimeStamp() = timestamp; + vertex.setNContributors(std::max(1L, std::ranges::count_if(mcReader.getTracks(iSrc, iEve), [](const auto& track) { + if (!track.isPrimary() || track.GetPt() < 0.05 || std::abs(track.GetEta()) > 1.1) { + return false; + } + const auto* particle = o2::O2DatabasePDG::Instance()->GetParticle(track.GetPdgCode()); + return particle && particle->Charge() != 0; + }))); + vertex.setXYZ(static_cast(event.GetX()), static_cast(event.GetY()), static_cast(event.GetZ())); + vertex.setChi2(1.f); + constexpr float covariance = 25.e-4f; + vertex.setSigmaX(covariance); + vertex.setSigmaY(covariance); + vertex.setSigmaZ(covariance); + mSession.frame.addPrimaryVertex(vertex); + const o2::MCCompLabel label{o2::MCCompLabel::maxTrackID(), iEve, iSrc, false}; + mSession.frame.addPrimaryVertexLabel(o2::itsmft::tracking::VertexLabel{label, 1.f}); + mcReader.releaseTracksForSourceAndEvent(iSrc, iEve); + } + LOGP(info, "ITS CA imposed {} pv collisions from MC truth", mSession.frame.getPrimaryVertices().size()); +} + +void CATrackerDPL::configureROFViews(gsl::span rofs) +{ + const auto& detector = mTracker->getDetectorConfiguration(); + const auto& alpParams = o2::itsmft::DPLAlpideParam::Instance(); + const int nOrbitsPerTF = o2::base::GRPGeomHelper::getNHBFPerTF(); + const auto timings = mSession.layerTimings(alpParams, nOrbitsPerTF, detector.addTimeError); + mSession.configureTiming(timings, [](int) { return true; }); + (void)rofs; +} + +void CATrackerDPL::initialiseTracking() +{ + const auto mode = mOptions.mode; + auto plan = o2::itsmft::TrackingMode::getTrackingPlan(o2::detectors::DetID::ITS, mode); + for (auto& pass : plan.iterations) { + pass.UseDiamond = mOptions.vertexSource == VertexSource::Diamond; + } + LOGP(info, "ITS CA tracker initialized in {} mode with {} iteration(s)", + o2::itsmft::TrackingMode::toString(mode), plan.iterations.size()); + if (plan.iterations.empty()) { + return; + } + + mTrackerTraits = std::make_unique(); + std::shared_ptr taskArena; + const auto& commonParams = o2::itsmft::ITSCommonCATrackerParam::Instance(); + mTrackerTraits->setNThreads(mOptions.nThreads, taskArena); + + const auto maxMemory = plan.execution.MaxMemory; + o2::itsmft::tracking::TrackerInitialization configuration{ + .catalog = {o2::itsmft::tracking::kITSStaticSurfaceCatalog.data(), + static_cast(o2::itsmft::tracking::kITSStaticSurfaceCatalog.size())}, + .layout = o2::itsmft::tracking::makeDetectorLayout(o2::itsmft::tracking::LayerMask{commonParams.holeLayerMask}), + .plan = std::move(plan), + .memoryPool = std::make_shared(maxMemory)}; + + mTracker = std::make_unique(); + const auto result = mTracker->initialize(mSession.frame, configuration); + if (!result.ok()) { + LOGP(fatal, "ITS CA tracker failed to initialize static configuration (error={} iteration={} layout={})", + static_cast(result.error), result.failedIteration, static_cast(result.layoutError)); + } +} + +o2::itsmft::tracking::TrackingOutcome CATrackerDPL::processTimeFrame( + gsl::span rofs, + gsl::span clusters, + gsl::span patterns, + const o2::dataformats::MCTruthContainer* labels) +{ + if (!isActive()) { + LOGP(info, "ITS CA tracking mode is off, skipping TimeFrame processing"); + return o2::itsmft::tracking::TrackingOutcome::Success; + } + mSession.frame.setBz(o2::base::Propagator::Instance()->getNominalBz()); + o2::itsmft::tracking::ClusterSourceInput source; + source.id = o2::itsmft::tracking::ClusterSourceId{0}; + source.detector = o2::detectors::DetID::ITS; + source.clusters = clusters; + source.patterns = patterns; + source.rofs = rofs; + source.dictionary = mDictionary; + source.labels = labels; + source.layerToSurface = kLayerToLayout; + source.decoder = mClusterDecoder.get(); + return mSession.process(*mTracker, *mTrackerTraits, source, [&](const o2::InteractionRecord& origin) { + if (mOptions.vertexSource == VertexSource::Truth) { + addTruthSeedingVertices(origin, rofs); + mSession.vertices.update(mSession.frame.getPrimaryVertices().data(), mSession.frame.getPrimaryVertices().size()); + } }, [&](const o2::itsmft::tracking::TrackingResult& result) { + if (!completePublication(mPublication, mSession.frame, *mTracker, result)) { + throw std::runtime_error{"failed to seal ITS tracking compatibility"}; + } }); +} + +void CATrackerDPL::init(InitContext&) +{ + o2::base::GRPGeomHelper::instance().setRequest(mGGCCDBRequest); +} + +void CATrackerDPL::run(ProcessingContext& pc) +{ + auto publicationCleanup = mPublication.cleanupOnExit(); + updateTimeDependentParams(pc); + + auto rofsinput = pc.inputs().get>("ROframes"); + + if (decideCATrackerPublicationAction(isActive(), o2::itsmft::tracking::TrackingOutcome::Success) == CATrackerPublicationAction::PublishInactiveEmpty) { + pc.outputs().make>(Output{"ITS", "ITSTrackROF", 0}, + rofsinput.begin(), rofsinput.end()); + pc.outputs().make>(Output{"ITS", "TRACKS", 0}); + pc.outputs().make>(Output{"ITS", "TRACKCLSID", 0}); + return; + } + + auto compClusters = pc.inputs().get>("compClusters"); + gsl::span patterns = pc.inputs().get>("patterns"); + + const dataformats::MCTruthContainer* labels = nullptr; + if (mUseMC && pc.inputs().getPos("labels") >= 0) { + labels = pc.inputs().get*>("labels").release(); + } + + LOGP(info, "ITS CA input pulled {} compressed clusters in {} RO frames ({} pattern bytes)", + compClusters.size(), rofsinput.size(), patterns.size()); + + auto cleanup = mSession.cleanupOnExit(); + configureROFViews(gsl::span(rofsinput.data(), rofsinput.size())); + const auto trackingResult = processTimeFrame(gsl::span(rofsinput.data(), rofsinput.size()), + gsl::span(compClusters.data(), compClusters.size()), + patterns, labels); + + if (decideCATrackerPublicationAction(isActive(), trackingResult) == CATrackerPublicationAction::SkipDroppedTimeFrame) { + LOGP(error, "ITS CA tracking dropped this TimeFrame ({} ROFs, {} clusters); publishing nothing and continuing with the next TimeFrame", + rofsinput.size(), compClusters.size()); + cleanup.frameAlreadyReset(); + return; + } + + { + mSession.publicationClock.emplace(mSession.overlap.getView().getClockLayer()); + const o2::itsmft::tracking::GenericTrackPublicationContext context{ + o2::detectors::DetID::ITS, o2::itsmft::tracking::ClusterSourceId{0}, + gsl::span{rofsinput.data(), rofsinput.size()}, *mSession.publicationClock, + kLayerToLayout, + &mSession.externalIndices, &mSession.clusterSizes}; + o2::itsmft::tracking::GenericTrackOutputAdapterError error = o2::itsmft::tracking::GenericTrackOutputAdapterError::None; + const auto staged = o2::itsmft::tracking::stageITSGenericTrackOutput(mSession.frame, context, mCompatibility, mUseMC, error); + if (!staged) { + throw std::runtime_error{"ITS GenericTrack output staging failed"}; + } + + o2::itsmft::tracking::copyTrackingOutputColumns(pc.outputs(), Output{"ITS", "ITSTrackROF", 0}, + Output{"ITS", "TRACKS", 0}, Output{"ITS", "TRACKCLSID", 0}, *staged); + LOGP(info, "ITS CA pushed {} tracks in {} ROFs", staged->tracks.size(), staged->trackROFs.size()); + if (mUseMC) { + pc.outputs().snapshot(Output{"ITS", "TRACKSMCTR", 0}, staged->labels); + LOGP(info, "ITS CA pushed {} track MC labels", staged->labels.size()); + } + } +} + +void CATrackerDPL::updateTimeDependentParams(ProcessingContext& pc) +{ + o2::base::GRPGeomHelper::instance().checkUpdates(pc); + pc.inputs().get*>("itsalppar"); + if (!mTrackingInitialised) { + mTrackingInitialised = true; + initialiseTracking(); + } + static bool initOnceDone = false; + if (!initOnceDone) { + initOnceDone = true; + if (pc.inputs().getPos("itsTGeo") >= 0) { + pc.inputs().get("itsTGeo"); + } + pc.inputs().get("itscldict"); + o2::its::GeometryTGeo::Instance()->fillMatrixCache(o2::math_utils::bit2Mask(o2::math_utils::TransformType::T2L, + o2::math_utils::TransformType::T2GRot, + o2::math_utils::TransformType::T2G)); + } +} + +void CATrackerDPL::finaliseCCDB(ConcreteDataMatcher& matcher, void* obj) +{ + if (o2::base::GRPGeomHelper::instance().finaliseCCDB(matcher, obj)) { + return; + } + if (matcher == ConcreteDataMatcher("ITS", "CLUSDICT", 0)) { + LOG(info) << "ITS CA input cluster dictionary updated"; + mDictionary = static_cast(obj); + return; + } + if (matcher == ConcreteDataMatcher("ITS", "ALPIDEPARAM", 0)) { + LOG(info) << "ITS CA input Alpide param updated"; + o2::itsmft::DPLAlpideParam::Instance().printKeyValues(); + return; + } + if (matcher == ConcreteDataMatcher("ITS", "GEOMTGEO", 0)) { + LOG(info) << "ITS CA input GeometryTGeo loaded from CCDB"; + o2::its::GeometryTGeo::adopt(static_cast(obj)); + o2::its::GeometryTGeo::Instance()->fillMatrixCache(o2::math_utils::bit2Mask(o2::math_utils::TransformType::T2L, + o2::math_utils::TransformType::T2GRot, + o2::math_utils::TransformType::T2G)); + // The catalog has static process lifetime; geometry adoption remains + // necessary for raw cluster decoding. + return; + } +} + +DataProcessorSpec getCATrackerSpec(const WorkflowOptions& options) +{ + const bool useMC = options.useMC; + const bool useGeom = options.useFullGeometry; + std::vector inputs; + inputs.emplace_back("compClusters", "ITS", "COMPCLUSTERS", 0, Lifetime::Timeframe); + inputs.emplace_back("patterns", "ITS", "PATTERNS", 0, Lifetime::Timeframe); + inputs.emplace_back("ROframes", "ITS", "CLUSTERSROF", 0, Lifetime::Timeframe); + inputs.emplace_back("itscldict", "ITS", "CLUSDICT", 0, Lifetime::Condition, ccdbParamSpec("ITS/Calib/ClusterDictionary")); + inputs.emplace_back("itsalppar", "ITS", "ALPIDEPARAM", 0, Lifetime::Condition, ccdbParamSpec("ITS/Config/AlpideParam")); + + if (useMC) { + inputs.emplace_back("labels", "ITS", "CLUSTERSMCTR", 0, Lifetime::Timeframe); + } + + auto ggRequest = std::make_shared(false, + true, + false, + true, + true, + useGeom ? o2::base::GRPGeomRequest::Aligned : o2::base::GRPGeomRequest::None, + inputs, + true); + if (!useGeom) { + ggRequest->addInput({"itsTGeo", "ITS", "GEOMTGEO", 0, Lifetime::Condition, framework::ccdbParamSpec("ITS/Config/Geometry")}, inputs); + } + + std::vector outputs; + outputs.emplace_back("ITS", "TRACKS", 0, Lifetime::Timeframe); + outputs.emplace_back("ITS", "TRACKCLSID", 0, Lifetime::Timeframe); + outputs.emplace_back("ITS", "ITSTrackROF", 0, Lifetime::Timeframe); + if (useMC) { + outputs.emplace_back("ITS", "TRACKSMCTR", 0, Lifetime::Timeframe); + } + + return DataProcessorSpec{ + "its-ca-tracker", + inputs, + outputs, + AlgorithmSpec{adaptFromTask(ggRequest, options)}, + Options{}}; +} + +} // namespace o2::its::ca diff --git a/Detectors/ITSMFT/ITS/workflow-ca/src/ConfigPreflight.cxx b/Detectors/ITSMFT/ITS/workflow-ca/src/ConfigPreflight.cxx new file mode 100644 index 0000000000000..c5dc31ffb86e7 --- /dev/null +++ b/Detectors/ITSMFT/ITS/workflow-ca/src/ConfigPreflight.cxx @@ -0,0 +1,113 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSCAWorkflow/ConfigPreflight.h" + +#include +#include +#include "Framework/ConfigContext.h" +#include "Framework/ConfigParamRegistry.h" + +#include "CommonUtils/ConfigurableParam.h" +#include "CommonUtils/StringUtils.h" +#include "Framework/Logger.h" +#include "ITSMFTTracking/TrackingConfigParam.h" + +namespace o2::its::ca +{ + +namespace +{ +// This is an ITS common-CA workflow policy, not a generic tracking-parameter +// validation. Keep the legacy namespace spelling local to the workflow that +// rejects it, so the shared parameter library does not expose a workflow API. +constexpr std::string_view kLegacyITSNamespace = "ITSCATrackerParam"; +} // namespace + +void applyConfigKeyValuesOrFatal(const std::string& configKeyValues) +{ + // Mirror ConfigurableParam::updateFromString()'s tokenization: split on + // ';', trim each token, skip empty tokens, and split at the first '='. + // Malformed tokens remain the configurator's responsibility. + const auto tokens = o2::utils::Str::tokenize(configKeyValues, ';', true); + for (const auto& token : tokens) { + const auto eq = token.find('='); + if (eq == std::string::npos || eq == 0 || eq == token.size() - 1) { + continue; + } + const auto key = token.substr(0, eq); + const auto dot = key.find('.'); + const auto ns = dot == std::string::npos ? key : key.substr(0, dot); + if (ns == kLegacyITSNamespace) { + LOGP(fatal, + "ITS common-CA tracker workflow rejects legacy '{}' --configKeyValues override ('{}'); " + "use the dedicated 'ITSCommonCATrackerParam' namespace instead", + ns, token); + } + } + o2::conf::ConfigurableParam::updateFromString(configKeyValues); +} + +void requireSupportedTrackingModeOrFatal(o2::itsmft::TrackingMode::Type mode) +{ + if (mode != o2::itsmft::TrackingMode::Sync && mode != o2::itsmft::TrackingMode::Async) { + LOGP(fatal, + "ITS common-CA tracker workflow supports tracking-mode 'sync' and 'async'; '{}' is not supported", + o2::itsmft::TrackingMode::toString(mode)); + } +} + +VertexSource resolveVertexSource(const std::string& explicitSource, bool useDiamond, bool useTruth) +{ + if (!explicitSource.empty() && explicitSource != "diamond" && explicitSource != "truth") { + throw std::invalid_argument("--vertex-source must be diamond or truth"); + } + const bool diamond = useDiamond || explicitSource == "diamond"; + const bool truth = useTruth || explicitSource == "truth"; + if (diamond == truth) { + throw std::invalid_argument( + "Select exactly one ITS vertex source: --vertex-source={diamond,truth}; " + "legacy aliases ITSCommonCATrackerParam.useDiamond and ITSVertexerParam.useTruthSeeding must agree"); + } + return diamond ? VertexSource::Diamond : VertexSource::Truth; +} + +WorkflowOptions readWorkflowOptions(const o2::framework::ConfigContext& context) +{ + const auto& options = context.options(); + applyConfigKeyValuesOrFatal(options.get("configKeyValues")); + WorkflowOptions result; + result.mode = o2::itsmft::TrackingMode::fromString(options.get("tracking-mode")); + requireSupportedTrackingModeOrFatal(result.mode); + o2::itsmft::TrackingMode::validateCommonCAOptions(o2::detectors::DetID::ITS); + const auto& params = o2::itsmft::ITSCommonCATrackerParam::Instance(); + result.vertexSource = resolveVertexSource(options.get("vertex-source"), params.useDiamond, + o2::its::VertexerParamConfig::Instance().useTruthSeeding); + result.nThreads = params.nThreads; + if (result.nThreads <= 0) { + throw std::invalid_argument("ITSCommonCATrackerParam.nThreads must be > 0"); + } + result.truthContext = options.get("truth-context"); + if (result.vertexSource == VertexSource::Truth && result.truthContext.empty()) { + throw std::invalid_argument("--truth-context must name the digitization context for --vertex-source=truth"); + } + result.useMC = !options.get("disable-mc"); + result.useFullGeometry = options.get("use-geom") || options.get("use-full-geometry"); + + result.writeRootOutput = !options.get("disable-root-output"); + LOGP(info, "ITS CA resolved: mode={} threads={} vertex={} truth context={} geometry={} MC={} ROOT output={}", + o2::itsmft::TrackingMode::toString(result.mode), result.nThreads, + result.vertexSource == VertexSource::Diamond ? "diamond" : "truth", result.truthContext, + result.useFullGeometry ? "full" : "ITS", result.useMC, result.writeRootOutput); + return result; +} + +} // namespace o2::its::ca diff --git a/Detectors/ITSMFT/ITS/workflow-ca/src/its-ca-tracker-workflow.cxx b/Detectors/ITSMFT/ITS/workflow-ca/src/its-ca-tracker-workflow.cxx new file mode 100644 index 0000000000000..c2f5847068777 --- /dev/null +++ b/Detectors/ITSMFT/ITS/workflow-ca/src/its-ca-tracker-workflow.cxx @@ -0,0 +1,69 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// @file its-ca-tracker-workflow.cxx +/// \brief ITS common-CA tracker workflow: tracking on ITS cluster +/// inputs with tracker-only outputs. + +#include +#include + +#include "CommonUtils/ConfigurableParam.h" +#include "DetectorsRaw/HBFUtilsInitializer.h" +#include "Framework/CallbacksPolicy.h" +#include "Framework/CompletionPolicyHelpers.h" +#include "Framework/ConfigParamSpec.h" +#include "ITSCAWorkflow/CATrackerSpec.h" +#include "ITSCAWorkflow/ConfigPreflight.h" +#include "ITSMFTCAWriter/ITSCATrackWriterSpec.h" +#include "ITSMFTTracking/Configuration.h" + +using namespace o2::framework; + +void customize(std::vector& policies) +{ + o2::raw::HBFUtilsInitializer::addNewTimeSliceCallback(policies); +} + +void customize(std::vector& policies) +{ + policies.push_back(CompletionPolicyHelpers::consumeWhenAllOrdered(".*(?:ITS|its).*[W,w]riter.*")); +} + +void customize(std::vector& workflowOptions) +{ + workflowOptions.push_back(ConfigParamSpec{"disable-mc", VariantType::Bool, false, {"disable MC labels"}}); + workflowOptions.push_back(ConfigParamSpec{"disable-root-output", VariantType::Bool, false, {"do not write output root files"}}); + workflowOptions.push_back(ConfigParamSpec{"vertex-source", VariantType::String, "", {"diamond or truth; alternatively select exactly one legacy vertex alias"}}); + workflowOptions.push_back(ConfigParamSpec{"truth-context", VariantType::String, "collisioncontext.root", {"digitization context for truth vertices, independent of MC output labels"}}); + workflowOptions.push_back(ConfigParamSpec{"use-full-geometry", VariantType::Bool, false, {"alias for --use-geom"}}); + workflowOptions.push_back(ConfigParamSpec{"use-geom", VariantType::Bool, false, {"use geometry from the global geometry manager"}}); + workflowOptions.push_back(ConfigParamSpec{"tracking-mode", VariantType::String, "sync", {"ITS tracking mode: 'sync' or 'async'"}}); + workflowOptions.push_back(ConfigParamSpec{"configKeyValues", VariantType::String, "", {"Semicolon separated key=value strings (e.g. ITSCommonCATrackerParam.useDiamond=true)"}}); + o2::raw::HBFUtilsInitializer::addConfigOption(workflowOptions); +} + +#include "Framework/runDataProcessing.h" + +WorkflowSpec defineDataProcessing(ConfigContext const& config) +{ + // Help constructs device descriptions without starting tracking. + const auto options = config.helpOnCommandLine() ? o2::its::ca::WorkflowOptions{} : o2::its::ca::readWorkflowOptions(config); + WorkflowSpec specs; + specs.emplace_back(o2::its::ca::getCATrackerSpec(options)); + if (options.writeRootOutput) { + specs.emplace_back(o2::its::ca::getTrackWriterSpec(options.useMC)); + } + + o2::raw::HBFUtilsInitializer hbfIni(config, specs); + + return specs; +} diff --git a/Detectors/ITSMFT/ITS/workflow-ca/test/testITSCAConfigPreflight.cxx b/Detectors/ITSMFT/ITS/workflow-ca/test/testITSCAConfigPreflight.cxx new file mode 100644 index 0000000000000..0bbb77593ec4d --- /dev/null +++ b/Detectors/ITSMFT/ITS/workflow-ca/test/testITSCAConfigPreflight.cxx @@ -0,0 +1,201 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// Driver configuration validation before constructing any DPL device. + +#define BOOST_TEST_MODULE ITSMFT ITSCAConfigPreflight +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include + +#include +#include + +#include "CommonUtils/ConfigurableParam.h" +#include "ITSCAWorkflow/ConfigPreflight.h" +#include "Framework/ConfigContext.h" +#include "Framework/ConfigParamStore.h" +#include "Framework/ParamRetriever.h" +#include "Framework/ServiceRegistry.h" +#include "ITSMFTTracking/TrackingConfigParam.h" + +using namespace o2::its::ca; + +namespace +{ +struct FatalToExceptionFixture { + FatalToExceptionFixture() + { + fair::Logger::OnFatal([]() { throw std::runtime_error("fatal"); }); + } +}; +} // namespace + +// --- applyConfigKeyValuesOrFatal(): preflight runs before the update ------- + +BOOST_FIXTURE_TEST_CASE(LegacyNamespaceIsRejectedBeforeAnyUpdate, FatalToExceptionFixture) +{ + // Sentinel: if the rejection did not actually run before + // ConfigurableParam::updateFromString(), this legacy-namespace string + // would still throw from updateFromString() itself (unknown param), so + // this alone would not distinguish "preflight fired first" from "update + // itself fatal'd" -- the meaningful assertion is in the next test, which + // confirms the dedicated param was NOT mutated by the rejected string. + BOOST_CHECK_THROW(applyConfigKeyValuesOrFatal("ITSCATrackerParam.trackFollowerTop=1"), std::runtime_error); +} + +BOOST_FIXTURE_TEST_CASE(RejectedStringNeverReachesConfigurableParamUpdate, FatalToExceptionFixture) +{ + // A malicious/confused string mixing a real dedicated-namespace override + // with an offending legacy one must not have its dedicated part applied + // either -- the whole string is rejected pre-update, atomically. + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "useDiamond", false); + BOOST_CHECK_THROW( + applyConfigKeyValuesOrFatal("ITSCommonCATrackerParam.useDiamond=true;ITSCATrackerParam.trackFollowerTop=1"), + std::runtime_error); + BOOST_CHECK_EQUAL(o2::itsmft::ITSCommonCATrackerParam::Instance().useDiamond, false); +} + +BOOST_FIXTURE_TEST_CASE(DedicatedNamespaceIsAcceptedAndApplied, FatalToExceptionFixture) +{ + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "useDiamond", false); + BOOST_CHECK_NO_THROW(applyConfigKeyValuesOrFatal("ITSCommonCATrackerParam.useDiamond=true")); + BOOST_CHECK_EQUAL(o2::itsmft::ITSCommonCATrackerParam::Instance().useDiamond, true); + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "useDiamond", false); +} + +BOOST_FIXTURE_TEST_CASE(EmptyStringIsAcceptedAndApplied, FatalToExceptionFixture) +{ + BOOST_CHECK_NO_THROW(applyConfigKeyValuesOrFatal("")); +} + +BOOST_FIXTURE_TEST_CASE(LegacyNamespaceWithoutFieldIsRejected, FatalToExceptionFixture) +{ + BOOST_CHECK_THROW(applyConfigKeyValuesOrFatal("ITSCATrackerParam=1"), std::runtime_error); +} + +BOOST_FIXTURE_TEST_CASE(MixedInputRejectsLegacyNamespaceInEitherPosition, FatalToExceptionFixture) +{ + for (const auto* config : {"ITSCATrackerParam.trackFollowerTop=1;ITSCommonCATrackerParam.useDiamond=true", + "ITSCommonCATrackerParam.useDiamond=true;ITSCATrackerParam.trackFollowerTop=1"}) { + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "useDiamond", false); + BOOST_CHECK_THROW(applyConfigKeyValuesOrFatal(config), std::runtime_error); + BOOST_CHECK_EQUAL(o2::itsmft::ITSCommonCATrackerParam::Instance().useDiamond, false); + } +} + +BOOST_FIXTURE_TEST_CASE(OuterWhitespaceAndInternalKeyWhitespaceKeepNamespace, FatalToExceptionFixture) +{ + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "useDiamond", false); + BOOST_CHECK_THROW( + applyConfigKeyValuesOrFatal(" ITSCommonCATrackerParam.useDiamond=true ; ITSCATrackerParam.trackFollowerTop=1 "), + std::runtime_error); + BOOST_CHECK_EQUAL(o2::itsmft::ITSCommonCATrackerParam::Instance().useDiamond, false); + + BOOST_CHECK_THROW(applyConfigKeyValuesOrFatal("ITSCATrackerParam.trackFollowerTop = 1"), std::runtime_error); +} + +BOOST_FIXTURE_TEST_CASE(EmptyEntriesAndUnrelatedNamespacesAreAccepted, FatalToExceptionFixture) +{ + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "dropTFUponFailure", false); + o2::conf::ConfigurableParam::setValue("ITSVertexerParam", "nIterations", 1); + BOOST_CHECK_NO_THROW(applyConfigKeyValuesOrFatal( + ";;ITSCommonCATrackerParam.dropTFUponFailure=true;;;ITSVertexerParam.nIterations=2;;")); + BOOST_CHECK_EQUAL(o2::itsmft::ITSCommonCATrackerParam::Instance().dropTFUponFailure, true); + BOOST_CHECK_EQUAL(o2::its::VertexerParamConfig::Instance().nIterations, 2); + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "dropTFUponFailure", false); + o2::conf::ConfigurableParam::setValue("ITSVertexerParam", "nIterations", 1); +} + +BOOST_FIXTURE_TEST_CASE(MalformedTokensRemainConfiguratorErrors, FatalToExceptionFixture) +{ + for (const auto* config : {"ITSCATrackerParamNoEquals", "=ITSCATrackerParam.x", "ITSCATrackerParam.x="}) { + BOOST_CHECK_THROW(applyConfigKeyValuesOrFatal(config), std::runtime_error); + } +} + +BOOST_FIXTURE_TEST_CASE(RepeatedAcceptedAndRejectedCallsRemainDeterministic, FatalToExceptionFixture) +{ + for (int i = 0; i < 5; ++i) { + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "useDiamond", false); + BOOST_CHECK_THROW(applyConfigKeyValuesOrFatal("ITSCATrackerParam.trackFollowerTop=1"), std::runtime_error); + BOOST_CHECK_NO_THROW(applyConfigKeyValuesOrFatal("ITSCommonCATrackerParam.useDiamond=true")); + BOOST_CHECK_EQUAL(o2::itsmft::ITSCommonCATrackerParam::Instance().useDiamond, true); + } + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "useDiamond", false); +} + +// --- requireSupportedTrackingModeOrFatal(): Sync and Async are accepted --- + +BOOST_FIXTURE_TEST_CASE(SupportedModesAreAccepted, FatalToExceptionFixture) +{ + BOOST_CHECK_NO_THROW(requireSupportedTrackingModeOrFatal(o2::itsmft::TrackingMode::Sync)); + BOOST_CHECK_NO_THROW(requireSupportedTrackingModeOrFatal(o2::itsmft::TrackingMode::Async)); +} + +BOOST_FIXTURE_TEST_CASE(UnsupportedModesFailClosed, FatalToExceptionFixture) +{ + const std::array rejected{ + o2::itsmft::TrackingMode::Off, o2::itsmft::TrackingMode::Unset, o2::itsmft::TrackingMode::Cosmics}; + for (const auto mode : rejected) { + BOOST_CHECK_THROW(requireSupportedTrackingModeOrFatal(mode), std::runtime_error); + } +} + +BOOST_AUTO_TEST_CASE(VertexSelectionIsExplicitAndLegacyAliasesMustAgree) +{ + BOOST_CHECK_THROW(resolveVertexSource("", false, false), std::invalid_argument); + BOOST_CHECK_THROW(resolveVertexSource("", true, true), std::invalid_argument); + BOOST_CHECK_THROW(resolveVertexSource("truth", true, false), std::invalid_argument); + BOOST_CHECK_THROW(resolveVertexSource("diamond", false, true), std::invalid_argument); + BOOST_CHECK_THROW(resolveVertexSource("unknown", false, false), std::invalid_argument); + BOOST_CHECK(resolveVertexSource("", true, false) == VertexSource::Diamond); + BOOST_CHECK(resolveVertexSource("", false, true) == VertexSource::Truth); + BOOST_CHECK(resolveVertexSource("diamond", false, false) == VertexSource::Diamond); + BOOST_CHECK(resolveVertexSource("truth", false, false) == VertexSource::Truth); + BOOST_CHECK(resolveVertexSource("diamond", true, false) == VertexSource::Diamond); + BOOST_CHECK(resolveVertexSource("truth", false, true) == VertexSource::Truth); +} + +BOOST_AUTO_TEST_CASE(DriverResolvesTruthContextIndependentlyOfMCLabels) +{ + using namespace o2::framework; + std::vector specs{ + {"configKeyValues", VariantType::String, "ITSCommonCATrackerParam.useDiamond=false;ITSVertexerParam.useTruthSeeding=false", {"parameters"}}, + {"tracking-mode", VariantType::String, "async", {"mode"}}, + {"vertex-source", VariantType::String, "truth", {"vertices"}}, + {"truth-context", VariantType::String, "custom-context.root", {"context"}}, + {"disable-mc", VariantType::Bool, true, {"MC labels"}}, + {"disable-root-output", VariantType::Bool, false, {"output"}}, + {"use-geom", VariantType::Bool, false, {"geometry"}}, + {"use-full-geometry", VariantType::Bool, true, {"geometry alias"}}}; + auto store = std::make_unique(specs, std::vector>{}); + store->preload(); + store->activate(); + ConfigParamRegistry registry{std::move(store)}; + ServiceRegistry services; + ConfigContext context{registry, ServiceRegistryRef{services}, 0, nullptr}; + const auto resolved = readWorkflowOptions(context); + BOOST_CHECK(resolved.vertexSource == VertexSource::Truth); + BOOST_CHECK(!resolved.useMC); + BOOST_CHECK(resolved.useFullGeometry); + BOOST_CHECK_EQUAL(resolved.truthContext, "custom-context.root"); + registry.override("truth-context", std::string{}); + BOOST_CHECK_THROW(readWorkflowOptions(context), std::invalid_argument); + registry.override("vertex-source", std::string{"diamond"}); + BOOST_CHECK(readWorkflowOptions(context).vertexSource == VertexSource::Diamond); + registry.override("configKeyValues", std::string{"ITSCommonCATrackerParam.nThreads=0"}); + BOOST_CHECK_THROW(readWorkflowOptions(context), std::invalid_argument); + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "nThreads", 1); +} diff --git a/Detectors/ITSMFT/ITS/workflow-ca/test/testITSCATrackerDPLContract.cxx b/Detectors/ITSMFT/ITS/workflow-ca/test/testITSCATrackerDPLContract.cxx new file mode 100644 index 0000000000000..35918aced15ba --- /dev/null +++ b/Detectors/ITSMFT/ITS/workflow-ca/test/testITSCATrackerDPLContract.cxx @@ -0,0 +1,121 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// Gate 3 workflow-onboarding Slice 2: focused tests for the DPL input/output +// contract of o2::its::ca::getCATrackerSpec() -- MC/non-MC variants, and the +// hard requirement that no vertex-related OutputSpec (VERTICES, +// VERTICESROF, VERTICESMCTR, VERTICESMCPUR, or any fake substitute) is ever +// declared by this opt-in tracker-only workflow. + +#define BOOST_TEST_MODULE ITSMFT ITSCATrackerDPLContract +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include + +#include "Framework/DataProcessorSpec.h" +#include "Framework/DataSpecUtils.h" +#include "ITSCAWorkflow/CATrackerSpec.h" + +using namespace o2::framework; + +namespace +{ +bool hasInput(const std::vector& specs, const std::string& binding) +{ + return std::any_of(specs.begin(), specs.end(), [&binding](const InputSpec& s) { return s.binding == binding; }); +} + +bool hasOutput(const std::vector& specs, const std::string& desc) +{ + return std::any_of(specs.begin(), specs.end(), + [&desc](const OutputSpec& s) { return DataSpecUtils::describe(s).find(desc) != std::string::npos; }); +} +} // namespace + +BOOST_AUTO_TEST_CASE(NonMCContractHasNoLabelsInputOrMCOutputs) +{ + const auto spec = o2::its::ca::getCATrackerSpec({.useMC = false}); + + BOOST_CHECK(hasInput(spec.inputs, "compClusters")); + BOOST_CHECK(hasInput(spec.inputs, "patterns")); + BOOST_CHECK(hasInput(spec.inputs, "ROframes")); + BOOST_CHECK(hasInput(spec.inputs, "itscldict")); + BOOST_CHECK(hasInput(spec.inputs, "itsTGeo")); // useGeom=false: geometry CCDB requested explicitly + BOOST_CHECK(!hasInput(spec.inputs, "labels")); + + BOOST_CHECK(hasOutput(spec.outputs, "TRACKS")); + BOOST_CHECK(hasOutput(spec.outputs, "TRACKCLSID")); + BOOST_CHECK(hasOutput(spec.outputs, "ITSTrackROF")); + BOOST_CHECK(!hasOutput(spec.outputs, "TRACKSMCTR")); +} + +BOOST_AUTO_TEST_CASE(MCContractAddsLabelsInputAndMCOutput) +{ + const auto spec = o2::its::ca::getCATrackerSpec({.useMC = true}); + + BOOST_CHECK(hasInput(spec.inputs, "labels")); + BOOST_CHECK(hasOutput(spec.outputs, "TRACKSMCTR")); +} + +BOOST_AUTO_TEST_CASE(UseGeomOmitsExplicitGeometryInput) +{ + const auto spec = o2::its::ca::getCATrackerSpec({.useMC = false, .useFullGeometry = true}); + BOOST_CHECK(!hasInput(spec.inputs, "itsTGeo")); +} + +BOOST_AUTO_TEST_CASE(NoVertexRelatedOutputsArePresentEver) +{ + for (const bool useMC : {false, true}) { + const auto spec = o2::its::ca::getCATrackerSpec({.useMC = useMC}); + for (const auto& out : spec.outputs) { + const auto desc = DataSpecUtils::describe(out); + BOOST_CHECK_MESSAGE(desc.find("VERTICES") == std::string::npos, + "unexpected vertex-related output present: " << desc); + BOOST_CHECK_MESSAGE(desc.find("VERTEX") == std::string::npos, + "unexpected vertex-related output present: " << desc); + } + } +} + +BOOST_AUTO_TEST_CASE(DeviceNameIsStable) +{ + const auto spec = o2::its::ca::getCATrackerSpec({.useMC = false}); + BOOST_CHECK_EQUAL(spec.name, "its-ca-tracker"); +} + +BOOST_AUTO_TEST_CASE(PublicationCompatibilityIsClearedOnEveryWorkflowExit) +{ + o2::its::ca::PublicationAdapter publication; + o2::itsmft::tracking::ITSSharedClusterCompatibility compatibility; + publication.adoptITSSharedClusterCompatibility(&compatibility); + o2::itsmft::tracking::TimeFrame frame; + o2::itsmft::IterationParameters parameters; + for (bool fail : {false, true}) { + BOOST_REQUIRE(publication.completeAccepted({}, parameters, frame, true)); + BOOST_REQUIRE(compatibility.isSealed()); + try { + auto cleanup = publication.cleanupOnExit(); + BOOST_CHECK(!compatibility.isSealed()); + BOOST_REQUIRE(publication.completeAccepted({}, parameters, frame, true)); + BOOST_CHECK(compatibility.isSealed()); + if (fail) { + throw std::runtime_error{"publication failure"}; + } + } catch (const std::runtime_error&) { + BOOST_CHECK(fail); + } + BOOST_CHECK(!compatibility.isSealed()); + BOOST_CHECK(compatibility.entries().empty()); + } +} diff --git a/Detectors/ITSMFT/ITS/workflow-ca/test/testITSCATruthSeeding.cxx b/Detectors/ITSMFT/ITS/workflow-ca/test/testITSCATruthSeeding.cxx new file mode 100644 index 0000000000000..3835ab5ae4fa3 --- /dev/null +++ b/Detectors/ITSMFT/ITS/workflow-ca/test/testITSCATruthSeeding.cxx @@ -0,0 +1,75 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE ITS CA Truth Seeding +#define BOOST_TEST_DYN_LINK +#include + +#include +#include + +#include "ITSCAWorkflow/TruthSeeding.h" +#include "ITSMFTTracking/ROFLookupTables.h" + +using namespace o2::its::ca; +using namespace o2::itsmft::tracking; + +BOOST_AUTO_TEST_CASE(ConsecutiveFramesSelectTheirOwnCollisionsAndLookupROFs) +{ + const o2::InteractionRecord firstOrigin{0, 40}; + const std::array collisions{firstOrigin + 50, firstOrigin + 250}; + const std::array z{1.f, 7.f}; + // Two ROFs with nonzero delay and bias, matching the cluster loader. + const ROFTimingConfig timing{100, 10, 20, 0}; + for (int frame = 0; frame < 2; ++frame) { + const auto origin = firstOrigin + 200 * frame; + const auto first = computeROFIntervalBC(origin, origin, timing, 0); + const auto last = computeROFIntervalBC(origin + 100, origin, timing, 1); + BOOST_REQUIRE(first.ok() && last.ok()); + const ROFIntervalBC window{first.interval.begin, last.interval.end, 0, 0}; + std::vector vertices; + std::vector eventIds; + for (int event = 0; event < 2; ++event) { + if (const auto time = truthSeedingTime(collisions[event], origin, window, 50)) { + o2::its::Vertex vertex; + vertex.setXYZ(0.f, 0.f, z[event]); + vertex.getTimeStamp() = *time; + vertices.push_back(vertex); + eventIds.push_back(event); + } + } + BOOST_REQUIRE_EQUAL(vertices.size(), 1); + BOOST_CHECK_EQUAL(eventIds.front(), frame); + BOOST_CHECK_EQUAL(vertices.front().getZ(), z[frame]); + BOOST_CHECK_EQUAL(vertices.front().getTimeStamp().lower(), 50); + o2::its::ROFVertexLookupTable<1> lookup; + lookup.defineLayer(0, 2, 100, 10, 20, 0); + lookup.init(); + lookup.update(vertices.data(), vertices.size()); + BOOST_CHECK_EQUAL(lookup.getView().getVertices(0, 0).getEntries(), 1); + BOOST_CHECK_EQUAL(lookup.getView().getVertices(0, 1).getEntries(), 0); + } +} + +BOOST_AUTO_TEST_CASE(TruthTimingPreservesOverlapAndRejectsOutOfFrameEvents) +{ + const o2::InteractionRecord origin{0, 40}; + const ROFIntervalBC window{0, 200, 0, 0}; + const auto overlap = truthSeedingTime(origin - 10, origin, window, 50); + BOOST_REQUIRE(overlap); + BOOST_CHECK_EQUAL(overlap->lower(), 0); + BOOST_CHECK_EQUAL(overlap->upper(), 40); + BOOST_CHECK(!truthSeedingTime(origin - 50, origin, window, 50)); + BOOST_CHECK(!truthSeedingTime(origin + 200, origin, window, 50)); + BOOST_CHECK(!truthSeedingTime(o2::InteractionRecord{}, origin, window, 50)); + BOOST_CHECK(!truthSeedingTime(origin, origin, window, 0)); + BOOST_CHECK(!truthSeedingTime(origin, origin, {}, 50)); +} diff --git a/Detectors/ITSMFT/MFT/workflow/CMakeLists.txt b/Detectors/ITSMFT/MFT/workflow/CMakeLists.txt index b83699498a6b8..5f904c24011fb 100644 --- a/Detectors/ITSMFT/MFT/workflow/CMakeLists.txt +++ b/Detectors/ITSMFT/MFT/workflow/CMakeLists.txt @@ -12,21 +12,26 @@ o2_add_library(MFTWorkflow TARGETVARNAME targetName SOURCES src/RecoWorkflow.cxx + src/CARecoWorkflow.cxx + src/CAWorkflowOptions.cxx + src/CATrackerSpec.cxx src/TrackerSpec.cxx src/TrackReaderSpec.cxx - src/TrackWriterSpec.cxx src/MFTAssessmentSpec.cxx src/TracksToRecordsSpec.cxx PUBLIC_LINK_LIBRARIES O2::Framework O2::SimConfig O2::SimulationDataFormat O2::ITSMFTReconstruction + O2::ITSMFTTracking O2::MFTTracking O2::MFTAssessment O2::DataFormatsMFT O2::ITSMFTWorkflow + TBB::tbb O2::MFTAlignment - O2::GlobalTrackingWorkflowReaders) + O2::GlobalTrackingWorkflowReaders + O2::ITSMFTCAWriter) o2_add_executable(reco-workflow SOURCES src/mft-reco-workflow.cxx COMPONENT_NAME mft @@ -51,3 +56,31 @@ o2_add_executable(tracks2records-workflow SOURCES src/mft-tracks2records-workflow.cxx COMPONENT_NAME mft PUBLIC_LINK_LIBRARIES O2::MFTWorkflow) + +o2_add_executable(ca-tracker-workflow + SOURCES src/mft-ca-tracker-workflow.cxx + COMPONENT_NAME mft + PUBLIC_LINK_LIBRARIES O2::MFTWorkflow) + +o2_add_executable(ca-reco-workflow + SOURCES src/mft-ca-reco-workflow.cxx + COMPONENT_NAME mft + PUBLIC_LINK_LIBRARIES O2::MFTWorkflow) + +o2_add_test(ca-tracker-publication-decision + COMPONENT_NAME mft + LABELS "mft;workflow;itsmft" + SOURCES test/testCATrackerPublicationDecision.cxx + PUBLIC_LINK_LIBRARIES O2::MFTWorkflow) + +o2_add_test(ca-tracker-dpl-contract + COMPONENT_NAME mft + LABELS "mft;workflow;itsmft" + SOURCES test/testMFTCATrackerDPLContract.cxx + PUBLIC_LINK_LIBRARIES O2::MFTWorkflow) + +o2_add_test(ca-reco-workflow + COMPONENT_NAME mft + LABELS "mft;workflow;itsmft" + SOURCES test/testMFTCARecoWorkflow.cxx + PUBLIC_LINK_LIBRARIES O2::MFTWorkflow) diff --git a/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CARecoWorkflow.h b/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CARecoWorkflow.h new file mode 100644 index 0000000000000..2a27396fa1e65 --- /dev/null +++ b/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CARecoWorkflow.h @@ -0,0 +1,27 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef O2_MFT_CARECOWORKFLOW_H_ +#define O2_MFT_CARECOWORKFLOW_H_ + +/// @file CARecoWorkflow.h + +#include "Framework/WorkflowSpec.h" +#include "MFTWorkflow/CAWorkflowOptions.h" + +namespace o2::mft::ca_reco_workflow +{ + +framework::WorkflowSpec getWorkflow(const ca::WorkflowOptions& options); + +} // namespace o2::mft::ca_reco_workflow + +#endif // O2_MFT_CARECOWORKFLOW_H_ diff --git a/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CATrackerSpec.h b/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CATrackerSpec.h new file mode 100644 index 0000000000000..f10437407867e --- /dev/null +++ b/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CATrackerSpec.h @@ -0,0 +1,87 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// @file CATrackerSpec.h + +#ifndef O2_MFT_CATRACKERSPEC_H_ +#define O2_MFT_CATRACKERSPEC_H_ + +#include +#include +#include +#include + +#include "DetectorsBase/GRPGeomHelper.h" +#include "CommonDataFormat/IRFrame.h" +#include "Framework/DataProcessorSpec.h" +#include "Framework/Task.h" +#include "ITSMFTTracking/GenericTrackOutputAdapter.h" +#include "ITSMFTTracking/Configuration.h" +#include "MFTWorkflow/CAWorkflowOptions.h" +#include "ITSMFTTracking/ClusterDecoding.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "ITSMFTTracking/WorkflowSession.h" +#include "ITSMFTTracking/Tracker.h" +#include "ITSMFTTracking/TrackerTraits.h" +#include "ITSMFTTracking/ROFViews.h" +#include "ITSMFTTracking/ROFLookupTables.h" +#include "DataFormatsITSMFT/TopologyDictionary.h" +#include "SimulationDataFormat/MCCompLabel.h" + +namespace o2::mft +{ + +using o2::itsmft::tracking::CATrackerPublicationAction; +using o2::itsmft::tracking::decideCATrackerPublicationAction; + +/// MFT CA tracker DPL task. Owns the TimeFrame and composes the workflow +/// input/timing/publication edge with Tracker. +class CATrackerDPL : public o2::framework::Task +{ + public: + CATrackerDPL(std::shared_ptr gr, + ca::TrackerOptions options); + ~CATrackerDPL() override = default; + + void init(framework::InitContext& ic) final; + void run(framework::ProcessingContext& pc) final; + void finaliseCCDB(framework::ConcreteDataMatcher& matcher, void* obj) final; + + private: + void updateTimeDependentParams(framework::ProcessingContext& pc); + void configureROFViews(gsl::span rofs, + gsl::span irFrames); + void initialiseTracking(); + o2::itsmft::tracking::TrackingOutcome processTimeFrame( + gsl::span rofs, + gsl::span clusters, + gsl::span patterns, + const o2::dataformats::MCTruthContainer* labels); + bool isActive() const noexcept { return mTracker != nullptr && mTracker->isConfiguredFor(mSession.frame); } + + std::shared_ptr mGGCCDBRequest; + bool mUseMC = false; + bool mTrackingInitialised = false; + ca::TrackerOptions mOptions; + o2::itsmft::tracking::WorkflowSession mSession{"MFT", o2::itsmft::tracking::MFTNLayers}; + std::unique_ptr mTrackerTraits; + std::unique_ptr mTracker; + std::unique_ptr mClusterDecoder; + const o2::itsmft::TopologyDictionary* mDictionary = nullptr; + int mMFTROFrameLengthInBC = 0; +}; + +o2::framework::DataProcessorSpec getCATrackerSpec(const ca::TrackerOptions& options); + +} // namespace o2::mft + +#endif // O2_MFT_CATRACKERSPEC_H_ diff --git a/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CAWorkflowOptions.h b/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CAWorkflowOptions.h new file mode 100644 index 0000000000000..b59cb35523563 --- /dev/null +++ b/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CAWorkflowOptions.h @@ -0,0 +1,93 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef O2_MFT_CAWORKFLOWOPTIONS_H_ +#define O2_MFT_CAWORKFLOWOPTIONS_H_ + +#include +#include +#include "ITSMFTTracking/Configuration.h" + +namespace o2::framework +{ +class ConfigContext; +} +namespace o2::mft::ca +{ +enum class WorkflowKind { Reconstruction, + TrackerOnly }; +enum class InputStage { DigitsFile, + UpstreamDigits, + UpstreamClusters }; +enum class GeometrySource { MFT, + Full }; +enum class IRFrameSource { None, + File, + Upstream }; +enum class OutputPolicy { All, + TracksAndClusterROFs, + ClusterROFs, + None }; + +struct TrackerOptions { + bool useMC = true; + GeometrySource geometry = GeometrySource::MFT; + o2::itsmft::TrackingMode::Type mode = o2::itsmft::TrackingMode::Sync; + int nThreads = 1; + IRFrameSource irFrames = IRFrameSource::None; + bool filterIRFrames = false; +}; + +struct WorkflowOptions { + WorkflowKind kind = WorkflowKind::Reconstruction; + InputStage input = InputStage::DigitsFile; + OutputPolicy output = OutputPolicy::All; + TrackerOptions tracker; + bool staggering = false; + bool runTracking = true; // false removes devices; mode=Off retains an inactive tracker and its consumers. + bool assessment = false; + bool processGenerated = true; + bool tracksToRecords = false; + std::vector diagnostics; +}; + +// External flags live only at this compatibility boundary. The resolver has +// no singleton, field/geometry, or DPL device dependencies. +struct WorkflowOptionInput { + WorkflowKind kind = WorkflowKind::Reconstruction; + bool useMC = true; + bool staggering = false; + bool fullGeometry = false; + bool useIRFrames = false; + bool upstreamDigits = false; + bool upstreamClusters = false; + bool clusterROFsOnly = false; + bool disableRootOutput = false; + bool runTracking = true; + bool assessment = false; + bool processGenerated = true; + bool tracksToRecords = false; + o2::itsmft::TrackingMode::Type mode = o2::itsmft::TrackingMode::Sync; + int nThreads = 1; +}; + +struct TrackerOptionAliases { + int mode = -1; + int nThreads = 1; // Effective value after applying --nThreads, then configKeyValues. + bool filterIRFrames = false; +}; + +// Parameter aliases override CLI values, identically in both entry points. +// Conflicts are reported with both setting names; invalid values throw. +WorkflowOptions resolveWorkflowOptions(const WorkflowOptionInput&, const TrackerOptionAliases&); +WorkflowOptions readWorkflowOptions(const o2::framework::ConfigContext&, WorkflowKind); +} // namespace o2::mft::ca +#endif diff --git a/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/TrackerSpec.h b/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/TrackerSpec.h index 3112e3efef5e6..3adbee07877aa 100644 --- a/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/TrackerSpec.h +++ b/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/TrackerSpec.h @@ -50,7 +50,7 @@ class TrackerDPL : public o2::framework::Task ///< set MFT ROFrame duration in microseconds void setMFTROFrameLengthMUS(float fums); - ///< set MFT ROFrame duration in BC (continuous mode only) + ///< Set MFT ROFrame duration in BC for continuous mode. void setMFTROFrameLengthInBC(int nbc); int mMFTROFrameLengthInBC = 0; ///< MFT RO frame in BC (for MFT cont. mode only) float mMFTROFrameLengthMUS = -1.; ///< MFT RO frame in \mus diff --git a/Detectors/ITSMFT/MFT/workflow/src/CARecoWorkflow.cxx b/Detectors/ITSMFT/MFT/workflow/src/CARecoWorkflow.cxx new file mode 100644 index 0000000000000..1dc4201bcd419 --- /dev/null +++ b/Detectors/ITSMFT/MFT/workflow/src/CARecoWorkflow.cxx @@ -0,0 +1,64 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// @file CARecoWorkflow.cxx + +#include "MFTWorkflow/CARecoWorkflow.h" + +#include "GlobalTrackingWorkflowReaders/IRFrameReaderSpec.h" +#include "ITSMFTCAWriter/MFTCATrackWriterSpec.h" +#include "ITSMFTWorkflow/ClustererSpec.h" +#include "ITSMFTWorkflow/ClusterWriterSpec.h" +#include "ITSMFTWorkflow/DigitReaderSpec.h" +#include "MFTWorkflow/CATrackerSpec.h" +#include "MFTWorkflow/MFTAssessmentSpec.h" +#include "MFTWorkflow/TracksToRecordsSpec.h" + +namespace o2::mft::ca_reco_workflow +{ + +framework::WorkflowSpec getWorkflow(const ca::WorkflowOptions& options) +{ + using namespace ca; + framework::WorkflowSpec specs; + const auto& tracker = options.tracker; + const bool useGeom = tracker.geometry == GeometrySource::Full; + const bool writeTracks = options.output == OutputPolicy::All || options.output == OutputPolicy::TracksAndClusterROFs; + if (options.kind == WorkflowKind::Reconstruction) { + if (options.input == InputStage::DigitsFile) { + specs.emplace_back(o2::itsmft::getMFTDigitReaderSpec(tracker.useMC, options.staggering, false, true, "mftdigits.root")); + } + if (options.input != InputStage::UpstreamClusters) { + specs.emplace_back(o2::itsmft::getMFTClustererSpec(tracker.useMC, options.staggering)); + } + if (options.output != OutputPolicy::None) { + specs.emplace_back(o2::itsmft::getMFTClusterWriterSpec(tracker.useMC, options.staggering, options.output != OutputPolicy::All)); + } + } + if (options.runTracking) { + if (tracker.irFrames == IRFrameSource::File) { + specs.emplace_back(o2::globaltracking::getIRFrameReaderSpec("ITS", 0, "its-irframe-reader", "o2_its_irframe.root")); + } + specs.emplace_back(o2::mft::getCATrackerSpec(tracker)); + if (writeTracks) { + specs.emplace_back(o2::mft::getTrackWriterSpec(tracker.useMC, true)); + } + if (options.assessment) { + specs.emplace_back(o2::mft::getMFTAssessmentSpec(tracker.useMC, useGeom, options.processGenerated)); + } + if (options.tracksToRecords) { + specs.emplace_back(o2::mft::getTracksToRecordsSpec()); + } + } + return specs; +} + +} // namespace o2::mft::ca_reco_workflow diff --git a/Detectors/ITSMFT/MFT/workflow/src/CATrackerSpec.cxx b/Detectors/ITSMFT/MFT/workflow/src/CATrackerSpec.cxx new file mode 100644 index 0000000000000..3075da6870b08 --- /dev/null +++ b/Detectors/ITSMFT/MFT/workflow/src/CATrackerSpec.cxx @@ -0,0 +1,336 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// @file CATrackerSpec.cxx + +#include "MFTWorkflow/CATrackerSpec.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "CommonDataFormat/IRFrame.h" +#include "DataFormatsITSMFT/CompCluster.h" +#include "DataFormatsITSMFT/DPLAlpideParam.h" +#include "DataFormatsITSMFT/ROFRecord.h" +#include "DataFormatsITSMFT/TopologyDictionary.h" +#include "DataFormatsMFT/TrackMFT.h" +#include "DetectorsBase/GeometryManager.h" +#include "Framework/CCDBParamSpec.h" +#include "Framework/DataProcessorSpec.h" +#include "Framework/Logger.h" +#include "ITSMFTTracking/Tracker.h" +#include "ITSMFTTracking/GenericTrackOutputAdapter.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/SurfaceTiming.h" +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "DetectorsBase/Propagator.h" +#include +#include "CommonConstants/LHCConstants.h" +#include "MFTBase/GeometryTGeo.h" +#include "MFTTracking/Constants.h" +#include "MFTTracking/MFTTrackingParam.h" +#include "SimulationDataFormat/MCCompLabel.h" +#include "SimulationDataFormat/MCTruthContainer.h" + +using namespace o2::framework; + +namespace o2::mft +{ + +namespace +{ +using namespace o2::itsmft::tracking; + +template +constexpr std::array detectorLocalToLayoutLayers() +{ + std::array order{}; + for (int i = 0; i < NLayers; ++i) { + order[i] = LayerId{static_cast(i)}; + } + return order; +} + +inline constexpr auto kLayerToLayout = detectorLocalToLayoutLayers(); + +bool rofOverlapsIRFrames(const o2::itsmft::ROFRecord& rof, int rofLengthInBC, + gsl::span irFrames) +{ + o2::InteractionRecord start{rof.getBCData()}; + const o2::InteractionRecord end = start + rofLengthInBC - 1; + const o2::dataformats::IRFrame reference{start, end}; + for (const auto& ir : irFrames) { + if (ir.info > 0 && reference.getOverlap(ir).isValid()) { + return true; + } + } + return false; +} + +} // namespace + +CATrackerDPL::CATrackerDPL(std::shared_ptr gr, ca::TrackerOptions options) + : mGGCCDBRequest(std::move(gr)), mUseMC(options.useMC), mOptions(options) +{ + mClusterDecoder = std::make_unique(); +} + +void CATrackerDPL::configureROFViews(gsl::span rofs, + gsl::span irFrames) +{ + const auto& detector = mTracker->getDetectorConfiguration(); + const auto& alpParams = o2::itsmft::DPLAlpideParam::Instance(); + const bool continuous = o2::base::GRPGeomHelper::instance().getGRPECS()->isDetContinuousReadOut(o2::detectors::DetID::MFT); + mMFTROFrameLengthInBC = continuous ? alpParams.roFrameLengthInBC : std::max(1, static_cast(alpParams.roFrameLengthTrig / (o2::constants::lhc::LHCBunchSpacingNS * 1e3))); + const int nOrbitsPerTF = o2::base::GRPGeomHelper::getNHBFPerTF(); + const auto timings = mSession.layerTimings(alpParams, nOrbitsPerTF, detector.addTimeError); + const auto& trackingParam = o2::mft::MFTTrackingParam::Instance(); + const bool useIrFilter = mOptions.filterIRFrames && !irFrames.empty(); + mSession.configureTiming(timings, [&](int rof) { + return rof >= static_cast(rofs.size()) || + ((!useIrFilter || rofOverlapsIRFrames(rofs[rof], mMFTROFrameLengthInBC, irFrames)) && + (!trackingParam.isMultCutRequested() || trackingParam.isPassingMultCut(rofs[rof].getNEntries()))); + }); +} + +void CATrackerDPL::initialiseTracking() +{ + const auto mode = mOptions.mode; + const auto& trackerParams = o2::itsmft::tracking::TrackerParamRef::get(); + auto plan = o2::itsmft::TrackingMode::getTrackingPlan(o2::detectors::DetID::MFT, mode); + LOGP(info, "MFT CA tracker initialized in {} mode with {} iteration(s)", + o2::itsmft::TrackingMode::toString(mode), plan.iterations.size()); + if (plan.iterations.empty()) { + return; + } + + mTrackerTraits = std::make_unique(); + std::shared_ptr taskArena; + mTrackerTraits->setNThreads(mOptions.nThreads, taskArena); + + const auto maxMemory = plan.execution.MaxMemory; + o2::itsmft::tracking::TrackerInitialization configuration{ + .catalog = {o2::itsmft::tracking::kMFTStaticSurfaceCatalog.data(), + static_cast(o2::itsmft::tracking::kMFTStaticSurfaceCatalog.size())}, + .layout = o2::itsmft::tracking::makeDetectorLayout(o2::itsmft::tracking::LayerMask{trackerParams.holeLayerMask}), + .plan = std::move(plan), + .memoryPool = std::make_shared(maxMemory)}; + + mTracker = std::make_unique(); + const auto result = mTracker->initialize(mSession.frame, configuration); + if (!result.ok()) { + LOGP(fatal, "MFT CA tracker failed to initialize static configuration (error={} iteration={} layout={})", + static_cast(result.error), result.failedIteration, static_cast(result.layoutError)); + } +} + +o2::itsmft::tracking::TrackingOutcome CATrackerDPL::processTimeFrame( + gsl::span rofs, + gsl::span clusters, + gsl::span patterns, + const o2::dataformats::MCTruthContainer* labels) +{ + if (!isActive()) { + LOGP(info, "MFT CA tracking mode is off, skipping TimeFrame processing"); + return o2::itsmft::tracking::TrackingOutcome::Success; + } + mSession.frame.setBz(o2::base::Propagator::Instance()->getNominalBz()); + o2::itsmft::tracking::ClusterSourceInput source; + source.id = o2::itsmft::tracking::ClusterSourceId{0}; + source.detector = o2::detectors::DetID::MFT; + source.clusters = clusters; + source.patterns = patterns; + source.rofs = rofs; + source.dictionary = mDictionary; + source.labels = labels; + source.layerToSurface = kLayerToLayout; + source.decoder = mClusterDecoder.get(); + return mSession.process(*mTracker, *mTrackerTraits, source, [](const o2::InteractionRecord&) {}, [](const o2::itsmft::tracking::TrackingResult&) {}); +} + +void CATrackerDPL::init(InitContext&) +{ + o2::base::GRPGeomHelper::instance().setRequest(mGGCCDBRequest); +} + +void CATrackerDPL::run(ProcessingContext& pc) +{ + updateTimeDependentParams(pc); + + auto rofsinput = pc.inputs().get>("ROframes"); + + if (decideCATrackerPublicationAction(isActive(), o2::itsmft::tracking::TrackingOutcome::Success) == CATrackerPublicationAction::PublishInactiveEmpty) { + // Existing production behavior, preserved exactly: publish the input + // ROFs verbatim (their firstEntry/nEntries are not rewritten here) plus + // empty track/cluster-index/seed-pattern outputs, when the tracker is + // not configured to run. + pc.outputs().make>(Output{"MFT", "MFTTrackROF", 0}, + rofsinput.begin(), rofsinput.end()); + pc.outputs().make>(Output{"MFT", "TRACKS", 0}); + pc.outputs().make>(Output{"MFT", "TRACKCLSID", 0}); + pc.outputs().make>(Output{"MFT", "TRACKSEEDPAT", 0}); + return; + } + + auto compClusters = pc.inputs().get>("compClusters"); + gsl::span patterns = pc.inputs().get>("patterns"); + + const dataformats::MCTruthContainer* labels = nullptr; + if (mUseMC && pc.inputs().getPos("labels") >= 0) { + labels = pc.inputs().get*>("labels").release(); + } + + gsl::span irFrames; + if (pc.inputs().getPos("IRFramesITS") >= 0) { + irFrames = pc.inputs().get>("IRFramesITS"); + } + + LOGP(info, "MFT CA input pulled {} compressed clusters in {} RO frames ({} pattern bytes)", + compClusters.size(), rofsinput.size(), patterns.size()); + + auto cleanup = mSession.cleanupOnExit(); + configureROFViews(gsl::span(rofsinput.data(), rofsinput.size()), irFrames); + const auto trackingResult = processTimeFrame(gsl::span(rofsinput.data(), rofsinput.size()), + gsl::span(compClusters.data(), compClusters.size()), + patterns, labels); + + if (decideCATrackerPublicationAction(isActive(), trackingResult) == CATrackerPublicationAction::SkipDroppedTimeFrame) { + LOGP(error, "MFT CA tracking dropped this TimeFrame ({} ROFs, {} clusters); publishing nothing and continuing with the next TimeFrame", + rofsinput.size(), compClusters.size()); + cleanup.frameAlreadyReset(); + return; + } + + { + mSession.publicationClock.emplace(mSession.overlap.getView().getClockLayer()); + const o2::itsmft::tracking::GenericTrackPublicationContext context{ + o2::detectors::DetID::MFT, o2::itsmft::tracking::ClusterSourceId{0}, + gsl::span{rofsinput.data(), rofsinput.size()}, *mSession.publicationClock, + kLayerToLayout, + &mSession.externalIndices, &mSession.clusterSizes}; + o2::itsmft::tracking::GenericTrackOutputAdapterError error = o2::itsmft::tracking::GenericTrackOutputAdapterError::None; + const auto staged = o2::itsmft::tracking::stageMFTGenericTrackOutput(mSession.frame, context, mUseMC, error); + if (!staged) { + throw std::runtime_error{"MFT GenericTrack output staging failed"}; + } + + o2::itsmft::tracking::copyTrackingOutputColumns(pc.outputs(), Output{"MFT", "MFTTrackROF", 0}, + Output{"MFT", "TRACKS", 0}, Output{"MFT", "TRACKCLSID", 0}, *staged); + auto& allSeedPatterns = pc.outputs().make>(Output{"MFT", "TRACKSEEDPAT", 0}); + allSeedPatterns.assign(staged->seedPatterns.begin(), staged->seedPatterns.end()); + LOGP(info, "MFT CA pushed {} tracks in {} ROFs", staged->tracks.size(), staged->trackROFs.size()); + if (mUseMC) { + pc.outputs().snapshot(Output{"MFT", "TRACKSMCTR", 0}, staged->labels); + LOGP(info, "MFT CA pushed {} track MC labels", staged->labels.size()); + } + } +} + +void CATrackerDPL::updateTimeDependentParams(ProcessingContext& pc) +{ + o2::base::GRPGeomHelper::instance().checkUpdates(pc); + if (!mTrackingInitialised) { + mTrackingInitialised = true; + initialiseTracking(); + } + static bool initOnceDone = false; + if (!initOnceDone) { + initOnceDone = true; + if (pc.inputs().getPos("mftTGeo") >= 0) { + pc.inputs().get("mftTGeo"); + } + pc.inputs().get("cldict"); + o2::mft::GeometryTGeo::Instance()->fillMatrixCache(o2::math_utils::bit2Mask(o2::math_utils::TransformType::T2L, + o2::math_utils::TransformType::T2GRot, + o2::math_utils::TransformType::T2G, + o2::math_utils::TransformType::L2G)); + } +} + +void CATrackerDPL::finaliseCCDB(ConcreteDataMatcher& matcher, void* obj) +{ + if (o2::base::GRPGeomHelper::instance().finaliseCCDB(matcher, obj)) { + return; + } + if (matcher == ConcreteDataMatcher("MFT", "CLUSDICT", 0)) { + LOG(info) << "MFT CA input cluster dictionary updated"; + mDictionary = static_cast(obj); + return; + } + if (matcher == ConcreteDataMatcher("MFT", "GEOMTGEO", 0)) { + LOG(info) << "MFT CA input GeometryTGeo loaded from CCDB"; + o2::mft::GeometryTGeo::adopt(static_cast(obj)); + o2::mft::GeometryTGeo::Instance()->fillMatrixCache(o2::math_utils::bit2Mask(o2::math_utils::TransformType::T2L, + o2::math_utils::TransformType::T2GRot, + o2::math_utils::TransformType::T2G, + o2::math_utils::TransformType::L2G)); + // The catalog has static process lifetime; geometry adoption remains + // necessary for raw cluster decoding. + return; + } +} + +DataProcessorSpec getCATrackerSpec(const ca::TrackerOptions& options) +{ + const bool useMC = options.useMC; + const bool useGeom = options.geometry == ca::GeometrySource::Full; + std::vector inputs; + inputs.emplace_back("compClusters", "MFT", "COMPCLUSTERS", 0, Lifetime::Timeframe); + inputs.emplace_back("patterns", "MFT", "PATTERNS", 0, Lifetime::Timeframe); + inputs.emplace_back("ROframes", "MFT", "CLUSTERSROF", 0, Lifetime::Timeframe); + inputs.emplace_back("cldict", "MFT", "CLUSDICT", 0, Lifetime::Condition, ccdbParamSpec("MFT/Calib/ClusterDictionary")); + + if (useMC) { + inputs.emplace_back("labels", "MFT", "CLUSTERSMCTR", 0, Lifetime::Timeframe); + } + + if (options.irFrames != ca::IRFrameSource::None) { + inputs.emplace_back("IRFramesITS", "ITS", "IRFRAMES", 0, Lifetime::Timeframe); + } + + auto ggRequest = std::make_shared(false, + true, + false, + true, + true, + useGeom ? o2::base::GRPGeomRequest::Aligned : o2::base::GRPGeomRequest::None, + inputs, + true); + if (!useGeom) { + ggRequest->addInput({"mftTGeo", "MFT", "GEOMTGEO", 0, Lifetime::Condition, framework::ccdbParamSpec("MFT/Config/Geometry")}, inputs); + } + + std::vector outputs; + outputs.emplace_back("MFT", "TRACKS", 0, Lifetime::Timeframe); + outputs.emplace_back("MFT", "MFTTrackROF", 0, Lifetime::Timeframe); + outputs.emplace_back("MFT", "TRACKCLSID", 0, Lifetime::Timeframe); + outputs.emplace_back("MFT", "TRACKSEEDPAT", 0, Lifetime::Timeframe); + if (useMC) { + outputs.emplace_back("MFT", "TRACKSMCTR", 0, Lifetime::Timeframe); + } + + return DataProcessorSpec{ + "mft-ca-tracker", + inputs, + outputs, + AlgorithmSpec{adaptFromTask(ggRequest, options)}, + Options{}}; +} + +} // namespace o2::mft diff --git a/Detectors/ITSMFT/MFT/workflow/src/CAWorkflowOptions.cxx b/Detectors/ITSMFT/MFT/workflow/src/CAWorkflowOptions.cxx new file mode 100644 index 0000000000000..e18f4fb817870 --- /dev/null +++ b/Detectors/ITSMFT/MFT/workflow/src/CAWorkflowOptions.cxx @@ -0,0 +1,131 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "MFTWorkflow/CAWorkflowOptions.h" + +#include +#include "CommonUtils/ConfigurableParam.h" +#include "DataFormatsITSMFT/DPLAlpideParamInitializer.h" +#include "Framework/ConfigContext.h" +#include "Framework/ConfigParamRegistry.h" +#include "Framework/Logger.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "MFTTracking/MFTTrackingParam.h" + +namespace o2::mft::ca +{ +WorkflowOptions resolveWorkflowOptions(const WorkflowOptionInput& input, const TrackerOptionAliases& aliases) +{ + using namespace o2::itsmft; + if (input.upstreamDigits && input.upstreamClusters) { + throw std::invalid_argument("--digits-from-upstream conflicts with --clusters-from-upstream; choose one input stage"); + } + if (input.mode < TrackingMode::Unset || input.mode > TrackingMode::Off || + aliases.mode < TrackingMode::Unset || aliases.mode > TrackingMode::Off) { + throw std::invalid_argument("Invalid --tracking-mode or MFTCATrackerParam.trackingMode"); + } + if (input.nThreads <= 0 || aliases.nThreads <= 0) { + throw std::invalid_argument("--nThreads and MFTCATrackerParam.nThreads must both be > 0"); + } + if (!input.runTracking && (input.assessment || input.tracksToRecords)) { + throw std::invalid_argument("--disable-tracking conflicts with --run-assessment/--run-tracks2records"); + } + WorkflowOptions result; + result.kind = input.kind; + result.input = input.kind == WorkflowKind::TrackerOnly || input.upstreamClusters ? InputStage::UpstreamClusters + : input.upstreamDigits ? InputStage::UpstreamDigits + : InputStage::DigitsFile; + result.output = input.disableRootOutput ? (input.clusterROFsOnly ? OutputPolicy::ClusterROFs : OutputPolicy::None) + : input.clusterROFsOnly ? OutputPolicy::TracksAndClusterROFs + : OutputPolicy::All; + result.tracker.useMC = input.useMC; + result.tracker.geometry = input.fullGeometry ? GeometrySource::Full : GeometrySource::MFT; + result.tracker.mode = aliases.mode == TrackingMode::Unset ? input.mode : static_cast(aliases.mode); + if (result.tracker.mode == TrackingMode::Unset) { + result.tracker.mode = TrackingMode::Sync; + } + result.tracker.nThreads = aliases.nThreads; + result.tracker.filterIRFrames = aliases.filterIRFrames; + if (input.useIRFrames || aliases.filterIRFrames) { + result.tracker.irFrames = result.input == InputStage::DigitsFile ? IRFrameSource::File : IRFrameSource::Upstream; + } + result.staggering = input.staggering; + result.runTracking = input.runTracking; + result.assessment = input.assessment; + result.processGenerated = input.processGenerated; + result.tracksToRecords = input.tracksToRecords; + if (aliases.mode != TrackingMode::Unset && input.mode != result.tracker.mode) { + result.diagnostics.push_back("MFTCATrackerParam.trackingMode=" + TrackingMode::toString(result.tracker.mode) + + " overrides --tracking-mode=" + TrackingMode::toString(input.mode)); + } + if (input.nThreads != aliases.nThreads) { + result.diagnostics.push_back("MFTCATrackerParam.nThreads=" + std::to_string(aliases.nThreads) + + " overrides --nThreads=" + std::to_string(input.nThreads)); + } + if (!input.runTracking && (input.useIRFrames || aliases.filterIRFrames)) { + result.diagnostics.push_back("--disable-tracking: --use-irframes/MFTTrackingParam.irFramesOnly have no tracker consumer"); + result.tracker.irFrames = IRFrameSource::None; + } + if (input.clusterROFsOnly && input.disableRootOutput) { + result.diagnostics.push_back("--cluster-rof-branch-only overrides --disable-root-output for the cluster ROF branch"); + } + return result; +} + +WorkflowOptions readWorkflowOptions(const o2::framework::ConfigContext& context, WorkflowKind kind) +{ + const auto& options = context.options(); + using Param = o2::itsmft::TrackerParamConfig; + (void)Param::Instance(); + WorkflowOptionInput input; + input.kind = kind; + input.nThreads = options.get("nThreads"); + // Apply the CLI alias first, then let explicit parameter keys override it. + o2::conf::ConfigurableParam::setValue("MFTCATrackerParam", "nThreads", input.nThreads); + o2::conf::ConfigurableParam::updateFromString(options.get("configKeyValues")); + input.mode = o2::itsmft::TrackingMode::fromString(options.get("tracking-mode")); + input.useMC = !options.get("disable-mc"); + input.disableRootOutput = options.get("disable-root-output"); + input.fullGeometry = options.get("use-geom") || options.get("use-full-geometry"); + + input.useIRFrames = options.get("use-irframes"); + if (kind == WorkflowKind::Reconstruction) { + input.upstreamDigits = options.get("digits-from-upstream"); + input.upstreamClusters = options.get("clusters-from-upstream"); + input.clusterROFsOnly = options.get("cluster-rof-branch-only"); + input.runTracking = !options.get("disable-tracking"); + input.assessment = options.get("run-assessment"); + input.processGenerated = !options.get("disable-process-gen"); + input.tracksToRecords = options.get("run-tracks2records"); + input.staggering = o2::itsmft::DPLAlpideParamInitializer::isMFTStaggeringEnabled(context); + } + o2::itsmft::TrackingMode::validateCommonCAOptions(o2::detectors::DetID::MFT); + const auto& params = Param::Instance(); + auto result = resolveWorkflowOptions(input, {params.trackingMode, params.nThreads, MFTTrackingParam::Instance().irFramesOnly}); + for (const auto& diagnostic : result.diagnostics) { + LOGP(info, "{}", diagnostic); + } + const auto inputName = result.input == InputStage::DigitsFile ? "digits file" : result.input == InputStage::UpstreamDigits ? "upstream digits" + : "upstream clusters+patterns+ROFs (and labels if MC enabled)"; + const auto irName = result.tracker.irFrames == IRFrameSource::None ? "none" : result.tracker.irFrames == IRFrameSource::File ? "file" + : "upstream ITS IR frames"; + const auto outputName = result.kind == WorkflowKind::TrackerOnly ? (result.output == OutputPolicy::None ? "none" : "tracks") : result.output == OutputPolicy::All ? "tracks+clusters" + : result.output == OutputPolicy::TracksAndClusterROFs ? "tracks+cluster ROFs" + : result.output == OutputPolicy::ClusterROFs ? "cluster ROFs" + : "none"; + LOGP(info, "MFT CA resolved: mode={} threads={} tracking={} input={} geometry={} IR source={} filter={} ROOT output={} MC={}", + o2::itsmft::TrackingMode::toString(result.tracker.mode), result.tracker.nThreads, + !result.runTracking ? "disabled" : result.tracker.mode == o2::itsmft::TrackingMode::Off ? "inactive" + : "active", + inputName, result.tracker.geometry == GeometrySource::Full ? "full" : "MFT", irName, result.tracker.filterIRFrames, outputName, result.tracker.useMC); + return result; +} +} // namespace o2::mft::ca diff --git a/Detectors/ITSMFT/MFT/workflow/src/RecoWorkflow.cxx b/Detectors/ITSMFT/MFT/workflow/src/RecoWorkflow.cxx index 178c1dd50f4df..e465db55ec44a 100644 --- a/Detectors/ITSMFT/MFT/workflow/src/RecoWorkflow.cxx +++ b/Detectors/ITSMFT/MFT/workflow/src/RecoWorkflow.cxx @@ -16,7 +16,7 @@ #include "ITSMFTWorkflow/ClusterWriterSpec.h" #include "MFTWorkflow/RecoWorkflow.h" #include "MFTWorkflow/TrackerSpec.h" -#include "MFTWorkflow/TrackWriterSpec.h" +#include "ITSMFTCAWriter/MFTCATrackWriterSpec.h" #include "ITSMFTWorkflow/DigitReaderSpec.h" #include "MFTWorkflow/MFTAssessmentSpec.h" #include "MFTWorkflow/TracksToRecordsSpec.h" diff --git a/Detectors/ITSMFT/MFT/workflow/src/TrackerSpec.cxx b/Detectors/ITSMFT/MFT/workflow/src/TrackerSpec.cxx index e3bd557435ec0..6abebd8331503 100644 --- a/Detectors/ITSMFT/MFT/workflow/src/TrackerSpec.cxx +++ b/Detectors/ITSMFT/MFT/workflow/src/TrackerSpec.cxx @@ -70,10 +70,8 @@ void TrackerDPL::run(ProcessingContext& pc) auto compClusters = pc.inputs().get>("compClusters"); auto ntracks = 0; - // code further down does assignment to the rofs and the altered object is used for output - // we therefore need a copy of the vector rather than an object created directly on the input data, - // the output vector however is created directly inside the message memory thus avoiding copy by - // snapshot + // The output ROFs are mutable copies of the input payload; the output vector + // is allocated directly in message memory. auto rofsinput = pc.inputs().get>("ROframes"); auto& rofs = pc.outputs().make>(Output{"MFT", "MFTTrackROF", 0}, rofsinput.begin(), rofsinput.end()); @@ -83,7 +81,7 @@ void TrackerDPL::run(ProcessingContext& pc) auto& trackingParam = MFTTrackingParam::Instance(); if (trackingParam.irFramesOnly) { - // selects only those ROFs that overlap ITS IRFrame + // Keep only ROFs overlapping an ITS IRFrame. LOG(info) << "MFTTracker IRFrame filter enabled: loading ITS IR Frames. "; auto irFrames = pc.inputs().get>("IRFramesITS"); filter = createIRFrameFilter(irFrames); @@ -187,7 +185,7 @@ void TrackerDPL::run(ProcessingContext& pc) } }; - // snippet to convert found tracks to final output tracks with separate cluster indices + // Convert tracks while collecting their separate cluster indices. auto copyTracks = [](auto& new_tracks, auto& allTracks, auto& allClusIdx) { for (auto& trc : new_tracks) { trc.setExternalClusterIndexOffset(allClusIdx.size()); diff --git a/Detectors/ITSMFT/MFT/workflow/src/TracksToRecordsSpec.cxx b/Detectors/ITSMFT/MFT/workflow/src/TracksToRecordsSpec.cxx index 0a7743795b686..d1045b9112f7a 100644 --- a/Detectors/ITSMFT/MFT/workflow/src/TracksToRecordsSpec.cxx +++ b/Detectors/ITSMFT/MFT/workflow/src/TracksToRecordsSpec.cxx @@ -85,7 +85,6 @@ void TracksToRecordsSpec::endOfStream(o2::framework::EndOfStreamContext& ec) //_____________________________________________________________ void TracksToRecordsSpec::sendOutput(DataAllocator& output) { - // TODO: figure out how to have record tree output redirected here and saved } ///_______________________________________ diff --git a/Detectors/ITSMFT/MFT/workflow/src/mft-ca-reco-workflow.cxx b/Detectors/ITSMFT/MFT/workflow/src/mft-ca-reco-workflow.cxx new file mode 100644 index 0000000000000..c056aeedd0375 --- /dev/null +++ b/Detectors/ITSMFT/MFT/workflow/src/mft-ca-reco-workflow.cxx @@ -0,0 +1,70 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// @file mft-ca-reco-workflow.cxx + +#include "MFTWorkflow/CARecoWorkflow.h" + +#include + +#include "CommonUtils/ConfigurableParam.h" +#include "DataFormatsITSMFT/DPLAlpideParamInitializer.h" +#include "DetectorsRaw/HBFUtilsInitializer.h" +#include "Framework/CallbacksPolicy.h" +#include "Framework/CompletionPolicyHelpers.h" +#include "ITSMFTTracking/TrackingConfigParam.h" + +using namespace o2::framework; + +void customize(std::vector& policies) +{ + o2::raw::HBFUtilsInitializer::addNewTimeSliceCallback(policies); +} + +void customize(std::vector& policies) +{ + policies.push_back(CompletionPolicyHelpers::consumeWhenAllOrdered(".*(?:MFT|mft).*[W,w]riter.*")); +} + +void customize(std::vector& workflowOptions) +{ + std::vector options{ + {"digits-from-upstream", o2::framework::VariantType::Bool, false, {"digits will be provided from upstream, skip digits reader"}}, + {"clusters-from-upstream", o2::framework::VariantType::Bool, false, {"clusters will be provided from upstream, skip clusterizer"}}, + {"disable-root-output", o2::framework::VariantType::Bool, false, {"do not write output root files"}}, + {"disable-mc", o2::framework::VariantType::Bool, false, {"disable MC propagation even if available"}}, + {"disable-tracking", o2::framework::VariantType::Bool, false, {"disable tracking step"}}, + {"run-assessment", o2::framework::VariantType::Bool, false, {"run MFT assessment workflow"}}, + {"disable-process-gen", o2::framework::VariantType::Bool, false, {"disable processing of all generated tracks (depends on --run-assessment)"}}, + {"configKeyValues", VariantType::String, "", {"Semicolon separated key=value strings"}}, + {"nThreads", VariantType::Int, 1, {"Number of CA tracker threads"}}, + {"use-geom", VariantType::Bool, false, {"alias for --use-full-geometry"}}, + {"use-full-geometry", o2::framework::VariantType::Bool, false, {"use full geometry instead of the light-weight MFT part"}}, + {"use-irframes", o2::framework::VariantType::Bool, false, {"consume ITS IR frames"}}, + {"tracking-mode", VariantType::String, "sync", {"sync,async,cosmics,unset,off; async uses 3 passes by default (MFTCATrackerParam.nIterations=-1); set nIterations=1 to retain one pass"}}, + {"run-tracks2records", o2::framework::VariantType::Bool, false, {"run MFT alignment tracks to records workflow"}}, + {"cluster-rof-branch-only", o2::framework::VariantType::Bool, false, {"writer will store only ClustersROF branch"}}}; + o2::raw::HBFUtilsInitializer::addConfigOption(options); + o2::itsmft::DPLAlpideParamInitializer::addMFTConfigOption(options); + std::swap(workflowOptions, options); +} + +#include "Framework/runDataProcessing.h" + +WorkflowSpec defineDataProcessing(ConfigContext const& configContext) +{ + const auto options = o2::mft::ca::readWorkflowOptions(configContext, o2::mft::ca::WorkflowKind::Reconstruction); + auto workflow = o2::mft::ca_reco_workflow::getWorkflow(options); + o2::conf::ConfigurableParam::writeINI("o2mftcarecoflow_configuration.ini"); + + o2::raw::HBFUtilsInitializer hbfInitializer(configContext, workflow); + return workflow; +} diff --git a/Detectors/ITSMFT/MFT/workflow/src/mft-ca-tracker-workflow.cxx b/Detectors/ITSMFT/MFT/workflow/src/mft-ca-tracker-workflow.cxx new file mode 100644 index 0000000000000..57e32bddcb6aa --- /dev/null +++ b/Detectors/ITSMFT/MFT/workflow/src/mft-ca-tracker-workflow.cxx @@ -0,0 +1,63 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// @file mft-ca-tracker-workflow.cxx + +#include +#include + +#include "CommonUtils/ConfigurableParam.h" +#include "DataFormatsITSMFT/DPLAlpideParamInitializer.h" +#include "DetectorsRaw/HBFUtilsInitializer.h" +#include "Framework/CallbacksPolicy.h" +#include "Framework/CompletionPolicyHelpers.h" +#include "Framework/ConfigParamSpec.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "MFTWorkflow/CARecoWorkflow.h" +#include "ITSMFTCAWriter/MFTCATrackWriterSpec.h" + +using namespace o2::framework; + +void customize(std::vector& policies) +{ + o2::raw::HBFUtilsInitializer::addNewTimeSliceCallback(policies); +} + +void customize(std::vector& policies) +{ + policies.push_back(CompletionPolicyHelpers::consumeWhenAllOrdered(".*(?:MFT|mft).*[W,w]riter.*")); +} + +void customize(std::vector& workflowOptions) +{ + workflowOptions.push_back(ConfigParamSpec{"disable-mc", VariantType::Bool, false, {"disable MC labels"}}); + workflowOptions.push_back(ConfigParamSpec{"disable-root-output", VariantType::Bool, false, {"do not write output root files"}}); + workflowOptions.push_back(ConfigParamSpec{"nThreads", VariantType::Int, 1, {"Number of CA tracker threads; MFTCATrackerParam.nThreads takes precedence"}}); + workflowOptions.push_back(ConfigParamSpec{"use-full-geometry", VariantType::Bool, false, {"alias for --use-geom"}}); + workflowOptions.push_back(ConfigParamSpec{"use-geom", VariantType::Bool, false, {"use geometry from the global geometry manager"}}); + workflowOptions.push_back(ConfigParamSpec{"use-irframes", VariantType::Bool, false, {"consume ITS IR frames"}}); + workflowOptions.push_back(ConfigParamSpec{"tracking-mode", VariantType::String, "sync", {"sync,async,cosmics,unset,off; async uses 3 passes by default (MFTCATrackerParam.nIterations=-1); set nIterations=1 to retain one pass"}}); + workflowOptions.push_back(ConfigParamSpec{"configKeyValues", VariantType::String, "", {"Semicolon separated key=value strings (e.g. MFTCATrackerParam.nIterations=1;MFTAlpideParam.roFrameLengthInBC=594)"}}); + o2::itsmft::DPLAlpideParamInitializer::addMFTConfigOption(workflowOptions); + o2::raw::HBFUtilsInitializer::addConfigOption(workflowOptions); +} + +#include "Framework/runDataProcessing.h" + +WorkflowSpec defineDataProcessing(ConfigContext const& config) +{ + const auto options = o2::mft::ca::readWorkflowOptions(config, o2::mft::ca::WorkflowKind::TrackerOnly); + auto workflow = o2::mft::ca_reco_workflow::getWorkflow(options); + + o2::raw::HBFUtilsInitializer hbfInitializer(config, workflow); + return workflow; +} diff --git a/Detectors/ITSMFT/MFT/workflow/src/mft-cluster-writer-workflow.cxx b/Detectors/ITSMFT/MFT/workflow/src/mft-cluster-writer-workflow.cxx index 99aad4d8c57f4..0f326eaaad5f9 100644 --- a/Detectors/ITSMFT/MFT/workflow/src/mft-cluster-writer-workflow.cxx +++ b/Detectors/ITSMFT/MFT/workflow/src/mft-cluster-writer-workflow.cxx @@ -18,7 +18,6 @@ using namespace o2::framework; void customize(std::vector& policies) { - // ordered policies for the writers policies.push_back(CompletionPolicyHelpers::consumeWhenAllOrdered(".*(?:MFT|mft).*[W,w]riter.*")); } diff --git a/Detectors/ITSMFT/MFT/workflow/src/mft-reco-workflow.cxx b/Detectors/ITSMFT/MFT/workflow/src/mft-reco-workflow.cxx index 494d36cc609ec..c26833cfec3e6 100644 --- a/Detectors/ITSMFT/MFT/workflow/src/mft-reco-workflow.cxx +++ b/Detectors/ITSMFT/MFT/workflow/src/mft-reco-workflow.cxx @@ -25,14 +25,11 @@ void customize(std::vector& policies) void customize(std::vector& policies) { - // ordered policies for the writers policies.push_back(CompletionPolicyHelpers::consumeWhenAllOrdered(".*(?:MFT|mft).*[W,w]riter.*")); } -// we need to add workflow options before including Framework/runDataProcessing void customize(std::vector& workflowOptions) { - // option allowing to set parameters std::vector options{ {"digits-from-upstream", o2::framework::VariantType::Bool, false, {"digits will be provided from upstream, skip digits reader"}}, {"clusters-from-upstream", o2::framework::VariantType::Bool, false, {"clusters will be provided from upstream, skip clusterizer"}}, diff --git a/Detectors/ITSMFT/MFT/workflow/test/testCATrackerPublicationDecision.cxx b/Detectors/ITSMFT/MFT/workflow/test/testCATrackerPublicationDecision.cxx new file mode 100644 index 0000000000000..b9dd057ed3349 --- /dev/null +++ b/Detectors/ITSMFT/MFT/workflow/test/testCATrackerPublicationDecision.cxx @@ -0,0 +1,42 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// The MFT workflow exposes the shared publication policy. The session suite +// exercises loading, recovery, completion and cleanup for both detector layouts; +// these checks retain the public MFT publish/skip decision contract. + +#define BOOST_TEST_MODULE MFT CA tracker publication decision +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include "ITSMFTTracking/Tracker.h" +#include "MFTWorkflow/CATrackerSpec.h" + +using namespace o2::mft; + +BOOST_AUTO_TEST_CASE(InactiveTrackerAlwaysPublishesEmptyRegardlessOfResultValue) +{ + BOOST_CHECK(decideCATrackerPublicationAction(false, o2::itsmft::tracking::TrackingOutcome::Success) == CATrackerPublicationAction::PublishInactiveEmpty); + BOOST_CHECK(decideCATrackerPublicationAction(false, o2::itsmft::tracking::TrackingOutcome::RecoverableDropped) == CATrackerPublicationAction::PublishInactiveEmpty); + BOOST_CHECK(decideCATrackerPublicationAction(false, o2::itsmft::tracking::TrackingOutcome::Structural) == CATrackerPublicationAction::PublishInactiveEmpty); +} + +BOOST_AUTO_TEST_CASE(ActiveTrackerWithRecoverableDropSkipsPublication) +{ + BOOST_CHECK(decideCATrackerPublicationAction(true, o2::itsmft::tracking::TrackingOutcome::RecoverableDropped) == CATrackerPublicationAction::SkipDroppedTimeFrame); +} + +BOOST_AUTO_TEST_CASE(ActiveTrackerWithNonDroppedResultPublishes) +{ + BOOST_CHECK(decideCATrackerPublicationAction(true, o2::itsmft::tracking::TrackingOutcome::Success) == CATrackerPublicationAction::PublishActiveResult); + BOOST_CHECK(decideCATrackerPublicationAction(true, o2::itsmft::tracking::TrackingOutcome::Structural) == CATrackerPublicationAction::PublishActiveResult); +} diff --git a/Detectors/ITSMFT/MFT/workflow/test/testMFTCARecoWorkflow.cxx b/Detectors/ITSMFT/MFT/workflow/test/testMFTCARecoWorkflow.cxx new file mode 100644 index 0000000000000..8a6f206ee873b --- /dev/null +++ b/Detectors/ITSMFT/MFT/workflow/test/testMFTCARecoWorkflow.cxx @@ -0,0 +1,189 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE MFTCARecoWorkflow +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include +#include + +#include "MFTWorkflow/CARecoWorkflow.h" +#include "Framework/ConfigContext.h" +#include "Framework/ConfigParamStore.h" +#include "Framework/ParamRetriever.h" +#include "Framework/ServiceRegistry.h" +#include "CommonUtils/ConfigurableParam.h" + +namespace +{ +bool hasDevice(const o2::framework::WorkflowSpec& workflow, std::string_view name) +{ + return std::any_of(workflow.begin(), workflow.end(), [name](const auto& spec) { return spec.name == name; }); +} +} // namespace + +BOOST_AUTO_TEST_CASE(DefaultWorkflowIsMonolithic) +{ + o2::mft::ca::WorkflowOptionInput input; + input.useMC = false; + const auto workflow = o2::mft::ca_reco_workflow::getWorkflow(o2::mft::ca::resolveWorkflowOptions(input, {})); + + BOOST_CHECK(hasDevice(workflow, "mft-digit-reader")); + BOOST_CHECK(hasDevice(workflow, "mft-clusterer")); + BOOST_CHECK(hasDevice(workflow, "mft-cluster-writer")); + BOOST_CHECK(hasDevice(workflow, "mft-ca-tracker")); + BOOST_CHECK(hasDevice(workflow, "mft-track-writer")); + BOOST_CHECK(!hasDevice(workflow, "mft-tracker")); +} + +BOOST_AUTO_TEST_CASE(UpstreamClustersCanRunTrackerOnly) +{ + o2::mft::ca::WorkflowOptionInput input; + input.useMC = false; + input.upstreamClusters = true; + input.disableRootOutput = true; + const auto workflow = o2::mft::ca_reco_workflow::getWorkflow(o2::mft::ca::resolveWorkflowOptions(input, {})); + + BOOST_REQUIRE_EQUAL(workflow.size(), 1); + BOOST_CHECK_EQUAL(workflow.front().name, "mft-ca-tracker"); +} + +BOOST_AUTO_TEST_CASE(ParameterAliasesHaveIdenticalPrecedenceForBothEntryPoints) +{ + using namespace o2::mft::ca; + WorkflowOptionInput input; + input.mode = o2::itsmft::TrackingMode::Sync; + input.nThreads = 4; + const TrackerOptionAliases aliases{1, 1, true}; + const auto reco = resolveWorkflowOptions(input, aliases); + input.kind = WorkflowKind::TrackerOnly; + const auto standalone = resolveWorkflowOptions(input, aliases); + BOOST_CHECK(reco.tracker.mode == o2::itsmft::TrackingMode::Async); + BOOST_CHECK(reco.tracker.mode == standalone.tracker.mode); + BOOST_CHECK_EQUAL(reco.tracker.nThreads, 1); + BOOST_CHECK_EQUAL(reco.tracker.nThreads, standalone.tracker.nThreads); + BOOST_CHECK(reco.tracker.filterIRFrames == standalone.tracker.filterIRFrames); + BOOST_REQUIRE_EQUAL(reco.diagnostics.size(), 2u); + BOOST_CHECK(reco.diagnostics[0].find("MFTCATrackerParam.trackingMode") != std::string::npos); + BOOST_CHECK(reco.diagnostics[0].find("--tracking-mode") != std::string::npos); + BOOST_CHECK(reco.diagnostics[1].find("MFTCATrackerParam.nThreads") != std::string::npos); + BOOST_CHECK(reco.diagnostics[1].find("--nThreads") != std::string::npos); +} + +BOOST_AUTO_TEST_CASE(InputAndIRRoutingMatrixMatchesGraphSubscriptions) +{ + using namespace o2::mft::ca; + for (int stage = 0; stage < 3; ++stage) { + for (const bool subscribe : {false, true}) { + for (const bool filter : {false, true}) { + WorkflowOptionInput input; + input.useMC = false; + input.upstreamDigits = stage == 1; + input.upstreamClusters = stage == 2; + input.useIRFrames = subscribe; + const auto resolved = resolveWorkflowOptions(input, {-1, 1, filter}); + const auto workflow = o2::mft::ca_reco_workflow::getWorkflow(resolved); + BOOST_CHECK_EQUAL(hasDevice(workflow, "mft-digit-reader"), stage == 0); + BOOST_CHECK_EQUAL(hasDevice(workflow, "mft-clusterer"), stage != 2); + BOOST_CHECK_EQUAL(hasDevice(workflow, "its-irframe-reader"), stage == 0 && (subscribe || filter)); + const auto tracker = std::find_if(workflow.begin(), workflow.end(), [](const auto& spec) { return spec.name == "mft-ca-tracker"; }); + BOOST_REQUIRE(tracker != workflow.end()); + const bool consumesIR = std::any_of(tracker->inputs.begin(), tracker->inputs.end(), [](const auto& spec) { return spec.binding == "IRFramesITS"; }); + BOOST_CHECK_EQUAL(consumesIR, subscribe || filter); + BOOST_CHECK_EQUAL(resolved.tracker.filterIRFrames, filter); + } + } + } +} + +BOOST_AUTO_TEST_CASE(OutputFlagsRetainTheirWriterPolicy) +{ + using namespace o2::mft::ca; + for (const bool disable : {false, true}) { + for (const bool rofs : {false, true}) { + WorkflowOptionInput input; + input.useMC = false; + input.disableRootOutput = disable; + input.clusterROFsOnly = rofs; + const auto workflow = o2::mft::ca_reco_workflow::getWorkflow(resolveWorkflowOptions(input, {})); + BOOST_CHECK_EQUAL(hasDevice(workflow, "mft-cluster-writer"), !disable || rofs); + BOOST_CHECK_EQUAL(hasDevice(workflow, "mft-track-writer"), !disable); + } + } +} + +BOOST_AUTO_TEST_CASE(DisabledAndInactiveTrackingHaveDistinctGraphs) +{ + using namespace o2::mft::ca; + WorkflowOptionInput input; + input.useMC = false; + input.mode = o2::itsmft::TrackingMode::Off; + auto workflow = o2::mft::ca_reco_workflow::getWorkflow(resolveWorkflowOptions(input, {})); + BOOST_CHECK(hasDevice(workflow, "mft-ca-tracker")); + BOOST_CHECK(hasDevice(workflow, "mft-track-writer")); + input.runTracking = false; + input.useIRFrames = true; + workflow = o2::mft::ca_reco_workflow::getWorkflow(resolveWorkflowOptions(input, {})); + BOOST_CHECK(!hasDevice(workflow, "mft-ca-tracker")); + BOOST_CHECK(!hasDevice(workflow, "mft-track-writer")); + BOOST_CHECK(!hasDevice(workflow, "its-irframe-reader")); + input.assessment = true; + BOOST_CHECK_THROW(resolveWorkflowOptions(input, {}), std::invalid_argument); + input.assessment = false; + input.tracksToRecords = true; + BOOST_CHECK_THROW(resolveWorkflowOptions(input, {}), std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(InvalidAliasesAndConflictingInputStagesFailBeforeGraphConstruction) +{ + using namespace o2::mft::ca; + WorkflowOptionInput input; + BOOST_CHECK_THROW(resolveWorkflowOptions(input, {99, 1, false}), std::invalid_argument); + BOOST_CHECK_THROW(resolveWorkflowOptions(input, {-1, 0, false}), std::invalid_argument); + input.nThreads = 0; + BOOST_CHECK_THROW(resolveWorkflowOptions(input, {}), std::invalid_argument); + input.nThreads = 1; + input.upstreamDigits = input.upstreamClusters = true; + BOOST_CHECK_THROW(resolveWorkflowOptions(input, {}), std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(DriverBoundaryAppliesThreadAndModeAliasesBeforeDeviceConstruction) +{ + using namespace o2::framework; + using namespace o2::mft::ca; + std::vector specs{ + {"nThreads", VariantType::Int, 4, {"threads"}}, + {"tracking-mode", VariantType::String, "sync", {"mode"}}, + {"configKeyValues", VariantType::String, "MFTCATrackerParam.nThreads=1;MFTCATrackerParam.trackingMode=1", {"parameters"}}}; + for (const auto* key : {"disable-mc", "disable-root-output", "use-geom", "use-full-geometry", "use-irframes", + "digits-from-upstream", "clusters-from-upstream", "cluster-rof-branch-only", "disable-tracking", + "run-assessment", "disable-process-gen", "run-tracks2records", "enable-mft-staggering"}) { + specs.push_back({key, VariantType::Bool, false, {key}}); + } + auto store = std::make_unique(specs, std::vector>{}); + store->preload(); + store->activate(); + ConfigParamRegistry registry{std::move(store)}; + ServiceRegistry services; + ConfigContext context{registry, ServiceRegistryRef{services}, 0, nullptr}; + const auto reco = readWorkflowOptions(context, WorkflowKind::Reconstruction); + const auto standalone = readWorkflowOptions(context, WorkflowKind::TrackerOnly); + BOOST_CHECK_EQUAL(reco.tracker.nThreads, 1); + BOOST_CHECK_EQUAL(reco.tracker.nThreads, standalone.tracker.nThreads); + BOOST_CHECK(reco.tracker.mode == o2::itsmft::TrackingMode::Async); + BOOST_CHECK(reco.tracker.mode == standalone.tracker.mode); + registry.override("configKeyValues", std::string{"MFTCATrackerParam.trackingMode=-1"}); + BOOST_CHECK_EQUAL(readWorkflowOptions(context, WorkflowKind::TrackerOnly).tracker.nThreads, 4); + o2::conf::ConfigurableParam::setValue("MFTCATrackerParam", "nThreads", 1); +} diff --git a/Detectors/ITSMFT/MFT/workflow/test/testMFTCATrackerDPLContract.cxx b/Detectors/ITSMFT/MFT/workflow/test/testMFTCATrackerDPLContract.cxx new file mode 100644 index 0000000000000..cadf20350b4be --- /dev/null +++ b/Detectors/ITSMFT/MFT/workflow/test/testMFTCATrackerDPLContract.cxx @@ -0,0 +1,71 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE MFTCATrackerDPLContract +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include + +#include "Framework/DataProcessorSpec.h" +#include "Framework/DataSpecUtils.h" +#include "MFTWorkflow/CATrackerSpec.h" + +using namespace o2::framework; + +namespace +{ +bool hasInput(const std::vector& specs, const std::string& binding) +{ + return std::any_of(specs.begin(), specs.end(), [&binding](const InputSpec& s) { return s.binding == binding; }); +} + +bool hasOutput(const std::vector& specs, const std::string& desc) +{ + return std::any_of(specs.begin(), specs.end(), + [&desc](const OutputSpec& s) { return DataSpecUtils::describe(s).find(desc) != std::string::npos; }); +} +} // namespace + +BOOST_AUTO_TEST_CASE(NonMCContractKeepsTheExistingMFTProducts) +{ + const auto spec = o2::mft::getCATrackerSpec({.useMC = false}); + BOOST_CHECK(hasInput(spec.inputs, "compClusters")); + BOOST_CHECK(hasInput(spec.inputs, "patterns")); + BOOST_CHECK(hasInput(spec.inputs, "ROframes")); + BOOST_CHECK(hasInput(spec.inputs, "cldict")); + BOOST_CHECK(hasInput(spec.inputs, "mftTGeo")); + BOOST_CHECK(!hasInput(spec.inputs, "labels")); + BOOST_CHECK(!hasInput(spec.inputs, "IRFramesITS")); + + BOOST_CHECK(hasOutput(spec.outputs, "TRACKS")); + BOOST_CHECK(hasOutput(spec.outputs, "TRACKCLSID")); + BOOST_CHECK(hasOutput(spec.outputs, "MFTTrackROF")); + BOOST_CHECK(hasOutput(spec.outputs, "TRACKSEEDPAT")); + BOOST_CHECK(!hasOutput(spec.outputs, "TRACKSMCTR")); +} + +BOOST_AUTO_TEST_CASE(MCAndIRFrameContractRemainOptional) +{ + const auto spec = o2::mft::getCATrackerSpec({.useMC = true, .irFrames = o2::mft::ca::IRFrameSource::Upstream}); + BOOST_CHECK(hasInput(spec.inputs, "labels")); + BOOST_CHECK(hasInput(spec.inputs, "IRFramesITS")); + BOOST_CHECK(hasOutput(spec.outputs, "TRACKSMCTR")); +} + +BOOST_AUTO_TEST_CASE(DeviceNameIsStableForWriterAssessmentAndAlignmentConsumers) +{ + const auto spec = o2::mft::getCATrackerSpec({.useMC = false, .geometry = o2::mft::ca::GeometrySource::Full}); + BOOST_CHECK_EQUAL(spec.name, "mft-ca-tracker"); + BOOST_CHECK(!hasInput(spec.inputs, "mftTGeo")); +} diff --git a/Detectors/ITSMFT/common/CMakeLists.txt b/Detectors/ITSMFT/common/CMakeLists.txt index 92b934020f109..4285447793a76 100644 --- a/Detectors/ITSMFT/common/CMakeLists.txt +++ b/Detectors/ITSMFT/common/CMakeLists.txt @@ -14,4 +14,5 @@ add_subdirectory(simulation) add_subdirectory(reconstruction) add_subdirectory(tracking) add_subdirectory(workflow) +add_subdirectory(workflow-ca-writer) add_subdirectory(data) diff --git a/Detectors/ITSMFT/common/tracking/CMakeLists.txt b/Detectors/ITSMFT/common/tracking/CMakeLists.txt index af69c29a8583c..b3aef683000f7 100644 --- a/Detectors/ITSMFT/common/tracking/CMakeLists.txt +++ b/Detectors/ITSMFT/common/tracking/CMakeLists.txt @@ -9,20 +9,62 @@ # granted to it by virtue of its status as an Intergovernmental Organization # or submit itself to any jurisdiction. +o2_add_library(ITSMFTTrackingParams + SOURCES src/TrackingConfigParam.cxx + PUBLIC_LINK_LIBRARIES O2::CommonUtils + O2::DetectorsCommonDataFormats) + +o2_target_root_dictionary(ITSMFTTrackingParams + HEADERS include/ITSMFTTracking/TrackingConfigParam.h + LINKDEF src/ITSMFTTrackingLinkDef.h) + o2_add_library(ITSMFTTracking + TARGETVARNAME targetName SOURCES src/BoundedAllocator.cxx src/CapacityEstimator.cxx src/ITSTrackingConfigParam.cxx src/SlabBumpAllocator.cxx - PUBLIC_LINK_LIBRARIES O2::CommonConstants - O2::CommonDataFormat - O2::CommonUtils - O2::DataFormatsITS - O2::FrameworkLogger - O2::GPUCommon - O2::MathUtils + src/IOUtils.cxx + src/Propagator.cxx + src/Configuration.cxx + src/TimeFrame.cxx + src/TimeFrameScratch.cxx + src/TrackerTraits.cxx + src/CandidateFinding.cxx + src/TrackerTraversalPreparation.cxx + src/TripletFitting.cxx + src/PropagatorBarrelOperations.cxx + src/PropagatorForwardOperations.cxx + src/MaterialPhysics.cxx + src/FamilyMaterialOperations.cxx + src/IndexTableConfiguration.cxx + src/TraversalTopology.cxx + src/Tracker.cxx + PUBLIC_LINK_LIBRARIES + O2::ITSMFTTrackingParams + O2::GPUCommon + O2::CommonConstants + O2::CommonDataFormat + O2::DetectorsCommonDataFormats + O2::DataFormatsITSMFT + O2::DataFormatsITS + O2::ITSMFTBase + O2::CommonUtils + O2::DetectorsBase + O2::FrameworkLogger + O2::MathUtils + Microsoft.GSL::GSL + O2::SimulationDataFormat + O2::ReconstructionDataFormats + O2::DataFormatsCalibration + O2::DataFormatsMFT + TBB::tbb PRIVATE_LINK_LIBRARIES - TBB::tbb) + O2::Framework + O2::FrameworkLogger + O2::ITSBase + O2::MFTBase + O2::MFTTracking) o2_target_root_dictionary(ITSMFTTracking HEADERS include/ITSMFTTracking/ITSTrackingConfigParam.h diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/CapacityEstimator.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/CapacityEstimator.h index 43b4e277fc290..7c35f7909c6c5 100644 --- a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/CapacityEstimator.h +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/CapacityEstimator.h @@ -82,6 +82,12 @@ class CapacityEstimator static_cast(static_cast(slot)); } + template + static constexpr KeyType makeKey(SlabSite site, int iteration, int variant, Identifier identifier) noexcept + { + return makeKey(site, iteration, variant, static_cast(identifier.value())); + } + static constexpr Decoded decodeKey(KeyType key) noexcept { return { diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Cell.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Cell.h new file mode 100644 index 0000000000000..21c81c6b69e3f --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Cell.h @@ -0,0 +1,235 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file Cell.h +/// \brief CA cell/track seed types with hole-layer support (ITS PR #15390) +/// + +#ifndef ALICEO2_ITSMFT_TRACKING_INCLUDE_CACELL_H_ +#define ALICEO2_ITSMFT_TRACKING_INCLUDE_CACELL_H_ + +#include +#include + +#include "DataFormatsITS/TimeEstBC.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/IdTypes.h" +#include "ITSMFTTracking/SurfaceTrackState.h" +#include "ITSMFTTracking/LayerMask.h" +#include "ITSMFTTracking/TripletFitting.h" +#include "ITSMFTTracking/Constants.h" +#include "GPUCommonDef.h" + +namespace o2::itsmft::tracking +{ + +struct CellNeighbour { + int cellTopology{-1}; + int cell{-1}; + int nextCellTopology{-1}; + int nextCell{-1}; + int level{-1}; +}; + +struct CellClusterReference { + int surfacePosition{o2::its::constants::UnusedIndex}; + int clusterIndex{o2::its::constants::UnusedIndex}; +}; + +/// Common non-`SurfaceKind`-templated CA cell/geometric-triplet value. +/// A CellSeed deliberately has no kinematic state or fit chi2; those first +/// exist after TrackerTraits materializes a TrackSeed. +class CellSeed final +{ + public: + GPUhdDefault() CellSeed() = default; + GPUhd() CellSeed(int innerL, int cl0, int cl1, int cl2, int trkl0, int trkl1, const o2::its::TimeEstBC& time) + : CellSeed(LayerMask(innerL, innerL + 1, innerL + 2), cl0, cl1, cl2, trkl0, trkl1, time) + { + } + GPUhd() CellSeed(LayerMask hitLayerMask, int cl0, int cl1, int cl2, int trkl0, int trkl1, const o2::its::TimeEstBC& time) + : mLevel(1), mTime(time) + { + setHitLayerMask(hitLayerMask); + auto& clusters = mClusters; + clusters[0] = cl0; + clusters[1] = cl1; + clusters[2] = cl2; + setFirstTrackletIndex(trkl0); + setSecondTrackletIndex(trkl1); + } + GPUhdDefault() CellSeed(const CellSeed&) = default; + GPUhdDefault() ~CellSeed() = default; + GPUhdDefault() CellSeed(CellSeed&&) = default; + GPUhdDefault() CellSeed& operator=(const CellSeed&) = default; + GPUhdDefault() CellSeed& operator=(CellSeed&&) = default; + + GPUhd() LayerMask getHitLayerMask() const { return LayerMask{mHitLayerMask}; } + GPUhd() void setHitLayerMask(LayerMask mask) { mHitLayerMask = mask.value(); } + GPUhd() int getInnerLayer() const { return getHitLayerMask().first(); } + GPUhd() int getFirstTrackletIndex() const { return mTracklets[0]; } + GPUhd() void setFirstTrackletIndex(int trkl) { mTracklets[0] = trkl; } + GPUhd() int getSecondTrackletIndex() const { return mTracklets[1]; } + GPUhd() void setSecondTrackletIndex(int trkl) { mTracklets[1] = trkl; } + GPUhd() int getLevel() const { return mLevel; } + GPUhd() void setLevel(int level) { mLevel = level; } + GPUhd() int* getLevelPtr() { return &mLevel; } + GPUhd() auto& getTimeStamp() noexcept { return mTime; } + GPUhd() const auto& getTimeStamp() const noexcept { return mTime; } + GPUhd() int getFirstClusterIndex() const { return mClusters[0]; } + GPUhd() int getSecondClusterIndex() const { return mClusters[1]; } + GPUhd() int getThirdClusterIndex() const { return mClusters[2]; } + GPUhd() auto& getClusters() { return mClusters; } + GPUhd() const auto& getClusters() const { return mClusters; } + GPUhd() TripletFitFactor& tripletFactor() noexcept { return mTripletFactor; } + GPUhd() const TripletFitFactor& tripletFactor() const noexcept { return mTripletFactor; } + GPUhd() CellClusterReference getClusterReference(int requestedSlot) const noexcept + { + if (requestedSlot < 0 || requestedSlot >= o2::its::constants::ClustersPerCell) { + return {}; + } + const auto mask = getHitLayerMask(); + int slot = 0; + for (int position = 0; position < 32; ++position) { + if (mask.has(position) && slot++ == requestedSlot) { + return {position, mClusters[requestedSlot]}; + } + } + return {}; + } + GPUhd() int getCluster(int layer) const + { + const int slot = getHitLayerMask().slot(layer); + return (slot >= 0 && slot < o2::its::constants::ClustersPerCell) ? mClusters[slot] : o2::its::constants::UnusedIndex; + } + + private: + uint32_t mHitLayerMask{0}; + int mLevel{o2::its::constants::UnusedIndex}; + std::array mTracklets = o2::its::constants::helpers::initArray(); + std::array mClusters = + o2::its::constants::helpers::initArray(); + o2::its::TimeEstBC mTime; + TripletFitFactor mTripletFactor{}; +}; + +static_assert(std::is_trivially_copyable_v); + +/// GPU-portable, non-templated whole-track seed with one cluster slot per +/// adopted-plan position. Fixed MaxLayoutSurfaces capacity is required for +/// device use, where heap allocation is unavailable. +/// +/// This fixed-capacity value is the sole common-CA whole-track seed +/// representation. +class TrackSeed final +{ + public: + static constexpr int MaxSurfaces = static_cast(MaxLayoutSurfaces); + + GPUhdDefault() TrackSeed() = default; + GPUhdDefault() TrackSeed(const TrackSeed&) = default; + GPUhdDefault() ~TrackSeed() = default; + GPUhdDefault() TrackSeed(TrackSeed&&) = default; + GPUhdDefault() TrackSeed& operator=(const TrackSeed&) = default; + GPUhdDefault() TrackSeed& operator=(TrackSeed&&) = default; + + // CellSeed's hit mask is positional in the same fixed-capacity domain. + GPUhd() TrackSeed(const CellSeed& cs, const SurfaceTrackState& state, float chi2) + : mState(state), mChi2(chi2), mLevel(cs.getLevel()), mTracklets{cs.getFirstTrackletIndex(), cs.getSecondTrackletIndex()}, mTime(cs.getTimeStamp()) + { + const auto hitMask = cs.getHitLayerMask(); + int slot = 0; + for (int position = 0; position < MaxSurfaces; ++position) { + if (hitMask.has(position)) { + mClusters[position] = cs.getClusters()[slot++]; + mHitLayerMask.set(position); + } + } + } + + GPUhd() int getActiveLayerCount() const noexcept { return mHitLayerMask.count(); } + GPUhd() int getInnerLayer() const noexcept { return mHitLayerMask.first(); } + GPUhd() bool hasCluster(int position) const noexcept + { + return position >= 0 && position < MaxSurfaces && mHitLayerMask.has(position); + } + + // Bounds-checked: an out-of-[0, MaxSurfaces) position safely + // returns UnusedIndex instead of indexing out of bounds. + GPUhd() int getCluster(int position) const noexcept + { + return (position >= 0 && position < MaxSurfaces) ? mClusters[position] : o2::its::constants::UnusedIndex; + } + + GPUhd() LayerMask getHitLayerMask() const noexcept { return mHitLayerMask; } + GPUhd() void setHitLayerMask(LayerMask mask) noexcept { mHitLayerMask = mask; } + GPUhd() void setCluster(int position, int clusterIndex) noexcept + { + if (position >= 0 && position < MaxSurfaces) { + mClusters[position] = clusterIndex; + } + } + + GPUhd() int getFirstClusterIndex() const noexcept { return getClusterBySlot(0); } + GPUhd() int getSecondClusterIndex() const noexcept { return getClusterBySlot(1); } + GPUhd() int getThirdClusterIndex() const noexcept { return getClusterBySlot(2); } + + GPUhd() auto& getClusters() noexcept { return mClusters; } + GPUhd() const auto& getClusters() const noexcept { return mClusters; } + + GPUhd() int getFirstTrackletIndex() const noexcept { return mTracklets[0]; } + GPUhd() void setFirstTrackletIndex(int trkl) noexcept { mTracklets[0] = trkl; } + GPUhd() int getSecondTrackletIndex() const noexcept { return mTracklets[1]; } + GPUhd() void setSecondTrackletIndex(int trkl) noexcept { mTracklets[1] = trkl; } + + GPUhd() float getChi2() const noexcept { return mChi2; } + GPUhd() void setChi2(float chi2) noexcept { mChi2 = chi2; } + GPUhd() int getLevel() const noexcept { return mLevel; } + GPUhd() void setLevel(int level) noexcept { mLevel = level; } + + GPUhd() auto& getTimeStamp() noexcept { return mTime; } + GPUhd() const auto& getTimeStamp() const noexcept { return mTime; } + + GPUhd() SurfaceTrackState& state() noexcept { return mState; } + GPUhd() const SurfaceTrackState& state() const noexcept { return mState; } + // Raw signed q/pT in slot 4 for cylinder and disk states; never squared. + GPUhd() float getQOverPt() const noexcept { return mState.parameters[4]; } + + private: + GPUhd() int getClusterBySlot(int requestedSlot) const noexcept + { + int slot = 0; + for (int position = 0; position < MaxSurfaces; ++position) { + if (hasCluster(position)) { + if (slot++ == requestedSlot) { + return mClusters[position]; + } + } + } + return o2::its::constants::UnusedIndex; + } + + SurfaceTrackState mState{}; + LayerMask mHitLayerMask{}; + float mChi2{o2::its::constants::UnsetValue}; + int mLevel{o2::its::constants::UnusedIndex}; + std::array mTracklets = o2::its::constants::helpers::initArray(); + std::array mClusters = o2::its::constants::helpers::initArray(); + o2::its::TimeEstBC mTime; +}; + +// TrackSeed crosses the host/device boundary by value. TimeEstBC prevents a +// standard-layout assertion; trivially copyable is the required property. +static_assert(std::is_trivially_copyable_v); + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_INCLUDE_CACELL_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ClusterDecoding.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ClusterDecoding.h new file mode 100644 index 0000000000000..3271c2a7ae8d0 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ClusterDecoding.h @@ -0,0 +1,239 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_CLUSTERDECODING_H_ +#define ALICEO2_ITSMFT_TRACKING_CLUSTERDECODING_H_ + +#include +#include +#include + +#include + +#include "DataFormatsITSMFT/ClusterPattern.h" +#include "DataFormatsITSMFT/CompCluster.h" +#include "DataFormatsITSMFT/TopologyDictionary.h" +#include "DetectorsCommonDataFormats/DetID.h" +#include "ITSMFTTracking/GlobalMeasurement.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/SurfaceMeasurement.h" + +namespace o2::itsmft::tracking +{ + +struct ClusterShape { + uint32_t nPixels{0}; + uint16_t rowSpan{0}; + uint16_t columnSpan{0}; +}; + +// Typed failures at the host compact-cluster decoding boundary; the loader +// adds source, ROF, and external-cluster context when it maps them. +enum class ClusterDecodeError : uint8_t { + None, + MissingDictionary, + TruncatedExplicitPattern, + MalformedExplicitPattern, + InvalidPatternId, + InvalidSensor, + InvalidLayer, + GeometryUnavailable, + OtherMalformedInput +}; + +// Host-only cursor for the source-local explicit-pattern byte stream. It owns +// no storage and checks the complete encoded pattern before using the +// unbounded ClusterPattern iterator. +class BoundedPatternCursor +{ + public: + explicit BoundedPatternCursor(gsl::span bytes) noexcept : mBytes(bytes) {} + + size_t consumed() const noexcept { return mPosition; } + size_t remaining() const noexcept { return mBytes.size() - mPosition; } + bool empty() const noexcept { return remaining() == 0; } + + ClusterDecodeError acquirePattern(o2::itsmft::ClusterPattern& pattern) noexcept + { + const auto available = remaining(); + if (available < 2) { + return ClusterDecodeError::TruncatedExplicitPattern; + } + + const auto rowSpan = mBytes[mPosition]; + const auto columnSpan = mBytes[mPosition + 1]; + if (rowSpan == 0 || columnSpan == 0 || + rowSpan > o2::itsmft::ClusterPattern::MaxRowSpan || + columnSpan > o2::itsmft::ClusterPattern::MaxColSpan) { + return ClusterDecodeError::MalformedExplicitPattern; + } + + const size_t nBits = static_cast(rowSpan) * columnSpan; + const size_t payloadBytes = (nBits + 7) / 8; + const size_t encodedBytes = 2 + payloadBytes; + if (available < encodedBytes) { + return ClusterDecodeError::TruncatedExplicitPattern; + } + + auto iterator = mBytes.begin() + mPosition; + o2::itsmft::ClusterPattern decoded{iterator}; + if (decoded.getNPixels() == 0) { + return ClusterDecodeError::MalformedExplicitPattern; + } + pattern = decoded; + mPosition += encodedBytes; + return ClusterDecodeError::None; + } + + private: + gsl::span mBytes{}; + size_t mPosition{0}; +}; + +// Host-side facts produced by compact-cluster and geometry decoding. +struct DecodedCluster { + GlobalPoint3F global{}; + // ITS geometry supplies its cylindrical tracking frame here. Disk + // projection uses global coordinates directly. + SurfaceFramePoint cylinderFrame{}; + // ALPIDE local row/column covariance. The detector projection determines + // which normalized axes these values describe. + SurfaceCovariance2F rowColumnCovariance{}; + ClusterShape shape{}; + int layer{-1}; +}; + +// Fallible host-side geometry decode. Source identity, ROF ownership, surface +// mapping, and the tracking/fitting representations are loader concerns. +struct ClusterDecodeResult { + DecodedCluster decoded{}; + ClusterDecodeError error{ClusterDecodeError::None}; + + bool ok() const noexcept { return error == ClusterDecodeError::None; } +}; + +// Project decoded ITS facts into the accepted cylindrical convention. +inline GlobalMeasurement makeCylinderGlobalMeasurement(const DecodedCluster& decoded, uint32_t clusterId) +{ + const float sine = std::sin(decoded.cylinderFrame.frameAngle); + const float cosine = std::cos(decoded.cylinderFrame.frameAngle); + const auto& covariance = decoded.rowColumnCovariance; + return GlobalMeasurement{ + decoded.global.x, + decoded.global.y, + decoded.global.z, + {sine * sine * covariance.uu, + -sine * cosine * covariance.uu, + -sine * covariance.uv, + cosine * cosine * covariance.uu, + cosine * covariance.uv, + covariance.vv}, + std::hypot(decoded.global.x, decoded.global.y), + std::atan2(decoded.global.y, decoded.global.x), + clusterId}; +} + +// Project decoded MFT facts into z-normal, global-x/global-y disk coordinates. +// ALPIDE row is established as global x and column as global y by the MFT +// geometry decoder. No legacy TrackingFrameInfo participates in this mapping. +inline GlobalMeasurement makeDiskGlobalMeasurement(const DecodedCluster& decoded, uint32_t clusterId) +{ + return GlobalMeasurement{ + decoded.global.x, + decoded.global.y, + decoded.global.z, + {decoded.rowColumnCovariance.uu, decoded.rowColumnCovariance.uv, 0.f, + decoded.rowColumnCovariance.vv, 0.f, 0.f}, + std::hypot(decoded.global.x, decoded.global.y), + std::atan2(decoded.global.y, decoded.global.x), + clusterId}; +} + +inline SurfaceMeasurement makeCylinderSurfaceMeasurement(const DecodedCluster& decoded) +{ + return {decoded.cylinderFrame, decoded.rowColumnCovariance}; +} + +inline SurfaceMeasurement makeDiskSurfaceMeasurement(const DecodedCluster& decoded) +{ + return {{decoded.global.z, decoded.global.x, decoded.global.y, 0.f}, + decoded.rowColumnCovariance}; +} + +} // namespace o2::itsmft::tracking + +namespace o2::itsmft::ioutils +{ +void fillMatrixCache(o2::detectors::DetID::ID detId); + +template +o2::itsmft::tracking::ClusterDecodeResult decodeCluster( + const o2::itsmft::CompClusterExt& c, + o2::itsmft::tracking::BoundedPatternCursor& patterns, + const o2::itsmft::TopologyDictionary* dict, + bool applySysErrors = true); +} // namespace o2::itsmft::ioutils + +namespace o2::itsmft::tracking +{ + +// Host-only loading boundary. Decoder implementations may call detector +// geometry, but this interface and its result never enter device views or CA +// loops. +class ClusterDecoder +{ + public: + virtual ~ClusterDecoder() = default; + + // Called once per source before its first cluster; no-op by default. + virtual void prepare() const {} + + virtual ClusterDecodeResult decode( + const o2::itsmft::CompClusterExt& cluster, + BoundedPatternCursor& patterns, + const o2::itsmft::TopologyDictionary* dict, + uint32_t externalIndex, + bool applySysErrors) const = 0; +}; + +// Geometry-backed decoder. It performs the established single-pass geometry, +// pattern, covariance, and systematic-error operations, then maps the decoded +// detector layer to a global LayerId. +template +class GeometryClusterDecoder final : public ClusterDecoder +{ + public: + void prepare() const override { o2::itsmft::ioutils::fillMatrixCache(DetId); } + + ClusterDecodeResult decode( + const o2::itsmft::CompClusterExt& cluster, + BoundedPatternCursor& patterns, + const o2::itsmft::TopologyDictionary* dict, + uint32_t, + bool applySysErrors) const override + { + // Check before evaluating GeometryTGeo::Instance(): constructing the + // geometry singleton without loaded geometry is fatal. + if (dict == nullptr) { + ClusterDecodeResult result; + result.error = ClusterDecodeError::MissingDictionary; + return result; + } + return o2::itsmft::ioutils::decodeCluster(cluster, patterns, dict, applySysErrors); + } +}; + +using ITSGeometryClusterDecoder = GeometryClusterDecoder; +using MFTGeometryClusterDecoder = GeometryClusterDecoder; + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_CLUSTERDECODING_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Configuration.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Configuration.h new file mode 100644 index 0000000000000..9ed73ca9d4a31 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Configuration.h @@ -0,0 +1,341 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file Configuration.h +/// \brief Shared CA tracking configuration for ITS and MFT +/// + +#ifndef ALICEO2_ITSMFT_TRACKING_CONFIGURATION_H_ +#define ALICEO2_ITSMFT_TRACKING_CONFIGURATION_H_ + +#include + +#ifndef GPUCA_GPUCODE +#include +#include "ITSMFTTracking/SurfaceDescriptor.h" +#endif + +#ifndef GPUCA_GPUCODE_DEVICE +#include +#include +#include +#include +#endif + +#include "CommonUtils/EnumFlags.h" +#include "DetectorsBase/Propagator.h" +#include "DetectorsCommonDataFormats/DetID.h" +#include "GPUCommonMath.h" +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "ITSMFTTracking/LayerMask.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "ITSMFTTracking/ITSTrackingConfigParam.h" + +namespace o2::itsmft +{ + +inline constexpr int ClustersPerCell = 3; + +// Dedicated steps in an iteration. +enum class IterationStep : uint16_t { + FirstPass = 0, + RebuildClusterLUT = 1, + UseUPCMask = 2, + SelectUPCVertices = 3, + // Reserved for legacy vertexing/follower configurations; the common + // tracker does not implement these steps. + ResetVertices = 4, + SkipROFsAboveThreshold = 5, + MarkVerticesAsUPC = 6, + TrackFollowerTop = 7, + TrackFollowerBot = 8, +}; +using IterationSteps = o2::utils::EnumFlags; + +static_assert(sizeof(IterationStep) == sizeof(uint16_t)); +static_assert(sizeof(IterationSteps) == sizeof(uint16_t)); +static_assert(static_cast(IterationStep::FirstPass) == 0); +static_assert(static_cast(IterationStep::RebuildClusterLUT) == 1); +static_assert(static_cast(IterationStep::UseUPCMask) == 2); +static_assert(static_cast(IterationStep::SelectUPCVertices) == 3); +static_assert(static_cast(IterationStep::ResetVertices) == 4); +static_assert(static_cast(IterationStep::SkipROFsAboveThreshold) == 5); +static_assert(static_cast(IterationStep::MarkVerticesAsUPC) == 6); +static_assert(static_cast(IterationStep::TrackFollowerTop) == 7); +static_assert(static_cast(IterationStep::TrackFollowerBot) == 8); + +// Time-frame execution policy, invariant across tracking passes. Thread +// scheduling remains in the workflow's resolved TrackerOptions. +struct TrackingExecutionPolicy { + size_t MaxMemory = std::numeric_limits::max(); + bool DropTFUponFailure = false; +}; + +// Parameters that may change from one tracking pass to the next. +struct IterationParameters { + tracking::LayerMask getActiveLayerMask() const noexcept + { + return tracking::LayerMask::span(0, NLayers - 1) & ~InactiveLayerMask; + } + + tracking::LayerMask getSeedingLayerMask() const noexcept + { + const auto activeLayers = getActiveLayerMask(); + return SeedingLayers.empty() ? activeLayers : (SeedingLayers & activeLayers); + } + + tracking::LayerMask getNonSeedingLayerMask() const noexcept + { + return tracking::LayerMask::span(0, NLayers - 1) & ~getSeedingLayerMask(); + } + + int getNSeedingLayers() const noexcept + { + return getSeedingLayerMask().count(); + } + + int getMinSeedingClusters() const noexcept + { + const int minClusters = MinTrackLength - (MaxHoles > 0 ? MaxHoles : 0); + const int minClustersWithCells = minClusters > ClustersPerCell ? minClusters : ClustersPerCell; + const int nSeedingLayers = getNSeedingLayers(); + return minClustersWithCells < nSeedingLayers ? minClustersWithCells : nSeedingLayers; + } + + int CellMinimumLevel() const noexcept + { + return getMinSeedingClusters() - ClustersPerCell + 1; + } + int NeighboursPerRoad() const noexcept { return getNSeedingLayers() - 3; } + int CellsPerRoad() const noexcept { return getNSeedingLayers() - 2; } + int TrackletsPerRoad() const noexcept { return getNSeedingLayers() - 1; } + IterationSteps PassFlags{IterationStep::FirstPass, IterationStep::RebuildClusterLUT}; + int NLayers = tracking::ITSNLayers; + bool UseDiamond = false; + float Diamond[3] = {0.f, 0.f, 0.f}; + float DiamondCov[6] = {25.e-6f, 0.f, 0.f, 25.e-6f, 0.f, 36.f}; + + /// General parameters + int MinTrackLength = 7; + int MaxHoles = 0; + // Positional static-graph surfaces disabled for this tracking pass. + tracking::LayerMask InactiveLayerMask = 0; + // Positional layers used to build tracklets, cells, and roads. Empty means all active layers. + tracking::LayerMask SeedingLayers = 0; + float NSigmaCut = 5; + float PVres = 1.e-2f; + /// Trackleting cuts + float TrackletMinPt = 0.3f; + /// Fitter parameters + // Common tracking applies nominal descriptor material; NONE disables external providers only. + o2::base::PropagatorImpl::MatCorrType CorrType = o2::base::PropagatorImpl::MatCorrType::USEMatCorrNONE; + float MaxChi2ClusterAttachment = 60.f; + float MaxChi2NDF = 30.f; + int ReseedIfShorter = 6; // Reseed final fit tracks shorter than this. + std::vector MinPt = {0.f, 0.f, 0.f, 0.f}; + tracking::LayerMask StartLayerMask = 0x7F; + bool RepeatRefitOut = false; // Repeat outward refit using inward refit as a seed. + bool ShiftRefToCluster = true; // Shift the linearization reference to the cluster after an update. + bool PerPrimaryVertexProcessing = false; + bool DoUPCIteration = false; + bool CreateArtefactLabels{false}; + // Reserved compatibility storage; top/bottom followers are unused by the common tracker. + float TrackFollowerNSigmaCutZ = 1.f; + float TrackFollowerNSigmaCutPhi = 1.f; + int TrackFollowerMaxHypotheses = 1; + + // Track-sharing selections. + bool AllowSharingFirstCluster = false; + float SharedClusterMaxDeltaPhi = 0.05f; // Maximum delta phi at a shared cluster. + float SharedClusterMaxDeltaEta = 0.03f; // Maximum delta eta at a shared cluster. + bool SharedClusterOppositeSign = false; // Require opposite-sign tracklets. + int SharedMaxClusters = 0; // Maximum shared clusters, excluding the first. +}; + +// Detector inputs accepted by the configuration interface. Tracker consumes +// these once to construct DetectorConfiguration; they are not retained in the +// per-iteration configuration. +struct DetectorParameters { + std::vector AddTimeError = {0, 0, 0, 0, 0, 0, 0}; + std::vector LayerZ{tracking::kITSLookupZHalfExtent.begin(), tracking::kITSLookupZHalfExtent.end()}; + std::vector LayerColHalfExtent{}; // Legacy PhiZ helper extent (cm); production lookup uses descriptor chartRange. + float IndexRowMin{0.f}; // Reserved legacy bound; production phi lookup starts at 0. + float IndexRowMax{0.f}; // Reserved legacy bound; production phi lookup ends at TwoPI. + std::vector LayerRadii = {2.33959f, 3.14076f, 3.91924f, 19.6213f, 24.5597f, 34.388f, 39.3329f}; + std::vector LayerResolution = {5.e-4f, 5.e-4f, 5.e-4f, 5.e-4f, 5.e-4f, 5.e-4f, 5.e-4f}; + std::vector SystError2Row = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f}; // Systematic row error squared per layer (ALPIDE X). + std::vector SystError2Col = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f}; // Systematic column error squared per layer (ALPIDE Z). + int ColBins{256}; // ITS: ZBins + int RowBins{128}; // ITS: PhiBins +}; + +// Single-pass host defaults/input bundle. Production plans store detector +// inputs and execution policy once, separately from the iteration records. +struct TrackingParameters : IterationParameters, DetectorParameters, TrackingExecutionPolicy { + std::string asString() const; +}; + +struct TrackingPlan { + DetectorParameters detector; + TrackingExecutionPolicy execution; + std::vector iterations; +}; + +#ifndef GPUCA_GPUCODE + +inline bool isRecognizedMatCorrType(o2::base::PropagatorF::MatCorrType corrType) noexcept +{ + return corrType == o2::base::PropagatorF::MatCorrType::USEMatCorrNONE || + corrType == o2::base::PropagatorF::MatCorrType::USEMatCorrTGeo || + corrType == o2::base::PropagatorF::MatCorrType::USEMatCorrLUT; +} + +struct AttachHitConfigView { + tracking::SurfaceCatalogView catalog; + o2::base::PropagatorF::MatCorrType corrType{o2::base::PropagatorF::MatCorrType::USEMatCorrNONE}; + + bool isValid(size_t expectedLayers) const noexcept + { + if (catalog.nSurfaces < expectedLayers || !catalog.surfaces || !isRecognizedMatCorrType(corrType)) { + return false; + } + for (size_t layer = 0; layer < expectedLayers; ++layer) { + const auto& material = catalog.surfaces[layer].material; + if (!o2::gpu::GPUCommonMath::Finite(material.xOverX0) || material.xOverX0 < 0.f || + !o2::gpu::GPUCommonMath::Finite(material.arealDensityGPerCm2) || material.arealDensityGPerCm2 < 0.f) { + return false; + } + } + return true; + } +}; + +inline AttachHitConfigView bindAttachHitConfig(tracking::SurfaceCatalogView catalog, + const IterationParameters& params) noexcept +{ + return {catalog, params.CorrType}; +} + +namespace tracking +{ + +enum class MaterialCorrectionModeSupport : uint8_t { + Supported, + Unsupported, + InvalidMode, + InvalidSurfaceKind +}; + +inline MaterialCorrectionModeSupport materialCorrectionModeSupport( + SurfaceKind kind, o2::base::PropagatorF::MatCorrType corrType) noexcept +{ + if (!isRecognizedMatCorrType(corrType)) { + return MaterialCorrectionModeSupport::InvalidMode; + } + if (kind != SurfaceKind::Cylinder && kind != SurfaceKind::Disk) { + return MaterialCorrectionModeSupport::InvalidSurfaceKind; + } + if (corrType != o2::base::PropagatorF::MatCorrType::USEMatCorrNONE) { + return MaterialCorrectionModeSupport::Unsupported; + } + return MaterialCorrectionModeSupport::Supported; +} + +} // namespace tracking + +#endif + +/// Reset tracking parameters to detector geometry defaults. +void resetDetectorDefaults(TrackingParameters& params, o2::detectors::DetID::ID detId); + +namespace TrackingMode +{ +enum Type : int8_t { + Unset = -1, + Sync = 0, + Async = 1, + Cosmics = 2, + Off = 3, +}; + +Type fromString(std::string_view str); +std::string toString(Type mode); +// Field-independent validation of common-CA public aliases. +void validateCommonCAOptions(detectors::DetID::ID detId); +TrackingPlan getTrackingPlan(o2::detectors::DetID::ID detId, Type mode); + +} // namespace TrackingMode + +struct VertexingParameters { + std::string asString() const; + + IterationSteps PassFlags{IterationStep::FirstPass, IterationStep::ResetVertices}; + std::vector LayerZ = {16.333f + 1, 16.333f + 1, 16.333f + 1, 42.140f + 1, 42.140f + 1, 73.745f + 1, 73.745f + 1}; + std::vector LayerRadii = {2.33959f, 3.14076f, 3.91924f, 19.6213f, 24.5597f, 34.388f, 39.3329f}; + int vertPerRofThreshold = 0; // Vertices per ROF that trigger a second round. + int ColBins = 1; + int RowBins = 128; + float zCut = -1.f; + float phiCut = -1.f; + float pairCut = -1.f; + float clusterCut = -1.f; + float coarseZWindow = -1.f; + float seedDedupZCut = -1.f; + float refitDedupZCut = -1.f; + float duplicateZCut = -1.f; + float finalSelectionZCut = -1.f; + float duplicateDistance2Cut = -1.f; + float tanLambdaCut = -1.f; + float NSigmaCut = -1; + float maxZPositionAllowed = -1.f; + int clusterContributorsCut = -1; + int suppressLowMultDebris = -1; + int seedMemberRadiusTime = -1; + int seedMemberRadiusZ = -1; + int maxTrackletsPerCluster = -1; + int phiSpan = -1; + int zSpan = -1; + bool SaveTimeBenchmarks = false; + + bool useTruthSeeding = false; // Replace found vertices with MC events. + + int nThreads = 1; + bool PrintMemory = false; // Print allocator usage in the epilog report. + size_t MaxMemory = std::numeric_limits::max(); + bool DropTFUponFailure = false; +}; + +} // namespace o2::itsmft + +namespace o2::itsmft::tracking +{ + +/// MFT uses o2::itsmft::TrackerParamConfig; ITS keeps its legacy parameter type. +template +struct TrackerParamRef; + +template <> +struct TrackerParamRef { + using Type = o2::itsmft::TrackerParamConfig; + static const Type& get() { return Type::Instance(); } + static constexpr int nLayers() { return Type::getNLayers(); } +}; + +template <> +struct TrackerParamRef { + using Type = o2::its::TrackerParamConfig; + static const Type& get() { return Type::Instance(); } + static constexpr int nLayers() { return ITSNLayers; } +}; + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_CONFIGURATION_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/DetectorLayout.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/DetectorLayout.h new file mode 100644 index 0000000000000..29387b9ff1e55 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/DetectorLayout.h @@ -0,0 +1,115 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_DETECTORLAYOUT_H_ +#define ALICEO2_ITSMFT_TRACKING_DETECTORLAYOUT_H_ + +#include +#include +#include + +#include + +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/LayerMask.h" + +namespace o2::itsmft::tracking +{ + +enum class DetectorLayoutError : uint8_t { + None, + EmptyCatalog, + TooManySurfaces, + InvalidComponentBoundary, + HoleLayersOutsideLayout +}; + +struct DetectorLayoutDefinition { + // First position of each component. Position zero is always required. + std::vector componentOffsets{0}; + LayerMask holeLayers{}; +}; + +inline DetectorLayoutDefinition makeDetectorLayout(LayerMask holeLayers = {}) +{ + DetectorLayoutDefinition definition; + definition.holeLayers = holeLayers; + return definition; +} + +// Immutable detector layout. LayerId is exactly the dense position of a layer +// descriptor in this container. Iteration-expanded topology belongs to the +// Tracker's IterationConfiguration; this type intentionally owns no edges, +// paths, adjacency, schedules, or mutable pass state. +class DetectorLayout +{ + public: + DetectorLayout() = default; + DetectorLayout(gsl::span layers, DetectorLayoutDefinition definition = {}) + : mLayers{layers.begin(), layers.end()}, mComponentOffsets{std::move(definition.componentOffsets)}, mHoleLayers{definition.holeLayers} + { + validate(); + } + + bool valid() const noexcept { return mError == DetectorLayoutError::None; } + DetectorLayoutError getError() const noexcept { return mError; } + bool empty() const noexcept { return mLayers.empty(); } + std::size_t size() const noexcept { return mLayers.size(); } + gsl::span getLayers() const noexcept { return mLayers; } + const SurfaceDescriptor& operator[](LayerId id) const { return mLayers.at(id.value()); } + gsl::span getComponentOffsets() const noexcept { return mComponentOffsets; } + LayerMask getHoleLayers() const noexcept { return mHoleLayers; } + SurfaceCatalogView getSurfaceCatalog() const noexcept { return {mLayers.data(), static_cast(mLayers.size())}; } + + bool sameComponent(uint16_t first, uint16_t second) const noexcept + { + if (first >= mLayers.size() || second >= mLayers.size()) { + return false; + } + const auto component = [this](uint16_t position) { + return std::upper_bound(mComponentOffsets.begin(), mComponentOffsets.end(), position) - mComponentOffsets.begin(); + }; + return component(first) == component(second); + } + + private: + void validate() noexcept + { + if (mLayers.empty()) { + mError = DetectorLayoutError::EmptyCatalog; + return; + } + if (mLayers.size() > MaxLayoutSurfaces) { + mError = DetectorLayoutError::TooManySurfaces; + return; + } + if (mComponentOffsets.empty() || mComponentOffsets.front() != 0 || mComponentOffsets.back() >= mLayers.size() || + !std::is_sorted(mComponentOffsets.begin(), mComponentOffsets.end()) || + std::adjacent_find(mComponentOffsets.begin(), mComponentOffsets.end()) != mComponentOffsets.end()) { + mError = DetectorLayoutError::InvalidComponentBoundary; + return; + } + if (!mHoleLayers.isSubsetOf(LayerMask::span(0, static_cast(mLayers.size()) - 1))) { + mError = DetectorLayoutError::HoleLayersOutsideLayout; + return; + } + mError = DetectorLayoutError::None; + } + + std::vector mLayers; + std::vector mComponentOffsets; + LayerMask mHoleLayers{}; + DetectorLayoutError mError{DetectorLayoutError::EmptyCatalog}; +}; + +} // namespace o2::itsmft::tracking + +#endif diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/GenericTrack.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/GenericTrack.h new file mode 100644 index 0000000000000..dc2e16899227f --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/GenericTrack.h @@ -0,0 +1,99 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_GENERICTRACK_H_ +#define ALICEO2_ITSMFT_TRACKING_GENERICTRACK_H_ + +#include +#include +#include +#include + +#include "GPUCommonDef.h" +#ifndef GPUCA_GPUCODE +#include "ITSMFTTracking/Cell.h" +#endif +#include "ITSMFTTracking/IdTypes.h" +#include "ITSMFTTracking/SurfaceTrackState.h" +#include "ITSMFTTracking/LayerMask.h" +#include "ITSMFTTracking/SurfaceTiming.h" + +namespace o2::itsmft::tracking +{ + +// Stable TimeFrame identity. clusterId is the pre-sort position in the +// per-surface measurement arrays; publication adapters translate it to any +// external index space. +struct TrackClusterReference { + LayerId layer{}; + uint16_t reserved{0}; + uint32_t clusterId{std::numeric_limits::max()}; + + GPUhdi() bool isValid() const noexcept { return layer.isValid() && clusterId != std::numeric_limits::max(); } +}; + +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(TrackClusterReference) == 8); +static_assert(alignof(TrackClusterReference) == 4); +static_assert(offsetof(TrackClusterReference, layer) == 0); +static_assert(offsetof(TrackClusterReference, clusterId) == 4); + +// Frame-owned result; [firstClusterRef, clusterRefEnd) is inner-to-outer and +// valid only with the same normalized event. +struct GenericTrack { + SurfaceTrackState innerState{}; + SurfaceTrackState outerState{}; + float chi2{0.f}; + GenericTrackTimestamp timestamp{}; + LayerMask hitLayers{}; + uint32_t firstClusterRef{0}; + uint32_t clusterRefEnd{0}; +}; + +#ifndef GPUCA_GPUCODE + +// Successful refit result; typed output remains adapter-owned. +struct TrackingCandidate { + TrackSeed seed; + GenericTrack track{}; + float phi{0.f}; + float eta{0.f}; + double charge{0.}; + + int getNumberOfClusters() const noexcept { return seed.getActiveLayerCount(); } + int getClusterIndex(int position) const noexcept { return seed.getCluster(position); } + int getFirstClusterLayer() const noexcept { return seed.getHitLayerMask().first(); } +}; + +#endif + +// Device-facing layout requirements. +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(GenericTrack) == 224); +static_assert(alignof(GenericTrack) == alignof(GenericTrackTimestamp)); + +// The caller supplies the current frame-owned reference-array size; do not +// infer validity from the track itself. +GPUhdi() constexpr bool isValidTrackRange(const GenericTrack& track, uint32_t trackClusterIndicesSize) noexcept +{ + return track.firstClusterRef <= track.clusterRefEnd && track.clusterRefEnd <= trackClusterIndicesSize; +} + +GPUhdi() constexpr uint32_t trackClusterRefCount(const GenericTrack& track) noexcept +{ + return track.clusterRefEnd - track.firstClusterRef; +} + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_GENERICTRACK_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/GenericTrackOutputAdapter.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/GenericTrackOutputAdapter.h new file mode 100644 index 0000000000000..37c25b3c4dd05 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/GenericTrackOutputAdapter.h @@ -0,0 +1,463 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_GENERICTRACKOUTPUTADAPTER_H_ +#define ALICEO2_ITSMFT_TRACKING_GENERICTRACKOUTPUTADAPTER_H_ + +// Pure host-side boundary for DPL adapters. It consumes immutable owner data +// plus workflow-owned ROF context and returns fully staged vectors. + +#include +#include +#include +#include +#include +#include + +#include + +#include "DataFormatsITS/TrackITS.h" +#include "DataFormatsITSMFT/ROFRecord.h" +#include "DataFormatsMFT/TrackMFT.h" +#include "DetectorsCommonDataFormats/DetID.h" +#include "ITSMFTTracking/detail/ITSSharedClusterCompatibility.h" +#include "ITSMFTTracking/detail/SurfaceTrackStateLegacyAdapters.h" +#include "ITSMFTTracking/SurfaceTiming.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/ROFLookupTables.h" + +namespace o2::itsmft::tracking +{ + +#ifndef GPUCA_GPUCODE + +// Host-only immutable output view around the established clock-layer +// implementation. Symmetry, clamping, and ROF lookup stay in LayerTiming. +class ClockTimingPublicationView +{ + public: + explicit ClockTimingPublicationView(const o2::its::LayerTiming& clock) : mClock{clock} {} + + std::optional makeTimeEstBC(const GenericTrackTimestamp& timestamp) const noexcept + { + if (!timestamp.isValid() || timestamp.begin < 0 || timestamp.end < 0 || + timestamp.begin > std::numeric_limits::max() || timestamp.end > std::numeric_limits::max()) { + return std::nullopt; + } + const auto width = static_cast(timestamp.end) - static_cast(timestamp.begin); + if (width > std::numeric_limits::max()) { + return std::nullopt; + } + return o2::its::TimeEstBC{static_cast(timestamp.begin), static_cast(width)}; + } + + std::optional makeOutputTimestamp(const GenericTrackTimestamp& timestamp) const noexcept + { + const auto asymmetric = makeTimeEstBC(timestamp); + if (!asymmetric) { + return std::nullopt; + } + auto symmetric = asymmetric->makeSymmetrical(); + const float clamp = mClock.mROFLength * 0.5f; + if (symmetric.getTimeStampError() > clamp) { + symmetric.setTimeStampError(clamp); + } + return symmetric; + } + + int getROF(const o2::its::TimeStamp& timestamp) const noexcept { return mClock.getROF(timestamp); } + uint32_t getROFCount() const noexcept { return mClock.mNROFsTF; } + const o2::its::LayerTiming& getLegacyClockLayer() const noexcept { return mClock; } + + private: + o2::its::LayerTiming mClock; +}; + +#endif // !GPUCA_GPUCODE + +enum class GenericTrackOutputAdapterError : uint8_t { + None, + TooManyGenericTracks, + InvalidTrackRange, + UnresolvedReference, + MixedDetector, + MixedSources, + InvalidExternalClusterIndex, + InvalidLayerLayout, + InvalidTimestamp, + InvalidROF, + InvalidState, + MissingCompatibility, + MissingMCLabels +}; + +struct GenericTrackOutputAdapterSelection { + std::vector globalIndices; +}; + +struct GenericTrackOutputOrderEntry { + uint32_t globalIndex{}; + o2::its::TimeStamp timestamp{}; +}; + +// This context is intentionally source-local. ROFRecord payload is copied +// only into the returned publication product, never into TimeFrame. +struct GenericTrackOutputTimingContext { + gsl::span inputROFs; + ClockTimingPublicationView clock; +}; + +struct GenericTrackPublicationContext { + o2::detectors::DetID::ID detector{}; + ClusterSourceId source{}; // Publication provenance; selection uses the bound layer mapping. + gsl::span inputROFs; + ClockTimingPublicationView clock; + gsl::span layerMapping; + const std::vector>* externalIndicesBySurface{nullptr}; + const std::vector>* clusterSizesBySurface{nullptr}; +}; + +struct ITSGenericTrackOutput { + std::vector tracks; + std::vector clusterIndices; + std::vector trackROFs; + std::vector labels; +}; + +struct MFTGenericTrackOutput { + std::vector tracks; + std::vector clusterIndices; + std::vector trackROFs; + std::vector seedPatterns; + std::vector labels; +}; + +inline std::optional selectGenericTracksForSurfaces( + const TimeFrame& frame, + gsl::span sourceSurfaces, + GenericTrackOutputAdapterError& error) +{ + error = GenericTrackOutputAdapterError::None; + const auto& tracks = frame.getGenericTracks(); + if (tracks.size() > std::numeric_limits::max()) { + error = GenericTrackOutputAdapterError::TooManyGenericTracks; + return std::nullopt; + } + GenericTrackOutputAdapterSelection selection; + const auto& references = frame.getTrackClusterIndices(); + selection.globalIndices.reserve(tracks.size()); + for (uint32_t globalIndex = 0; globalIndex < tracks.size(); ++globalIndex) { + const auto& track = tracks[globalIndex]; + if (!isValidTrackRange(track, static_cast(references.size()))) { + error = GenericTrackOutputAdapterError::InvalidTrackRange; + return std::nullopt; + } + bool requested = false; + bool foreign = false; + for (uint32_t i = track.firstClusterRef; i < track.clusterRefEnd; ++i) { + const auto& reference = references[i]; + if (!reference.isValid()) { + error = GenericTrackOutputAdapterError::UnresolvedReference; + return std::nullopt; + } + const bool match = std::find(sourceSurfaces.begin(), sourceSurfaces.end(), reference.layer) != sourceSurfaces.end(); + requested |= match; + foreign |= !match; + } + if (requested && foreign) { + error = GenericTrackOutputAdapterError::MixedDetector; + return std::nullopt; + } + if (requested) { + selection.globalIndices.push_back(globalIndex); + } + } + return selection; +} + +inline std::optional makeOutputTimestamp(const GenericTrackTimestamp& timestamp, + const ClockTimingPublicationView& clock, + GenericTrackOutputAdapterError& error) +{ + const auto result = clock.makeOutputTimestamp(timestamp); + if (!result) { + error = GenericTrackOutputAdapterError::InvalidTimestamp; + return std::nullopt; + } + return result; +} + +inline std::optional> makeLegacyOutputOrder( + const TimeFrame& frame, const GenericTrackOutputAdapterSelection& selection, + const ClockTimingPublicationView& clock, GenericTrackOutputAdapterError& error) +{ + std::vector ordered; + ordered.reserve(selection.globalIndices.size()); + for (const auto index : selection.globalIndices) { + const auto timestamp = makeOutputTimestamp(frame.getGenericTracks()[index].timestamp, clock, error); + if (!timestamp) { + return std::nullopt; + } + ordered.push_back({index, *timestamp}); + } + // Match Tracker::sortTracks(): lower timestamp edge, then chi2. + std::sort(ordered.begin(), ordered.end(), [&frame](const auto& left, const auto& right) { + const auto& leftTrack = frame.getGenericTracks()[left.globalIndex]; + const auto& rightTrack = frame.getGenericTracks()[right.globalIndex]; + const auto leftLower = left.timestamp.getTimeStamp() - left.timestamp.getTimeStampError(); + const auto rightLower = right.timestamp.getTimeStamp() - right.timestamp.getTimeStampError(); + if (leftLower != rightLower) { + return leftLower < rightLower; + } + return leftTrack.chi2 < rightTrack.chi2; + }); + return ordered; +} + +inline void finalizeROFs(std::vector& rofs, const std::vector& times, + const GenericTrackOutputTimingContext& context) +{ + for (auto& rof : rofs) { + rof.setFirstEntry(0); + rof.setNEntries(0); + } + for (const auto& time : times) { + const int rof = context.clock.getROF(time); + if (rof < 0 || static_cast(rof) >= rofs.size()) { + // Keep the track; omit only its TrackROF entry. + continue; + } + rofs[rof].setNEntries(rofs[rof].getNEntries() + 1); + } + std::vector counts(rofs.size()); + for (size_t i = 0; i < rofs.size(); ++i) { + counts[i] = rofs[i].getNEntries(); + } + std::exclusive_scan(counts.begin(), counts.end(), counts.begin(), 0); + for (size_t i = 0; i < rofs.size(); ++i) { + rofs[i].setFirstEntry(counts[i]); + } +} + +inline void setOutputClusterRange(o2::its::TrackITS& track, int first, int count) +{ + track.setClusterRefs(first, count); +} + +inline void setOutputClusterRange(o2::mft::TrackMFT& track, int first, int count) +{ + track.setExternalClusterIndexOffset(first); + track.setNumberOfPoints(count); +} + +template +inline bool collectReferences(const TimeFrame& frame, const GenericTrack& common, gsl::span layerMapping, + uint32_t maxLayers, std::vector& outputIndices, OutputTrack& output, + uint32_t& pattern, GenericTrackOutputAdapterError& error, + const std::vector>* externalIndicesBySurface, + const std::vector>* clusterSizesBySurface) +{ + const auto& references = frame.getTrackClusterIndices(); + std::vector byLayer(maxLayers, nullptr); + for (uint32_t ref = common.firstClusterRef; ref < common.clusterRefEnd; ++ref) { + const auto& key = references[ref]; + if (!key.isValid()) { + error = GenericTrackOutputAdapterError::UnresolvedReference; + return false; + } + const auto where = std::find(layerMapping.begin(), layerMapping.end(), key.layer); + if (where == layerMapping.end() || static_cast(where - layerMapping.begin()) >= maxLayers) { + error = GenericTrackOutputAdapterError::InvalidLayerLayout; + return false; + } + const auto layer = static_cast(where - layerMapping.begin()); + if (byLayer[layer] != nullptr) { + error = GenericTrackOutputAdapterError::InvalidLayerLayout; + return false; + } + byLayer[layer] = &key; + } + const int first = static_cast(outputIndices.size()); + uint32_t count = 0; + for (uint32_t layer = maxLayers; layer-- > 0;) { + const auto* reference = byLayer[layer]; + if (reference == nullptr) { + continue; + } + uint32_t externalIndex = reference->clusterId; + if (externalIndicesBySurface != nullptr) { + if (reference->layer.value() >= externalIndicesBySurface->size() || + reference->clusterId >= (*externalIndicesBySurface)[reference->layer.value()].size()) { + error = GenericTrackOutputAdapterError::InvalidExternalClusterIndex; + return false; + } + externalIndex = (*externalIndicesBySurface)[reference->layer.value()][reference->clusterId]; + } + if (externalIndex > static_cast(std::numeric_limits::max())) { + error = GenericTrackOutputAdapterError::InvalidExternalClusterIndex; + return false; + } + if (clusterSizesBySurface == nullptr || + reference->layer.value() >= clusterSizesBySurface->size() || + reference->clusterId >= (*clusterSizesBySurface)[reference->layer.value()].size()) { + error = GenericTrackOutputAdapterError::UnresolvedReference; + return false; + } + outputIndices.push_back(static_cast(externalIndex)); + output.setClusterSize(layer, (*clusterSizesBySurface)[reference->layer.value()][reference->clusterId]); + pattern |= 1u << layer; + ++count; + } + setOutputClusterRange(output, first, static_cast(count)); + return true; +} + +inline std::optional stageITSGenericTrackOutput(const TimeFrame& frame, + gsl::span surfaces, + const GenericTrackOutputTimingContext& context, + const ITSSharedClusterCompatibility& compatibility, + bool withMC, GenericTrackOutputAdapterError& error, + const std::vector>* externalIndicesBySurface = nullptr, + const std::vector>* clusterSizesBySurface = nullptr) +{ + const auto selection = selectGenericTracksForSurfaces(frame, surfaces, error); + if (!selection || (!selection->globalIndices.empty() && !compatibility.isSealed())) { + if (error == GenericTrackOutputAdapterError::None) + error = GenericTrackOutputAdapterError::MissingCompatibility; + return std::nullopt; + } + if (withMC && frame.getTrackLabels().size() != frame.getGenericTracks().size()) { + error = GenericTrackOutputAdapterError::MissingMCLabels; + return std::nullopt; + } + const auto ordered = makeLegacyOutputOrder(frame, *selection, context.clock, error); + if (!ordered) { + return std::nullopt; + } + ITSGenericTrackOutput staged; + staged.trackROFs.assign(context.inputROFs.begin(), context.inputROFs.end()); + staged.tracks.reserve(ordered->size()); + staged.labels.reserve(withMC ? ordered->size() : 0); + std::vector times; + times.reserve(ordered->size()); + for (const auto& orderedTrack : *ordered) { + const auto index = orderedTrack.globalIndex; + o2::track::TrackParCovF inner, outer; + const auto& common = frame.getGenericTracks()[index]; + if (!legacy::exportBarrelTrackParCov(common.innerState, inner) || !legacy::exportBarrelTrackParCov(common.outerState, outer)) { + error = GenericTrackOutputAdapterError::InvalidState; + return std::nullopt; + } + const auto it = std::lower_bound(compatibility.entries().begin(), compatibility.entries().end(), index, + [](const auto& entry, uint32_t value) { return entry.genericTrackIndex < value; }); + if (it == compatibility.entries().end() || it->genericTrackIndex != index) { + error = GenericTrackOutputAdapterError::MissingCompatibility; + return std::nullopt; + } + o2::its::TrackITS output{inner, common.chi2, outer}; + uint32_t pattern = 0; + if (!collectReferences(frame, common, surfaces, 7, staged.clusterIndices, output, pattern, error, + externalIndicesBySurface, clusterSizesBySurface)) + return std::nullopt; + output.setPattern(pattern); + output.setSharedClusters(it->hasSharedClusters); + output.getTimeStamp() = orderedTrack.timestamp; + staged.tracks.push_back(std::move(output)); + times.push_back(orderedTrack.timestamp); + if (withMC) + staged.labels.push_back(frame.getTrackLabels()[index]); + } + finalizeROFs(staged.trackROFs, times, context); + return staged; +} + +inline std::optional stageMFTGenericTrackOutput(const TimeFrame& frame, + gsl::span surfaces, + const GenericTrackOutputTimingContext& context, + bool withMC, GenericTrackOutputAdapterError& error, + const std::vector>* externalIndicesBySurface = nullptr, + const std::vector>* clusterSizesBySurface = nullptr) +{ + const auto selection = selectGenericTracksForSurfaces(frame, surfaces, error); + if (!selection) + return std::nullopt; + if (withMC && frame.getTrackLabels().size() != frame.getGenericTracks().size()) { + error = GenericTrackOutputAdapterError::MissingMCLabels; + return std::nullopt; + } + const auto ordered = makeLegacyOutputOrder(frame, *selection, context.clock, error); + if (!ordered) { + return std::nullopt; + } + MFTGenericTrackOutput staged; + staged.trackROFs.assign(context.inputROFs.begin(), context.inputROFs.end()); + staged.tracks.reserve(ordered->size()); + staged.seedPatterns.reserve(ordered->size()); + std::vector times; + times.reserve(ordered->size()); + for (const auto& orderedTrack : *ordered) { + const auto index = orderedTrack.globalIndex; + const auto& common = frame.getGenericTracks()[index]; + o2::track::TrackParCovFwd inner, outer; + if (!legacy::exportLegacyForwardTrackParCov(common.innerState, inner) || !legacy::exportLegacyForwardTrackParCov(common.outerState, outer)) { + error = GenericTrackOutputAdapterError::InvalidState; + return std::nullopt; + } + // Preserve the legacy TrackMFT object shape without claiming a seed-pT + // estimate from this tracker. TrackMFT does not initialize mInvQPtSeed. + outer.setTrackChi2(0.f); + o2::mft::TrackMFT output; + static_cast(output) = inner; + output.setOutParam(outer); + output.setTrackChi2(common.chi2); + output.setCA(true); + output.setInvQPtSeed(0.); + output.setChi2QPtSeed(0.); + uint32_t pattern = 0; + if (!collectReferences(frame, common, surfaces, 10, staged.clusterIndices, output, pattern, error, + externalIndicesBySurface, clusterSizesBySurface)) + return std::nullopt; + staged.tracks.push_back(std::move(output)); + staged.seedPatterns.push_back(static_cast(pattern)); + times.push_back(orderedTrack.timestamp); + if (withMC) + staged.labels.push_back(frame.getTrackLabels()[index]); + } + finalizeROFs(staged.trackROFs, times, context); + return staged; +} + +inline std::optional stageITSGenericTrackOutput(const TimeFrame& frame, const GenericTrackPublicationContext& context, + const ITSSharedClusterCompatibility& compatibility, bool withMC, + GenericTrackOutputAdapterError& error) +{ + if (context.detector != o2::detectors::DetID::ITS) { + error = GenericTrackOutputAdapterError::MixedDetector; + return std::nullopt; + } + return stageITSGenericTrackOutput(frame, context.layerMapping, {context.inputROFs, context.clock}, compatibility, withMC, error, + context.externalIndicesBySurface, context.clusterSizesBySurface); +} + +inline std::optional stageMFTGenericTrackOutput(const TimeFrame& frame, const GenericTrackPublicationContext& context, + bool withMC, GenericTrackOutputAdapterError& error) +{ + if (context.detector != o2::detectors::DetID::MFT) { + error = GenericTrackOutputAdapterError::MixedDetector; + return std::nullopt; + } + return stageMFTGenericTrackOutput(frame, context.layerMapping, {context.inputROFs, context.clock}, withMC, error, + context.externalIndicesBySurface, context.clusterSizesBySurface); +} + +} // namespace o2::itsmft::tracking + +#endif // ALICEO2_ITSMFT_TRACKING_GENERICTRACKOUTPUTADAPTER_H_ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/GlobalMeasurement.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/GlobalMeasurement.h new file mode 100644 index 0000000000000..85d53c268940c --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/GlobalMeasurement.h @@ -0,0 +1,90 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_GLOBALMEASUREMENT_H_ +#define ALICEO2_ITSMFT_TRACKING_GLOBALMEASUREMENT_H_ + +#include +#include +#include +#include + +#include "GPUCommonDef.h" +#include "ITSMFTTracking/IdTypes.h" + +namespace o2::itsmft::tracking +{ + +struct GlobalPoint3F { + float x; + float y; + float z; +}; + +struct GlobalCovariance3F { + float xx{0.f}; + float xy{0.f}; + float xz{0.f}; + float yy{0.f}; + float yz{0.f}; + float zz{0.f}; + + GPUhdi() float& operator[](std::size_t index) noexcept { return (&xx)[index]; } + GPUhdi() const float& operator[](std::size_t index) const noexcept { return (&xx)[index]; } +}; + +struct GlobalMeasurement { + enum CovarianceIndex : uint8_t { + XX, + XY, + XZ, + YY, + YZ, + ZZ + }; + + union { + struct { + float x; + float y; + float z; + }; + GlobalPoint3F position; + }; + GlobalCovariance3F covariance{}; + float radius{0.f}; + float phi{0.f}; + uint32_t clusterId{std::numeric_limits::max()}; + + GPUhdi() bool hasValidClusterId() const noexcept { return clusterId != std::numeric_limits::max(); } +}; + +#define O2_ITSMFT_ASSERT_GLOBAL_TYPE(Type, Size) \ + static_assert(std::is_standard_layout_v); \ + static_assert(std::is_trivially_copyable_v); \ + static_assert(sizeof(Type) == Size) + +O2_ITSMFT_ASSERT_GLOBAL_TYPE(GlobalMeasurement, 48); +O2_ITSMFT_ASSERT_GLOBAL_TYPE(GlobalPoint3F, 12); +O2_ITSMFT_ASSERT_GLOBAL_TYPE(GlobalCovariance3F, 24); + +#undef O2_ITSMFT_ASSERT_GLOBAL_TYPE + +static_assert(alignof(GlobalMeasurement) == 4); +static_assert(offsetof(GlobalMeasurement, x) == 0); +static_assert(offsetof(GlobalMeasurement, covariance) == 12); +static_assert(offsetof(GlobalMeasurement, radius) == 36); +static_assert(offsetof(GlobalMeasurement, phi) == 40); +static_assert(offsetof(GlobalMeasurement, clusterId) == 44); + +} // namespace o2::itsmft::tracking + +#endif // ALICEO2_ITSMFT_TRACKING_GLOBALMEASUREMENT_H_ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IOUtils.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IOUtils.h new file mode 100644 index 0000000000000..a34e4f9fe22be --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IOUtils.h @@ -0,0 +1,333 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file IOUtils.h +/// \brief Shared cluster I/O utilities for ITS and MFT (based on ITStracking/IOUtils.h) +/// + +#ifndef ALICEO2_ITSMFT_TRACKING_IOUTILS_H_ +#define ALICEO2_ITSMFT_TRACKING_IOUTILS_H_ + +#include +#include +#include + +#ifndef GPUCA_GPUCODE +#include +#include +#endif + +#include + +#include "DetectorsCommonDataFormats/DetID.h" +#include "ITSMFTBase/SegmentationAlpide.h" +#include "DataFormatsITSMFT/ClusterPattern.h" +#include "DataFormatsITSMFT/CompCluster.h" +#include "DataFormatsITSMFT/ROFRecord.h" +#include "DataFormatsITSMFT/TopologyDictionary.h" +#include "ITSMFTTracking/ClusterDecoding.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/ROFViews.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/SurfaceMeasurement.h" +#include "ITSMFTTracking/SurfaceTiming.h" +#include "MathUtils/Cartesian.h" +#include "SimulationDataFormat/MCTruthContainer.h" + +namespace o2::itsmft::ioutils +{ + +namespace detail +{ +constexpr bool isSensorInGeometry(int sensor, int geometrySize) noexcept +{ + return sensor >= 0 && sensor < geometrySize; +} + +constexpr bool isLayerInDetector(int layer, int detectorLayers) noexcept +{ + return layer >= 0 && layer < detectorLayers; +} + +/// Return whether cluster-decoding systematic errors are configured for `DetId`. +/// ITS is a no-op; MFT reads its live tracker configuration. +template +bool shouldApplySysErrors() +{ + if constexpr (DetId == o2::detectors::DetID::ITS) { + return false; + } else { + const auto& conf = o2::itsmft::tracking::TrackerParamRef::get(); + for (int il = 0; il < o2::itsmft::tracking::TrackerParamRef::nLayers(); il++) { + if (conf.sysErr2Row[il] > 0.f || conf.sysErr2Col[il] > 0.f) { + return true; + } + } + return false; + } +} + +/// Add configured systematic-error corrections to `sigma2Row` and `sigma2Col`. +/// ITS is a no-op. +template +void addSysErrors(int layerId, float& sigma2Row, float& sigma2Col) +{ + if constexpr (DetId == o2::detectors::DetID::ITS) { + (void)layerId; + (void)sigma2Row; + (void)sigma2Col; + } else { + const auto& conf = o2::itsmft::tracking::TrackerParamRef::get(); + sigma2Row += conf.sysErr2Row[layerId]; + sigma2Col += conf.sysErr2Col[layerId]; + } +} +} // namespace detail + +constexpr float DefClusErrorRow = o2::itsmft::SegmentationAlpide::PitchRow * 0.5f; +constexpr float DefClusErrorCol = o2::itsmft::SegmentationAlpide::PitchCol * 0.5f; +constexpr float DefClusError2Row = DefClusErrorRow * DefClusErrorRow; +constexpr float DefClusError2Col = DefClusErrorCol * DefClusErrorCol; + +void fillMatrixCache(o2::detectors::DetID::ID detId); + +/// Decode detector geometry and covariance for one compact cluster. +template +o2::itsmft::tracking::ClusterDecodeResult decodeCluster( + const CompClusterExt& c, + o2::itsmft::tracking::BoundedPatternCursor& patterns, + const TopologyDictionary* dict, + bool applySysErrors); + +template +o2::math_utils::Point3D extractClusterData(const CompClusterExt& c, iterator& iter, const TopologyDictionary* dict, T& sig2Row, T& sig2Col, unsigned int* clusterSize = nullptr, o2::itsmft::tracking::ClusterShape* clusterShape = nullptr) +{ + auto pattID = c.getPatternID(); + sig2Row = DefClusError2Row; + sig2Col = DefClusError2Col; // Default COG error (about half a pixel) + const auto setShape = [clusterSize, clusterShape](const ClusterPattern& patt, unsigned int nPixels) { + if (clusterSize != nullptr) { + *clusterSize = nPixels; + } + if (clusterShape != nullptr) { + *clusterShape = o2::itsmft::tracking::ClusterShape{ + nPixels, static_cast(patt.getRowSpan()), static_cast(patt.getColumnSpan())}; + } + }; + if (pattID != CompCluster::InvalidPatternID) { + sig2Row = dict->getErr2X(pattID); + sig2Col = dict->getErr2Z(pattID); + if (!dict->isGroup(pattID)) { + setShape(dict->getPattern(pattID), dict->getNpixels(pattID)); + return dict->getClusterCoordinates(c); + } + ClusterPattern patt(iter); + setShape(patt, patt.getNPixels()); + return dict->getClusterCoordinates(c, patt); + } + ClusterPattern patt(iter); + setShape(patt, patt.getNPixels()); + return dict->getClusterCoordinates(c, patt, false); +} + +template +struct ClusterDataDecodeResult { + o2::math_utils::Point3D coordinates{}; + T sig2Row{DefClusError2Row}; + T sig2Col{DefClusError2Col}; + o2::itsmft::tracking::ClusterShape shape{}; + o2::itsmft::tracking::ClusterDecodeError error{o2::itsmft::tracking::ClusterDecodeError::None}; + + bool ok() const noexcept { return error == o2::itsmft::tracking::ClusterDecodeError::None; } +}; + +// Bounded counterpart: acquire pattern bytes only after validating the encoding. +template +ClusterDataDecodeResult extractClusterDataBounded( + const CompClusterExt& c, + o2::itsmft::tracking::BoundedPatternCursor& patterns, + const TopologyDictionary* dict) +{ + ClusterDataDecodeResult result; + if (dict == nullptr) { + result.error = o2::itsmft::tracking::ClusterDecodeError::MissingDictionary; + return result; + } + + const auto pattID = c.getPatternID(); + if (pattID != CompCluster::InvalidPatternID) { + if (pattID >= dict->getSize()) { + result.error = o2::itsmft::tracking::ClusterDecodeError::InvalidPatternId; + return result; + } + result.sig2Row = dict->getErr2X(pattID); + result.sig2Col = dict->getErr2Z(pattID); + if (!dict->isGroup(pattID)) { + const auto& pattern = dict->getPattern(pattID); + result.shape = o2::itsmft::tracking::ClusterShape{ + static_cast(dict->getNpixels(pattID)), + static_cast(pattern.getRowSpan()), + static_cast(pattern.getColumnSpan())}; + result.coordinates = dict->getClusterCoordinates(c); + return result; + } + } + + ClusterPattern pattern; + result.error = patterns.acquirePattern(pattern); + if (!result.ok()) { + return result; + } + result.shape = o2::itsmft::tracking::ClusterShape{ + static_cast(pattern.getNPixels()), + static_cast(pattern.getRowSpan()), + static_cast(pattern.getColumnSpan())}; + result.coordinates = dict->getClusterCoordinates(c, pattern, pattID != CompCluster::InvalidPatternID); + return result; +} + +// Return coordinates as an array for TGeoMatrix callers. +template +std::array extractClusterDataA(const CompClusterExt& c, iterator& iter, const TopologyDictionary* dict, T& sig2Row, T& sig2Col) +{ + auto pattID = c.getPatternID(); + sig2Row = DefClusError2Row; + sig2Col = DefClusError2Col; // Default COG error (about half a pixel) + if (pattID != CompCluster::InvalidPatternID) { + sig2Row = dict->getErr2X(pattID); + sig2Col = dict->getErr2Z(pattID); + if (!dict->isGroup(pattID)) { + return dict->getClusterCoordinatesA(c); + } + ClusterPattern patt(iter); + return dict->getClusterCoordinatesA(c, patt); + } + ClusterPattern patt(iter); + return dict->getClusterCoordinatesA(c, patt, false); +} + +} // namespace o2::itsmft::ioutils + +namespace o2::itsmft::tracking +{ + +class TimeFrame; + +struct ClusterSourceInput { + ClusterSourceId id{}; + o2::detectors::DetID::ID detector{o2::detectors::DetID::ITS}; + gsl::span clusters{}; + gsl::span patterns{}; + gsl::span rofs{}; + const o2::itsmft::TopologyDictionary* dictionary{nullptr}; + const o2::dataformats::MCTruthContainer* labels{nullptr}; + gsl::span layerToSurface{}; + ROFTimingConfig timing{}; + const ClusterDecoder* decoder{nullptr}; + bool applySysErrors{true}; + RuntimeROFViews rofViews{}; +}; + +enum class MultiSourceLoadError : uint8_t { + None, + NonDenseSourceIds, + DuplicateSourceId, + UnsupportedDetector, + MissingDecoder, + InvalidROFRange, + InvalidLayerMapping, + DetectorSurfaceMismatch, + InconsistentDecoderMetadata, + TimingError, + SurfaceCatalogNotConfigured, + SurfaceCatalogStale, + MissingDictionary, + TruncatedExplicitPattern, + MalformedExplicitPattern, + InvalidPatternId, + InvalidSensor, + InvalidDecodedLayer, + GeometryUnavailable, + OtherMalformedInput, + TrailingPatternData, + FrameNotConfigured +}; + +struct LoadSourcesResult { + MultiSourceLoadError error{MultiSourceLoadError::None}; + ClusterSourceId source{}; + uint32_t rof{std::numeric_limits::max()}; + uint32_t clusterIndex{std::numeric_limits::max()}; + TimingBuildError timingDetail{TimingBuildError::None}; + bool ok() const noexcept { return error == MultiSourceLoadError::None; } +}; + +LoadSourcesResult loadSources(TimeFrame&, const SurfaceCatalogView&, + gsl::span, + const o2::InteractionRecord&, + std::vector>* externalIndicesBySurface = nullptr, + std::vector>* clusterSizesBySurface = nullptr); + +/// Reset, decode, and normalize all sources into a configured TimeFrame. +/// A failed load leaves the TimeFrame empty. +LoadSourcesResult loadTimeFrameSources(TimeFrame&, gsl::span, + SurfaceCatalogView, const o2::InteractionRecord&, + std::vector>* externalIndicesBySurface = nullptr, + std::vector>* clusterSizesBySurface = nullptr); + +/// Convenience wrapper for a single detector source. +LoadSourcesResult loadTimeFrameSource( + TimeFrame&, const ClusterDecoder&, const o2::InteractionRecord&, const ROFTimingConfig&, + gsl::span, gsl::span, + gsl::span, const itsmft::TopologyDictionary*, + const dataformats::MCTruthContainer*, o2::detectors::DetID::ID, + gsl::span, SurfaceCatalogView, bool applySysErrors = true, + std::vector>* externalIndicesBySurface = nullptr, + std::vector>* clusterSizesBySurface = nullptr); + +#ifndef GPUCA_GPUCODE +class RecoverableLoadFailure final : public std::runtime_error +{ + public: + explicit RecoverableLoadFailure(const LoadSourcesResult& result); + MultiSourceLoadError error() const noexcept { return mResult.error; } + const LoadSourcesResult& result() const noexcept { return mResult; } + + private: + LoadSourcesResult mResult; +}; + +enum class TimeFrameLoadFailureReason : uint8_t { + DictionaryNotConfigured, + NonUniformROFTiming, + ZeroROFCount, + LoadSourcesFailure +}; + +class TimeFrameLoadException final : public std::runtime_error +{ + public: + TimeFrameLoadException(TimeFrameLoadFailureReason, std::string); + explicit TimeFrameLoadException(const LoadSourcesResult&); + TimeFrameLoadFailureReason reason() const noexcept { return mReason; } + const LoadSourcesResult& loadResult() const noexcept { return mLoadResult; } + + private: + TimeFrameLoadFailureReason mReason; + LoadSourcesResult mLoadResult{}; +}; + +bool isRecoverableLoadError(MultiSourceLoadError, TimingBuildError) noexcept; +#endif + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_IOUTILS_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ITSMFTDetectorDefinitions.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ITSMFTDetectorDefinitions.h new file mode 100644 index 0000000000000..e2a4efc9ad356 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ITSMFTDetectorDefinitions.h @@ -0,0 +1,112 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_DETECTORDEFINITIONS_H_ +#define ALICEO2_ITSMFT_TRACKING_DETECTORDEFINITIONS_H_ + +#include +#include + +#include "DetectorsCommonDataFormats/DetID.h" +#include "ITSMFTTracking/SurfaceSpec.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "ITSMFTTracking/Constants.h" + +namespace o2::itsmft::tracking +{ + +static_assert(MFTNLayers % 2 == 0); +inline constexpr int MFTDisks = MFTNLayers / 2; +inline constexpr std::array kNominalITSLayerX0{ + 5.e-3f, 5.e-3f, 5.e-3f, 1.e-2f, 1.e-2f, 1.e-2f, 1.e-2f}; +inline constexpr float kMFTNominalRadLength = 0.042f; +inline constexpr std::array kMFTLookupRMin{ + 2.1f, 2.1f, 2.1f, 2.1f, 2.1f, 2.1f, 3.1f, 3.1f, 3.5f, 3.5f}; +inline constexpr std::array kMFTLookupRMax{ + 12.5f, 12.5f, 12.5f, 12.5f, 14.f, 14.f, 17.f, 17.f, 17.5f, 17.5f}; + +constexpr std::array makeNominalMFTLayerX0() +{ + std::array values{}; + // Each disk's budget is shared by its two sensor planes: the refit applies + // the nominal material once per attached surface. + for (auto& value : values) { + value = kMFTNominalRadLength / static_cast(MFTNLayers); + } + return values; +} + +inline constexpr std::array kNominalMFTLayerX0 = makeNominalMFTLayerX0(); + +constexpr NominalSurfaceMaterial itsLayerMaterial(std::size_t layer) noexcept +{ + const float x0 = kNominalITSLayerX0[layer]; + return {x0, x0 * o2::its::constants::Radl * o2::its::constants::Rho}; +} + +struct ITSSurfaceSpec { + inline static constexpr std::array surfaces{ + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::ITS), 0}, SurfaceKind::Cylinder, 2.3259652f, itsLayerMaterial(0), {-kITSLookupZHalfExtent[0], kITSLookupZHalfExtent[0]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::ITS), 1}, SurfaceKind::Cylinder, 3.1353536f, itsLayerMaterial(1), {-kITSLookupZHalfExtent[1], kITSLookupZHalfExtent[1]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::ITS), 2}, SurfaceKind::Cylinder, 3.9162421f, itsLayerMaterial(2), {-kITSLookupZHalfExtent[2], kITSLookupZHalfExtent[2]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::ITS), 3}, SurfaceKind::Cylinder, 19.58824f, itsLayerMaterial(3), {-kITSLookupZHalfExtent[3], kITSLookupZHalfExtent[3]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::ITS), 4}, SurfaceKind::Cylinder, 24.527159f, itsLayerMaterial(4), {-kITSLookupZHalfExtent[4], kITSLookupZHalfExtent[4]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::ITS), 5}, SurfaceKind::Cylinder, 34.354595f, itsLayerMaterial(5), {-kITSLookupZHalfExtent[5], kITSLookupZHalfExtent[5]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::ITS), 6}, SurfaceKind::Cylinder, 39.310642f, itsLayerMaterial(6), {-kITSLookupZHalfExtent[6], kITSLookupZHalfExtent[6]}}, + }; +}; + +static_assert(SurfaceSpec); +static_assert(SurfaceCount == ITSNLayers); + +constexpr NominalSurfaceMaterial mftLayerMaterial(std::size_t layer) noexcept +{ + const float x0 = kNominalMFTLayerX0[layer]; + return {x0, x0 * o2::its::constants::Radl * o2::its::constants::Rho}; +} + +struct MFTSurfaceSpec { + inline static constexpr std::array surfaces{ + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::MFT), 0}, SurfaceKind::Disk, -45.2889f, mftLayerMaterial(0), {kMFTLookupRMin[0], kMFTLookupRMax[0]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::MFT), 1}, SurfaceKind::Disk, -46.7111f, mftLayerMaterial(1), {kMFTLookupRMin[1], kMFTLookupRMax[1]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::MFT), 2}, SurfaceKind::Disk, -48.5889f, mftLayerMaterial(2), {kMFTLookupRMin[2], kMFTLookupRMax[2]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::MFT), 3}, SurfaceKind::Disk, -50.0111f, mftLayerMaterial(3), {kMFTLookupRMin[3], kMFTLookupRMax[3]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::MFT), 4}, SurfaceKind::Disk, -52.3889f, mftLayerMaterial(4), {kMFTLookupRMin[4], kMFTLookupRMax[4]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::MFT), 5}, SurfaceKind::Disk, -53.8111f, mftLayerMaterial(5), {kMFTLookupRMin[5], kMFTLookupRMax[5]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::MFT), 6}, SurfaceKind::Disk, -67.6889f, mftLayerMaterial(6), {kMFTLookupRMin[6], kMFTLookupRMax[6]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::MFT), 7}, SurfaceKind::Disk, -69.1111f, mftLayerMaterial(7), {kMFTLookupRMin[7], kMFTLookupRMax[7]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::MFT), 8}, SurfaceKind::Disk, -76.0889f, mftLayerMaterial(8), {kMFTLookupRMin[8], kMFTLookupRMax[8]}}, + StaticSurfaceDescriptor{{static_cast(o2::detectors::DetID::MFT), 9}, SurfaceKind::Disk, -77.5111f, mftLayerMaterial(9), {kMFTLookupRMin[9], kMFTLookupRMax[9]}}, + }; +}; + +static_assert(SurfaceSpec); +static_assert(SurfaceCount == MFTNLayers); + +template +consteval std::array> projectStaticSurfaceCatalog() noexcept +{ + std::array> result{}; + for (std::size_t i = 0; i < SurfaceCount; ++i) { + result[i] = toRuntimeSurfaceDescriptor(Spec::surfaces[i]); + } + return result; +} + +inline constexpr auto kITSStaticSurfaceCatalog = projectStaticSurfaceCatalog(); +inline constexpr auto kMFTStaticSurfaceCatalog = projectStaticSurfaceCatalog(); + +static_assert(kITSStaticSurfaceCatalog.size() == ITSNLayers); +static_assert(kMFTStaticSurfaceCatalog.size() == MFTNLayers); + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_DETECTORDEFINITIONS_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IdTypes.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IdTypes.h new file mode 100644 index 0000000000000..7a5cf198aa075 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IdTypes.h @@ -0,0 +1,82 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_IDTYPES_H_ +#define ALICEO2_ITSMFT_TRACKING_IDTYPES_H_ + +#include +#include +#include + +#include "GPUCommonDef.h" + +namespace o2::itsmft::tracking +{ + +// The coordinate convention of a surface and every state defined on it. +enum class SurfaceKind : uint8_t { + Undefined, + Cylinder, + Disk +}; + +static_assert(std::is_same_v, uint8_t>); +static_assert(sizeof(SurfaceKind) == sizeof(uint8_t)); + +namespace detail +{ +template +class Identifier +{ + public: + static constexpr ValueType InvalidValue = std::numeric_limits::max(); + + GPUhdDefault() constexpr Identifier() noexcept = default; + GPUhdDefault() explicit constexpr Identifier(ValueType value) noexcept : mValue{value} {} + + GPUhdi() constexpr ValueType value() const noexcept { return mValue; } + GPUhdi() constexpr bool isValid() const noexcept { return mValue != InvalidValue; } + GPUhdi() static constexpr Identifier invalid() noexcept { return Identifier{InvalidValue}; } + + GPUhdi() friend constexpr bool operator==(Identifier lhs, Identifier rhs) noexcept { return lhs.mValue == rhs.mValue; } + GPUhdi() friend constexpr bool operator!=(Identifier lhs, Identifier rhs) noexcept { return !(lhs == rhs); } + GPUhdi() friend constexpr bool operator<(Identifier lhs, Identifier rhs) noexcept { return lhs.mValue < rhs.mValue; } + + private: + ValueType mValue{InvalidValue}; +}; +} // namespace detail + +struct LayerIdTag; +struct EdgeIdTag; +struct CellPathIdTag; +struct ClusterSourceIdTag; + +using LayerId = detail::Identifier; +using EdgeId = detail::Identifier; +using CellPathId = detail::Identifier; +using ClusterSourceId = detail::Identifier; + +GPUhdi() constexpr bool isRecognizedSurfaceKind(SurfaceKind kind) noexcept +{ + return kind == SurfaceKind::Cylinder || kind == SurfaceKind::Disk; +} + +inline constexpr uint32_t MaxLayoutSurfaces = 32; +inline constexpr uint32_t MaxLayoutEdges = MaxLayoutSurfaces * (MaxLayoutSurfaces - 1); +inline constexpr uint32_t MaxLayoutPaths = MaxLayoutSurfaces * (MaxLayoutSurfaces - 1) * (MaxLayoutSurfaces - 1); + +static_assert(MaxLayoutEdges < EdgeId::InvalidValue); +static_assert(MaxLayoutPaths < CellPathId::InvalidValue); + +} // namespace o2::itsmft::tracking + +#endif diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IndexTableConfiguration.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IndexTableConfiguration.h new file mode 100644 index 0000000000000..66c432332acfc --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IndexTableConfiguration.h @@ -0,0 +1,96 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_INDEXTABLECONFIGURATION_H_ +#define ALICEO2_ITSMFT_TRACKING_INDEXTABLECONFIGURATION_H_ + +#include + +// Host-only: DetectorParameters owns std::vector members and is not +// device-compatible. Keep this boundary separate so existing host-binding +// consumers do not inherit IndexTableUtils.h's extra dependencies. +#ifndef GPUCA_GPUCODE + +#include +#include +#include + +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/IndexTableUtils.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" + +namespace o2::itsmft::tracking +{ + +enum class IndexTableConfigError : uint8_t { + None, + NonPositiveRowBins, + NonPositiveColBins, + RowColBinCountExceedsIndexRange, // Product exceeds int, the bin-index type. + InvalidActiveLayerCount, // Invalid active surface count. + InsufficientChartRanges, // Fewer descriptor chart ranges than active surfaces. + NonFiniteChartRange, // Chart bound is NaN or +/-Inf. + InvalidChartRange, // Chart maximum does not exceed its minimum. + InvalidSurfaceKind, // Neither Cylinder nor Disk. +}; + +/// Validates and binds detector inputs into `staged` for one coordinate kind. +/// Resolve `kind` from the validated DetectorLayout, never from NLayers or DetId. +/// On error, `staged` is unchanged. Call once per present kind during detector +/// initialization, outside iteration and candidate loops. +IndexTableConfigError bindIndexTableConfiguration(o2::itsmft::IndexTableUtilsCore& staged, + const DetectorParameters& params, + int activeSurfaceCount, + SurfaceKind kind, + gsl::span chartRanges) noexcept; + +/// True iff all fields stored by setIndexTableParams match between `a` and +/// `b`. Used to verify that a non-FirstPass iteration matches the +/// TimeFrame-owned configuration before reusing or resorting its LUT. +inline bool indexTableConfigurationsMatch(const o2::itsmft::IndexTableUtilsCore& a, + const o2::itsmft::IndexTableUtilsCore& b, + int activeSurfaceCount) noexcept +{ + if (a.getCoordType() != b.getCoordType() || + a.getNrowBins() != b.getNrowBins() || + a.getNcolBins() != b.getNcolBins() || + a.getRowOrigin() != b.getRowOrigin() || + a.getRowCoordinateSpan() != b.getRowCoordinateSpan()) { + return false; + } + if (activeSurfaceCount <= 0 || activeSurfaceCount > o2::itsmft::IndexTableUtilsCore::MaxLayers) { + return false; + } + for (int iLayer = 0; iLayer < activeSurfaceCount; ++iLayer) { + if (a.getLayerColMin(iLayer) != b.getLayerColMin(iLayer) || + a.getLayerColMax(iLayer) != b.getLayerColMax(iLayer)) { + return false; + } + } + return true; +} + +/// Checked size_t multiplication for index-table allocation sizes. Returns +/// false, leaving `result` unset, if `a * b` overflows size_t. +inline bool checkedIndexTableSizeProduct(std::size_t a, std::size_t b, std::size_t& result) noexcept +{ + if (a != 0 && b > std::numeric_limits::max() / a) { + return false; + } + result = a * b; + return true; +} + +} // namespace o2::itsmft::tracking + +#endif // GPUCA_GPUCODE + +#endif /* ALICEO2_ITSMFT_TRACKING_INDEXTABLECONFIGURATION_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IndexTableConfigurationSet.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IndexTableConfigurationSet.h new file mode 100644 index 0000000000000..66fd08ffd0fee --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IndexTableConfigurationSet.h @@ -0,0 +1,68 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_INDEXTABLECONFIGURATIONSET_H_ +#define ALICEO2_ITSMFT_TRACKING_INDEXTABLECONFIGURATIONSET_H_ + +#include +#include +#include "ITSMFTTracking/IndexTableUtils.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" + +namespace o2::itsmft::tracking +{ +// Owning cache: one all-layer lookup configuration for each coordinate kind, +// plus a compact surface-to-kind mapping. Copies never borrow another owner. +class IndexTableConfigurationSet +{ + public: + bool reset(SurfaceCatalogView catalog) noexcept + { + *this = {}; + if (catalog.nSurfaces > MaxLayoutSurfaces || (catalog.nSurfaces && !catalog.surfaces)) { + return false; + } + for (uint32_t layer = 0; layer < catalog.nSurfaces; ++layer) { + const auto kind = catalog.surfaces[layer].kind; + if (kind != SurfaceKind::Cylinder && kind != SurfaceKind::Disk) { + *this = {}; + return false; + } + const auto slot = kind == SurfaceKind::Cylinder ? 0 : 1; + mKindByLayer[layer] = slot; + mPresent[slot] = true; + } + mLayers = catalog.nSurfaces; + return true; + } + void clear() noexcept { *this = {}; } + size_t size() const noexcept { return mLayers; } + size_t configurationCount() const noexcept { return size_t(mPresent[0]) + size_t(mPresent[1]); } + bool hasKind(SurfaceKind kind) const noexcept { return (kind == SurfaceKind::Cylinder || kind == SurfaceKind::Disk) && mPresent[kind == SurfaceKind::Cylinder ? 0 : 1]; } + IndexTableUtilsCore& forKind(SurfaceKind kind) noexcept + { + assert(hasKind(kind)); + return mByKind[kind == SurfaceKind::Cylinder ? 0 : 1]; + } + const IndexTableUtilsCore& operator[](size_t layer) const noexcept + { + assert(layer < mLayers); + return mByKind[mKindByLayer[layer]]; + } + + private: + std::array mByKind; + std::array mKindByLayer{}; + std::array mPresent{}; + uint32_t mLayers = 0; +}; +} // namespace o2::itsmft::tracking +#endif diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IndexTableUtils.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IndexTableUtils.h new file mode 100644 index 0000000000000..fef35918de4e1 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IndexTableUtils.h @@ -0,0 +1,221 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file IndexTableUtils.h +/// \brief Shared index-table utilities for periodic-phi surface charts +/// + +#ifndef ALICEO2_ITSMFT_TRACKING_INDEXTABLEUTILS_H_ +#define ALICEO2_ITSMFT_TRACKING_INDEXTABLEUTILS_H_ + +#include +#include +#include + +#include + +#include "CommonConstants/MathConstants.h" +#include "GPUCommonMath.h" +#include "GPUCommonDef.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/IdTypes.h" + +namespace o2::itsmft +{ + +enum class IndexTableCoordType : uint8_t { PhiZ, + PhiR }; + +namespace index_table_utils +{ +GPUhdi() float getNormalizedPhi(float phi) +{ + phi -= o2::constants::math::TwoPI * o2::gpu::GPUCommonMath::Floor(phi * (1.f / o2::constants::math::TwoPI)); + return phi; +} +} // namespace index_table_utils + +/// Row/column LUT helper. Charts have periodic phi rows and a +/// descriptor-bounded linear column. +/// MaxLayoutSurfaces storage keeps GPUhdi() access device-portable; callers +/// must not query unpopulated runtime-plan positions. +class IndexTableUtilsCore +{ + public: + static constexpr int MaxLayers = static_cast(o2::itsmft::tracking::MaxLayoutSurfaces); + + /// Configure LUT geometry with a row interval and per-surface column intervals. + /// `layerColHalfExtent` may be shorter than MaxLayers (the common case -- + /// real detectors have far fewer than 32 layers); anything beyond its size + /// is left at its previous value, exactly as it would be untouched by a + /// caller that never re-populates it. + void setIndexTableParams(IndexTableCoordType coordType, int nRowBins, int nColBins, + float rowMin, float rowMax, + gsl::span layerColMin, + gsl::span layerColMax) + { + mCoordType = coordType; + mRowOrigin = 0.f; + mRowCoordinateSpan = rowMax - rowMin; + mInverseRowBinSize = (mRowCoordinateSpan > 0.f) ? static_cast(nRowBins) / mRowCoordinateSpan : 0.f; + mNcolBins = nColBins; + mNrowBins = nRowBins; + const int nLayers = std::min({static_cast(layerColMin.size()), static_cast(layerColMax.size()), MaxLayers}); + for (int iLayer{0}; iLayer < nLayers; ++iLayer) { + mLayerColMin[iLayer] = layerColMin[iLayer]; + mLayerColMax[iLayer] = layerColMax[iLayer]; + mInverseColBinSize[iLayer] = static_cast(nColBins) / (layerColMax[iLayer] - layerColMin[iLayer]); + } + } + + void setIndexTableParams(IndexTableCoordType coordType, int nRowBins, int nColBins, + float rowMin, float rowMax, + gsl::span layerColHalfExtent) + { + std::array minima{}; + std::array maxima{}; + const int count = std::min(static_cast(layerColHalfExtent.size()), MaxLayers); + for (int iLayer = 0; iLayer < count; ++iLayer) { + minima[iLayer] = -layerColHalfExtent[iLayer]; + maxima[iLayer] = layerColHalfExtent[iLayer]; + } + setIndexTableParams(coordType, nRowBins, nColBins, rowMin, rowMax, + gsl::span{minima.data(), static_cast(count)}, + gsl::span{maxima.data(), static_cast(count)}); + } + + /// Fill LUT geometry from any struct exposing RowBins, ColBins and LayerZ (ITS phi-z). + template + void setTrackingParameters(const T& params) + { + const auto extents = layerColHalfExtentFrom(params); + setIndexTableParams(IndexTableCoordType::PhiZ, params.RowBins, params.ColBins, + 0.f, o2::constants::math::TwoPI, gsl::span{extents.data(), static_cast(extents.count)}); + } + + GPUhdi() float getInverseColCoordinate(const int layerIndex) const + { + return mInverseColBinSize[layerIndex]; + } + + GPUhdi() int getColBinIndex(const int layerIndex, const float colCoordinate) const + { + return (colCoordinate - mLayerColMin[layerIndex]) * mInverseColBinSize[layerIndex]; + } + + GPUhdi() int getRowBinIndex(const float rowCoordinate) const + { + return rowCoordinate * mInverseRowBinSize; + } + + GPUhdi() int getBinIndex(const int colIndex, const int rowIndex) const + { + return o2::gpu::GPUCommonMath::Min(rowIndex * mNcolBins + colIndex, (mNcolBins * mNrowBins) - 1); + } + + GPUhdi() int countRowSelectedBins(const int* indexTable, const int rowBinIndex, + const int minColBinIndex, const int maxColBinIndex) const + { + const int firstBinIndex{getBinIndex(minColBinIndex, rowBinIndex)}; + const int maxBinIndex{firstBinIndex + maxColBinIndex - minColBinIndex + 1}; + + return indexTable[maxBinIndex] - indexTable[firstBinIndex]; + } + + void print() const; + + GPUhdi() int getNcolBins() const { return mNcolBins; } + GPUhdi() int getNrowBins() const { return mNrowBins; } + GPUhdi() float getLayerColHalfExtent(int i) const { return 0.5f * (mLayerColMax[i] - mLayerColMin[i]); } + GPUhdi() float getLayerColMin(int i) const { return mLayerColMin[i]; } + GPUhdi() float getLayerColMax(int i) const { return mLayerColMax[i]; } + GPUhdi() void setNcolBins(const int colBins) { mNcolBins = colBins; } + GPUhdi() void setNrowBins(const int rowBins) { mNrowBins = rowBins; } + GPUhdi() IndexTableCoordType getCoordType() const { return mCoordType; } + /// Row origin/span, needed alongside the other getters to detect a + /// configuration mismatch between a freshly bound IndexTableUtils and one + /// already owned by a TimeFrame (LUT-reuse invariant); not test-only. + GPUhdi() float getRowOrigin() const { return mRowOrigin; } + GPUhdi() float getRowCoordinateSpan() const { return mRowCoordinateSpan; } + + private: + /// Fixed-capacity result of layerColHalfExtentFrom(); count is the number of + /// available entries, never above MaxLayers. + struct LayerExtents { + std::array values{}; + int count{0}; + const float* data() const noexcept { return values.data(); } + }; + + template + static LayerExtents layerColHalfExtentFrom(const T& params) + { + LayerExtents extents; + if constexpr (requires { params.LayerColHalfExtent; }) { + const auto& colExtents = params.LayerColHalfExtent.empty() ? params.LayerZ : params.LayerColHalfExtent; + extents.count = std::min(static_cast(colExtents.size()), MaxLayers); + for (int iLayer{0}; iLayer < extents.count; ++iLayer) { + extents.values[iLayer] = colExtents[iLayer]; + } + } else { + extents.count = std::min(static_cast(params.LayerZ.size()), MaxLayers); + for (int iLayer{0}; iLayer < extents.count; ++iLayer) { + extents.values[iLayer] = params.LayerZ[iLayer]; + } + } + return extents; + } + + int mNcolBins = 0; + int mNrowBins = 0; + float mInverseRowBinSize = 0.f; + float mRowOrigin = 0.f; + float mRowCoordinateSpan = o2::constants::math::TwoPI; + IndexTableCoordType mCoordType{IndexTableCoordType::PhiZ}; + std::array mLayerColMin{}; + std::array mLayerColMax{}; + std::array mInverseColBinSize{}; +}; + +inline void IndexTableUtilsCore::print() const +{ + printf("NcolBins: %d, NrowBins: %d, InverseRowBinSize: %f\n", mNcolBins, mNrowBins, mInverseRowBinSize); + for (int iLayer{0}; iLayer < MaxLayers; ++iLayer) { + printf("Layer %d: ColRange: [%f, %f], InverseColBinSize: %f\n", iLayer, mLayerColMin[iLayer], mLayerColMax[iLayer], mInverseColBinSize[iLayer]); + } +} + +/// Coordinate-neutral periodic-phi lookup. The operation is not templated on +/// nLayers -- see IndexTableUtilsCore's own doc; callers supply the runtime +/// plan slot and the surface descriptor determines the column coordinate. +GPUhdi() int4 getBinsPhiColumn(float phi, const int layerIndex, + float col, float maxDeltaCol, float maxDeltaRow, + const IndexTableUtilsCore& utils) +{ + const float colRangeMin = col - maxDeltaCol; + const float rowRangeMin = (maxDeltaRow > o2::constants::math::PI) ? 0.f : phi - maxDeltaRow; + const float colRangeMax = col + maxDeltaCol; + const float rowRangeMax = (maxDeltaRow > o2::constants::math::PI) ? o2::constants::math::TwoPI : phi + maxDeltaRow; + + if (colRangeMax < utils.getLayerColMin(layerIndex) || + colRangeMin > utils.getLayerColMax(layerIndex) || colRangeMin > colRangeMax) { + return int4{-1, -1, -1, -1}; + } + + return int4{o2::gpu::GPUCommonMath::Max(0, utils.getColBinIndex(layerIndex, colRangeMin)), + utils.getRowBinIndex(index_table_utils::getNormalizedPhi(rowRangeMin)), + o2::gpu::GPUCommonMath::Min(utils.getNcolBins() - 1, utils.getColBinIndex(layerIndex, colRangeMax)), + utils.getRowBinIndex(index_table_utils::getNormalizedPhi(rowRangeMax))}; +} + +} // namespace o2::itsmft + +#endif /* ALICEO2_ITSMFT_TRACKING_INDEXTABLEUTILS_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IterationConfiguration.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IterationConfiguration.h new file mode 100644 index 0000000000000..49a2be8ce741f --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IterationConfiguration.h @@ -0,0 +1,79 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_ITERATIONCONFIGURATION_H_ +#define ALICEO2_ITSMFT_TRACKING_ITERATIONCONFIGURATION_H_ + +#include +#include +#include + +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/IndexTableConfigurationSet.h" +#include "ITSMFTTracking/TraversalTopology.h" +#include "ITSMFTTracking/detail/TrackingKernelParameters.h" + +namespace o2::itsmft::tracking +{ + +// Tracker-owned data derived once from the invariant detector layout. +struct DetectorConfiguration { + std::vector layerRadii; // Lookup radii, deliberately distinct from descriptor reference coordinates. + IndexTableConfigurationSet indexTableConfigs; + std::vector positionResolutions; + std::vector addTimeError; + std::vector layerResolution; + std::vector systError2Row; + std::vector systError2Col; +}; + +// Tracker-owned, immutable instructions for one tracking iteration. +struct IterationConfiguration { + IterationParameters parameters; + TraversalTopology topology; + TrackingKernelParameters kernelParameters{}; + + // Dense IDs index the owned topology directly; schedules retain their own order. + auto edgeIds() const noexcept + { + return std::views::iota(uint16_t{0}, static_cast(topology.edges.size())) | + std::views::transform([](uint16_t id) { return EdgeId{id}; }); + } + auto cellIds() const noexcept + { + return std::views::iota(uint16_t{0}, static_cast(topology.paths.size())) | + std::views::transform([](uint16_t id) { return CellPathId{id}; }); + } + + bool hasLayer(LayerId id) const noexcept + { + return id.isValid() && id.value() < topology.nLayers; + } + + std::optional getEdgeSlot(EdgeId id) const noexcept + { + return id.isValid() && id.value() < topology.edges.size() ? std::optional{id.value()} : std::nullopt; + } + + std::optional getCellSlot(CellPathId id) const noexcept + { + return id.isValid() && id.value() < topology.paths.size() ? std::optional{id.value()} : std::nullopt; + } + + TraversalTopologyView getTopologyView(SurfaceCatalogView catalog) const noexcept + { + return topology.getView(catalog); + } +}; + +} // namespace o2::itsmft::tracking + +#endif diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/LayerMask.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/LayerMask.h new file mode 100644 index 0000000000000..9a2c900920c7f --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/LayerMask.h @@ -0,0 +1,116 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_LAYERMASK_H_ +#define ALICEO2_ITSMFT_TRACKING_LAYERMASK_H_ + +#include +#include + +#ifndef GPUCA_GPUCODE +#include +#include +#endif + +#include "GPUCommonDef.h" +#include "GPUCommonMath.h" +#include "ITSMFTTracking/Constants.h" + +namespace o2::itsmft::tracking +{ + +struct LayerMask { + GPUhdDefault() constexpr LayerMask() noexcept = default; + GPUhdDefault() constexpr LayerMask(uint32_t mask) noexcept : mBits{mask} {} + GPUhdDefault() constexpr LayerMask(int layer0, int layer1, int layer2) noexcept + : mBits{(uint32_t(1) << layer0) | (uint32_t(1) << layer1) | (uint32_t(1) << layer2)} + { + } + GPUhdi() constexpr operator uint32_t() const noexcept { return mBits; } + GPUhdi() constexpr uint32_t value() const noexcept { return mBits; } + GPUhdi() constexpr void set(int layer) noexcept { mBits |= (uint32_t(1) << layer); } + GPUhdi() constexpr void reset(int layer) noexcept { mBits &= ~(uint32_t(1) << layer); } + + GPUhdi() LayerMask operator~() const noexcept { return LayerMask{~mBits}; } + GPUhdi() LayerMask operator&(LayerMask other) const noexcept { return LayerMask{mBits & other.mBits}; } + GPUhdi() LayerMask operator|(LayerMask other) const noexcept { return LayerMask{mBits | other.mBits}; } + GPUhdi() LayerMask& operator&=(LayerMask other) noexcept + { + mBits &= other.mBits; + return *this; + } + GPUhdi() LayerMask& operator|=(LayerMask other) noexcept + { + mBits |= other.mBits; + return *this; + } + + GPUhdi() bool empty() const noexcept { return mBits == 0; } + GPUhdi() bool has(int layer) const noexcept { return mBits & (uint32_t(1) << layer); } + GPUhdi() bool isSubsetOf(LayerMask allowed) const noexcept { return (*this & ~allowed).empty(); } + GPUhdi() bool isAllowedHoleMask(int maxHoles, LayerMask allowedHoleMask) const noexcept + { + const int allowedHoles = maxHoles > 0 ? maxHoles : 0; + return count() <= allowedHoles && isSubsetOf(allowedHoleMask); + } + GPUhdi() bool isAllowed(int maxHoles, LayerMask allowedHoleMask) const noexcept + { + return holeMask().isAllowedHoleMask(maxHoles, allowedHoleMask); + } + GPUhdi() int length() const noexcept { return empty() ? 0 : last() - first() + 1; } + GPUhdi() int count() const noexcept { return static_cast(o2::gpu::GPUCommonMath::Popcount(mBits)); } + GPUhdi() int first() const noexcept { return mBits ? static_cast(o2::gpu::GPUCommonMath::Ctz(mBits)) : o2::its::constants::UnusedIndex; } + GPUhdi() int last() const noexcept { return mBits ? 31 - static_cast(o2::gpu::GPUCommonMath::Clz(mBits)) : o2::its::constants::UnusedIndex; } + GPUhdi() LayerMask holeMask() const noexcept + { + return empty() ? LayerMask{0} : (span(first(), last()) & ~(*this)); + } + + GPUhdi() int slot(int layer) const noexcept + { + if (!has(layer)) { + return o2::its::constants::UnusedIndex; + } + const uint32_t lowerLayers = (uint32_t(1) << layer) - 1; + return static_cast(o2::gpu::GPUCommonMath::Popcount(static_cast(mBits) & lowerLayers)); + } + + static GPUhdi() LayerMask span(int fromLayer, int toLayer) noexcept + { + if (fromLayer > toLayer) { + return 0; + } + const uint32_t upper = toLayer >= 31 ? uint32_t{0xffffffff} : (uint32_t(1) << (toLayer + 1)) - 1; + const uint32_t lower = (uint32_t(1) << fromLayer) - 1; + return upper & ~lower; + } + + static GPUhdi() LayerMask skipped(int fromLayer, int toLayer) noexcept + { + return (toLayer - fromLayer <= 1) ? LayerMask{0} : span(fromLayer + 1, toLayer - 1); + } + +#ifndef GPUCA_GPUCODE + std::string asString() const { return fmt::format("{:032b}", mBits); } +#endif + + private: + uint32_t mBits{0}; +}; + +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(LayerMask) == sizeof(uint32_t)); +static_assert(alignof(LayerMask) == alignof(uint32_t)); + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_LAYERMASK_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/MaterialPhysics.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/MaterialPhysics.h new file mode 100644 index 0000000000000..abcc7c4b8a9c6 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/MaterialPhysics.h @@ -0,0 +1,152 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_MATERIALPHYSICS_H_ +#define ALICEO2_ITSMFT_TRACKING_MATERIALPHYSICS_H_ + +#include +#include +#include + +#include "ReconstructionDataFormats/PID.h" + +// This header and its implementation are host-only; GPU compilation is not +// supported. + +namespace o2::itsmft::tracking::material +{ + +// Material traversal direction relative to the particle momentum, independent +// of any propagation or covariance sign convention in the caller. +enum class MaterialTraversalDirection : uint8_t { + AlongMomentum = 0, + OppositeMomentum = 1 +}; + +// Unsigned, path-integrated material budget. Both fields are non-negative; +// direction is supplied separately. +struct IntegratedMaterialBudget { + float xOverX0; ///< thickness in units of radiation length + float arealDensityGPerCm2; ///< crossed length*density, g/cm^2 +}; + +// Reasons a scalar material-physics operation can fail. SourceSurfaceKindMismatch, +// NonFiniteState, InvalidStateKinematics, and InvalidCovariance are reserved +// for future full-state operations and are never emitted by +// calculateMaterialPhysics(). +enum class MaterialFailureReason : uint8_t { + None = 0, + SourceSurfaceKindMismatch = 1, + NonFiniteState = 2, + InvalidStateKinematics = 3, + InvalidPID = 4, + ChargedMasslessPID = 5, + InvalidDirection = 6, + InvalidMaterial = 7, + StoppedInMaterial = 8, + MomentumBelowMinimum = 9, + ExcessiveScattering = 10, + InvalidCovariance = 11, + NonFiniteResult = 12 +}; + +enum class MaterialOperationFlags : uint8_t { + None = 0, + SubstepCountClamped = 1 +}; + +// Result of one scalar material-physics evaluation. On failure, +// momentumBeforeGeV echoes the input, failure gives the reason, and all other +// fields are deterministic zero/None values with no physical meaning. +// reserved is always zero. +struct MaterialOperationResult { + float momentumBeforeGeV; + float momentumAfterGeV; + float signedEnergyChangeGeV; + float highlandTheta2Rad2; + float relativeInverseMomentumVariance; + uint8_t energyLossSubsteps; + MaterialOperationFlags flags; + MaterialFailureReason failure; + uint8_t reserved; + + bool ok() const noexcept { return failure == MaterialFailureReason::None; } +}; + +// Lock the current in-memory layout; this is not a serialized or device ABI. +#define O2_ITSMFT_MATERIAL_ASSERT_LAYOUT(Type, Size, Alignment) \ + static_assert(std::is_standard_layout_v); \ + static_assert(std::is_trivially_copyable_v); \ + static_assert(sizeof(Type) == Size); \ + static_assert(alignof(Type) == Alignment) + +O2_ITSMFT_MATERIAL_ASSERT_LAYOUT(IntegratedMaterialBudget, 8, 4); +O2_ITSMFT_MATERIAL_ASSERT_LAYOUT(MaterialOperationResult, 24, 4); + +#undef O2_ITSMFT_MATERIAL_ASSERT_LAYOUT + +static_assert(offsetof(MaterialOperationResult, momentumBeforeGeV) == 0); +static_assert(offsetof(MaterialOperationResult, momentumAfterGeV) == 4); +static_assert(offsetof(MaterialOperationResult, signedEnergyChangeGeV) == 8); +static_assert(offsetof(MaterialOperationResult, highlandTheta2Rad2) == 12); +static_assert(offsetof(MaterialOperationResult, relativeInverseMomentumVariance) == 16); +static_assert(offsetof(MaterialOperationResult, energyLossSubsteps) == 20); +static_assert(offsetof(MaterialOperationResult, flags) == 21); +static_assert(offsetof(MaterialOperationResult, failure) == 22); +static_assert(offsetof(MaterialOperationResult, reserved) == 23); + +// Detector-neutral, PID/absCharge-aware scalar material-physics kernel. +// pid supplies the mass; absCharge supplies |q| for energy-loss and +// scattering scale factors. It need not equal PID::getCharge(). For +// absCharge == 0, validation still runs, then the operation succeeds with +// unchanged momentum and zero material effects. +// +// Validation precedence (first failure wins): invalid direction, negative +// material, non-positive momentum, invalid PID, then a charged massless PID. +// The PID range is checked before accessing its mass. +// +// For charged massive states, non-positive beta^2 is rejected before either +// material-effect calculation. Failure gives NonFiniteResult. +// +// For charged massive states, momentumGeV is the caller-selected physical +// momentum; no covariance projection is performed. Energy loss uses the same +// capped-substep Bethe-Bloch algorithm as +// o2::track::TrackParametrizationWithError::correctForMaterial(). The +// requested substep count is +// 1 + floor(|dE_full| / eKin * o2::track::ELoss2EKinThreshInv) +// with a range-bounded float-to-int conversion, capped at +// o2::track::MaxELossIter (50). flags marks SubstepCountClamped when the +// request exceeds 50. All arealDensityGPerCm2 is processed; only the +// granularity changes. Bethe-Bloch is recomputed from the current momentum +// at each substep. +// MaterialTraversalDirection::AlongMomentum subtracts energy per substep; +// OppositeMomentum adds it; signedEnergyChangeGeV is always +// Eafter - Ebefore. A particle whose energy would fall to or below its rest +// mass fails with StoppedInMaterial; a particle that completes with +// momentum below 0.01 GeV/c fails with MomentumBelowMinimum. +// +// highlandTheta2Rad2 and relativeInverseMomentumVariance use the simplified +// O2 Highland variance (no logarithmic correction) and pre-material momentum, +// energy, and beta. Both scale with absCharge^2. highlandTheta2Rad2 > pi^2 +// fails with ExcessiveScattering. +// +// This kernel does not construct track states, detector geometry, or +// ITS/MFT/topology/propagation objects. +MaterialOperationResult calculateMaterialPhysics( + float momentumGeV, + o2::track::PID pid, + uint8_t absCharge, + MaterialTraversalDirection direction, + IntegratedMaterialBudget material) noexcept; + +} // namespace o2::itsmft::tracking::material + +#endif // ALICEO2_ITSMFT_TRACKING_MATERIALPHYSICS_H_ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Propagator.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Propagator.h new file mode 100644 index 0000000000000..4c6636c1c94bb --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Propagator.h @@ -0,0 +1,85 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_PROPAGATOR_H_ +#define ALICEO2_ITSMFT_TRACKING_PROPAGATOR_H_ + +#include "GPUCommonDef.h" + +#ifndef GPUCA_GPUCODE + +#include "ITSMFTTracking/MaterialPhysics.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/SurfaceTrackState.h" +#include "ITSMFTTracking/SurfaceMeasurement.h" +#include "ITSMFTTracking/SurfaceStateOperationResult.h" + +// Descriptor-driven propagation using the material and kind resolved from +// SurfaceDescriptor and SurfaceCatalogView. +namespace o2::itsmft::tracking +{ + +class Propagator +{ + public: + // Convert to the target descriptor's convention, then propagate, apply its + // material, gate the residual and update using the nonlinear seed fit. + // State and chi2 are committed only after complete success. + static bool attachMeasurement(SurfaceTrackState& state, const SurfaceDescriptor& targetSurface, + const SurfaceMeasurement& measurement, float bz, + material::MaterialTraversalDirection direction, + bool chi2GateEnabled, float maxChi2, float& chi2, + OperationFailureReason& reason) noexcept; + + // Compatibility chi2 for two states in the same surface convention. The + // coordinate convention is selected from the states, never by the caller. + static bool stateChi2(const SurfaceTrackState& reference, const SurfaceTrackState& candidate, + float& chi2, OperationFailureReason& reason) noexcept; + + // Propagate in the state’s current surface convention to its target + // reference coordinate. Disk transport uses helix propagation for + // |bz| > 0.01f and linear transport otherwise. Both objects are unchanged + // on failure when a linearization reference is supplied. + static bool propagateToReference(SurfaceTrackState& state, float targetReferenceCoordinate, float bz, + OperationFailureReason& reason) noexcept; + static bool propagateToReference(SurfaceTrackState& state, SurfaceTrackParameters& linRef, + float targetReferenceCoordinate, float bz, + OperationFailureReason& reason) noexcept; + + // Re-express the state on the fixed target plane through its nominal point: + // fixed z for Disk, fixed local x and radial alpha for Cylinder. Transport + // the covariance with the surface-intersection Jacobian, including the + // direction variation in bz. A matching kind is a no-op. + // + // Preserves absCharge, PID, and all fields outside the parameter convention. + // Rejects tangent/unsupported directions and non-finite conversions without + // changing the state. Cylinder targets require an outward radial direction. + static bool convertKind(SurfaceTrackState& state, SurfaceKind targetKind, float bz, + OperationFailureReason& reason) noexcept; + + // Propagate to a measurement, converting the state to the target surface + // kind when needed, then applying material, the chi2 gate, and the update. + // State, reference, and chi2 are committed only after complete success. + // + // The incoming chi2 must be finite and non-negative. maxChi2 is validated + // the same way when the gate is enabled. + static bool propagateToMeasurement(SurfaceTrackState& state, SurfaceTrackParameters& linRef, + const SurfaceDescriptor& targetSurface, const SurfaceMeasurement& targetMeasurement, + float bz, material::MaterialTraversalDirection direction, + bool chi2GateEnabled, float maxChi2, float& chi2, + bool shiftReferenceToMeasurement, OperationFailureReason& reason) noexcept; +}; + +} // namespace o2::itsmft::tracking + +#endif // GPUCA_GPUCODE + +#endif /* ALICEO2_ITSMFT_TRACKING_PROPAGATOR_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ROFLookupTables.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ROFLookupTables.h index e6259ee576f10..77b7fcb02abb1 100644 --- a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ROFLookupTables.h +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ROFLookupTables.h @@ -16,6 +16,10 @@ #include #include #include +#include +#include +#include +#include #include #include @@ -30,342 +34,78 @@ #include "DataFormatsITS/Vertex.h" #include "GPUCommonMath.h" #include "GPUCommonDef.h" +#include "ITSMFTTracking/ROFViews.h" -namespace o2::its +namespace o2::itsmft::tracking { -// Layer timing definition -struct LayerTiming { - using BCType = TimeStampType; - using BCRange = dataformats::RangeReference; - BCType mNROFsTF{0}; // number of ROFs per timeframe - BCType mROFLength{0}; // ROF length in BC - BCType mROFDelay{0}; // delay of ROFs wrt start of first orbit in TF in BC - BCType mROFBias{0}; // bias wrt to the LHC clock in BC - BCType mROFAddTimeErr{0}; // additionally imposed uncertainty on ROF time in BC - - // return start of ROF in BC - // this does not account for the opt. error! - GPUhdi() BCType getROFStartInBC(BCType rofId) const noexcept - { - assert(rofId < mNROFsTF && rofId >= 0); - return (mROFLength * rofId) + mROFDelay + mROFBias; - } - - // return end of ROF in BCs - // this does not account for the opt. error! - GPUhdi() BCType getROFEndInBC(BCType rofId) const noexcept - { - assert(rofId < mNROFsTF); - return getROFStartInBC(rofId) + mROFLength; - } - - // return (clamped) time-interval of rof - GPUhdi() TimeEstBC getROFTimeBounds(BCType rofId, bool withError = false) const noexcept - { - if (withError) { - int64_t start = getROFStartInBC(rofId); - int64_t end = getROFEndInBC(rofId); - start = o2::gpu::CAMath::Max(start - mROFAddTimeErr, int64_t(0)); - end += mROFAddTimeErr; - return {static_cast(start), static_cast(end - start)}; - } - return {getROFStartInBC(rofId), static_cast(mROFLength)}; - } - - // return which ROF this BC belongs to - GPUhdi() BCType getROF(BCType bc) const noexcept - { - const BCType offset = mROFDelay + mROFBias; - if (bc <= offset) { - return 0; - } - return (bc - offset) / mROFLength; - } - - // return which ROF this timestamp belongs by its lower edge - GPUhdi() BCType getROF(TimeStamp ts) const noexcept - { - const BCType offset = mROFDelay + mROFBias; - const BCType bc = (ts.getTimeStamp() < ts.getTimeStampError()) ? BCType(0) : static_cast(o2::gpu::CAMath::Floor(ts.getTimeStamp() - ts.getTimeStampError())); - if (bc <= offset) { - return 0; - } - return (bc - offset) / mROFLength; - } - - // return which ROF this floating point (number of BCs) time belongs - GPUhdi() BCType getROF(float time) const noexcept - { - const float offset = static_cast(mROFDelay + mROFBias); - if (time <= offset) { - return 0; - } - return static_cast((time - offset) / mROFLength); - } - - GPUhdi() bool intersectROF(BCType rof, float lower, float upper) const noexcept - { - const auto rofTS = getROFTimeBounds(rof, true); - return static_cast(rofTS.upper()) > lower && upper > static_cast(rofTS.lower()); - } - - // return clamped ROF range with strictly positive overlap with timestamp interval - GPUhdi() BCRange getROFRange(TimeStamp ts) const noexcept - { - const float lower = ts.getTimeStamp() - ts.getTimeStampError(); - const float upper = ts.getTimeStamp() + ts.getTimeStampError(); - return getROFRange(lower, upper); - } - - GPUhdi() BCRange getROFRange(TimeEstBC ts) const noexcept - { - return getROFRange(static_cast(ts.lower()), static_cast(ts.upper())); - } - - GPUhdi() BCRange getROFRange(float lower, float upper) const noexcept - { - const BCType maxROF = mNROFsTF - 1; - BCType first = o2::gpu::CAMath::Clamp(getROF(lower - mROFAddTimeErr), BCType{0}, maxROF); - BCType last = o2::gpu::CAMath::Clamp(getROF(upper + mROFAddTimeErr), BCType{0}, maxROF); - - if (first <= last && !intersectROF(first, lower, upper)) { - ++first; - } - if (last >= first && !intersectROF(last, lower, upper)) { - --last; - } - return {first, first <= last ? static_cast(last - first + 1) : BCType{0}}; - } - -#ifndef GPUCA_GPUCODE - GPUh() std::string asString() const - { - return std::format("NROFsPerTF {:4} ROFLength {:4} ({:4} per Orbit) ROFDelay {:4} ROFBias {:4} ROFAddTimeErr {:4}", mNROFsTF, mROFLength, (o2::constants::lhc::LHCMaxBunches / mROFLength), mROFDelay, mROFBias, mROFAddTimeErr); - } - - GPUh() void print() const - { - LOG(info) << asString(); - } -#endif -}; +using LayerTiming = ROFTimingLayer; // Base class for lookup to define layers -template class LayerTimingBase { protected: - LayerTiming mLayers[NLayers]; + std::vector mLayers; public: using T = LayerTiming::BCType; - LayerTimingBase() = default; + explicit LayerTimingBase(int32_t nLayers = 0) + { + if (nLayers < 0) { + throw std::invalid_argument{"negative ROF layer count"}; + } + mLayers.resize(nLayers); + } GPUh() void defineLayer(int32_t layer, T nROFsTF, T rofLength, T rofDelay, T rofBias, T rofTE) { - assert(layer >= 0 && layer < NLayers); + assert(layer >= 0 && layer < getEntries()); mLayers[layer] = {nROFsTF, rofLength, rofDelay, rofBias, rofTE}; } GPUh() void defineLayer(int32_t layer, const LayerTiming& timing) { - assert(layer >= 0 && layer < NLayers); + assert(layer >= 0 && layer < getEntries()); mLayers[layer] = timing; } - GPUhdi() const LayerTiming& getLayer(int32_t layer) const + GPUh() const LayerTiming& getLayer(int32_t layer) const { - assert(layer >= 0 && layer < NLayers); + assert(layer >= 0 && layer < getEntries()); return mLayers[layer]; } - GPUhdi() constexpr int32_t getEntries() noexcept { return NLayers; } + GPUh() int32_t getEntries() const noexcept { return static_cast(mLayers.size()); } #ifndef GPUCA_GPUCODE GPUh() void print() const { LOGP(info, "Imposed time structure:"); - for (int32_t iL{0}; iL < NLayers; ++iL) { + for (int32_t iL{0}; iL < getEntries(); ++iL) { LOGP(info, "\tLayer:{} {}", iL, mLayers[iL].asString()); } } #endif }; -// GPU friendly view of the table below -template -struct ROFOverlapTableView { - const TableEntry* mFlatTable{nullptr}; - const TableIndex* mIndices{nullptr}; - const LayerTiming* mLayers{nullptr}; - - GPUhdi() const LayerTiming& getLayer(int32_t layer) const noexcept - { - assert(layer >= 0 && layer < NLayers); - return mLayers[layer]; - } - - GPUh() int32_t getClock() const noexcept - { - // we take the fastest layer as clock - int32_t fastest = 0; - uint32_t maxNROFs{0}; - for (int32_t iL{0}; iL < NLayers; ++iL) { - const auto& layer = getLayer(iL); - // by definition the fastest layer has the most ROFs - // this also solves the problem of a delay large than ROFLength - // if mNROFsTF is correct - if (layer.mNROFsTF > maxNROFs) { - fastest = iL; - maxNROFs = layer.mNROFsTF; - } - } - return fastest; - } - - GPUh() const LayerTiming& getClockLayer() const noexcept - { - return mLayers[getClock()]; - } - - GPUhdi() const TableEntry& getOverlap(int32_t from, int32_t to, size_t rofIdx) const noexcept - { - assert(from < NLayers && to < NLayers); - const size_t linearIdx = (from * NLayers) + to; - const auto& idx = mIndices[linearIdx]; - assert(rofIdx < idx.getEntries()); - return mFlatTable[idx.getFirstEntry() + rofIdx]; - } - - GPUhdi() bool doROFsOverlap(int32_t layer0, size_t rof0, int32_t layer1, size_t rof1) const noexcept - { - if (layer0 == layer1) { // layer is compatible with itself - return rof0 == rof1; - } - - assert(layer0 < NLayers && layer1 < NLayers); - const size_t linearIdx = (layer0 * NLayers) + layer1; - const auto& idx = mIndices[linearIdx]; - - if (rof0 >= idx.getEntries()) { - return false; - } - - const auto& overlap = mFlatTable[idx.getFirstEntry() + rof0]; - - if (overlap.getEntries() == 0) { - return false; - } - - const size_t firstCompatible = overlap.getFirstEntry(); - const size_t lastCompatible = firstCompatible + overlap.getEntries() - 1; - return rof1 >= firstCompatible && rof1 <= lastCompatible; - } - - GPUhdi() TimeEstBC getTimeStamp(int32_t layer0, size_t rof0, int32_t layer1, size_t rof1) const noexcept - { - assert(layer0 < NLayers && layer1 < NLayers); - assert(doROFsOverlap(layer0, rof0, layer1, rof1)); - // retrieves the combined timestamp - // e.g., taking one cluster from rof0 and one from rof1 - // and constructing a tracklet (doublet) what is its time - // this assumes that the rofs overlap, e.g. doROFsOverlap -> true - // get timestamp including margins from rof0 and rof1 - const auto t0 = mLayers[layer0].getROFTimeBounds(rof0, true); - const auto t1 = mLayers[layer1].getROFTimeBounds(rof1, true); - return t0 + t1; - } - -#ifndef GPUCA_GPUCODE - /// Print functions - GPUh() void printAll() const - { - for (int32_t i = 0; i < NLayers; ++i) { - for (int32_t j = 0; j < NLayers; ++j) { - if (i != j) { - printMapping(i, j); - } - } - } - printSummary(); - } - - GPUh() void printMapping(int32_t from, int32_t to) const - { - if (from == to) { - LOGP(error, "No self-lookup supported"); - return; - } - - constexpr int w_index = 10; - constexpr int w_first = 12; - constexpr int w_last = 12; - constexpr int w_count = 10; - - LOGF(info, "Overlap mapping: Layer %d -> Layer %d", from, to); - LOGP(info, "From: {}", mLayers[from].asString()); - LOGP(info, "To : {}", mLayers[to].asString()); - LOGF(info, "%*s | %*s | %*s | %*s", w_index, "ROF.index", w_first, "First.ROF", w_last, "Last.ROF", w_count, "Count"); - LOGF(info, "%.*s-+-%.*s-+-%.*s-+-%.*s", w_index, "----------", w_first, "------------", w_last, "------------", w_count, "----------"); - - const size_t linearIdx = (from * NLayers) + to; - const auto& idx = mIndices[linearIdx]; - for (int32_t i = 0; i < idx.getEntries(); ++i) { - const auto& overlap = getOverlap(from, to, i); - LOGF(info, "%*d | %*d | %*d | %*d", w_index, i, w_first, overlap.getFirstEntry(), w_last, overlap.getEntriesBound() - 1, w_count, overlap.getEntries()); - } - } - - GPUh() void printSummary() const - { - uint32_t totalEntries{0}; - size_t flatTableSize{0}; - - for (int32_t i = 0; i < NLayers; ++i) { - for (int32_t j = 0; j < NLayers; ++j) { - if (i != j) { - const size_t linearIdx = (i * NLayers) + j; - const auto& idx = mIndices[linearIdx]; - totalEntries += idx.getEntries(); - flatTableSize += idx.getEntries(); - } - } - } - - for (int32_t i = 0; i < NLayers; ++i) { - mLayers[i].print(); - } - - const uint32_t totalBytes = (flatTableSize * sizeof(TableEntry)) + (static_cast(NLayers * NLayers) * sizeof(TableIndex)); - LOGF(info, "------------------------------------------------------------"); - LOGF(info, "Total overlap table size: %u entries", totalEntries); - LOGF(info, "Flat table size: %zu entries", flatTableSize); - LOGF(info, "Total view size: %u bytes", totalBytes); - LOGF(info, "------------------------------------------------------------"); - } -#endif -}; - // Precalculated lookup table to find overlapping ROFs in another layer given a ROF index in the current layer -template -class ROFOverlapTable : public LayerTimingBase +class ROFOverlapTable : public LayerTimingBase { public: - using T = LayerTimingBase::T; + using T = LayerTimingBase::T; using TableEntry = dataformats::RangeReference; using TableIndex = dataformats::RangeReference; - using View = ROFOverlapTableView; - ROFOverlapTable() = default; + using View = ROFOverlapView; + explicit ROFOverlapTable(int32_t nLayers = 0) : LayerTimingBase(nLayers), mIndices(static_cast(nLayers) * nLayers) {} GPUh() void init() { - std::vector table[NLayers][NLayers]; - for (int32_t i{0}; i < NLayers; ++i) { - for (int32_t j{0}; j < NLayers; ++j) { + std::vector> table(static_cast(getEntries()) * getEntries()); + for (int32_t i{0}; i < getEntries(); ++i) { + for (int32_t j{0}; j < getEntries(); ++j) { if (i != j) { // we do not need self-lookup - buildMapping(i, j, table[i][j]); + buildMapping(i, j, table[static_cast(i) * getEntries() + j]); } } } @@ -376,8 +116,9 @@ class ROFOverlapTable : public LayerTimingBase { View view; view.mFlatTable = mFlatTable.data(); - view.mIndices = mIndices; - view.mLayers = this->mLayers; + view.mIndices = mIndices.data(); + view.mLayers = mLayers.data(); + view.mLayerCount = getEntries(); return view; } @@ -387,11 +128,12 @@ class ROFOverlapTable : public LayerTimingBase view.mFlatTable = deviceFlatTablePtr; view.mIndices = deviceIndicesPtr; view.mLayers = deviceLayerTimingPtr; + view.mLayerCount = getEntries(); return view; } GPUh() size_t getFlatTableSize() const noexcept { return mFlatTable.size(); } - static GPUh() constexpr size_t getIndicesSize() { return static_cast(NLayers * NLayers); } + GPUh() size_t getIndicesSize() const noexcept { return mIndices.size(); } private: GPUh() void buildMapping(int32_t from, int32_t to, std::vector& table) @@ -430,26 +172,27 @@ class ROFOverlapTable : public LayerTimingBase } } - GPUh() void flatten(const std::vector table[NLayers][NLayers]) + GPUh() void flatten(const std::vector>& table) { size_t total{0}; - for (int32_t i{0}; i < NLayers; ++i) { - for (int32_t j{0}; j < NLayers; ++j) { + for (int32_t i{0}; i < getEntries(); ++i) { + for (int32_t j{0}; j < getEntries(); ++j) { if (i != j) { // we do not need self-lookup - total += table[i][j].size(); + total += table[static_cast(i) * getEntries() + j].size(); } } } + mFlatTable.clear(); mFlatTable.reserve(total); - for (int32_t i{0}; i < NLayers; ++i) { - for (int32_t j{0}; j < NLayers; ++j) { - size_t idx = (i * NLayers) + j; + for (int32_t i{0}; i < getEntries(); ++i) { + for (int32_t j{0}; j < getEntries(); ++j) { + size_t idx = static_cast(i) * getEntries() + j; if (i != j) { mIndices[idx].setFirstEntry(static_cast(mFlatTable.size())); - mIndices[idx].setEntries(static_cast(table[i][j].size())); - mFlatTable.insert(mFlatTable.end(), table[i][j].begin(), table[i][j].end()); + mIndices[idx].setEntries(static_cast(table[static_cast(i) * getEntries() + j].size())); + mFlatTable.insert(mFlatTable.end(), table[static_cast(i) * getEntries() + j].begin(), table[static_cast(i) * getEntries() + j].end()); } else { mIndices[idx] = {0, 0}; } @@ -457,141 +200,39 @@ class ROFOverlapTable : public LayerTimingBase } } - TableIndex mIndices[NLayers * NLayers]; + std::vector mIndices; std::vector mFlatTable; }; -// GPU friendly view of the table below -template -struct ROFVertexLookupTableView { - const TableEntry* mFlatTable{nullptr}; - const TableIndex* mIndices{nullptr}; - const LayerTiming* mLayers{nullptr}; - - GPUhdi() const LayerTiming& getLayer(int32_t layer) const noexcept - { - assert(layer >= 0 && layer < NLayers); - return mLayers[layer]; - } - - GPUhdi() const TableEntry& getVertices(int32_t layer, size_t rofIdx) const noexcept - { - assert(layer < NLayers); - const auto& idx = mIndices[layer]; - assert(rofIdx < idx.getEntries()); - return mFlatTable[idx.getFirstEntry() + rofIdx]; - } - - GPUh() int32_t getMaxVerticesPerROF() const noexcept - { - int32_t maxCount = 0; - for (int32_t layer = 0; layer < NLayers; ++layer) { - const auto& idx = mIndices[layer]; - for (int32_t i = 0; i < idx.getEntries(); ++i) { - const auto& entry = mFlatTable[idx.getFirstEntry() + i]; - maxCount = o2::gpu::CAMath::Max(maxCount, static_cast(entry.getEntries())); - } - } - return maxCount; - } - - // Check if a specific vertex is compatible with a given ROF - GPUhdi() bool isVertexCompatible(int32_t layer, size_t rofIdx, const Vertex& vertex) const noexcept - { - assert(layer < NLayers); - const auto& layerDef = mLayers[layer]; - int64_t rofLower = o2::gpu::CAMath::Max((int64_t)layerDef.getROFStartInBC(rofIdx) - (int64_t)layerDef.mROFAddTimeErr, int64_t(0)); - int64_t rofUpper = (int64_t)layerDef.getROFEndInBC(rofIdx) + layerDef.mROFAddTimeErr; - auto vLower = (int64_t)vertex.getTimeStamp().lower(); - auto vUpper = (int64_t)vertex.getTimeStamp().upper(); - return vUpper >= rofLower && vLower < rofUpper; - } - -#ifndef GPUCA_GPUCODE - GPUh() void printAll() const - { - for (int32_t i = 0; i < NLayers; ++i) { - printLayer(i); - } - printSummary(); - } - - GPUh() void printLayer(int32_t layer) const - { - constexpr int w_rof = 10; - constexpr int w_first = 12; - constexpr int w_last = 12; - constexpr int w_count = 10; - - LOGF(info, "Vertex lookup: Layer %d", layer); - LOGF(info, "%*s | %*s | %*s | %*s", w_rof, "ROF.index", w_first, "First.Vtx", w_last, "Last.Vtx", w_count, "Count"); - LOGF(info, "%.*s-+-%.*s-+-%.*s-+-%.*s", w_rof, "----------", w_first, "------------", w_last, "------------", w_count, "----------"); - - const auto& idx = mIndices[layer]; - for (int32_t i = 0; i < idx.getEntries(); ++i) { - const auto& entry = mFlatTable[idx.getFirstEntry() + i]; - int first = entry.getFirstEntry(); - int count = entry.getEntries(); - int last = first + count - 1; - LOGF(info, "%*d | %*d | %*d | %*d", w_rof, i, w_first, first, w_last, last, w_count, count); - } - } - - GPUh() void printSummary() const - { - uint32_t totalROFs{0}; - uint32_t totalVertexRefs{0}; - - for (int32_t i = 0; i < NLayers; ++i) { - const auto& idx = mIndices[i]; - totalROFs += idx.getEntries(); - - for (int32_t j = 0; j < idx.getEntries(); ++j) { - const auto& entry = mFlatTable[idx.getFirstEntry() + j]; - totalVertexRefs += entry.getEntries(); - } - } - - const uint32_t totalBytes = (totalROFs * sizeof(TableEntry)) + (NLayers * sizeof(TableIndex)); - LOGF(info, "------------------------------------------------------------"); - LOGF(info, "Total ROFs in table: %u", totalROFs); - LOGF(info, "Total vertex references: %u", totalVertexRefs); - LOGF(info, "Total view size: %u bytes", totalBytes); - LOGF(info, "------------------------------------------------------------"); - } -#endif -}; - // Precalculated lookup table to find vertices compatible with ROFs // Given a layer and ROF index, returns the range of vertices that overlap in time. // The vertex time is defined as symmetrical [t0-e,t0+e] // It needs to be guaranteed that the input vertices are sorted by their lower-bound! // additionally compatibliyty has to be queried per vertex! -template -class ROFVertexLookupTable : public LayerTimingBase +class ROFVertexLookupTable : public LayerTimingBase { public: - using T = LayerTimingBase::T; + using T = LayerTimingBase::T; using BCType = LayerTiming::BCType; using TableEntry = dataformats::RangeReference; using TableIndex = dataformats::RangeReference; - using View = ROFVertexLookupTableView; + using View = ROFVertexLookupView; - ROFVertexLookupTable() = default; + explicit ROFVertexLookupTable(int32_t nLayers = 0) : LayerTimingBase(nLayers), mIndices(nLayers) {} GPUh() size_t getFlatTableSize() const noexcept { return mFlatTable.size(); } - static GPUh() constexpr size_t getIndicesSize() { return NLayers; } + GPUh() size_t getIndicesSize() const noexcept { return mIndices.size(); } // Build the lookup table given a sorted array of vertices // vertices must be sorted by timestamp, then by error (secondary) - GPUh() void init(const Vertex* vertices, size_t nVertices) + GPUh() void init(const o2::its::Vertex* vertices, size_t nVertices) { if (nVertices > std::numeric_limits::max()) { LOGF(fatal, "too many vertices %zu, max supported is %u", nVertices, std::numeric_limits::max()); } - std::vector table[NLayers]; - for (int32_t layer{0}; layer < NLayers; ++layer) { + std::vector> table(getEntries()); + for (int32_t layer{0}; layer < getEntries(); ++layer) { buildMapping(layer, vertices, nVertices, table[layer]); } flatten(table); @@ -601,12 +242,12 @@ class ROFVertexLookupTable : public LayerTimingBase GPUh() void init() { size_t total{0}; - for (int32_t layer{0}; layer < NLayers; ++layer) { + for (int32_t layer{0}; layer < getEntries(); ++layer) { total += this->mLayers[layer].mNROFsTF; } mFlatTable.resize(total, {0, 0}); size_t offset = 0; - for (int32_t layer{0}; layer < NLayers; ++layer) { + for (int32_t layer{0}; layer < getEntries(); ++layer) { size_t nROFs = this->mLayers[layer].mNROFsTF; mIndices[layer].setFirstEntry(static_cast(offset)); mIndices[layer].setEntries(static_cast(nROFs)); @@ -615,10 +256,10 @@ class ROFVertexLookupTable : public LayerTimingBase } // Recalculate lookup table with new vertices - GPUh() void update(const Vertex* vertices, size_t nVertices) + GPUh() void update(const o2::its::Vertex* vertices, size_t nVertices) { size_t offset = 0; - for (int32_t layer{0}; layer < NLayers; ++layer) { + for (int32_t layer{0}; layer < getEntries(); ++layer) { const auto& idx = mIndices[layer]; size_t nROFs = idx.getEntries(); for (size_t iROF = 0; iROF < nROFs; ++iROF) { @@ -632,8 +273,9 @@ class ROFVertexLookupTable : public LayerTimingBase { View view; view.mFlatTable = mFlatTable.data(); - view.mIndices = mIndices; - view.mLayers = this->mLayers; + view.mIndices = mIndices.data(); + view.mLayers = mLayers.data(); + view.mLayerCount = getEntries(); return view; } @@ -643,12 +285,13 @@ class ROFVertexLookupTable : public LayerTimingBase view.mFlatTable = deviceFlatTablePtr; view.mIndices = deviceIndicesPtr; view.mLayers = deviceLayerTimingPtr; + view.mLayerCount = getEntries(); return view; } private: // Build the mapping for one layer - GPUh() void buildMapping(int32_t layer, const Vertex* vertices, size_t nVertices, std::vector& table) + GPUh() void buildMapping(int32_t layer, const o2::its::Vertex* vertices, size_t nVertices, std::vector& table) { const auto& layerDef = this->mLayers[layer]; table.resize(layerDef.mNROFsTF); @@ -672,7 +315,7 @@ class ROFVertexLookupTable : public LayerTimingBase } // Update a single ROF's vertex mapping - GPUh() void updateROFMapping(int32_t layer, size_t iROF, const Vertex* vertices, size_t nVertices, size_t flatTableIdx) + GPUh() void updateROFMapping(int32_t layer, size_t iROF, const o2::its::Vertex* vertices, size_t nVertices, size_t flatTableIdx) { const auto& layerDef = this->mLayers[layer]; int64_t rofLower = o2::gpu::CAMath::Max((int64_t)layerDef.getROFStartInBC(iROF) - (int64_t)layerDef.mROFAddTimeErr, int64_t(0)); @@ -693,7 +336,7 @@ class ROFVertexLookupTable : public LayerTimingBase } // Binary search for first vertex where lowerBC >= targetBC - GPUh() size_t binarySearchFirst(const Vertex* vertices, size_t nVertices, size_t searchStart, BCType targetBC) const + GPUh() size_t binarySearchFirst(const o2::its::Vertex* vertices, size_t nVertices, size_t searchStart, BCType targetBC) const { size_t left = searchStart; size_t right = nVertices; @@ -710,102 +353,50 @@ class ROFVertexLookupTable : public LayerTimingBase } // Compress the temporary table into a single flat table - GPUh() void flatten(const std::vector table[NLayers]) + GPUh() void flatten(const std::vector>& table) { // Count total entries size_t total{0}; - for (int32_t i{0}; i < NLayers; ++i) { + for (int32_t i{0}; i < getEntries(); ++i) { total += table[i].size(); } + mFlatTable.clear(); mFlatTable.reserve(total); // Build flat table and indices - for (int32_t i{0}; i < NLayers; ++i) { + for (int32_t i{0}; i < getEntries(); ++i) { mIndices[i].setFirstEntry(static_cast(mFlatTable.size())); mIndices[i].setEntries(static_cast(table[i].size())); mFlatTable.insert(mFlatTable.end(), table[i].begin(), table[i].end()); } } - TableIndex mIndices[NLayers]; + std::vector mIndices; std::vector mFlatTable; }; -// GPU-friendly view of the ROF mask table -template -struct ROFMaskTableView { - const TableEntry* mFlatMask{nullptr}; - const TableIndex* mLayerROFOffsets{nullptr}; // size NLayers+1 - - GPUhdi() bool isROFEnabled(int32_t layer, int32_t rofId) const noexcept - { - assert(layer >= 0 && layer < NLayers); - return mFlatMask[mLayerROFOffsets[layer] + rofId] != 0u; - } - -#ifndef GPUCA_GPUCODE - GPUh() void printAll() const - { - for (int32_t i = 0; i < NLayers; ++i) { - printLayer(i); - } - } - - GPUh() void printLayer(int32_t layer) const - { - constexpr int w_rof = 10; - constexpr int w_active = 10; - int32_t nROFs = mLayerROFOffsets[layer + 1] - mLayerROFOffsets[layer]; - LOGF(info, "Mask table: Layer %d", layer); - LOGF(info, "%*s | %*s", w_rof, "ROF", w_active, "Enabled"); - LOGF(info, "%.*s-+-%.*s", w_rof, "----------", w_active, "----------"); - for (int32_t i = 0; i < nROFs; ++i) { - LOGF(info, "%*d | %*d", w_rof, i, w_active, (int)isROFEnabled(layer, i)); - } - } - - GPUh() std::string asString(int32_t layer) const - { - int32_t nROFs = mLayerROFOffsets[layer + 1] - mLayerROFOffsets[layer]; - int32_t enabledROFs = 0; - for (int32_t j = 0; j < nROFs; ++j) { - if (isROFEnabled(layer, j)) { - ++enabledROFs; - } - } - return std::format("ROFMask on Layer {} ROFs enabled: {}/{}", layer, enabledROFs, nROFs); - } - - GPUh() void print(int32_t layer) const - { - LOG(info) << asString(layer); - } -#endif -}; - // Per-ROF per-layer boolean mask (uint8_t for GPU compatibility). -template -class ROFMaskTable : public LayerTimingBase +class ROFMaskTable : public LayerTimingBase { public: - using T = LayerTimingBase::T; + using T = LayerTimingBase::T; using BCRange = dataformats::RangeReference; using TableIndex = uint32_t; using TableEntry = uint8_t; - using View = ROFMaskTableView; + using View = ROFMaskView; - ROFMaskTable() = default; - GPUh() explicit ROFMaskTable(const LayerTimingBase& timingBase) : LayerTimingBase(timingBase) { init(); } + explicit ROFMaskTable(int32_t nLayers = 0) : LayerTimingBase(nLayers), mLayerROFOffsets(static_cast(nLayers) + 1, 0) {} + GPUh() explicit ROFMaskTable(const LayerTimingBase& timingBase) : LayerTimingBase(timingBase), mLayerROFOffsets(static_cast(getEntries()) + 1, 0) { init(); } GPUh() void init() { int32_t totalROFs = 0; - for (int32_t layer{0}; layer < NLayers; ++layer) { + for (int32_t layer{0}; layer < getEntries(); ++layer) { mLayerROFOffsets[layer] = totalROFs; totalROFs += this->getLayer(layer).mNROFsTF; } - mLayerROFOffsets[NLayers] = totalROFs; // sentinel + mLayerROFOffsets[getEntries()] = totalROFs; // sentinel mFlatMask.resize(totalROFs, 0u); } @@ -813,14 +404,14 @@ class ROFMaskTable : public LayerTimingBase GPUh() void setROFEnabled(int32_t layer, int32_t rofId, uint8_t state = 1) noexcept { - assert(layer >= 0 && layer < NLayers); + assert(layer >= 0 && layer < getEntries()); assert(rofId >= 0 && rofId < mLayerROFOffsets[layer + 1] - mLayerROFOffsets[layer]); mFlatMask[mLayerROFOffsets[layer] + rofId] = state; } GPUh() void setROFsEnabled(int32_t layer, int32_t firstRof, int32_t nRofs, uint8_t state = 1) noexcept { - assert(layer >= 0 && layer < NLayers); + assert(layer >= 0 && layer < getEntries()); assert(firstRof >= 0); assert(firstRof + nRofs <= mLayerROFOffsets[layer + 1] - mLayerROFOffsets[layer]); std::memset(mFlatMask.data() + mLayerROFOffsets[layer] + firstRof, state, nRofs); @@ -831,7 +422,7 @@ class ROFMaskTable : public LayerTimingBase { const int32_t bcStart = t.getFirstEntry(); const int32_t bcEnd = t.getEntriesBound(); - for (int32_t layer{0}; layer < NLayers; ++layer) { + for (int32_t layer{0}; layer < getEntries(); ++layer) { const auto& lay = this->getLayer(layer); const int32_t offset = mLayerROFOffsets[layer]; for (int32_t rofId{0}; rofId < lay.mNROFsTF; ++rofId) { @@ -864,6 +455,7 @@ class ROFMaskTable : public LayerTimingBase GPUh() void swap(ROFMaskTable& other) noexcept { + std::swap(mLayers, other.mLayers); std::swap(mFlatMask, other.mFlatMask); std::swap(mLayerROFOffsets, other.mLayerROFOffsets); } @@ -872,7 +464,8 @@ class ROFMaskTable : public LayerTimingBase { View view; view.mFlatMask = mFlatMask.data(); - view.mLayerROFOffsets = mLayerROFOffsets; + view.mLayerROFOffsets = mLayerROFOffsets.data(); + view.mLayerCount = getEntries(); return view; } @@ -881,14 +474,71 @@ class ROFMaskTable : public LayerTimingBase View view; view.mFlatMask = deviceFlatMaskPtr; view.mLayerROFOffsets = deviceOffsetPtr; + view.mLayerCount = getEntries(); return view; } private: - TableIndex mLayerROFOffsets[NLayers + 1] = {0}; + std::vector mLayerROFOffsets; std::vector mFlatMask; }; +} // namespace o2::itsmft::tracking + +namespace o2::its +{ +using LayerTiming = o2::itsmft::tracking::LayerTiming; + +// Keep the fixed-layer API for legacy ITS callers; storage and algorithms are +// shared with the runtime tables used by the common tracker. +template +class LayerTimingBase : public o2::itsmft::tracking::LayerTimingBase +{ + public: + LayerTimingBase() : o2::itsmft::tracking::LayerTimingBase(NLayers) {} + GPUhdi() constexpr int32_t getEntries() const noexcept { return NLayers; } +}; + +template +using ROFOverlapTableView = o2::itsmft::tracking::ROFOverlapView; +template +using ROFVertexLookupTableView = o2::itsmft::tracking::ROFVertexLookupView; +template +using ROFMaskTableView = o2::itsmft::tracking::ROFMaskView; + +template +class ROFOverlapTable : public o2::itsmft::tracking::ROFOverlapTable +{ + public: + ROFOverlapTable() : o2::itsmft::tracking::ROFOverlapTable(NLayers) {} + GPUhdi() constexpr int32_t getEntries() const noexcept { return NLayers; } + static GPUh() constexpr size_t getIndicesSize() { return static_cast(NLayers) * NLayers; } +}; + +template +class ROFVertexLookupTable : public o2::itsmft::tracking::ROFVertexLookupTable +{ + public: + ROFVertexLookupTable() : o2::itsmft::tracking::ROFVertexLookupTable(NLayers) {} + GPUhdi() constexpr int32_t getEntries() const noexcept { return NLayers; } + static GPUh() constexpr size_t getIndicesSize() { return NLayers; } +}; + +template +class ROFMaskTable : public o2::itsmft::tracking::ROFMaskTable +{ + public: + ROFMaskTable() : o2::itsmft::tracking::ROFMaskTable(NLayers) {} + GPUh() explicit ROFMaskTable(const o2::itsmft::tracking::LayerTimingBase& timing) + : o2::itsmft::tracking::ROFMaskTable(timing) + { + if (timing.getEntries() != NLayers) { + throw std::invalid_argument{"ROF mask layer count differs from legacy table extent"}; + } + } + GPUh() void swap(ROFMaskTable& other) noexcept { o2::itsmft::tracking::ROFMaskTable::swap(other); } + GPUhdi() constexpr int32_t getEntries() const noexcept { return NLayers; } +}; } // namespace o2::its #endif diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ROFViews.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ROFViews.h new file mode 100644 index 0000000000000..6074b211b9641 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ROFViews.h @@ -0,0 +1,382 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_ROFVIEWS_H_ +#define ALICEO2_ITSMFT_TRACKING_ROFVIEWS_H_ + +#include +#include +#include +#include + +#ifndef GPUCA_GPUCODE +#include +#endif + +#include "CommonConstants/LHCConstants.h" +#include "CommonDataFormat/RangeReference.h" +#include "DataFormatsITS/TimeEstBC.h" +#include "DataFormatsITS/Vertex.h" +#include "GPUCommonMath.h" +#include "GPUCommonDef.h" + +#ifndef GPUCA_GPUCODE +#include "Framework/Logger.h" +#endif + +namespace o2::itsmft::tracking +{ + +/// Runtime timing data used by the non-owning ROF views. The detector-side +/// fixed-capacity table builders use this same type, so the timing arithmetic +/// has one implementation at the application/core boundary. +struct ROFTimingLayer { + using BCType = o2::its::TimeStampType; + using BCRange = o2::dataformats::RangeReference; + + BCType mNROFsTF{0}; + BCType mROFLength{0}; + BCType mROFDelay{0}; + BCType mROFBias{0}; + BCType mROFAddTimeErr{0}; + + GPUhdi() BCType getROFStartInBC(BCType rofId) const noexcept + { + assert(rofId < mNROFsTF && rofId >= 0); + return (mROFLength * rofId) + mROFDelay + mROFBias; + } + + GPUhdi() BCType getROFEndInBC(BCType rofId) const noexcept + { + assert(rofId < mNROFsTF); + return getROFStartInBC(rofId) + mROFLength; + } + + GPUhdi() o2::its::TimeEstBC getROFTimeBounds(BCType rofId, bool withError = false) const noexcept + { + if (withError) { + int64_t start = getROFStartInBC(rofId); + int64_t end = getROFEndInBC(rofId); + start = o2::gpu::CAMath::Max(start - mROFAddTimeErr, int64_t(0)); + end += mROFAddTimeErr; + return {static_cast(start), static_cast(end - start)}; + } + return {getROFStartInBC(rofId), static_cast(mROFLength)}; + } + + GPUhdi() BCType getROF(BCType bc) const noexcept + { + const BCType offset = mROFDelay + mROFBias; + if (bc <= offset) { + return 0; + } + return (bc - offset) / mROFLength; + } + + GPUhdi() BCType getROF(o2::its::TimeStamp ts) const noexcept + { + const BCType offset = mROFDelay + mROFBias; + const BCType bc = (ts.getTimeStamp() < ts.getTimeStampError()) ? BCType(0) : static_cast(o2::gpu::CAMath::Floor(ts.getTimeStamp() - ts.getTimeStampError())); + if (bc <= offset) { + return 0; + } + return (bc - offset) / mROFLength; + } + + GPUhdi() BCType getROF(float time) const noexcept + { + const float offset = static_cast(mROFDelay + mROFBias); + if (time <= offset) { + return 0; + } + return static_cast((time - offset) / mROFLength); + } + + GPUhdi() bool intersectROF(BCType rof, float lower, float upper) const noexcept + { + const auto rofTS = getROFTimeBounds(rof, true); + return static_cast(rofTS.upper()) > lower && upper > static_cast(rofTS.lower()); + } + + GPUhdi() BCRange getROFRange(o2::its::TimeStamp ts) const noexcept + { + return getROFRange(ts.getTimeStamp() - ts.getTimeStampError(), ts.getTimeStamp() + ts.getTimeStampError()); + } + + GPUhdi() BCRange getROFRange(o2::its::TimeEstBC ts) const noexcept + { + return getROFRange(static_cast(ts.lower()), static_cast(ts.upper())); + } + + GPUhdi() BCRange getROFRange(float lower, float upper) const noexcept + { + const BCType maxROF = mNROFsTF - 1; + BCType first = o2::gpu::CAMath::Clamp(getROF(lower - mROFAddTimeErr), BCType{0}, maxROF); + BCType last = o2::gpu::CAMath::Clamp(getROF(upper + mROFAddTimeErr), BCType{0}, maxROF); + + if (first <= last && !intersectROF(first, lower, upper)) { + ++first; + } + if (last >= first && !intersectROF(last, lower, upper)) { + --last; + } + return {first, first <= last ? static_cast(last - first + 1) : BCType{0}}; + } + +#ifndef GPUCA_GPUCODE + GPUh() std::string asString() const + { + return std::format("NROFsPerTF {:4} ROFLength {:4} ({:4} per Orbit) ROFDelay {:4} ROFBias {:4} ROFAddTimeErr {:4}", mNROFsTF, mROFLength, (o2::constants::lhc::LHCMaxBunches / mROFLength), mROFDelay, mROFBias, mROFAddTimeErr); + } + + GPUh() void print() const + { + LOG(info) << asString(); + } +#endif +}; + +template +struct ROFOverlapView { + const TableEntry* mFlatTable{nullptr}; + const TableIndex* mIndices{nullptr}; + const ROFTimingLayer* mLayers{nullptr}; + int32_t mLayerCount{0}; + + GPUhdi() const ROFTimingLayer& getLayer(int32_t layer) const noexcept + { + assert(layer >= 0 && layer < mLayerCount); + return mLayers[layer]; + } + + GPUh() int32_t getClock() const noexcept + { + int32_t fastest = 0; + uint32_t maxNROFs{0}; + for (int32_t iL{0}; iL < mLayerCount; ++iL) { + const auto& layer = getLayer(iL); + if (layer.mNROFsTF > maxNROFs) { + fastest = iL; + maxNROFs = layer.mNROFsTF; + } + } + return fastest; + } + + GPUh() const ROFTimingLayer& getClockLayer() const noexcept { return mLayers[getClock()]; } + + GPUhdi() const TableEntry& getOverlap(int32_t from, int32_t to, size_t rofIdx) const noexcept + { + assert(from < mLayerCount && to < mLayerCount); + const auto& idx = mIndices[(from * mLayerCount) + to]; + assert(rofIdx < idx.getEntries()); + return mFlatTable[idx.getFirstEntry() + rofIdx]; + } + + GPUhdi() bool doROFsOverlap(int32_t layer0, size_t rof0, int32_t layer1, size_t rof1) const noexcept + { + if (layer0 == layer1) { + return rof0 == rof1; + } + assert(layer0 < mLayerCount && layer1 < mLayerCount); + const auto& idx = mIndices[(layer0 * mLayerCount) + layer1]; + if (rof0 >= idx.getEntries()) { + return false; + } + const auto& overlap = mFlatTable[idx.getFirstEntry() + rof0]; + if (overlap.getEntries() == 0) { + return false; + } + const size_t firstCompatible = overlap.getFirstEntry(); + const size_t lastCompatible = firstCompatible + overlap.getEntries() - 1; + return rof1 >= firstCompatible && rof1 <= lastCompatible; + } + + GPUhdi() o2::its::TimeEstBC getTimeStamp(int32_t layer0, size_t rof0, int32_t layer1, size_t rof1) const noexcept + { + assert(layer0 < mLayerCount && layer1 < mLayerCount); + assert(doROFsOverlap(layer0, rof0, layer1, rof1)); + return mLayers[layer0].getROFTimeBounds(rof0, true) + mLayers[layer1].getROFTimeBounds(rof1, true); + } + +#ifndef GPUCA_GPUCODE + GPUh() void printAll() const + { + for (int32_t i = 0; i < mLayerCount; ++i) { + for (int32_t j = 0; j < mLayerCount; ++j) { + if (i != j) { + printMapping(i, j); + } + } + } + printSummary(); + } + + GPUh() void printMapping(int32_t from, int32_t to) const + { + if (from == to) { + LOGP(error, "No self-lookup supported"); + return; + } + const auto& idx = mIndices[(from * mLayerCount) + to]; + LOGF(info, "Overlap mapping: Layer %d -> Layer %d", from, to); + LOGP(info, "From: {}", mLayers[from].asString()); + LOGP(info, "To : {}", mLayers[to].asString()); + for (int32_t i = 0; i < idx.getEntries(); ++i) { + const auto& overlap = getOverlap(from, to, i); + LOGF(info, "%d -> first %d count %d", i, overlap.getFirstEntry(), overlap.getEntries()); + } + } + + GPUh() void printSummary() const + { + uint32_t totalEntries{0}; + size_t flatTableSize{0}; + for (int32_t i = 0; i < mLayerCount; ++i) { + for (int32_t j = 0; j < mLayerCount; ++j) { + if (i != j) { + const auto& idx = mIndices[(i * mLayerCount) + j]; + totalEntries += idx.getEntries(); + flatTableSize += idx.getEntries(); + } + } + } + LOGF(info, "Total overlap table size: %u entries", totalEntries); + LOGF(info, "Flat table size: %zu entries", flatTableSize); + } +#endif +}; + +template +struct ROFVertexLookupView { + const TableEntry* mFlatTable{nullptr}; + const TableIndex* mIndices{nullptr}; + const ROFTimingLayer* mLayers{nullptr}; + int32_t mLayerCount{0}; + + GPUhdi() const ROFTimingLayer& getLayer(int32_t layer) const noexcept + { + assert(layer >= 0 && layer < mLayerCount); + return mLayers[layer]; + } + + GPUhdi() const TableEntry& getVertices(int32_t layer, size_t rofIdx) const noexcept + { + assert(layer >= 0 && layer < mLayerCount); + const auto& idx = mIndices[layer]; + assert(rofIdx < idx.getEntries()); + return mFlatTable[idx.getFirstEntry() + rofIdx]; + } + + GPUh() int32_t getMaxVerticesPerROF() const noexcept + { + int32_t maxCount = 0; + for (int32_t layer = 0; layer < mLayerCount; ++layer) { + const auto& idx = mIndices[layer]; + for (int32_t i = 0; i < idx.getEntries(); ++i) { + maxCount = o2::gpu::CAMath::Max(maxCount, static_cast(mFlatTable[idx.getFirstEntry() + i].getEntries())); + } + } + return maxCount; + } + + GPUhdi() bool isVertexCompatible(int32_t layer, size_t rofIdx, const o2::its::Vertex& vertex) const noexcept + { + assert(layer >= 0 && layer < mLayerCount); + const auto& layerDef = mLayers[layer]; + int64_t rofLower = o2::gpu::CAMath::Max(static_cast(layerDef.getROFStartInBC(rofIdx)) - static_cast(layerDef.mROFAddTimeErr), int64_t(0)); + int64_t rofUpper = static_cast(layerDef.getROFEndInBC(rofIdx)) + layerDef.mROFAddTimeErr; + auto vLower = static_cast(vertex.getTimeStamp().lower()); + auto vUpper = static_cast(vertex.getTimeStamp().upper()); + return vUpper >= rofLower && vLower < rofUpper; + } + +#ifndef GPUCA_GPUCODE + GPUh() void printAll() const + { + for (int32_t layer = 0; layer < mLayerCount; ++layer) { + const auto& idx = mIndices[layer]; + LOGF(info, "Vertex lookup: Layer %d, ROFs %u", layer, idx.getEntries()); + } + } +#endif +}; + +template +struct ROFMaskView { + const TableEntry* mFlatMask{nullptr}; + const TableIndex* mLayerROFOffsets{nullptr}; + int32_t mLayerCount{0}; + + GPUhdi() bool isROFEnabled(int32_t layer, int32_t rofId) const noexcept + { + assert(layer >= 0 && layer < mLayerCount); + return mFlatMask[mLayerROFOffsets[layer] + rofId] != 0u; + } + +#ifndef GPUCA_GPUCODE + GPUh() void printLayer(int32_t layer) const + { + constexpr int wROF = 10; + constexpr int wActive = 10; + const int32_t nROFs = mLayerROFOffsets[layer + 1] - mLayerROFOffsets[layer]; + LOGF(info, "Mask table: Layer %d", layer); + LOGF(info, "%*s | %*s", wROF, "ROF", wActive, "Enabled"); + LOGF(info, "%.*s-+-%.*s", wROF, "----------", wActive, "----------"); + for (int32_t rof = 0; rof < nROFs; ++rof) { + LOGF(info, "%*d | %*d", wROF, rof, wActive, static_cast(isROFEnabled(layer, rof))); + } + } + + GPUh() std::string asString(int32_t layer) const + { + const int32_t nROFs = mLayerROFOffsets[layer + 1] - mLayerROFOffsets[layer]; + int32_t enabledROFs = 0; + for (int32_t rof = 0; rof < nROFs; ++rof) { + if (isROFEnabled(layer, rof)) { + ++enabledROFs; + } + } + return std::format("ROFMask on Layer {} ROFs enabled: {}/{}", layer, enabledROFs, nROFs); + } + + GPUh() void print(int32_t layer) const + { + LOG(info) << asString(layer); + } + + GPUh() void printAll() const + { + for (int32_t layer = 0; layer < mLayerCount; ++layer) { + printLayer(layer); + } + } +#endif +}; + +using RuntimeROFTableEntry = o2::dataformats::RangeReference; +using RuntimeROFOverlapView = ROFOverlapView; +using RuntimeROFVertexLookupView = ROFVertexLookupView; +using RuntimeROFMaskView = ROFMaskView; + +/// A non-owning event view assembled by an ITS/MFT adapter. The core sees one +/// runtime context, while detector-specific fixed-capacity tables stay at the +/// adapter edge that owns their lifetime. +struct RuntimeROFViews { + RuntimeROFOverlapView overlap{}; + RuntimeROFVertexLookupView vertexLookup{}; + RuntimeROFMaskView mask{}; + RuntimeROFMaskView upcMask{}; +}; + +} // namespace o2::itsmft::tracking + +#endif // ALICEO2_ITSMFT_TRACKING_ROFVIEWS_H_ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/RefitDriver.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/RefitDriver.h new file mode 100644 index 0000000000000..c7128ff229ccb --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/RefitDriver.h @@ -0,0 +1,283 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_REFITDRIVER_H_ +#define ALICEO2_ITSMFT_TRACKING_REFITDRIVER_H_ + +#include "GPUCommonDef.h" + +#ifndef GPUCA_GPUCODE + +#include +#include + +#include + +#include "CommonConstants/MathConstants.h" +#include "ITSMFTTracking/Cell.h" +#include "ITSMFTTracking/GlobalMeasurement.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/Propagator.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/SurfaceStateOperationResult.h" +#include "ReconstructionDataFormats/TrackParametrization.h" + +// Descriptor-driven refit built on Propagator operations. +namespace o2::itsmft::tracking +{ + +namespace detail +{ + +struct RefitMeasurementSlot { + SurfaceMeasurement measurement{}; + LayerId surface{}; + bool present{false}; +}; + +/// Builds an ordered refit leg; holes remain explicit. +inline gsl::span assembleRefitLegSlots( + const TrackSeed& seed, + const TimeFrame& frame, + gsl::span> layerGlobals, + int start, int end, int step, + gsl::span out, + bool& valid) noexcept +{ + valid = layerGlobals.size() <= MaxLayoutSurfaces; + int position = 0; + for (int surfacePosition = start; surfacePosition != end && position < static_cast(out.size()); surfacePosition += step) { + const int clsIdx = seed.getCluster(surfacePosition); + if (clsIdx == o2::its::constants::UnusedIndex) { + out[position++] = {}; + continue; + } + if (!valid || clsIdx < 0 || static_cast(clsIdx) >= layerGlobals[surfacePosition].size()) { + valid = false; + return {}; + } + const auto& global = layerGlobals[surfacePosition][clsIdx]; + const auto surface = LayerId{static_cast(surfacePosition)}; + const auto* measurement = frame.getSurfaceMeasurement(surface, global.clusterId); + if (measurement == nullptr) { + valid = false; + return {}; + } + out[position++] = RefitMeasurementSlot{*measurement, surface, true}; + } + return gsl::span(out.data(), position); +} + +// Holes are skipped; present slots must resolve to a descriptor. Commit state, +// reference, chi2 and count only after the full leg succeeds. +inline bool driveRefitLeg(SurfaceTrackState& state, SurfaceTrackParameters& linRef, + float& chi2, uint32_t& acceptedHitCount, + gsl::span orderedSlots, SurfaceCatalogView surfaceCatalog, + float bz, material::MaterialTraversalDirection direction, + bool shiftReferenceToMeasurement, float maxChi2, OperationFailureReason& reason) noexcept +{ + if (chi2 < 0.f) { + reason = OperationFailureReason::PredictedChi2Failure; + return false; + } + + SurfaceTrackState scratchState = state; + SurfaceTrackParameters scratchLinRef = linRef; + float scratchChi2 = chi2; + uint32_t scratchAcceptedHitCount = 0; + constexpr uint32_t kChi2GateMinAcceptedHits = 3; + for (const auto& slot : orderedSlots) { + if (!slot.present) { + continue; + } + if (!slot.surface.isValid() || !(surfaceCatalog.nSurfaces == 0 || surfaceCatalog.surfaces != nullptr) || + !(slot.surface.value() < surfaceCatalog.nSurfaces)) { + reason = OperationFailureReason::InvalidSurfaceCatalogAssociation; + return false; + } + const SurfaceDescriptor& descriptor = surfaceCatalog.getSurface(slot.surface); + if (!Propagator::propagateToMeasurement(scratchState, scratchLinRef, descriptor, slot.measurement, bz, direction, + scratchAcceptedHitCount >= kChi2GateMinAcceptedHits, maxChi2, scratchChi2, + shiftReferenceToMeasurement, reason)) { + return false; + } + ++scratchAcceptedHitCount; + } + state = scratchState; + linRef = scratchLinRef; + chi2 = scratchChi2; + acceptedHitCount = scratchAcceptedHitCount; + return true; +} + +} // namespace detail + +// Reset a refit leg to a loose diagonal covariance. +GPUhdi() void resetCovarianceForRefit(SurfaceTrackState& state) noexcept +{ + for (auto& element : state.covariance) { + element = 0.f; + } + if (state.kind == SurfaceKind::Cylinder) { + state.covariance[packedCovarianceIndex(0, 0)] = o2::track::kCY2max; + state.covariance[packedCovarianceIndex(1, 1)] = o2::track::kCZ2max; + state.covariance[packedCovarianceIndex(2, 2)] = o2::track::kCSnp2max; + state.covariance[packedCovarianceIndex(3, 3)] = o2::track::kCTgl2max; + const float q2pt = state.parameters[4]; + state.covariance[packedCovarianceIndex(4, 4)] = q2pt * q2pt * o2::track::kC1Pt2max; + } else { + constexpr float kCPhi2maxForward = o2::constants::math::PI * o2::constants::math::PI; + state.covariance[packedCovarianceIndex(0, 0)] = o2::track::kCY2max; + state.covariance[packedCovarianceIndex(1, 1)] = o2::track::kCY2max; + state.covariance[packedCovarianceIndex(2, 2)] = kCPhi2maxForward; + state.covariance[packedCovarianceIndex(3, 3)] = o2::track::kCTgl2max; + const float invQPt = state.parameters[4]; + state.covariance[packedCovarianceIndex(4, 4)] = invQPt * invQPt * o2::track::kC1Pt2max; + } +} + +// parameters[4] is signed q/pT for both coordinate conventions. +GPUhdi() float ptFromQOverPt(float q2pt, uint8_t absCharge) noexcept +{ + float ptInv = std::abs(q2pt); + if (ptInv < o2::track::MinPTInv) { + ptInv = o2::track::MinPTInv; + } + if (absCharge > 1) { + ptInv /= static_cast(absCharge); + } + return 1.f / ptInv; +} + +// Refit inward, outward, then optionally inward again; commit on success. +inline bool fitTrackSeedLegs( + const TrackSeed& seed, + const TimeFrame& frame, + gsl::span> layerGlobals, + SurfaceCatalogView surfaceCatalog, + float bz, + bool shiftReferenceToMeasurement, + float maxChi2ClusterAttachment, + float maxChi2NDF, + bool repeatRefitOut, + gsl::span minPt, + SurfaceTrackState& outParamIn, + SurfaceTrackState& outParamOut, + float& outChi2, + OperationFailureReason& reason) noexcept +{ + if (layerGlobals.empty() || layerGlobals.size() > MaxLayoutSurfaces) { + reason = OperationFailureReason::InvalidSurfaceCatalogAssociation; + return false; + } + // Legs run sequentially; reuse bounded storage without allocating inside + // this noexcept refit. Only the active portion is exposed to the assembler. + std::array slotsBuffer{}; + const gsl::span activeSlots{slotsBuffer.data(), layerGlobals.size()}; + auto legAcceptable = [](const SurfaceTrackState& state, float chi2, uint32_t acceptedHitCount, + float maxQoverPt, float maxChi2NDFValue) noexcept -> bool { + if (!(std::abs(state.parameters[4]) < maxQoverPt)) { + return false; + } + return chi2 < maxChi2NDFValue * static_cast(static_cast(acceptedHitCount) * 2 - 5); + }; + + // Leg A: inward. + SurfaceTrackState stateA = seed.state(); + SurfaceTrackParameters linRefA{stateA}; + resetCovarianceForRefit(stateA); + float chi2A = 0.f; + uint32_t acceptedA = 0; + const int activeSurfaceCount = static_cast(layerGlobals.size()); + bool validSlots = false; + const auto slotsA = detail::assembleRefitLegSlots(seed, frame, layerGlobals, 0, activeSurfaceCount, 1, activeSlots, validSlots); + if (!validSlots) { + reason = OperationFailureReason::InvalidSurfaceCatalogAssociation; + return false; + } + if (!detail::driveRefitLeg(stateA, linRefA, chi2A, acceptedA, slotsA, surfaceCatalog, bz, + material::MaterialTraversalDirection::AlongMomentum, shiftReferenceToMeasurement, + maxChi2ClusterAttachment, reason)) { + return false; + } + if (!legAcceptable(stateA, chi2A, acceptedA, o2::constants::math::VeryBig, maxChi2NDF)) { + reason = OperationFailureReason::LegAcceptanceFailure; + return false; + } + + // Leg B: outward; this is the reported inner result. + SurfaceTrackState stateB = stateA; + SurfaceTrackParameters linRefB{stateB}; + resetCovarianceForRefit(stateB); + float chi2B = 0.f; + uint32_t acceptedB = 0; + const auto slotsB = detail::assembleRefitLegSlots(seed, frame, layerGlobals, activeSurfaceCount - 1, -1, -1, activeSlots, validSlots); + if (!validSlots) { + reason = OperationFailureReason::InvalidSurfaceCatalogAssociation; + return false; + } + if (!detail::driveRefitLeg(stateB, linRefB, chi2B, acceptedB, slotsB, surfaceCatalog, bz, + material::MaterialTraversalDirection::OppositeMomentum, shiftReferenceToMeasurement, + maxChi2ClusterAttachment, reason)) { + return false; + } + if (!legAcceptable(stateB, chi2B, acceptedB, 50.f, maxChi2NDF)) { + reason = OperationFailureReason::LegAcceptanceFailure; + return false; + } + + // MinPt uses the seed's attached-cluster count. + const int nClAttached = seed.getHitLayerMask().count(); + const int minPtSlot = activeSurfaceCount - nClAttached; + if (minPtSlot >= 0 && minPtSlot < static_cast(minPt.size())) { + const float minPtThreshold = minPt[minPtSlot]; + if (minPtThreshold > 0.f && ptFromQOverPt(stateB.parameters[4], stateB.absCharge) < minPtThreshold) { + reason = OperationFailureReason::MinPtFailure; + return false; + } + } + + // Optional leg C: inward again. + SurfaceTrackState stateOut = stateA; + if (repeatRefitOut) { + SurfaceTrackState stateC = stateB; + SurfaceTrackParameters linRefC{stateC}; + resetCovarianceForRefit(stateC); + float chi2C = 0.f; + uint32_t acceptedC = 0; + const auto slotsC = detail::assembleRefitLegSlots(seed, frame, layerGlobals, 0, activeSurfaceCount, 1, activeSlots, validSlots); + if (!validSlots) { + reason = OperationFailureReason::InvalidSurfaceCatalogAssociation; + return false; + } + if (!detail::driveRefitLeg(stateC, linRefC, chi2C, acceptedC, slotsC, surfaceCatalog, bz, + material::MaterialTraversalDirection::AlongMomentum, shiftReferenceToMeasurement, + maxChi2ClusterAttachment, reason)) { + return false; + } + if (!legAcceptable(stateC, chi2C, acceptedC, o2::constants::math::VeryBig, maxChi2NDF)) { + reason = OperationFailureReason::LegAcceptanceFailure; + return false; + } + stateOut = stateC; + } + + outParamIn = stateB; + outParamOut = stateOut; + outChi2 = chi2B; + return true; +} + +} // namespace o2::itsmft::tracking + +#endif // GPUCA_GPUCODE + +#endif /* ALICEO2_ITSMFT_TRACKING_REFITDRIVER_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceDescriptor.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceDescriptor.h new file mode 100644 index 0000000000000..972d946645430 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceDescriptor.h @@ -0,0 +1,93 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_SURFACEDESCRIPTOR_H_ +#define ALICEO2_ITSMFT_TRACKING_SURFACEDESCRIPTOR_H_ + +#include +#include +#include + +#include "GPUCommonDef.h" +#include "ITSMFTTracking/IdTypes.h" + +namespace o2::itsmft::tracking +{ + +// Nominal normal-incidence material; zero denotes material not configured. +struct NominalSurfaceMaterial { + float xOverX0{0.f}; + float arealDensityGPerCm2{0.f}; +}; + +struct SurfaceChartRange { + float min{0.f}; + float max{0.f}; + + GPUhdi() constexpr bool isValid() const noexcept { return min < max; } +}; + +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(NominalSurfaceMaterial) == 8); +static_assert(alignof(NominalSurfaceMaterial) == 4); +static_assert(offsetof(NominalSurfaceMaterial, xOverX0) == 0); +static_assert(offsetof(NominalSurfaceMaterial, arealDensityGPerCm2) == 4); +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(SurfaceChartRange) == 8); + +// Immutable surface geometry and nominal material. Its LayerId is the dense +// position of this descriptor in DetectorLayout and is intentionally not +// duplicated here. +struct SurfaceDescriptor { + uint16_t detectorSurfaceIndex{0}; + uint8_t detectorId{0}; + SurfaceKind kind{SurfaceKind::Undefined}; + uint16_t flags{0}; + float referenceCoordinate{0.f}; // nominal radius for cylinders, z for disks + NominalSurfaceMaterial material{}; + SurfaceChartRange chartRange{}; +}; + +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(SurfaceDescriptor) == 28); +static_assert(alignof(SurfaceDescriptor) == 4); +static_assert(offsetof(SurfaceDescriptor, detectorSurfaceIndex) == 0); +static_assert(offsetof(SurfaceDescriptor, detectorId) == 2); +static_assert(offsetof(SurfaceDescriptor, kind) == 3); +static_assert(offsetof(SurfaceDescriptor, flags) == 4); +static_assert(offsetof(SurfaceDescriptor, referenceCoordinate) == 8); +static_assert(offsetof(SurfaceDescriptor, material) == 12); +static_assert(offsetof(SurfaceDescriptor, chartRange) == 20); + +// Non-owning surface-catalog view. Topology, timing and measurements stay +// outside this POD so loading and propagation do not depend on them. +struct SurfaceCatalogView { + const SurfaceDescriptor* surfaces{nullptr}; + uint32_t nSurfaces{0}; + + GPUhdi() uint32_t getSurfaceIndex(LayerId id) const + { + return id.isValid() && id.value() < nSurfaces ? id.value() : nSurfaces; + } + + GPUhdi() bool hasSurface(LayerId id) const { return getSurfaceIndex(id) < nSurfaces; } + GPUhdi() const SurfaceDescriptor& getSurface(LayerId id) const { return surfaces[getSurfaceIndex(id)]; } +}; + +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_SURFACEDESCRIPTOR_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceMeasurement.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceMeasurement.h new file mode 100644 index 0000000000000..e1e8b1a2a79f3 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceMeasurement.h @@ -0,0 +1,57 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_SURFACEMEASUREMENT_H_ +#define ALICEO2_ITSMFT_TRACKING_SURFACEMEASUREMENT_H_ + +#include + +#include "GPUCommonDef.h" +namespace o2::itsmft::tracking +{ + +// q is normal to the surface. The measured coordinates are always (u, v). +struct SurfaceFramePoint { + float q{0.f}; + float u{0.f}; + float v{0.f}; + float frameAngle{0.f}; +}; + +// Packed symmetric covariance of the measured (u, v) coordinates. +struct SurfaceCovariance2F { + float uu{0.f}; + float uv{0.f}; + float vv{0.f}; +}; + +struct SurfaceMeasurement { + SurfaceFramePoint frame{}; + SurfaceCovariance2F covariance{}; +}; + +#define O2_ITSMFT_ASSERT_DEVICE_TYPE(Type, Size) \ + static_assert(std::is_standard_layout_v); \ + static_assert(std::is_trivially_copyable_v); \ + static_assert(sizeof(Type) == Size) + +O2_ITSMFT_ASSERT_DEVICE_TYPE(SurfaceFramePoint, 16); +O2_ITSMFT_ASSERT_DEVICE_TYPE(SurfaceCovariance2F, 12); +O2_ITSMFT_ASSERT_DEVICE_TYPE(SurfaceMeasurement, 28); +static_assert(alignof(SurfaceMeasurement) == 4); +static_assert(offsetof(SurfaceMeasurement, frame) == 0); +static_assert(offsetof(SurfaceMeasurement, covariance) == 16); + +#undef O2_ITSMFT_ASSERT_DEVICE_TYPE + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_SURFACEMEASUREMENT_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceSpec.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceSpec.h new file mode 100644 index 0000000000000..af4f82810a8d4 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceSpec.h @@ -0,0 +1,234 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_SURFACESPEC_H_ +#define ALICEO2_ITSMFT_TRACKING_SURFACESPEC_H_ + +#include +#include +#include +#include + +#include "GPUCommonDef.h" +#include "GPUCommonMath.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" + +namespace o2::itsmft::tracking +{ + +// Detector-qualified identity of a logical tracking surface. The detector ID +// remains open to every detector representable by the existing uint8_t field. +struct DetectorLayerIdentity { + uint8_t detectorId{0}; + uint16_t detectorSurfaceIndex{0}; + + GPUhdi() friend constexpr bool operator==(DetectorLayerIdentity lhs, DetectorLayerIdentity rhs) noexcept + { + return lhs.detectorId == rhs.detectorId && lhs.detectorSurfaceIndex == rhs.detectorSurfaceIndex; + } + GPUhdi() friend constexpr bool operator!=(DetectorLayerIdentity lhs, DetectorLayerIdentity rhs) noexcept { return !(lhs == rhs); } +}; + +struct StaticSurfaceDescriptor { + DetectorLayerIdentity identity{}; + SurfaceKind kind{SurfaceKind::Undefined}; + float nominalReferenceCoordinate{0.f}; + NominalSurfaceMaterial material{}; + SurfaceChartRange chartRange{}; +}; + +// Precondition: source belongs to a validated SurfaceSpec. This is an +// ideal/static layout projection only; it neither validates nor repairs an +// arbitrary descriptor. Runtime geometry observations do not define another +// catalogue. +GPUhdi() constexpr SurfaceDescriptor toRuntimeSurfaceDescriptor(const StaticSurfaceDescriptor& source) noexcept +{ + return SurfaceDescriptor{source.identity.detectorSurfaceIndex, + source.identity.detectorId, + source.kind, + 0, + source.nominalReferenceCoordinate, + source.material, + source.chartRange}; +} + +#define O2_ITSMFT_ASSERT_STATIC_SURFACE_TYPE(Type, Size, Alignment) \ + static_assert(std::is_standard_layout_v); \ + static_assert(std::is_trivially_copyable_v); \ + static_assert(sizeof(Type) == Size); \ + static_assert(alignof(Type) == Alignment) + +O2_ITSMFT_ASSERT_STATIC_SURFACE_TYPE(DetectorLayerIdentity, 4, 2); +O2_ITSMFT_ASSERT_STATIC_SURFACE_TYPE(StaticSurfaceDescriptor, 28, 4); + +static_assert(offsetof(DetectorLayerIdentity, detectorId) == 0); +static_assert(offsetof(DetectorLayerIdentity, detectorSurfaceIndex) == 2); +static_assert(offsetof(StaticSurfaceDescriptor, identity) == 0); +static_assert(offsetof(StaticSurfaceDescriptor, kind) == 4); +static_assert(offsetof(StaticSurfaceDescriptor, nominalReferenceCoordinate) == 8); +static_assert(offsetof(StaticSurfaceDescriptor, material) == 12); +static_assert(offsetof(StaticSurfaceDescriptor, chartRange) == 20); + +#undef O2_ITSMFT_ASSERT_STATIC_SURFACE_TYPE + +namespace detail +{ +template +struct IsStaticSurfaceArray : std::false_type { +}; + +template +struct IsStaticSurfaceArray> : std::true_type { +}; + +constexpr bool isEnabled(SurfaceKind kind) noexcept +{ + return isRecognizedSurfaceKind(kind); +} + +template +consteval bool hasStaticSurfaceArray() +{ + if constexpr (!requires { Spec::surfaces; }) { + return false; + } else { + using Array = std::remove_cv_t; + if constexpr (!IsStaticSurfaceArray::value) { + return false; + } else { + // Being usable as a non-type template argument proves static lifetime + // and constant initialization of the canonical array. + return requires { typename std::integral_constant; }; + } + } +} + +template +consteval bool validateSurfaceArray(const std::array& surfaces) +{ + if constexpr (N > MaxLayoutSurfaces) { + return false; + } + + for (std::size_t i = 0; i < N; ++i) { + const auto& surface = surfaces[i]; + if (!isEnabled(surface.kind) || !o2::gpu::GPUCommonMath::Finite(surface.nominalReferenceCoordinate) || + (surface.kind == SurfaceKind::Cylinder && surface.nominalReferenceCoordinate <= 0.f)) { + return false; + } + + if (!o2::gpu::GPUCommonMath::Finite(surface.material.xOverX0) || surface.material.xOverX0 < 0.f || + !o2::gpu::GPUCommonMath::Finite(surface.material.arealDensityGPerCm2) || surface.material.arealDensityGPerCm2 < 0.f) { + return false; + } + + for (std::size_t other = i + 1; other < N; ++other) { + if (surface.identity == surfaces[other].identity) { + return false; + } + } + } + + // Detector-local indices form an independent dense [0, N) range for every + // arbitrary detector ID represented in the catalogue. + for (std::size_t i = 0; i < N; ++i) { + const auto detectorId = surfaces[i].identity.detectorId; + std::size_t detectorCount = 0; + for (const auto& surface : surfaces) { + detectorCount += surface.identity.detectorId == detectorId; + } + for (std::size_t expected = 0; expected < detectorCount; ++expected) { + bool found = false; + for (const auto& surface : surfaces) { + found = found || + (surface.identity.detectorId == detectorId && surface.identity.detectorSurfaceIndex == expected); + } + if (!found) { + return false; + } + } + } + return true; +} + +template +consteval bool validateSurfaceSpecDefinition() +{ + return validateSurfaceArray(Spec::surfaces); +} +} // namespace detail + +template +// Structural requirement only: canonical inline-static-array shape and +// lifetime. Catalogue contents are deliberately not accepted by this concept. +concept SurfaceSpecDefinition = detail::hasStaticSurfaceArray(); + +template +// A consumer-facing SurfaceSpec has both the required definition shape and a +// fully validated catalogue. +concept SurfaceSpec = SurfaceSpecDefinition && detail::validateSurfaceSpecDefinition(); + +template +inline constexpr std::size_t SurfaceCount = std::tuple_size_v>; + +template +consteval bool validateSurfaceSpec() +{ + if constexpr (!SurfaceSpecDefinition) { + return false; + } else { + return detail::validateSurfaceSpecDefinition(); + } +} + +namespace detail +{ +template +consteval auto concatenate() +{ + constexpr auto countA = std::tuple_size_v>; + constexpr auto countB = std::tuple_size_v>; + std::array result{}; + std::size_t output = 0; + for (const auto& surface : A::surfaces) { + result[output++] = surface; + } + for (const auto& surface : B::surfaces) { + result[output++] = surface; + } + return result; +} + +template +consteval bool surfaceSpecsCanBeConcatenated() +{ + if constexpr (!SurfaceSpec || !SurfaceSpec) { + return false; + } else if constexpr (SurfaceCount + SurfaceCount > MaxLayoutSurfaces) { + return false; + } else { + return validateSurfaceArray(concatenate()); + } +} +} // namespace detail + +template +inline constexpr bool SurfaceSpecsCanBeConcatenated = detail::surfaceSpecsCanBeConcatenated(); + +template +struct ConcatenatedSurfaceSpec { + static_assert(SurfaceSpecsCanBeConcatenated, "SurfaceSpecs cannot be concatenated into a valid catalogue"); + inline static constexpr auto surfaces = detail::concatenate(); +}; + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_SURFACESPEC_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceStateOperationResult.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceStateOperationResult.h new file mode 100644 index 0000000000000..1b065b133108b --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceStateOperationResult.h @@ -0,0 +1,58 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_SURFACESTATEOPERATIONRESULT_H_ +#define ALICEO2_ITSMFT_TRACKING_SURFACESTATEOPERATIONRESULT_H_ + +#include + +namespace o2::itsmft::tracking +{ + +enum class OperationFailureReason : uint8_t { + SourceSurfaceKindMismatch = 0, + NonFiniteInput = 1, + NonFiniteOutput = 2, + InvalidCovariance = 3, + UnreachableTarget = 4, + PropagationFailure = 5, + MaterialFailure = 6, + PredictedChi2Failure = 7, + UpdateFailure = 8, + RotationFailure = 9, + AlphaMismatch = 10, + ReferenceCoordinateMismatch = 11, + // Finite-input forward seed with z-ordering or transverse separation at or + // below the strict 1e-6f geometry boundary; distinct from numeric failures. + SeedGeometryDegenerate = 12, + // A present (non-hole) measurement has an invalid LayerId/catalog + // association; no propagation, material, or chi2 arithmetic ran. + InvalidSurfaceCatalogAssociation = 14, + // A completed refit leg failed `|Q2Pt| < maxQoverPt && chi2 < + // maxChi2NDF*(nCl*2-5)` after per-hit processing. + LegAcceptanceFailure = 15, + // The seed-level minimum-pT check failed after the outward leg, keyed by + // the active traversal count and attached-cluster count. + MinPtFailure = 16, + // A nonzero reseedIfShorter is unsupported and is rejected before any leg + // operation or output mutation. + ReseedNotSupported = 17, + // Surface-kind conversion was attempted but failed (invalid target kind + // or direction boundary), distinct from a + // source-kind mismatch where conversion was not attempted. + SurfaceKindConversionFailure = 18 +}; + +static_assert(sizeof(OperationFailureReason) == sizeof(uint8_t)); + +} // namespace o2::itsmft::tracking + +#endif // ALICEO2_ITSMFT_TRACKING_SURFACESTATEOPERATIONRESULT_H_ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceTiming.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceTiming.h new file mode 100644 index 0000000000000..3b9fdb50ea689 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceTiming.h @@ -0,0 +1,229 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_SURFACETIMING_H_ +#define ALICEO2_ITSMFT_TRACKING_SURFACETIMING_H_ + +#include +#include +#include +#include + +#include "GPUCommonDef.h" + +#ifndef GPUCA_GPUCODE +#include + +#include "CommonDataFormat/InteractionRecord.h" +#include "ITSMFTTracking/ROFLookupTables.h" +#endif + +namespace o2::itsmft::tracking +{ + +// TimeFrame-relative bunch-crossing coordinate. Host timing is signed 64-bit; +// any narrower device representation requires an explicit checked conversion. +using TFBC = int64_t; + +// Half-open [begin, end) TF-relative BC interval for one source ROF. +// sourceROF is source-local; cross-source checks use interval intersection. +struct ROFIntervalBC { + TFBC begin{0}; + TFBC end{0}; + uint32_t sourceROF{std::numeric_limits::max()}; + uint32_t flags{0}; + + // The default interval is invalid. A valid interval has a real source ROF + // and strictly positive half-open extent. + GPUhdi() constexpr bool isValid() const noexcept + { + return sourceROF != std::numeric_limits::max() && begin < end; + } + // Compute the non-negative width in uint64_t: signed subtraction can + // overflow for valid intervals spanning more than INT64_MAX BC. + GPUhdi() constexpr uint64_t length() const noexcept + { + return static_cast(end) - static_cast(begin); + } +}; + +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); + +// Detector-neutral half-open time estimate for a track/cell/road. It has no +// source-ROF identity and is standard-layout/trivially copyable for GenericTrack. +struct GenericTrackTimestamp { + TFBC begin{0}; + TFBC end{0}; + + GPUhdi() constexpr bool isValid() const noexcept { return begin < end; } + // Half-open intersection; adjacent intervals and invalid intervals do not + // intersect. + GPUhdi() constexpr bool isCompatible(const GenericTrackTimestamp& other) const noexcept + { + return isValid() && other.isValid() && begin < other.end && other.begin < end; + } +}; + +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(GenericTrackTimestamp) == 16); +static_assert(alignof(GenericTrackTimestamp) == alignof(TFBC)); +static_assert(offsetof(GenericTrackTimestamp, begin) == 0); +static_assert(offsetof(GenericTrackTimestamp, end) == 8); + +// Per-source readout timing configuration. ROF start uses the source ROF's +// InteractionRecord plus delay and bias; rofAddTimeErr is applied only by +// widen(), not folded into the stored interval. +struct ROFTimingConfig { + TFBC rofLength{0}; + TFBC rofDelay{0}; + TFBC rofBias{0}; + TFBC rofAddTimeErr{0}; +}; + +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); + +enum class TimingBuildError : uint8_t { + None, + InvalidROFLength, + InvalidSourceROF, + Overflow +}; + +struct ROFIntervalBuildResult { + ROFIntervalBC interval{}; + TimingBuildError error{TimingBuildError::None}; + + // Success requires both no error and a valid interval; the default result is + // therefore not successful even though error defaults to None. + constexpr bool ok() const noexcept { return error == TimingBuildError::None && interval.isValid(); } +}; + +enum class WidenError : uint8_t { + None, + InvalidInterval, + InvalidMargin, + LowerBoundOverflow, + UpperBoundOverflow +}; + +struct WidenResult { + ROFIntervalBC interval{}; + WidenError error{WidenError::None}; + + // Match ROFIntervalBuildResult::ok(): a default result is not successful. + constexpr bool ok() const noexcept { return error == WidenError::None && interval.isValid(); } +}; + +#ifndef GPUCA_GPUCODE + +namespace detail +{ +inline bool checkedAddBC(TFBC a, TFBC b, TFBC& out) noexcept +{ + if (b >= 0) { + if (a > std::numeric_limits::max() - b) { + return false; + } + } else { + if (a < std::numeric_limits::min() - b) { + return false; + } + } + out = a + b; + return true; +} +} // namespace detail + +// origin is the explicit InteractionRecord for the loaded frame; rofIR is the +// source ROF's own record, used as-is for continuous and triggered readout. +inline ROFIntervalBuildResult computeROFIntervalBC(const o2::InteractionRecord& rofIR, + const o2::InteractionRecord& origin, + const ROFTimingConfig& cfg, + uint32_t sourceROF) noexcept +{ + if (sourceROF == std::numeric_limits::max()) { + return {{}, TimingBuildError::InvalidSourceROF}; + } + if (cfg.rofLength <= 0) { + return {{}, TimingBuildError::InvalidROFLength}; + } + const TFBC anchor = static_cast(rofIR.differenceInBC(origin)); + TFBC begin{0}; + TFBC withDelay{0}; + TFBC end{0}; + if (!detail::checkedAddBC(anchor, cfg.rofDelay, withDelay) || + !detail::checkedAddBC(withDelay, cfg.rofBias, begin) || + !detail::checkedAddBC(begin, cfg.rofLength, end)) { + return {{}, TimingBuildError::Overflow}; + } + return {ROFIntervalBC{begin, end, sourceROF, 0}, TimingBuildError::None}; +} + +// Widen an interval by an explicit non-negative margin. Invalid input, +// negative margins, and bound overflow are reported rather than wrapped. +inline WidenResult widen(const ROFIntervalBC& interval, TFBC margin) noexcept +{ + if (!interval.isValid()) { + return {{}, WidenError::InvalidInterval}; + } + if (margin < 0) { + return {{}, WidenError::InvalidMargin}; + } + TFBC newBegin{0}; + if (!detail::checkedAddBC(interval.begin, -margin, newBegin)) { + return {{}, WidenError::LowerBoundOverflow}; + } + TFBC newEnd{0}; + if (!detail::checkedAddBC(interval.end, margin, newEnd)) { + return {{}, WidenError::UpperBoundOverflow}; + } + return {ROFIntervalBC{newBegin, newEnd, interval.sourceROF, interval.flags}, WidenError::None}; +} + +struct UniformROFTimingResult { + ROFTimingConfig config{}; + bool uniform{false}; +}; + +// The returned source-level config is valid only when all layers agree on +// length, delay, bias, and additional timing error; otherwise uniform is false. +inline UniformROFTimingResult deriveUniformROFTimingConfig(gsl::span perLayer) noexcept +{ + if (perLayer.empty()) { + return {}; + } + const auto& ref = perLayer[0]; + for (const auto& lt : perLayer) { + if (lt.mROFLength != ref.mROFLength || lt.mROFDelay != ref.mROFDelay || + lt.mROFBias != ref.mROFBias || lt.mROFAddTimeErr != ref.mROFAddTimeErr) { + return {}; + } + } + return {ROFTimingConfig{static_cast(ref.mROFLength), static_cast(ref.mROFDelay), + static_cast(ref.mROFBias), static_cast(ref.mROFAddTimeErr)}, + true}; +} + +#endif // GPUCA_GPUCODE + +// Cross-source compatibility uses half-open interval intersection, not ROF +// ordinal equality; adjacent or invalid intervals do not intersect. +GPUhdi() constexpr bool intersects(const ROFIntervalBC& a, const ROFIntervalBC& b) noexcept +{ + return a.isValid() && b.isValid() && a.begin < b.end && b.begin < a.end; +} + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_SURFACETIMING_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceTrackState.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceTrackState.h new file mode 100644 index 0000000000000..e62e045cac788 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceTrackState.h @@ -0,0 +1,127 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_SURFACETRACKSTATE_H_ +#define ALICEO2_ITSMFT_TRACKING_SURFACETRACKSTATE_H_ + +#include +#include +#include +#include + +#include "GPUCommonDef.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ReconstructionDataFormats/PID.h" + +namespace o2::itsmft::tracking +{ + +// The interpretation of parameters and covariance is selected by kind: +// Barrel: (Y, Z, Snp, Tgl, Q2Pt), referenceCoordinate is local X, alpha is frame angle. +// Forward: (X, Y, Phi, Tgl, Q2Pt), referenceCoordinate is global Z, alpha is unused (zero). + +// Fitted surface state. The field order keeps the device-facing representation +// compact while the parameter-only linearization state remains independent. +struct SurfaceTrackState { + float parameters[5]{}; + float covariance[15]{}; + float referenceCoordinate{0.f}; + float alpha{0.f}; + SurfaceKind kind{SurfaceKind::Undefined}; + uint8_t flags{0}; + uint8_t absCharge{0}; + o2::track::PID pid{o2::track::PID::Pion}; + + GPUhdi() constexpr bool hasRecognizedKind() const noexcept { return isRecognizedSurfaceKind(kind); } +}; + +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(SurfaceTrackState) == 92); +static_assert(alignof(SurfaceTrackState) == 4); +static_assert(offsetof(SurfaceTrackState, parameters) == 0); +static_assert(offsetof(SurfaceTrackState, covariance) == 20); +static_assert(offsetof(SurfaceTrackState, referenceCoordinate) == 80); +static_assert(offsetof(SurfaceTrackState, alpha) == 84); +static_assert(offsetof(SurfaceTrackState, kind) == 88); +static_assert(offsetof(SurfaceTrackState, flags) == 89); +static_assert(offsetof(SurfaceTrackState, absCharge) == 90); +static_assert(offsetof(SurfaceTrackState, pid) == 91); + +// Covariance-free surface parameters used as the propagation linearization +// point paired with one SurfaceTrackState. +struct SurfaceTrackParameters { + float parameters[5]{}; + float referenceCoordinate{0.f}; + float alpha{0.f}; + SurfaceKind kind{SurfaceKind::Undefined}; + + GPUhdi() constexpr SurfaceTrackParameters() noexcept = default; + GPUhdi() constexpr explicit SurfaceTrackParameters(const SurfaceTrackState& state) noexcept + : referenceCoordinate{state.referenceCoordinate}, alpha{state.alpha}, kind{state.kind} + { + for (uint8_t i = 0; i < 5; ++i) { + parameters[i] = state.parameters[i]; + } + } + + GPUhdi() constexpr bool hasRecognizedKind() const noexcept { return isRecognizedSurfaceKind(kind); } +}; + +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(SurfaceTrackParameters) == 32); +static_assert(alignof(SurfaceTrackParameters) == 4); +static_assert(offsetof(SurfaceTrackParameters, parameters) == 0); +static_assert(offsetof(SurfaceTrackParameters, referenceCoordinate) == 20); +static_assert(offsetof(SurfaceTrackParameters, alpha) == 24); +static_assert(offsetof(SurfaceTrackParameters, kind) == 28); + +GPUhdi() constexpr uint8_t packedCovarianceIndex(uint8_t row, uint8_t column) noexcept +{ + return row >= column ? row * (row + 1) / 2 + column : column * (column + 1) / 2 + row; +} + +// Sanitize a packed covariance after a successful mutation. Diagonal values +// are made non-negative and capped, with corresponding row/column rescaling; +// off-diagonals are then limited to their pairwise Cauchy-Schwarz bounds. +GPUhdi() void sanitizeCovariance(SurfaceTrackState& state, const float (&maxDiagonal)[5]) noexcept +{ + auto& c = state.covariance; + for (uint8_t i = 0; i < 5; ++i) { + const uint8_t diagIndex = packedCovarianceIndex(i, i); + c[diagIndex] = c[diagIndex] < 0.f ? -c[diagIndex] : c[diagIndex]; + if (c[diagIndex] > maxDiagonal[i]) { + const float scale = std::sqrt(maxDiagonal[i] / c[diagIndex]); + c[diagIndex] = maxDiagonal[i]; + for (uint8_t j = 0; j < 5; ++j) { + if (j != i) { + c[packedCovarianceIndex(i, j)] *= scale; + } + } + } + } + for (uint8_t i = 0; i < 5; ++i) { + for (uint8_t j = 0; j < i; ++j) { + const float bound = std::sqrt(c[packedCovarianceIndex(i, i)] * c[packedCovarianceIndex(j, j)]); + const uint8_t offIndex = packedCovarianceIndex(i, j); + if (c[offIndex] > bound) { + c[offIndex] = bound; + } else if (c[offIndex] < -bound) { + c[offIndex] = -bound; + } + } + } +} + +} // namespace o2::itsmft::tracking + +#endif // ALICEO2_ITSMFT_TRACKING_SURFACETRACKSTATE_H_ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TimeFrame.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TimeFrame.h new file mode 100644 index 0000000000000..364ce1c681d29 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TimeFrame.h @@ -0,0 +1,222 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file TimeFrame.h +/// \brief Passive common TimeFrame owner. +/// +/// TimeFrame owns the invariant detector layout, measurements and navigation, +/// generic results, tracking scratch, and allocator state. The +/// application owns raw ROFs, publication state, and workflow state. + +#ifndef ALICEO2_ITSMFT_TRACKING_TIMEFRAME_H_ +#define ALICEO2_ITSMFT_TRACKING_TIMEFRAME_H_ + +#include +#include +#include +#include +#include + +#include + +#include "DataFormatsITS/Vertex.h" +#include "SimulationDataFormat/MCCompLabel.h" +#include "SimulationDataFormat/MCTruthContainer.h" +#include "ITSMFTTracking/GenericTrack.h" +#include "ITSMFTTracking/CapacityEstimator.h" +#include "ITSMFTTracking/GlobalMeasurement.h" +#include "ITSMFTTracking/SurfaceMeasurement.h" +#include "ITSMFTTracking/DetectorLayout.h" +#include "ITSMFTTracking/TrackingPrimitives.h" +#include "ITSMFTTracking/IndexTableConfigurationSet.h" +#include "ITSMFTTracking/ROFViews.h" +#include "ITSMFTTracking/detail/TimeFrameScratch.h" +#include "ITSMFTTracking/BoundedAllocator.h" + +namespace o2::itsmft::tracking +{ + +using Vertex = o2::its::Vertex; +using VertexLabel = o2::its::VertexLabel; + +struct TimeFrame { + TimeFrame() = default; + TimeFrame(const TimeFrame&) = delete; + TimeFrame& operator=(const TimeFrame&) = delete; + virtual ~TimeFrame() = default; + + const Vertex& getPrimaryVertex(const int ivtx) const { return mPrimaryVertices[ivtx]; } + auto& getPrimaryVertices() { return mPrimaryVertices; }; + auto getPrimaryVerticesNum() { return mPrimaryVertices.size(); }; + const auto& getPrimaryVertices() const { return mPrimaryVertices; }; + auto& getPrimaryVerticesLabels() { return mPrimaryVerticesLabels; }; + void addPrimaryVertex(const Vertex& vertex); + void addPrimaryVertexLabel(const VertexLabel& label) { mPrimaryVerticesLabels.push_back(label); } + + void resetBeamXY(const float x, const float y, const float w = 0); + void setBeamPosition(const float x, const float y, const float s2, const float base = 50.f, const float systematic = 0.f) + { + isBeamPositionOverridden = true; + resetBeamXY(x, y, s2 / o2::gpu::CAMath::Sqrt((base * base) + systematic)); + mBeamPositionVariance = s2; + } + + float getBeamX() const { return mBeamPos[0]; } + float getBeamY() const { return mBeamPos[1]; } + float getBeamPositionVariance() const { return mBeamPositionVariance; } + std::array& getBeamXY() { return mBeamPos; } + + void setBz(float bz) { mBz = bz; } + float getBz() const { return mBz; } + + gsl::span getGlobalMeasurements(LayerId surface) const; + gsl::span getGlobalMeasurements(LayerId surface); + void addMeasurement(LayerId surface, GlobalMeasurement global, + const SurfaceMeasurement& measurement); + void addMeasurement(LayerId surface, GlobalMeasurement global, + const SurfaceMeasurement& measurement, + gsl::span labels); + void setHasMCInformation(bool value) noexcept { mHasMCInformation = value; } + const SurfaceMeasurement* getSurfaceMeasurement(LayerId layer, uint32_t clusterId) const noexcept; + gsl::span getLabels(LayerId layer, uint32_t clusterId) const; + uint32_t getNMeasurementSurfaces() const noexcept { return static_cast(mLayerGlobalMeasurements.size()); } + std::size_t getTotalMeasurements() const noexcept; + + int getTotalClusters() const { return static_cast(getTotalMeasurements()); } + bool empty() const { return getTotalMeasurements() == 0; } + int getSortedIndex(int rofId, int layer, int idx) const { return mROFramesClusters[layer][rofId] + idx; } + int getSortedStartIndex(int rofId, int layer) const { return mROFramesClusters[layer][rofId]; } + int getNrof(int layer) const + { + return mROFramesClusters[layer].empty() ? 0 : static_cast(mROFramesClusters[layer].size()) - 1; + } + gsl::span getClustersOnLayer(int rofId, int layer); + gsl::span getClustersOnLayer(int rofId, int layer) const; + auto& getClusters() noexcept { return mLayerGlobalMeasurements; } + const auto& getClusters() const noexcept { return mLayerGlobalMeasurements; } + gsl::span getClustersPerROFrange(int rofMin, int range, int layer) const; + gsl::span getROFramesClustersPerROFrange(int rofMin, int range, int layer) const; + gsl::span getROFrameClusters(int layer) const; + gsl::span getIndexTable(int rofId, int layer); + int getClusterROF(int layer, int cluster) const; + int getTotalClustersPerROFrange(int rofMin, int range, int layer) const; + + bool isClusterUsed(int layer, uint32_t clusterId) const; + void markUsedCluster(int layer, uint32_t clusterId); + gsl::span getUsedClusters(int layer); + std::size_t getNumberOfClusters() const; + std::size_t getNumberOfUsedClusters() const; + + float getMinR(int layer) const { return mMinR[layer]; } + float getMaxR(int layer) const { return mMaxR[layer]; } + float getMinZ(int layer) const { return mMinZ[layer]; } + float getMaxZ(int layer) const { return mMaxZ[layer]; } + const auto& getIndexTableUtils() const { return mIndexTableUtils[0]; } + const auto& getIndexTableUtils(int layer) const { return mIndexTableUtils[layer]; } + + void setROFViews(RuntimeROFViews views) noexcept; + void setROFNavigation(std::size_t position, gsl::span boundaries, + RuntimeROFViews views, uint16_t localLayer); + const RuntimeROFViews& getROFViews() const noexcept { return mROFViews; } + const RuntimeROFViews& getROFViews(int layer) const noexcept { return mROFViewsBySurface.empty() ? mROFViews : mROFViewsBySurface[layer]; } + int getROFLocalLayer(int layer) const noexcept { return mROFLocalLayerBySurface.empty() ? layer : mROFLocalLayerBySurface[layer]; } + const ROFTimingLayer& getROFTiming(int layer) const noexcept { return getROFViews(layer).overlap.getLayer(getROFLocalLayer(layer)); } + const RuntimeROFTableEntry& getROFOverlap(int fromLayer, int toLayer, int rof) const noexcept; + bool isROFEnabled(int layer, int rof) const noexcept; + bool isVertexCompatible(int layer, int rof, const Vertex& vertex) const noexcept; + o2::its::TimeEstBC getROFTimeStamp(int fromLayer, int fromROF, int toLayer, int toROF) const noexcept; + int getMaxVerticesPerROF() const noexcept; + const RuntimeROFOverlapView& getROFOverlapView() const noexcept { return mROFViews.overlap; } + const RuntimeROFVertexLookupView& getROFVertexLookupView() const noexcept { return mROFViews.vertexLookup; } + const RuntimeROFMaskView& getROFMaskView() const noexcept { return mUseUPC ? mROFViews.upcMask : mROFViews.mask; } + void useUPCMask() noexcept { mUseUPC = true; } + gsl::span getPrimaryVertices(int layer, int rofId) const; + + bool hasMCinformation() const noexcept; + gsl::span getClusterLabels(int layer, int cluster) const; + + // Clear TimeFrame data while preserving configuration and allocator identity. + void resetTimeFrame() noexcept; + + TimeFrameScratch& getScratch(); + const TimeFrameScratch& getScratch() const; + CapacityEstimator& getCapacityEstimator() noexcept { return mCapacityEstimator; } + const CapacityEstimator& getCapacityEstimator() const noexcept { return mCapacityEstimator; } + + bool configure(DetectorLayout&& layout, std::size_t maxEdges, std::size_t maxCells, + std::shared_ptr memoryPool); + bool isConfigured() const noexcept { return mConfigurationValid; } + const DetectorLayout& getLayout() const noexcept { return mLayout; } + + // Results are valid only with this TimeFrame's measurements. + auto& getGenericTracks() { return mGenericTracks; } + const auto& getGenericTracks() const { return mGenericTracks; } + auto& getTrackLabels() { return mTrackLabels; } + const auto& getTrackLabels() const { return mTrackLabels; } + // Flat inner-to-outer references; IDs are stable pre-sort positions in the + // TimeFrame-owned per-surface arrays. + auto& getTrackClusterIndices() { return mTrackClusterIndices; } + const auto& getTrackClusterIndices() const { return mTrackClusterIndices; } + + /// memory management + void setMemoryPool(std::shared_ptr pool); + auto& getMemoryPool() const noexcept { return mMemoryPool; } + + private: + // Must outlive containers allocated from it (reverse destruction order). + std::shared_ptr mMemoryPool; + + // TimeFrame and cross-iteration tracking state. + std::vector> mROFramesClusters; + std::vector> mIndexTables; + std::vector> mLayerUsedClusters; + IndexTableConfigurationSet mIndexTableUtils; + std::vector mMinR; + std::vector mMaxR; + std::vector mMinZ; + std::vector mMaxZ; + + RuntimeROFViews mROFViews{}; + std::vector mROFViewsBySurface; + std::vector mROFLocalLayerBySurface; + bool mUseUPC{false}; + + float mBz = 5.; + unsigned int mNTotalLowPtVertices = 0; + int mBeamPosWeight = 0; + std::array mBeamPos = {0.f, 0.f}; + float mBeamPositionVariance = 0.f; + bool isBeamPositionOverridden = false; + + bounded_vector mPrimaryVertices; + bounded_vector mPrimaryVerticesLabels; + + bounded_vector mGenericTracks; + bounded_vector mTrackLabels; + bounded_vector mTrackClusterIndices; + + std::vector> mLayerGlobalMeasurements; + std::vector> mLayerSurfaceMeasurements; + std::vector> mLayerClusterLabels; + bool mHasMCInformation{false}; + + bool mConfigurationValid = false; + DetectorLayout mLayout; + TimeFrameScratch mScratch; + CapacityEstimator mCapacityEstimator; + void prepareIndexTables(const IndexTableConfigurationSet& indexTableConfigs); + void prepareClusters(int maxLayers); + friend class Tracker; +}; + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_TIMEFRAME_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Tracker.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Tracker.h new file mode 100644 index 0000000000000..9c20ccc968fcd --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Tracker.h @@ -0,0 +1,119 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file Tracker.h +/// \brief Tracker orchestrator. +/// + +#ifndef ALICEO2_ITSMFT_TRACKING_TRACKER_H_ +#define ALICEO2_ITSMFT_TRACKING_TRACKER_H_ + +#include +#include +#include +#include + +#include + +#include + +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/IterationConfiguration.h" +#include "ITSMFTTracking/DetectorLayout.h" +#include "ITSMFTTracking/detail/TimeFrameScratch.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/TrackerTraits.h" + +namespace o2::itsmft::tracking +{ + +struct TrackerTestAccess; + +/// `run()` returns `Success`, or `RecoverableDropped` for a recoverable +/// per-TimeFrame resource failure (`MemoryLimitExceeded` or `std::bad_alloc`) +/// when `DropTFUponFailure` is enabled. +/// Structural and unclassified failures, and recoverable failures with +/// dropping disabled, propagate as exceptions. +enum class TrackingOutcome : uint8_t { + Success, + RecoverableDropped, + Structural +}; + +/// Complete return value for paths that do not throw. `elapsedMs` is meaningful +/// only when `outcome == Success`; it is 0.f otherwise. +struct TrackingResult { + TrackingOutcome outcome{TrackingOutcome::Success}; + float elapsedMs{0.f}; + // Accepted-result counts are indexed by configured iteration. + std::vector acceptedTrackCounts; +}; + +struct TrackerInitialization { + SurfaceCatalogView catalog; + DetectorLayoutDefinition layout; + TrackingPlan plan; + std::shared_ptr memoryPool; +}; + +enum class TrackerInitializationError : uint8_t { + None, + EmptyConfiguration, + FrameAlreadyConfigured, + MissingCatalog, + MissingMemoryPool, + LayoutInvalid, + TraversalPlanBuildFailed, + DuplicateSource, + CapacityMismatch +}; + +struct TrackerInitializationResult { + TrackerInitializationError error{TrackerInitializationError::None}; + std::size_t failedIteration{static_cast(-1)}; + DetectorLayoutError layoutError{DetectorLayoutError::None}; + bool ok() const noexcept { return error == TrackerInitializationError::None; } +}; + +class Tracker +{ + public: + TrackerInitializationResult initialize(TimeFrame& frame, const TrackerInitialization& configuration); + + gsl::span getIterationConfigurations() const noexcept { return mIterations; } + const TrackingExecutionPolicy& getExecutionPolicy() const noexcept { return mExecutionPolicy; } + const DetectorConfiguration& getDetectorConfiguration() const noexcept { return mDetectorConfiguration; } + const IterationConfiguration* getIterationConfiguration(std::size_t iteration) const noexcept + { + return iteration < mIterations.size() ? &mIterations[iteration] : nullptr; + } + bool isConfiguredFor(const TimeFrame& frame) const noexcept; + + /// Run all configured iterations. Returns `Success` on success or + /// `RecoverableDropped` when an allowed recoverable per-TF failure is + /// dropped. The event is reset before a dropped return or propagated error. + TrackingResult run(TimeFrame& frame, TrackerTraits& traits); + + private: + friend struct TrackerTestAccess; + gsl::span> prepareTimeFrame( + TimeFrame& frame, std::array, MaxLayoutSurfaces>& measurements) const; + void configureBeamPosition(TimeFrame& frame) const; + void initializeIteration(IterationContext& context) const; + void computeTracksMClabels(TimeFrame& frame) const; + TrackingExecutionPolicy mExecutionPolicy; + DetectorConfiguration mDetectorConfiguration; + std::vector mIterations; + const TimeFrame* mFrame = nullptr; +}; +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_TRACKER_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackerTraits.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackerTraits.h new file mode 100644 index 0000000000000..3185eaf3300e0 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackerTraits.h @@ -0,0 +1,162 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file TrackerTraits.h +/// \brief Shared CA tracker traits: same ITS-style tracklet/cell/road logic; MFT uses x-y LUT and forward refit +/// + +#ifndef ALICEO2_ITSMFT_TRACKING_TRACKERTRAITS_H_ +#define ALICEO2_ITSMFT_TRACKING_TRACKERTRAITS_H_ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/GenericTrack.h" +#include "ITSMFTTracking/IterationConfiguration.h" +#include "ITSMFTTracking/SurfaceStateOperationResult.h" +#include "ITSMFTTracking/detail/TimeFrameScratch.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/SurfaceMeasurement.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/detail/TrackingKernelParameters.h" +#include "ITSMFTTracking/BoundedAllocator.h" + +namespace o2::itsmft::tracking +{ + +struct TrackerTestAccess; + +struct IterationContext { + int iteration{-1}; + TimeFrame& frame; + TimeFrameScratch& scratch; + TraversalTopologyView topology{}; + const DetectorConfiguration& detectorConfiguration; + const IterationConfiguration& configuration; + // Borrows the caller's span sequence and the frame's measurements. Both must + // outlive this synchronous traversal; loading/resetting the frame invalidates it. + gsl::span> layerGlobalMeasurements; + float bz{0.f}; + + IterationContext(int iterationValue, TimeFrame& frameValue, TimeFrameScratch& scratchValue, + TraversalTopologyView topologyValue, const IterationConfiguration& configurationValue, + const DetectorConfiguration& detectorConfigurationValue, + gsl::span> layerGlobalMeasurementsValue, + float bzValue) + : iteration{iterationValue}, frame{frameValue}, scratch{scratchValue}, topology{topologyValue}, detectorConfiguration{detectorConfigurationValue}, configuration{configurationValue}, layerGlobalMeasurements{layerGlobalMeasurementsValue}, bz{bzValue} + { + } +}; + +enum class TraversalFailureReason : uint8_t { + MissingLayout, + StaleLayout, + IterationOutOfRange, + SparseTopologyMismatch, + InvalidTraversalSchedule, + MixedSurfaceKindLayout, + SurfaceKindMismatch, + InvalidSurfaceParameters, + // The iteration's index-table configuration is structurally invalid. + InvalidIndexTableConfiguration, + // A non-FirstPass configuration disagrees with the TimeFrame's configuration or LUT. + IndexTableConfigurationMismatch, + // Reserved legacy code; also used for an invalid iteration-to-layout layer count. + // Raised before tracking state is touched; the descriptor is never overwritten. + LegacyMaterialMismatch, + // The active SurfaceKind does not support the configured MatCorrType. + // This structural error is reset and rethrown regardless of drop policy. + // An unrecognized CorrType is reported separately by AttachHitConfigView::isValid(). + UnsupportedMaterialCorrectionMode, + // Per-position normalized measurements disagree with the loaded frame or + // compatibility data. Raised before tracking state is touched; spans commit only on success. + NormalizedMeasurementMismatch, + // The iteration configuration cannot translate a traversal ID to a compact scratch slot. + // This is a binding/layout mismatch, detected before scratch access. + TraversalBindingMismatch +}; + +class TraversalException final : public std::runtime_error +{ + public: + TraversalException(int iteration, TraversalFailureReason reason) + : std::runtime_error{"CA traversal initialization failed at iteration " + std::to_string(iteration) + " (reason=" + std::to_string(static_cast(reason)) + ")"}, + mIteration{iteration}, + mReason{reason} + { + } + + int getIteration() const noexcept { return mIteration; } + TraversalFailureReason getReason() const noexcept { return mReason; } + + private: + int mIteration{-1}; + TraversalFailureReason mReason{TraversalFailureReason::MissingLayout}; +}; + +// Backend implementation of a traversal supplied explicitly by Tracker. +class TrackerTraits +{ + public: + virtual ~TrackerTraits() = default; + // The production caller supplies all event and iteration state explicitly. + void runTraversal(IterationContext& view); + + virtual const char* getName() const noexcept { return "CPU"; } + virtual bool isGPU() const noexcept { return false; } + void setNThreads(int n, std::shared_ptr& arena); + int getNThreads() { return mTaskArena->max_concurrency(); } + + private: + friend struct TrackerTestAccess; + + void acceptTracks(IterationContext& context, int iteration, + bounded_vector& tracks, + bounded_vector>& firstClusters); + + // Tracklet and cell enumeration are common; coordinate selection is owned + // by their operation leaves. + void computeLayerTracklets(IterationContext& context, int iteration, int iVertex); + void computeLayerCells(IterationContext& context, int iteration); + void findCellsNeighbours(IterationContext& context, int iteration); + + void findRoads(IterationContext& context, int iteration); + + bool buildTrackSeed(IterationContext& context, int cellPathId, + const CellSeed& cell, TrackSeed& output, + OperationFailureReason& reason) const; + + // Neighbour processing helper; it does not encode a detector layer count. + template + void processNeighbours(IterationContext& context, int iteration, CellPathId startingPath, + int defaultCellPathId, int startLevel, int currentLevel, + const bounded_vector& currentCellSeed, + const bounded_vector& currentCellId, + const bounded_vector& currentCellPathId, + bounded_vector& updatedCellSeed, + bounded_vector& updatedCellId, + bounded_vector& updatedCellPathIds, + const TrackingKernelParameters& params); + + std::shared_ptr mTaskArena; +}; + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_TRACKERTRAITS_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackingConfigParam.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackingConfigParam.h new file mode 100644 index 0000000000000..9abd75dc558c8 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackingConfigParam.h @@ -0,0 +1,145 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_CONFIG_PARAM_H_ +#define ALICEO2_ITSMFT_TRACKING_CONFIG_PARAM_H_ + +#include +#include +#include +#include + +#include "CommonUtils/ConfigurableParam.h" +#include "CommonUtils/ConfigurableParamHelper.h" +#include "DetectorsCommonDataFormats/DetID.h" + +namespace o2::itsmft::tracking +{ +/// ITS CA layer count. +constexpr int ITSNLayers = 7; +/// MFT CA half-disk layer count. +constexpr int MFTNLayers = 10; +/// Maximum CA iterations. +constexpr int MaxIter = 4; +/// Minimum accepted CA track length for the detector presets. +constexpr int kCAMinTrackLength = 4; +inline constexpr std::array kITSLookupZHalfExtent{ + 16.333f + 1.f, 16.333f + 1.f, 16.333f + 1.f, + 42.140f + 1.f, 42.140f + 1.f, 73.745f + 1.f, 73.745f + 1.f}; +} // namespace o2::itsmft::tracking + +namespace o2::itsmft +{ + +/// Minimal configuration for opt-in ITS common-CA tracking. +/// It does not use the registered name "ITSCATrackerParam", which belongs to the +/// legacy o2::its::TrackerParamConfig. +/// Implemented workflow controls plus reserved diagnostic aliases; unsupported +/// overrides are rejected by common-CA option validation. Defaults preserve the detector tracking +/// baseline for both supported modes. +/// +/// diamondPos, pvRes, and useDiamond define the static vertex/beam constraint +/// consumed by the shared TrackerTraits. +struct ITSCommonCATrackerParam : public o2::conf::ConfigurableParamHelper { + bool dropTFUponFailure = false; + bool printMemory = false; // Reserved alias: true is rejected (no memory report). + size_t maxMemory = std::numeric_limits::max(); + bool saveTimeBenchmarks = false; // Reserved alias: true is rejected (no benchmark writer). + bool useDiamond = false; + float diamondPos[3] = {0.f, 0.f, 0.f}; // Diamond vertex position when useDiamond is set. + float pvRes = -1.f; // Diamond-vertex PV resolution; <=0 keeps the default. + uint16_t holeLayerMask = 0; // Detector layers that may be absent from accepted tracks. + + /// Number of tbb::task_arena threads for the ITS common-CA tracker. + /// This dedicated field is separate from the legacy ITS configuration. + /// Must be > 0; validated where consumed because ConfigurableParam + /// structs cannot reject construction. + int nThreads = 1; + + O2ParamDef(ITSCommonCATrackerParam, "ITSCommonCATrackerParam"); +}; + +template +struct TrackerParamConfig : public o2::conf::ConfigurableParamHelper> { + static constexpr std::string_view getParamName() + { + return "MFTCATrackerParam"; + } + + static constexpr int MinTrackLength = tracking::kCAMinTrackLength; + static constexpr int MaxTrackLength = tracking::MFTNLayers; + static constexpr int getNLayers() { return tracking::MFTNLayers; } + + std::string materialModel = "nominal"; // Implemented provider: nominal descriptor material. + bool useMatCorrTGeo = false; // Legacy alias: true requests unsupported TGeo and is rejected. + bool useFastMaterial = true; // Legacy alias: true selects nominal; false requests unsupported LUT. + int addTimeError[getNLayers()] = {0}; // Tracking window width in BC. + int minTrackLgtIter[o2::itsmft::tracking::MaxIter] = {}; // Async minimum track length per iteration; <=0 keeps preset. + uint32_t startLayerMask[o2::itsmft::tracking::MaxIter] = {}; // Per-pass starts; 0 keeps the preset, bits must name detector layers. + int maxHolesIter[o2::itsmft::tracking::MaxIter] = {}; // Maximum missing internal layers per iteration. + uint16_t holeLayerMask = 0; // Detector layers that may be absent from accepted tracks. + float minPtIterLgt[o2::itsmft::tracking::MaxIter * (MaxTrackLength - MinTrackLength + 1)] = {}; // Async minimum pT by track length; <=0 keeps preset. + float sysErr2Row[getNLayers()] = {0}; // Systematic sensor-row variance for candidate windows (cm^2). + float sysErr2Col[getNLayers()] = {0}; // Systematic sensor-column variance for candidate windows (cm^2). + float maxChi2ClusterAttachment = -1.f; + float maxChi2NDF = -1.f; + float nSigmaCut = -1.f; + float deltaTanLres = -1.f; // Reserved alias: overrides are rejected (no consumer). + float minPt = -1.f; + float pvRes = -1.f; + int LUTbinsU = 64; // Radial bins in the MFT PhiR index (radius in cm). + int LUTbinsV = 128; // Phi bins in the MFT PhiR index (angle in radians). + float diamondPos[3] = {0.f, 0.f, 0.f}; // Diamond vertex for MFT seeds (cm). + bool useDiamond = true; // Compatibility constraint: MFT requires true. + bool perPrimaryVertexProcessing = false; // Compatibility constraint: MFT requires false. + bool saveTimeBenchmarks = false; // Reserved alias: true is rejected (no benchmark writer). + bool overrideBeamEstimation = false; // Reserved alias: true is rejected (no MFT beam estimation). + int trackingMode = -1; // -1: use --tracking-mode; 0: sync, 1: async, 2: cosmics, 3: off. + bool doUPCIteration = false; // Reserved alias: true is rejected (no MFT UPC preset). + int nIterations = -1; // -1 uses all mode preset passes; otherwise a positive limit no larger than the preset. + int reseedIfShorter = 6; // Reserved while reseeding is developed; currently diagnosed as ineffective. + bool shiftRefToCluster{true}; // Shift the linearization reference to the cluster after update. + bool repeatRefitOut{false}; // Repeat outward refit using the inward refit as a seed. + bool createArtefactLabels{false}; // Create labels for artefacts on the fly. + + int nThreads = 1; + bool printMemory = false; // Reserved alias: true is rejected (no memory report). + size_t maxMemory = std::numeric_limits::max(); + bool dropTFUponFailure = false; + bool fataliseUponFailure = true; // Reserved alias: false is rejected; use dropTFUponFailure. + + // Selection of tracks sharing clusters. + bool allowSharingFirstCluster = false; // Allow sharing the first cluster. + float sharedClusterMaxDeltaPhi = 0.05f; // Maximum delta phi at the cluster. + float sharedClusterMaxDeltaEta = 0.03f; // Maximum delta eta at the cluster. + bool sharedClusterOppositeSign = false; // Require opposite-sign tracklets. + + O2ParamDef(TrackerParamConfig, getParamName().data()); + + private: + static_assert(N == o2::detectors::DetID::MFT, "common ITS settings use ITSCommonCATrackerParam"); +}; + +template +TrackerParamConfig TrackerParamConfig::sInstance; + +} // namespace o2::itsmft + +namespace framework +{ +template +struct is_messageable; +template <> +struct is_messageable> : std::true_type { +}; +} // namespace framework + +#endif /* ALICEO2_ITSMFT_TRACKING_CONFIG_PARAM_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackingPrimitives.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackingPrimitives.h new file mode 100644 index 0000000000000..c578ae778796b --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackingPrimitives.h @@ -0,0 +1,57 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_TRACKINGPRIMITIVES_H_ +#define ALICEO2_ITSMFT_TRACKING_TRACKINGPRIMITIVES_H_ + +#include "DataFormatsITS/TimeEstBC.h" +#include "GPUCommonDef.h" +#include "ITSMFTTracking/Constants.h" +#include "ITSMFTTracking/MathUtils.h" + +#include + +namespace o2::itsmft::tracking +{ + +// Per-iteration connection between two sorted measurement locators. +struct Tracklet { + GPUhdDefault() Tracklet() = default; + GPUhd() Tracklet(int first, int second, float tanL, float azimuth, const o2::its::TimeEstBC& time) + : firstClusterIndex{first}, secondClusterIndex{second}, tanLambda{tanL}, phi{azimuth}, mTime{time} + { + } + + GPUhd() bool operator<(const Tracklet& other) const noexcept + { + return firstClusterIndex != other.firstClusterIndex ? firstClusterIndex < other.firstClusterIndex + : secondClusterIndex < other.secondClusterIndex; + } + GPUhd() bool operator==(const Tracklet& other) const noexcept + { + return firstClusterIndex == other.firstClusterIndex && secondClusterIndex == other.secondClusterIndex; + } + GPUhd() bool isCompatible(const Tracklet& other) const { return mTime.isCompatible(other.mTime); } + GPUhd() auto& getTimeStamp() noexcept { return mTime; } + GPUhd() const auto& getTimeStamp() const noexcept { return mTime; } + + int firstClusterIndex{o2::its::constants::UnusedIndex}; + int secondClusterIndex{o2::its::constants::UnusedIndex}; + float tanLambda{o2::its::constants::UnsetValue}; + float phi{o2::its::constants::UnsetValue}; + o2::its::TimeEstBC mTime; +}; + +static_assert(std::is_trivially_copyable_v); + +} // namespace o2::itsmft::tracking + +#endif // ALICEO2_ITSMFT_TRACKING_TRACKINGPRIMITIVES_H_ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TraversalTopology.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TraversalTopology.h new file mode 100644 index 0000000000000..b87a19f7cc22f --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TraversalTopology.h @@ -0,0 +1,145 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_TRAVERSALTOPOLOGY_H_ +#define ALICEO2_ITSMFT_TRACKING_TRAVERSALTOPOLOGY_H_ + +#include +#include + +#ifndef GPUCA_GPUCODE +#include +#include +#include "ITSMFTTracking/DetectorLayout.h" +#endif + +#include "ITSMFTTracking/IdTypes.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/LayerMask.h" + +namespace o2::itsmft +{ +struct IterationParameters; +} + +namespace o2::itsmft::tracking +{ + +struct Edge { + LayerId from{}; + LayerId to{}; +}; + +struct CellPath { + EdgeId first{}; + EdgeId second{}; +}; + +struct TopologyRange { + uint32_t firstEntry{0}; + uint32_t entries{0}; + + uint32_t getFirstEntry() const noexcept { return firstEntry; } + uint32_t getEntries() const noexcept { return entries; } + uint32_t getEntriesBound() const noexcept { return firstEntry + entries; } +}; + +struct TraversalTopologyView { + SurfaceCatalogView catalog{}; + uint32_t nLayers{0}; + const LayerId* activeSurfaceList{nullptr}; + uint32_t nActiveSurfaces{0}; + LayerMask activeLayers{}; + const Edge* edges{nullptr}; + uint32_t nEdges{0}; + const CellPath* paths{nullptr}; + uint32_t nPaths{0}; + const uint32_t* pathsByFirstEdgeOffsets{nullptr}; + const CellPathId* pathsByFirstEdge{nullptr}; + const CellPathId* scheduledPaths{nullptr}; + uint32_t nScheduledPaths{0}; + const CellPathId* roadStartPaths{nullptr}; + uint32_t nRoadStartPaths{0}; + const uint32_t* roadStartComponentOffsets{nullptr}; + uint32_t nRoadStartComponentOffsets{0}; + LayerMask seedingLayers{}; + + const SurfaceDescriptor& getSurface(LayerId id) const { return catalog.getSurface(id); } + SurfaceCatalogView getSurfaceCatalogView() const noexcept { return catalog; } + const Edge& getEdge(EdgeId id) const { return edges[id.value()]; } + const CellPath& getPath(CellPathId id) const { return paths[id.value()]; } + TopologyRange getPathsStartingWithEdge(EdgeId edge) const + { + const auto index = edge.value(); + return {pathsByFirstEdgeOffsets[index], pathsByFirstEdgeOffsets[index + 1] - pathsByFirstEdgeOffsets[index]}; + } +}; + +#ifndef GPUCA_GPUCODE +struct TraversalTopology { + uint16_t nLayers{0}; + std::vector activeSurfaceList; + LayerMask activeLayers{}; + LayerMask seedingLayers{}; + std::vector edges; + std::vector paths; + std::vector pathsByFirstEdgeOffsets; + std::vector pathsByFirstEdge; + std::vector scheduledPaths; + std::vector roadStartPaths; + std::vector roadStartComponentOffsets; + + TraversalTopologyView getView(SurfaceCatalogView catalog) const noexcept + { + return {catalog, + nLayers, + activeSurfaceList.data(), static_cast(activeSurfaceList.size()), + activeLayers, + edges.data(), static_cast(edges.size()), + paths.data(), static_cast(paths.size()), + pathsByFirstEdgeOffsets.data(), pathsByFirstEdge.data(), + scheduledPaths.data(), static_cast(scheduledPaths.size()), + roadStartPaths.data(), static_cast(roadStartPaths.size()), + roadStartComponentOffsets.data(), static_cast(roadStartComponentOffsets.size()), + seedingLayers}; + } +}; + +enum class TraversalTopologyError : uint8_t { + None, + InvalidLayout, + LayerCountMismatch, + NegativeMaxHoles, + NoActiveSurfaces, + TooManyEdges, + TooManyPaths +}; + +struct TraversalTopologyBuildResult { + std::optional topology; + TraversalTopologyError error{TraversalTopologyError::None}; + + bool ok() const noexcept { return topology.has_value(); } +}; + +// Derive one iteration's topology from the invariant detector layout and the +// Tracker-owned iteration parameters. +TraversalTopologyBuildResult deriveTraversalTopology(const DetectorLayout& layout, + const o2::itsmft::IterationParameters& parameters); + +#endif // GPUCA_GPUCODE + +static_assert(sizeof(CellPath) == 4); +static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v); + +} // namespace o2::itsmft::tracking + +#endif diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TripletFitting.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TripletFitting.h new file mode 100644 index 0000000000000..58fff3542ee42 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TripletFitting.h @@ -0,0 +1,76 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_TRIPLETFITTING_H_ +#define ALICEO2_ITSMFT_TRACKING_TRIPLETFITTING_H_ + +#include +#include + +#include "GPUCommonDef.h" +#include "ITSMFTTracking/GlobalMeasurement.h" + +namespace o2::itsmft::tracking +{ + +struct TripletKinkVector { + float theta{0.f}; + float phi{0.f}; +}; + +// Theta and phi rows of the hit-coordinate Jacobian H. +struct TripletHitJacobian { + std::array theta{}; + std::array phi{}; +}; + +// Linearized local-triplet factor from Eq. (19) of the General Triplet Track +// Fit. H is evaluated at kappaRef = -Psi_phi / rho_phi and hit slot i maps to +// CellSeed::getClusterReference(i). Measurement and MS covariances are added +// when adjacent triplets are compared. +struct TripletFitFactor { + TripletKinkVector psi{}; + TripletKinkVector rho{}; + std::array h{}; + + GPUhdi() bool isValid() const noexcept + { + return rho.phi != 0.f; + } +}; + +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(TripletFitFactor) == 88); + +struct AdjacentTripletFitResult { + float curvature{0.}; + float curvatureVariance{0.}; + float chi2{0.}; +}; + +bool makeTripletFitFactor( + const std::array& measurements, + TripletFitFactor& factor) noexcept; + +// Minimize Eq. (19) for adjacent triplets sharing one curvature. measurements +// are the four unique ordered hits; angularVariance is the space-angle MS +// variance for each triplet. +bool fitAdjacentTripletFactors( + const TripletFitFactor& firstFactor, + const TripletFitFactor& secondFactor, + const std::array& measurements, + const std::array& angularVariance, + AdjacentTripletFitResult& result) noexcept; + +} // namespace o2::itsmft::tracking + +#endif // ALICEO2_ITSMFT_TRACKING_TRIPLETFITTING_H_ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/WorkflowSession.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/WorkflowSession.h new file mode 100644 index 0000000000000..94141011c7346 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/WorkflowSession.h @@ -0,0 +1,284 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_WORKFLOWSESSION_H_ +#define ALICEO2_ITSMFT_TRACKING_WORKFLOWSESSION_H_ + +#include +#include +#include +#include +#include +#include +#include "CommonConstants/LHCConstants.h" +#include "Framework/Logger.h" +#include "ITSMFTTracking/GenericTrackOutputAdapter.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/ROFLookupTables.h" +#include "ITSMFTTracking/Tracker.h" + +namespace o2::itsmft::tracking +{ +enum class CATrackerPublicationAction { + PublishInactiveEmpty, + PublishActiveResult, + SkipDroppedTimeFrame, +}; +inline CATrackerPublicationAction decideCATrackerPublicationAction(bool active, TrackingOutcome outcome) noexcept +{ + if (!active) { + return CATrackerPublicationAction::PublishInactiveEmpty; + } + return outcome == TrackingOutcome::RecoverableDropped ? CATrackerPublicationAction::SkipDroppedTimeFrame + : CATrackerPublicationAction::PublishActiveResult; +} + +// The common columns are copied into framework-owned output storage before the +// session is reset. Detector-specific columns (MFT seed patterns, MC) stay explicit. +template +void copyTrackingOutputColumns(Allocator& outputs, Output rofs, Output tracks, Output indices, const Staged& staged) +{ + outputs.template make>(rofs, staged.trackROFs.begin(), staged.trackROFs.end()); + outputs.template make>(tracks, staged.tracks.begin(), staged.tracks.end()); + outputs.template make>(indices, staged.clusterIndices.begin(), staged.clusterIndices.end()); +} + +// Own every backing store borrowed by a single detector's workflow views. +// Detector-specific selection, truth vertices and output formats stay in the task. +class WorkflowSession +{ + public: + WorkflowSession(const char* detectorName, int nLayers) + : overlap(nLayers), vertices(nLayers), mask(nLayers), upcMask(nLayers), mDetectorName(detectorName) {} + + TimeFrame frame; + std::vector> externalIndices; + std::vector> clusterSizes; + ROFOverlapTable overlap; + ROFVertexLookupTable vertices; + ROFMaskTable mask; + ROFMaskTable upcMask; + std::optional publicationClock; + + class Cleanup + { + public: + explicit Cleanup(WorkflowSession& session) : mSession(session) {} + Cleanup(const Cleanup&) = delete; + Cleanup& operator=(const Cleanup&) = delete; + ~Cleanup() noexcept + { + if (mResetFrame) { + mSession.reset(); + } + mSession.invalidatePublication(); + } + // Both the loader recovery and Tracker::run have already reset a dropped TF. + void frameAlreadyReset() noexcept { mResetFrame = false; } + + private: + WorkflowSession& mSession; + bool mResetFrame = true; + }; + Cleanup cleanupOnExit() { return Cleanup{*this}; } + + void reset() noexcept + { + externalIndices.clear(); + clusterSizes.clear(); + frame.resetTimeFrame(); + } + void invalidatePublication() noexcept + { + publicationClock.reset(); + externalIndices.clear(); + clusterSizes.clear(); + frame.setROFViews({}); + } + + template + std::vector layerTimings(const AlpideParameters& alpide, int nOrbits, + const std::vector& addTimeError) const + { + const int nLayers = overlap.getEntries(); + if (addTimeError.size() != nLayers) { + throw TimeFrameLoadException{TimeFrameLoadFailureReason::NonUniformROFTiming, + std::string(mDetectorName) + " CA timing-error layer count differs from the workflow layout"}; + } + std::vector timings(nLayers); + for (int layer = 0; layer < nLayers; ++layer) { + const auto length = alpide.getROFLengthInBC(layer); + if (length <= 0) { + throw TimeFrameLoadException{TimeFrameLoadFailureReason::NonUniformROFTiming, + std::string(mDetectorName) + " CA per-layer ROF timing has a non-positive ROF length"}; + } + const auto rofsPerOrbit = o2::constants::lhc::LHCMaxBunches / static_cast(length); + timings[layer] = {.mNROFsTF = rofsPerOrbit * static_cast(nOrbits), + .mROFLength = static_cast(length), + .mROFDelay = static_cast(alpide.getROFDelayInBC(layer)), + .mROFBias = static_cast(alpide.getROFBiasInBC(layer)), + .mROFAddTimeErr = addTimeError[layer]}; + if (timings[layer].mNROFsTF == 0) { + throw TimeFrameLoadException{TimeFrameLoadFailureReason::ZeroROFCount, + std::string(mDetectorName) + " CA per-layer ROF timing yields zero ROFs per TimeFrame"}; + } + } + return timings; + } + + template + void configureTiming(gsl::span timings, AcceptROF&& accept) + { + const int nLayers = overlap.getEntries(); + if (timings.size() != nLayers || !deriveUniformROFTimingConfig(timings).uniform) { + throw TimeFrameLoadException{TimeFrameLoadFailureReason::NonUniformROFTiming, + std::string(mDetectorName) + " CA per-layer ROF timing configuration has an unexpected layer count or is not uniform"}; + } + // Only owned timing structure survives between TFs. The key includes every + // layer's extent and timing fields, so readout/CCDB changes rebuild it. + publicationClock.reset(); + frame.setROFViews({}); + if (!matchesTiming(timings)) { + ROFOverlapTable nextOverlap{nLayers}; + ROFVertexLookupTable nextVertices{nLayers}; + for (int layer = 0; layer < nLayers; ++layer) { + nextOverlap.defineLayer(layer, timings[layer]); + nextVertices.defineLayer(layer, timings[layer]); + } + nextOverlap.init(); + nextVertices.init(); + ROFMaskTable nextMask{nextOverlap}; + std::vector nextTimingKey(timings.begin(), timings.end()); + overlap = std::move(nextOverlap); + vertices = std::move(nextVertices); + mask = std::move(nextMask); + mTimingKey = std::move(nextTimingKey); + } + // Vertex contents and selection are event-local even on a cache hit. Views + // are rebound only after refresh succeeds; a throwing filter leaves no + // partially refreshed event published and the next call can reuse the key. + vertices.update(nullptr, 0); + mask.resetMask(); + for (int rof = 0; rof < static_cast(timings[0].mNROFsTF); ++rof) { + if (accept(rof)) { + for (int layer = 0; layer < nLayers; ++layer) { + mask.setROFEnabled(layer, rof, 1); + } + } + } + frame.setROFViews({overlap.getView(), vertices.getView(), mask.getView(), upcMask.getView()}); + } + + template + bool loadWithRecovery(bool dropOnFailure, Load&& load) + { + try { + load(); + return true; + } catch (const RecoverableLoadFailure& error) { + LOGP(error, "{} CA loading recoverably failed: {}", mDetectorName, error.what()); + reset(); + if (!dropOnFailure) { + throw; + } + } catch (const BoundedMemoryResource::MemoryLimitExceeded& error) { + LOGP(error, "{} CA loading exceeded memory limit: {}", mDetectorName, error.what()); + reset(); + if (!dropOnFailure) { + throw; + } + } catch (const std::bad_alloc& error) { + LOGP(error, "{} CA loading allocation failed: {}", mDetectorName, error.what()); + reset(); + if (!dropOnFailure) { + throw; + } + } catch (const TimeFrameLoadException& error) { + LOGP(error, "{} CA loading hit a structural failure: {}", mDetectorName, error.what()); + reset(); + throw; + } catch (const std::exception& error) { + LOGP(error, "{} CA loading failed with an unclassified exception: {}", mDetectorName, error.what()); + reset(); + throw; + } + return false; + } + + template + TrackingOutcome process(Tracker& tracker, TrackerTraits& traits, ClusterSourceInput source, + AfterLoad&& afterLoad, Complete&& complete) + { + const auto views = frame.getROFViews(); + if (views.overlap.mLayerCount > 0 && source.rofs.size() != views.overlap.getLayer(0).mNROFsTF) { + LOGP(warn, "{} CA ROF count differs from continuous timing expectation: received {} expected {}", + mDetectorName, source.rofs.size(), views.overlap.getLayer(0).mNROFsTF); + } + const auto origin = source.rofs.empty() ? o2::InteractionRecord{} : source.rofs.front().getBCData(); + if (!loadWithRecovery(tracker.getExecutionPolicy().DropTFUponFailure, [&] { + if (!source.dictionary) { + throw TimeFrameLoadException{TimeFrameLoadFailureReason::DictionaryNotConfigured, + std::string(mDetectorName) + " CA tracker cluster dictionary is not available"}; + } + if (views.overlap.mLayerCount <= 0) { + throw TimeFrameLoadException{TimeFrameLoadFailureReason::NonUniformROFTiming, + std::string(mDetectorName) + " CA tracker received no adapter-owned runtime ROF timing view"}; + } + const auto& clock = views.overlap.getLayer(0); + source.timing = {clock.mROFLength, clock.mROFDelay, clock.mROFBias, clock.mROFAddTimeErr}; + source.rofViews = views; + const auto loaded = loadTimeFrameSources(frame, gsl::span{&source, 1}, + frame.getLayout().getSurfaceCatalog(), origin, &externalIndices, &clusterSizes); + if (!loaded.ok()) { + if (isRecoverableLoadError(loaded.error, loaded.timingDetail)) { + throw RecoverableLoadFailure{loaded}; + } + throw TimeFrameLoadException{loaded}; + } + afterLoad(origin); + })) { + return TrackingOutcome::RecoverableDropped; + } + const auto result = tracker.run(frame, traits); + if (result.outcome != TrackingOutcome::RecoverableDropped) { + complete(result); + } + if (result.outcome == TrackingOutcome::RecoverableDropped) { + LOGP(warn, "{} CA tracking failed for this TF", mDetectorName); + } else { + LOGP(info, "{} CA tracking produced {} tracks in {:.2f} ms", mDetectorName, frame.getGenericTracks().size(), result.elapsedMs); + } + return result.outcome; + } + + private: + bool matchesTiming(gsl::span timings) const noexcept + { + if (mTimingKey.size() != timings.size()) { + return false; + } + for (std::size_t layer = 0; layer < timings.size(); ++layer) { + const auto& cached = mTimingKey[layer]; + const auto& next = timings[layer]; + if (cached.mNROFsTF != next.mNROFsTF || cached.mROFLength != next.mROFLength || + cached.mROFDelay != next.mROFDelay || cached.mROFBias != next.mROFBias || + cached.mROFAddTimeErr != next.mROFAddTimeErr) { + return false; + } + } + return true; + } + + const char* mDetectorName; + std::vector mTimingKey; +}; +} // namespace o2::itsmft::tracking +#endif diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/CandidateFinding.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/CandidateFinding.h new file mode 100644 index 0000000000000..0ccb7889ec1ac --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/CandidateFinding.h @@ -0,0 +1,79 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_CANDIDATEFINDING_H_ +#define ALICEO2_ITSMFT_TRACKING_CANDIDATEFINDING_H_ + +#ifndef GPUCA_GPUCODE +#include "ITSMFTTracking/GlobalMeasurement.h" +#include "ITSMFTTracking/IndexTableUtils.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/TrackingPrimitives.h" +#endif + +#ifndef GPUCA_GPUCODE +namespace o2::dataformats +{ +template +class Vertex; +} +namespace o2::its +{ +class TimeEstBC; +using Vertex = o2::dataformats::Vertex; +} // namespace o2::its +#endif + +namespace o2::itsmft::tracking +{ + +#ifndef GPUCA_GPUCODE + +struct TrackletProjectionCache { + int fromLayer; + int toLayer; + float fromRadius; + float toRadius; + float targetMinR; + float targetMaxR; + float targetMinZ; + float targetMaxZ; + float sourcePositionResolution; + float edgeMSAngle; + float edgePhiCut; +}; + +struct TrackletSearchWindow { + int4 bins; + float sourceReferenceCoordinate{0.f}; + float sourceProjectedCoordinate{0.f}; + float slope{0.f}; + float varianceConstant{0.f}; + float varianceLinear{0.f}; + float varianceQuadratic{0.f}; + float phiPrediction{0.f}; + float phiVariance{0.f}; +}; + +bool projectTrackletSearchWindow(const GlobalMeasurement& sourceMeasurement, + const o2::its::Vertex& vertex, + float beamPositionVariance, + SurfaceKind kind, + const TrackletProjectionCache& edgeCache, + const o2::itsmft::IndexTableUtilsCore& indexUtils, + float nSigmaCut, + TrackletSearchWindow& out); + +#endif + +} // namespace o2::itsmft::tracking + +#endif diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/ITSSharedClusterCompatibility.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/ITSSharedClusterCompatibility.h new file mode 100644 index 0000000000000..6c90d543c64ba --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/ITSSharedClusterCompatibility.h @@ -0,0 +1,147 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_ITSSHAREDCLUSTERCOMPATIBILITY_H_ +#define ALICEO2_ITSMFT_TRACKING_ITSSHAREDCLUSTERCOMPATIBILITY_H_ + +#ifndef GPUCA_GPUCODE + +#include +#include +#include +#include +#include +#include +#include + +namespace o2::itsmft::tracking +{ + +// ITS output-compatibility sidecar. The pre-sort sequence maps accepted-track +// slots to global GenericTrack indices; entries() exposes the sealed sparse +// sequence after the final serial markTracks() pass. +struct ITSSharedClusterCompatibilityEntry { + uint32_t genericTrackIndex{}; + bool hasSharedClusters{}; +}; + +enum class ITSSharedClusterCompatibilitySealStep : uint8_t { + BeforeReserve +}; + +class ITSSharedClusterCompatibility +{ + public: + const std::vector& entries() const noexcept { return mEntries; } + bool isSealed() const noexcept { return mSealed; } + size_t pendingSize() const noexcept { return mPendingIndices.size(); } + + void clear() noexcept + { + mPendingIndices.clear(); + mEntries.clear(); + mSealed = false; + } + + bool replaceFromAcceptedTrackIndices(gsl::span indices, + gsl::span sharedFlags) + { + std::vector staged; + staged.reserve(indices.size()); + uint32_t previous = 0; + bool havePrevious = false; + for (const auto index : indices) { + if ((havePrevious && previous >= index) || index >= sharedFlags.size()) { + return false; + } + staged.push_back({index, sharedFlags[index] != 0}); + previous = index; + havePrevious = true; + } + mPendingIndices.clear(); + mEntries.swap(staged); + mSealed = true; + return true; + } + + // Called after the final serial markTracks() pass, while indices still align + // with the unreordered accepted slots. + template + bool sealFromMarkedTracks(const Tracks& tracks, Hook&& hook) + { + if (mSealed || tracks.size() != mPendingIndices.size()) { + return false; + } + std::vector staged; + hook(ITSSharedClusterCompatibilitySealStep::BeforeReserve); + staged.reserve(mPendingIndices.size()); + uint32_t previous = 0; + bool havePrevious = false; + for (size_t i = 0; i < mPendingIndices.size(); ++i) { + const auto index = mPendingIndices[i]; + if ((havePrevious && previous >= index)) { + return false; + } + staged.push_back({index, tracks[i].hasSharedClusters()}); + previous = index; + havePrevious = true; + } + mEntries.swap(staged); + mSealed = true; + return true; + } + + template + bool sealFromMarkedTracks(const Tracks& tracks) + { + return sealFromMarkedTracks(tracks, [](ITSSharedClusterCompatibilitySealStep) {}); + } + + private: + friend class ITSSharedClusterCompatibilityTransaction; + std::vector mPendingIndices; + std::vector mEntries; + bool mSealed = false; +}; + +// Transactional association between a pre-sort accepted slot and GenericTrack. +class ITSSharedClusterCompatibilityTransaction +{ + public: + explicit ITSSharedClusterCompatibilityTransaction(ITSSharedClusterCompatibility& sidecar) + : mSidecar{sidecar}, mOldSize{sidecar.mPendingIndices.size()} + { + } + + bool validate(uint32_t genericTrackIndex) const noexcept + { + return !mSidecar.mSealed && (mSidecar.mPendingIndices.empty() || mSidecar.mPendingIndices.back() < genericTrackIndex); + } + void reserve() { mSidecar.mPendingIndices.reserve(mOldSize + 1); } + void append(uint32_t genericTrackIndex) + { + if (!validate(genericTrackIndex)) { + throw std::logic_error{"invalid ITS shared-cluster compatibility index"}; + } + mSidecar.mPendingIndices.push_back(genericTrackIndex); + } + void rollback() noexcept { mSidecar.mPendingIndices.resize(mOldSize); } + + private: + ITSSharedClusterCompatibility& mSidecar; + size_t mOldSize; +}; + +} // namespace o2::itsmft::tracking + +#endif // !GPUCA_GPUCODE + +#endif // ALICEO2_ITSMFT_TRACKING_ITSSHAREDCLUSTERCOMPATIBILITY_H_ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/MFTFwdTrackHelpers.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/MFTFwdTrackHelpers.h new file mode 100644 index 0000000000000..45088f76d434a --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/MFTFwdTrackHelpers.h @@ -0,0 +1,130 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file MFTFwdTrackHelpers.h +/// \brief Forward-track coordinate helpers for MFT CA candidate finding +/// + +#ifndef ALICEO2_ITSMFT_TRACKING_MFTFWDTRACKHELPERS_H_ +#define ALICEO2_ITSMFT_TRACKING_MFTFWDTRACKHELPERS_H_ + +#include +#include + +#include "CommonConstants/MathConstants.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/Constants.h" +#include "MFTTracking/Constants.h" +#include "ReconstructionDataFormats/TrackFwd.h" + +namespace o2::itsmft::tracking::detail +{ + +/// MFT CA uses o2::mft::constants::mft::LayersNumber half-disk layers (same index as GeometryTGeo::getLayer). +/// Physical disk index is halfLayer / 2; ROFOverlapTable stores one LayerTiming per half-layer. + +inline float mftLayerZ(int layer) +{ + return o2::mft::constants::mft::LayerZCoordinate()[layer]; +} + +inline void mftTrackletProject(float xCl, float yCl, float zCl, float pvX, float pvY, float pvZ, + float zFrom, float zTo, float bz, float minPt, + float& xProj, float& yProj) +{ + if (std::abs(bz) > 0.01f && minPt > 0.f) { + const float dxTan = xCl - pvX; + const float dyTan = yCl - pvY; + const float dzTan = zCl - pvZ; + const float drTan = std::sqrt(dxTan * dxTan + dyTan * dyTan); + float invQPt = 1.f / minPt; + float tanl = (drTan > 1e-6f) ? -std::abs(dzTan) / drTan : -1.f; + float phi = (drTan > 1e-6f) ? std::atan2(dyTan, dxTan) : 0.f; + if (std::abs(tanl) > 1e-6f) { + const float k = std::abs(o2::constants::math::B2C * bz); + const float hz = (bz > 0.f) ? 1.f : -1.f; + phi -= 0.5f * hz * invQPt * dzTan * k / tanl; + } + ROOT::Math::SVector params{xCl, yCl, phi, tanl, invQPt}; + ROOT::Math::SMatrix> cov{}; + cov(0, 0) = cov(1, 1) = cov(2, 2) = cov(3, 3) = 1.; + const double qptSigma = std::clamp(static_cast(std::abs(invQPt)), 1., 10.); + cov(4, 4) = qptSigma * qptSigma; + o2::track::TrackParCovFwd track{zCl, params, cov, 0.}; + track.propagateToZhelix(zTo, bz); + xProj = static_cast(track.getX()); + yProj = static_cast(track.getY()); + } else { + const float dz0 = zFrom - pvZ; + if (std::abs(dz0) < 1e-6f) { + xProj = xCl; + yProj = yCl; + return; + } + const float w = (zTo - pvZ) / dz0; + xProj = pvX + w * (xCl - pvX); + yProj = pvY + w * (yCl - pvY); + } +} + +inline void mftTrackletProject(float xCl, float yCl, float zCl, float pvX, float pvY, float pvZ, + int fromLayer, int toLayer, float bz, float minPt, + float& xProj, float& yProj) +{ + mftTrackletProject(xCl, yCl, zCl, pvX, pvY, pvZ, mftLayerZ(fromLayer), mftLayerZ(toLayer), + bz, minPt, xProj, yProj); +} + +inline void mftTrackletSigmaXY(float x0, float y0, float pvX, float pvY, float pvZ, + float sigma2X0, float sigma2Y0, float sigma2PvX, float sigma2PvY, float sigma2PvZ, + float zFrom, float zTo, float rLayerFrom, float meanDeltaZ, float msAngle, + float bendingAngle, float xProj, float yProj, float& sigmaX, float& sigmaY) +{ + const float dz0 = zFrom - pvZ; + const float tanlRef = (std::abs(rLayerFrom) > 1e-6f) ? zFrom / rLayerFrom : 0.f; + const float sigma2MS = meanDeltaZ * meanDeltaZ * msAngle * msAngle * (tanlRef * tanlRef + 1.f); + if (std::abs(dz0) < o2::its::constants::Tolerance) { + sigmaX = std::sqrt(sigma2X0 + sigma2PvX + sigma2MS); + sigmaY = std::sqrt(sigma2Y0 + sigma2PvY + sigma2MS); + } else { + const float w = (zTo - pvZ) / dz0; + const float invDz0 = w / dz0; + const float sigma2W = invDz0 * invDz0 * sigma2PvZ; + const float dx0 = x0 - pvX; + const float dy0 = y0 - pvY; + const float oneMinusW = 1.f - w; + sigmaX = std::sqrt(oneMinusW * oneMinusW * sigma2PvX + w * w * sigma2X0 + dx0 * dx0 * sigma2W + sigma2MS); + sigmaY = std::sqrt(oneMinusW * oneMinusW * sigma2PvY + w * w * sigma2Y0 + dy0 * dy0 * sigma2W + sigma2MS); + } + const float rProj = std::hypot(xProj, yProj); + if (rProj > 1e-6f && bendingAngle > 0.f) { + const float dr = rProj * bendingAngle; + const float invR = 1.f / rProj; + const float sinPhi = yProj * invR; + const float cosPhi = xProj * invR; + sigmaX = std::sqrt(sigmaX * sigmaX + dr * dr * sinPhi * sinPhi); + sigmaY = std::sqrt(sigmaY * sigmaY + dr * dr * cosPhi * cosPhi); + } +} + +inline void mftTrackletSigmaXY(float x0, float y0, float pvX, float pvY, float pvZ, + float sigma2X0, float sigma2Y0, float sigma2PvX, float sigma2PvY, float sigma2PvZ, + int fromLayer, int toLayer, float rLayerFrom, float meanDeltaZ, float msAngle, + float bendingAngle, float xProj, float yProj, float& sigmaX, float& sigmaY) +{ + mftTrackletSigmaXY(x0, y0, pvX, pvY, pvZ, sigma2X0, sigma2Y0, sigma2PvX, sigma2PvY, sigma2PvZ, + mftLayerZ(fromLayer), mftLayerZ(toLayer), rLayerFrom, meanDeltaZ, msAngle, + bendingAngle, xProj, yProj, sigmaX, sigmaY); +} + +} // namespace o2::itsmft::tracking::detail + +#endif /* ALICEO2_ITSMFT_TRACKING_MFTFWDTRACKHELPERS_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/SurfaceStateOperations.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/SurfaceStateOperations.h new file mode 100644 index 0000000000000..9e4f93eeeef57 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/SurfaceStateOperations.h @@ -0,0 +1,81 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_DETAIL_SURFACESTATEOPERATIONS_H_ +#define ALICEO2_ITSMFT_TRACKING_DETAIL_SURFACESTATEOPERATIONS_H_ + +#include "ITSMFTTracking/MaterialPhysics.h" +#include "ITSMFTTracking/SurfaceTrackState.h" +#include "ITSMFTTracking/SurfaceMeasurement.h" +#include "ITSMFTTracking/SurfaceStateOperationResult.h" + +// Coordinate-family leaves used only by Propagator and their numerical +// tests. Production callers use Propagator's +// descriptor/state-driven API rather than selecting a family themselves. +namespace o2::itsmft::tracking::detail +{ +namespace barrel +{ +bool rotate(SurfaceTrackState& state, float targetAlpha, OperationFailureReason& reason) noexcept; +bool propagate(SurfaceTrackState& state, float targetX, float bz, OperationFailureReason& reason) noexcept; +bool predictedChi2(const SurfaceTrackState& state, const SurfaceMeasurement& measurement, float& chi2, + OperationFailureReason& reason) noexcept; +bool update(SurfaceTrackState& state, const SurfaceMeasurement& measurement, float& chi2, + OperationFailureReason& reason) noexcept; +material::MaterialOperationResult correctForMaterial(SurfaceTrackState& state, material::IntegratedMaterialBudget materialBudget, + material::MaterialTraversalDirection direction) noexcept; +material::MaterialOperationResult correctForMaterial(SurfaceTrackState& state, SurfaceTrackParameters& linRef, + material::IntegratedMaterialBudget materialBudget, + material::MaterialTraversalDirection direction) noexcept; +bool stateChi2(const SurfaceTrackState& reference, const SurfaceTrackState& candidate, float& chi2, + OperationFailureReason& reason) noexcept; + +#ifndef GPUCA_GPUCODE +bool rotate(SurfaceTrackState& state, SurfaceTrackParameters& linRef, float targetAlpha, float bz, + OperationFailureReason& reason) noexcept; +bool propagate(SurfaceTrackState& state, SurfaceTrackParameters& linRef, float targetX, float bz, + OperationFailureReason& reason) noexcept; +bool shiftReferenceToMeasurement(SurfaceTrackParameters& linRef, const SurfaceMeasurement& measurement, + OperationFailureReason& reason) noexcept; +#endif +} // namespace barrel + +namespace forward +{ +bool propagate(SurfaceTrackState& state, float targetZ, float bz, OperationFailureReason& reason) noexcept; +bool propagate(SurfaceTrackState& state, SurfaceTrackParameters& linRef, + float targetZ, float bz, OperationFailureReason& reason) noexcept; +bool predictedChi2(const SurfaceTrackState& state, const SurfaceMeasurement& measurement, float& chi2, + OperationFailureReason& reason) noexcept; +bool update(SurfaceTrackState& state, const SurfaceMeasurement& measurement, float& chi2, + OperationFailureReason& reason) noexcept; +constexpr float highlandTheta2(float inverseMomentum, float xOverX0) noexcept +{ + const float theta = 0.0136f * inverseMomentum; + return theta * theta * xOverX0; +} +bool correctForMaterial(SurfaceTrackState& state, float xOverX0, OperationFailureReason& reason) noexcept; +material::MaterialOperationResult correctForMaterial(SurfaceTrackState& state, material::IntegratedMaterialBudget materialBudget, + material::MaterialTraversalDirection direction) noexcept; +material::MaterialOperationResult correctForMaterial(SurfaceTrackState& state, SurfaceTrackParameters& linRef, + material::IntegratedMaterialBudget materialBudget, + material::MaterialTraversalDirection direction) noexcept; +bool stateChi2(const SurfaceTrackState& reference, const SurfaceTrackState& candidate, float& chi2, + OperationFailureReason& reason) noexcept; + +#ifndef GPUCA_GPUCODE +bool shiftReferenceToMeasurement(SurfaceTrackParameters& linRef, const SurfaceMeasurement& measurement, + OperationFailureReason& reason) noexcept; +#endif +} // namespace forward +} // namespace o2::itsmft::tracking::detail + +#endif diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/SurfaceTrackStateLegacyAdapters.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/SurfaceTrackStateLegacyAdapters.h new file mode 100644 index 0000000000000..9a680bba1c7ee --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/SurfaceTrackStateLegacyAdapters.h @@ -0,0 +1,149 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_SURFACETRACKSTATELEGACYADAPTERS_H_ +#define ALICEO2_ITSMFT_TRACKING_SURFACETRACKSTATELEGACYADAPTERS_H_ + +#include "GPUCommonDef.h" +#include "GPUCommonMath.h" + +#if !defined(GPUCA_GPUCODE) + +#include + +#include "ITSMFTTracking/SurfaceTrackState.h" +#include "ReconstructionDataFormats/TrackFwd.h" +#include "ReconstructionDataFormats/Track.h" + +namespace o2::itsmft::tracking::legacy +{ + +inline bool canNarrowToFiniteFloat(double value) noexcept +{ + constexpr double maximum = std::numeric_limits::max(); + return value >= -maximum && value <= maximum && + o2::gpu::GPUCommonMath::Finite(static_cast(value)); +} + +// Host-only boundary for retained legacy state conversion. Production state +// operations use SurfaceTrackState directly. +inline bool importBarrelTrackParCov(const o2::track::TrackParCovF& source, SurfaceTrackState& destination) noexcept +{ + SurfaceTrackState scratch{}; + scratch.referenceCoordinate = source.getX(); + scratch.alpha = source.getAlpha(); + for (uint8_t i = 0; i < 5; ++i) { + scratch.parameters[i] = source.getParam(i); + } + for (uint8_t i = 0; i < 15; ++i) { + scratch.covariance[i] = source.getCov()[i]; + } + scratch.kind = SurfaceKind::Cylinder; + scratch.absCharge = static_cast(source.getAbsCharge()); + scratch.pid = source.getPID(); + destination = scratch; + return true; +} + +inline bool exportBarrelTrackParCov(const SurfaceTrackState& source, o2::track::TrackParCovF& destination) noexcept +{ + if (source.kind != SurfaceKind::Cylinder) { + return false; + } + o2::track::TrackParCovF::params_t parameters{}; + o2::track::TrackParCovF::covMat_t covariance{}; + for (uint8_t i = 0; i < 5; ++i) { + parameters[i] = source.parameters[i]; + } + for (uint8_t i = 0; i < 15; ++i) { + covariance[i] = source.covariance[i]; + } + const o2::track::TrackParCovF scratch{source.referenceCoordinate, source.alpha, parameters, covariance, source.absCharge, source.pid}; + destination = scratch; + return true; +} + +inline bool importLegacyForwardTrackParCov(const o2::track::TrackParCovFwd& source, SurfaceTrackState& destination) noexcept +{ + SurfaceTrackState scratch{}; + const auto& covariance = source.getCovariances(); + const double parameters[] = {source.getX(), source.getY(), source.getPhi(), source.getTanl(), source.getInvQPt()}; + if (!canNarrowToFiniteFloat(source.getZ())) { + return false; + } + for (uint8_t i = 0; i < 5; ++i) { + if (!canNarrowToFiniteFloat(parameters[i])) { + return false; + } + } + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column <= row; ++column) { + if (!canNarrowToFiniteFloat(covariance(row, column))) { + return false; + } + } + } + const float referenceCoordinate = static_cast(source.getZ()); + scratch.referenceCoordinate = referenceCoordinate; + scratch.alpha = 0.f; + for (uint8_t i = 0; i < 5; ++i) { + const float value = static_cast(parameters[i]); + scratch.parameters[i] = value; + } + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column <= row; ++column) { + const float value = static_cast(covariance(row, column)); + scratch.covariance[packedCovarianceIndex(row, column)] = value; + } + } + scratch.kind = SurfaceKind::Disk; + scratch.absCharge = 1; + scratch.pid = o2::track::PID::Pion; + destination = scratch; + return true; +} + +// Host-only inverse used by output staging; reconstructs the legacy payload +// from the common float representation. +inline bool exportLegacyForwardTrackParCov(const SurfaceTrackState& source, o2::track::TrackParCovFwd& destination) noexcept +{ + if (source.kind != SurfaceKind::Disk) { + return false; + } + o2::track::SMatrix5 parameters{}; + o2::track::SMatrix55Sym covariance{}; + for (uint8_t i = 0; i < 5; ++i) { + if (!o2::gpu::GPUCommonMath::Finite(source.parameters[i])) { + return false; + } + parameters[i] = source.parameters[i]; + } + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column <= row; ++column) { + const auto value = source.covariance[packedCovarianceIndex(row, column)]; + if (!o2::gpu::GPUCommonMath::Finite(value)) { + return false; + } + covariance(row, column) = value; + } + } + if (!o2::gpu::GPUCommonMath::Finite(source.referenceCoordinate)) { + return false; + } + destination = o2::track::TrackParCovFwd{source.referenceCoordinate, parameters, covariance, 0.}; + return true; +} + +} // namespace o2::itsmft::tracking::legacy + +#endif // !defined(GPUCA_GPUCODE) + +#endif // ALICEO2_ITSMFT_TRACKING_SURFACETRACKSTATELEGACYADAPTERS_H_ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/TimeFrameScratch.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/TimeFrameScratch.h new file mode 100644 index 0000000000000..c9df96fbfb972 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/TimeFrameScratch.h @@ -0,0 +1,114 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file TimeFrameScratch.h +/// \brief Runtime-plan-owned, detector-neutral CA workspace. +/// +/// Host storage follows the runtime surface graph; device capacities remain +/// fixed. TimeFrame owns the workspace, while adapters own raw ROFs. +#ifndef ALICEO2_ITSMFT_TRACKING_TimeFrameScratch_H_ +#define ALICEO2_ITSMFT_TRACKING_TimeFrameScratch_H_ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ITSMFTTracking/Cell.h" +#include "ITSMFTTracking/TrackingPrimitives.h" +#include "ITSMFTTracking/BoundedAllocator.h" +#include "SimulationDataFormat/MCCompLabel.h" + +namespace o2::itsmft::tracking +{ + +/// Detector-neutral CA state rebuilt for each tracking iteration. Operations +/// receive scalar sizes and spans; this type never depends on TimeFrame. +class TimeFrameScratch +{ + private: + // Pool must outlive allocator-backed members. + std::shared_ptr mMemoryPool; + + public: + TimeFrameScratch() = default; + ~TimeFrameScratch() = default; + TimeFrameScratch(const TimeFrameScratch&) = delete; + TimeFrameScratch& operator=(const TimeFrameScratch&) = delete; + TimeFrameScratch(TimeFrameScratch&&) = delete; + TimeFrameScratch& operator=(TimeFrameScratch&&) = delete; + + /// Size reusable edge and cell storage; setMemoryPool() comes first. + void configureStorage(std::size_t nEdges, std::size_t nCells); + void beginIteration(std::size_t nEdges, std::size_t nCells, + gsl::span trackletLookupSizes); + std::size_t getNEdges() const noexcept { return mNEdges; } + std::size_t getNCells() const noexcept { return mNCells; } + + /// Clear iteration state without changing plan sizes. + void reset(); + + /// Release plan-sized storage while preserving this object's identity. + void clearStorage() noexcept; + + /// Reseat allocator-backed containers. + void setMemoryPool(std::shared_ptr pool); + auto& getMemoryPool() const noexcept { return mMemoryPool; } + float getEdgePhiCut(int edgeId) const { return mEdgePhiCuts[edgeId]; } + float getEdgeMSAngle(int edgeId) const { return mEdgeMSAngles[edgeId]; } + auto& getEdgePhiCuts() { return mEdgePhiCuts; } + auto& getEdgeMSAngles() { return mEdgeMSAngles; } + auto& getTrackletsLabel(int layer) { return mTrackletLabels[layer]; } + auto& getCellsLabel(int layer) { return mCellLabels[layer]; } + + auto& getTracklets() { return mTracklets; } + auto& getTrackletsLookupTable() { return mTrackletsLookupTable; } + + auto& getCells() { return mCells; } + const auto& getCells() const { return mCells; } + + auto& getCellsLookupTable() { return mCellsLookupTable; } + auto& getCellsNeighbours() { return mCellsNeighbours; } + auto& getCellsNeighboursTopology() { return mCellsNeighboursTopology; } + auto& getCellsNeighboursLUT() { return mCellsNeighboursLUT; } + size_t getNumberOfCells() const; + size_t getNumberOfTracklets() const; + size_t getNumberOfNeighbours() const; + + // ---- Per-iteration surface and CA construction state ---- + std::vector> mTracklets; + std::vector> mTrackletsLookupTable; + std::vector> mTrackletLabels; + bounded_vector mEdgePhiCuts; + bounded_vector mEdgeMSAngles; + std::vector> mCells; + std::vector> mCellsLookupTable; + std::vector> mCellsNeighbours; + std::vector> mCellsNeighboursTopology; + std::vector> mCellsNeighboursLUT; + std::vector> mCellLabels; + + private: + void clearResizeEdgeStorage(std::size_t nEdges); + void clearResizeCellStorage(std::size_t nCells); + + std::size_t mNEdges{0}; + std::size_t mNCells{0}; +}; + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_TimeFrameScratch_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/TrackerTraversalPreparation.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/TrackerTraversalPreparation.h new file mode 100644 index 0000000000000..7a870e631da4f --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/TrackerTraversalPreparation.h @@ -0,0 +1,53 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_TRACKERTRAVERSALPREPARATION_H_ +#define ALICEO2_ITSMFT_TRACKING_TRACKERTRAVERSALPREPARATION_H_ + +#ifndef GPUCA_GPUCODE +#include + +#endif + +namespace o2::itsmft::tracking +{ + +#ifndef GPUCA_GPUCODE + +struct CylinderLayerScatteringInputs { + float layerxX0; +}; + +struct DiskLayerScatteringInputs { + float layerxX0; + float layerRadius; + float referenceCoordinate; +}; + +float cylinderLayerMultipleScatteringAngle(const CylinderLayerScatteringInputs& inputs, float trackletMinPt); +float diskLayerMultipleScatteringAngle(const DiskLayerScatteringInputs& inputs, float trackletMinPt); + +float clampEdgeCurvature(float oneOverR, float outerRadius) noexcept; + +struct EdgeScatteringBendingPrep { + float msAngle; + float phiCut; +}; + +EdgeScatteringBendingPrep prepareEdgeScatteringAndBending( + gsl::span perLayerMSAngle, int fromLayer, int toLayer, + float r1, float r2, float clampedOneOverR, float res1, float res2) noexcept; + +#endif + +} // namespace o2::itsmft::tracking + +#endif diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/TrackingKernelParameters.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/TrackingKernelParameters.h new file mode 100644 index 0000000000000..e2f73d644fd6e --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/TrackingKernelParameters.h @@ -0,0 +1,59 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_DETAIL_TRACKINGKERNELPARAMETERS_H_ +#define ALICEO2_ITSMFT_TRACKING_DETAIL_TRACKINGKERNELPARAMETERS_H_ + +#include +#include +#include + +#include "GPUCommonDef.h" +#include "GPUCommonMath.h" + +namespace o2::itsmft::tracking +{ + +/// Compact device-facing tracking configuration. Lengths are in cm, momentum in GeV/c, +/// angles and their resolutions in radians, and chi-square quantities are +/// dimensionless. +struct TrackingKernelParameters { + float trackletMinPt{0.3f}; + float nSigmaCut{5.f}; + float maxChi2ClusterAttachment{60.f}; + float maxChi2NDF{30.f}; + float pvResolution{1.e-2f}; + + GPUhdi() bool isValid() const noexcept + { + if (!o2::gpu::GPUCommonMath::Finite(trackletMinPt) || trackletMinPt <= 0.f || + !o2::gpu::GPUCommonMath::Finite(nSigmaCut) || nSigmaCut <= 0.f || + !o2::gpu::GPUCommonMath::Finite(maxChi2ClusterAttachment) || maxChi2ClusterAttachment <= 0.f || + !o2::gpu::GPUCommonMath::Finite(maxChi2NDF) || maxChi2NDF <= 0.f) { + return false; + } + return o2::gpu::GPUCommonMath::Finite(pvResolution) && pvResolution >= 0.f; + } +}; + +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(TrackingKernelParameters) == 20); +static_assert(alignof(TrackingKernelParameters) == alignof(float)); +static_assert(offsetof(TrackingKernelParameters, trackletMinPt) == 0); +static_assert(offsetof(TrackingKernelParameters, nSigmaCut) == 4); +static_assert(offsetof(TrackingKernelParameters, maxChi2ClusterAttachment) == 8); +static_assert(offsetof(TrackingKernelParameters, maxChi2NDF) == 12); +static_assert(offsetof(TrackingKernelParameters, pvResolution) == 16); + +} // namespace o2::itsmft::tracking + +#endif /* ALICEO2_ITSMFT_TRACKING_DETAIL_TRACKINGKERNELPARAMETERS_H_ */ diff --git a/Detectors/ITSMFT/common/tracking/src/CandidateFinding.cxx b/Detectors/ITSMFT/common/tracking/src/CandidateFinding.cxx new file mode 100644 index 0000000000000..e7f2ab8903ddc --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/CandidateFinding.cxx @@ -0,0 +1,98 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSMFTTracking/detail/CandidateFinding.h" + +#include "DataFormatsITS/Vertex.h" +#include "ITSMFTTracking/IndexTableUtils.h" +#include "ITSMFTTracking/Constants.h" +#include "ITSMFTTracking/MathUtils.h" + +namespace o2::itsmft::tracking +{ + +bool projectTrackletSearchWindow( + const GlobalMeasurement& sourceMeasurement, + const o2::its::Vertex& vertex, + float beamPositionVariance, + SurfaceKind kind, + const TrackletProjectionCache& edgeCache, + const o2::itsmft::IndexTableUtilsCore& indexUtils, + float nSigmaCut, + TrackletSearchWindow& out) +{ + const bool disk = kind == SurfaceKind::Disk; + const float referenceCoordinate = disk ? sourceMeasurement.z : sourceMeasurement.radius; + const float referenceOrigin = disk ? vertex.getZ() : 0.f; + const float projectedCoordinate = disk ? sourceMeasurement.radius : sourceMeasurement.z; + const float projectedOrigin = disk ? 0.f : vertex.getZ(); + const float targetMin = disk ? edgeCache.targetMinZ : edgeCache.targetMinR; + const float targetMax = disk ? edgeCache.targetMaxZ : edgeCache.targetMaxR; + const float referenceDelta = referenceCoordinate - referenceOrigin; + const float projectedDelta = projectedCoordinate - projectedOrigin; + if (!(targetMin <= targetMax) || + !(o2::gpu::CAMath::Abs(referenceDelta) > o2::its::constants::Tolerance) || + (disk && !(projectedDelta > o2::its::constants::Tolerance))) { + return false; + } + + const float slope = projectedDelta / referenceDelta; // tan(lambda) for cylinders, 1/tan(lambda) for disks + const float targetCoordinate = 0.5f * (targetMin + targetMax); + const float referenceToTarget = targetCoordinate - referenceCoordinate; + const float prediction = projectedCoordinate + slope * referenceToTarget; + if (disk && !(prediction > 0.f)) { + return false; + } + + const float sourceCoordinateVariance = o2::its::math_utils::Sq(edgeCache.sourcePositionResolution); + const float referenceOriginVariance = disk ? vertex.getSigmaZ2() : beamPositionVariance; + const float projectedOriginVariance = disk ? beamPositionVariance : vertex.getSigmaZ2(); + const float inverseReferenceDelta = 1.f / referenceDelta; + const float sourceVarianceScale = (1.f + o2::its::math_utils::Sq(slope)) * sourceCoordinateVariance; + const float originVarianceScale = projectedOriginVariance + o2::its::math_utils::Sq(slope) * referenceOriginVariance; + const float edgeMSVarianceScale = o2::its::math_utils::Sq(edgeCache.edgeMSAngle); + const float varianceConstant = sourceVarianceScale; + const float varianceLinear = 2.f * inverseReferenceDelta * sourceVarianceScale; + const float varianceQuadratic = o2::its::math_utils::Sq(inverseReferenceDelta) * + (sourceVarianceScale + originVarianceScale) + + edgeMSVarianceScale; + const float minDelta = targetMin - referenceCoordinate; + const float minPrediction = projectedCoordinate + slope * minDelta; + const float minVariance = varianceConstant + minDelta * (varianceLinear + minDelta * varianceQuadratic); + const float maxDelta = targetMax - referenceCoordinate; + const float maxPrediction = projectedCoordinate + slope * maxDelta; + const float maxVariance = varianceConstant + maxDelta * (varianceLinear + maxDelta * varianceQuadratic); + const float lowerBound = o2::gpu::CAMath::Min(minPrediction - nSigmaCut * o2::gpu::CAMath::Sqrt(minVariance), + maxPrediction - nSigmaCut * o2::gpu::CAMath::Sqrt(maxVariance)); + const float upperBound = o2::gpu::CAMath::Max(minPrediction + nSigmaCut * o2::gpu::CAMath::Sqrt(minVariance), + maxPrediction + nSigmaCut * o2::gpu::CAMath::Sqrt(maxVariance)); + const float searchPrediction = 0.5f * (lowerBound + upperBound); + const float searchHalfWidth = 0.5f * (upperBound - lowerBound); + + const auto bins = o2::itsmft::getBinsPhiColumn(sourceMeasurement.phi, edgeCache.toLayer, + searchPrediction, searchHalfWidth, + edgeCache.edgePhiCut, indexUtils); + if (bins.x < 0) { + return false; + } + out = {bins, + referenceCoordinate, + projectedCoordinate, + slope, + varianceConstant, + varianceLinear, + varianceQuadratic, + sourceMeasurement.phi, + o2::its::math_utils::Sq(edgeCache.edgePhiCut / nSigmaCut)}; + return true; +} + +} // namespace o2::itsmft::tracking diff --git a/Detectors/ITSMFT/common/tracking/src/Configuration.cxx b/Detectors/ITSMFT/common/tracking/src/Configuration.cxx new file mode 100644 index 0000000000000..8726de7c751eb --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/Configuration.cxx @@ -0,0 +1,429 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DetectorsBase/Propagator.h" +#include "Framework/Logger.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "ITSMFTTracking/Constants.h" +#include "MFTTracking/Constants.h" + +namespace +{ +constexpr bool iequals(std::string_view a, std::string_view b) +{ + return std::equal(a.begin(), a.end(), b.begin(), b.end(), + [](char x, char y) { return std::tolower(x) == std::tolower(y); }); +} +} // namespace + +namespace o2::itsmft +{ + +std::string TrackingParameters::asString() const +{ + std::string str = std::format("NColB:{} NRowB:{} PerVtx:{} DropFail:{} TtklMinPt:{:.2f} MinCl:{}", ColBins, RowBins, PerPrimaryVertexProcessing, DropTFUponFailure, TrackletMinPt, MinTrackLength); + auto isSet = [](auto e) { return e >= 0; }; + auto isAnySet = [&isSet](auto v) { return !v.empty() && std::any_of(v.begin(), v.end(), isSet); }; + bool first = true; + for (int il = NLayers; il >= MinTrackLength; il--) { + int slot = NLayers - il; + if (slot < (int)MinPt.size() && MinPt[slot] > 0) { + if (first) { + first = false; + str += " MinPt: "; + } + str += std::format("L{}:{:.2f} ", il, MinPt[slot]); + } + } + if (isAnySet(SystError2Row) || isAnySet(SystError2Col)) { + str += " SystErrRow/Col:"; + for (size_t i = 0; i < SystError2Row.size(); i++) { + str += std::format("{:.2e}/{:.2e} ", SystError2Row[i], SystError2Col[i]); + } + } + if (isAnySet(AddTimeError)) { + str += " AddTimeError:"; + for (unsigned int i : AddTimeError) { + str += std::format("{} ", i); + } + } + if (SharedMaxClusters) { + str += std::format(" ShaMaxCls:{} ", SharedMaxClusters); + } + if (AllowSharingFirstCluster) { + str += std::format(" ShaClsDPhi:{} ShaClsDEta:{} ShaClsSign:{}", SharedClusterMaxDeltaPhi, SharedClusterMaxDeltaEta, SharedClusterOppositeSign); + } + if (MaxHoles) { + str += std::format(" MaxHoles:{}", MaxHoles); + } + if (!InactiveLayerMask.empty()) { + str += std::format(" InactiveMask:{}", InactiveLayerMask.asString()); + } + if (!SeedingLayers.empty()) { + str += std::format(" SeedingLayers:{}", SeedingLayers.asString()); + } + if (std::numeric_limits::max() != MaxMemory) { + str += std::format(" MemLimit {:.2f} GB", double(MaxMemory) / (1024.f * 1024.f * 1024.f)); + } + return str; +} + +std::string VertexingParameters::asString() const +{ + std::string str = std::format("NColB:{} NRowB:{} MinVtxCont:{} SupLowMultDebris:{} MaxTrkltCls:{} ZCut:{} PhCut:{} PairCut:{} ClCut:{} SeedRad:{}x{}", + ColBins, RowBins, clusterContributorsCut, suppressLowMultDebris, maxTrackletsPerCluster, zCut, phiCut, pairCut, clusterCut, seedMemberRadiusTime, seedMemberRadiusZ); + if (std::numeric_limits::max() != MaxMemory) { + str += std::format(" MemLimit {:.2f} GB", double(MaxMemory) / (1024.f * 1024.f * 1024.f)); + } + return str; +} + +void resetDetectorDefaults(TrackingParameters& p, detectors::DetID::ID detId) +{ + if (detId == detectors::DetID::ITS) { + p = TrackingParameters{}; + p.MinPt.assign(tracking::ITSNLayers - tracking::kCAMinTrackLength + 1, 0.f); + return; + } + + if (detId == detectors::DetID::MFT) { + namespace mftc = o2::mft::constants; + namespace mft = mftc::mft; + constexpr int nLayers = o2::mft::constants::mft::LayersNumber; + + p = TrackingParameters{}; + p.NLayers = nLayers; + p.LayerZ.clear(); + p.LayerZ.reserve(nLayers); + for (float z : mft::LayerZCoordinate()) { + p.LayerZ.push_back(std::abs(z)); + } + p.LayerColHalfExtent.assign(mftc::index_table::RMax.begin(), mftc::index_table::RMax.end()); + p.IndexRowMin = -20.f; + p.IndexRowMax = 20.f; + p.LayerRadii.resize(nLayers); + for (int i{0}; i < nLayers; ++i) { + p.LayerRadii[i] = 0.5f * (mftc::index_table::RMin[i] + mftc::index_table::RMax[i]); + } + p.LayerResolution.assign(nLayers, mft::Resolution); + p.SystError2Row.assign(nLayers, 0.f); + p.SystError2Col.assign(nLayers, 0.f); + p.AddTimeError.assign(nLayers, 0u); + p.ColBins = 64; + p.RowBins = 128; + p.UseDiamond = true; + p.PerPrimaryVertexProcessing = false; + p.StartLayerMask = (1u << nLayers) - 1u; + p.MinPt.assign(TrackerParamConfig::MaxTrackLength - TrackerParamConfig::MinTrackLength + 1, 0.f); + return; + } + + LOGP(fatal, "Unsupported detector id {} in resetDetectorDefaults", static_cast(detId)); +} + +namespace TrackingMode +{ + +Type fromString(std::string_view str) +{ + constexpr std::array smodes = { + std::pair{"sync", Sync}, + std::pair{"async", Async}, + std::pair{"cosmics", Cosmics}, + std::pair{"unset", Unset}, + std::pair{"off", Off}}; + + const auto it = std::find_if(smodes.begin(), smodes.end(), [&str](const auto& pair) { + return iequals(str, pair.first); + }); + if (it == smodes.end()) { + LOGP(fatal, "Unrecognized CA tracking mode '{}'", str); + } + return it->second; +} + +std::string toString(Type mode) +{ + switch (mode) { + case Sync: + return "sync"; + case Async: + return "async"; + case Cosmics: + return "cosmics"; + case Unset: + return "unset"; + case Off: + return "off"; + } + LOGP(fatal, "Unrecognized CA tracking mode {}", static_cast(mode)); + return ""; +} + +void validateCommonCAOptions(detectors::DetID::ID detId) +{ + const auto reject = [](bool unsupported, std::string_view field, std::string_view supported) { + if (unsupported) { + throw std::invalid_argument(std::string(field) + " has no implementing common-CA consumer; use " + std::string(supported)); + } + }; + if (detId == detectors::DetID::ITS) { + const auto& tc = ITSCommonCATrackerParam::Instance(); + reject(tc.printMemory, "ITSCommonCATrackerParam.printMemory", "false"); + reject(tc.saveTimeBenchmarks, "ITSCommonCATrackerParam.saveTimeBenchmarks", "false"); + return; + } + if (detId != detectors::DetID::MFT) { + throw std::invalid_argument("Unsupported detector in common-CA option validation"); + } + const auto& tc = TrackerParamConfig::Instance(); + reject(tc.printMemory, "MFTCATrackerParam.printMemory", "false"); + reject(tc.saveTimeBenchmarks, "MFTCATrackerParam.saveTimeBenchmarks", "false"); + reject(!tc.fataliseUponFailure, "MFTCATrackerParam.fataliseUponFailure", "true; dropTFUponFailure controls recoverable drops"); + reject(tc.deltaTanLres != -1.f, "MFTCATrackerParam.deltaTanLres", "-1"); + reject(tc.doUPCIteration, "MFTCATrackerParam.doUPCIteration", "false"); + reject(tc.overrideBeamEstimation, "MFTCATrackerParam.overrideBeamEstimation", "false"); + if (!tc.useDiamond || tc.perPrimaryVertexProcessing) { + throw std::invalid_argument("MFT common CA requires MFTCATrackerParam.useDiamond=true and MFTCATrackerParam.perPrimaryVertexProcessing=false"); + } +} + +TrackingPlan getTrackingPlan(detectors::DetID::ID detId, Type mode) +{ + validateCommonCAOptions(detId); + TrackingParameters defaults; + resetDetectorDefaults(defaults, detId); + TrackingPlan plan{std::move(static_cast(defaults)), {}, {}}; + auto& trackParams = plan.iterations; + if (detId == detectors::DetID::ITS) { + const auto& tc = ITSCommonCATrackerParam::Instance(); + if (mode == Async) { + trackParams.assign(3, defaults); + trackParams[1].TrackletMinPt = 0.2f; + trackParams[2].TrackletMinPt = 0.1f; + trackParams[0].MinPt[0] = 1.f / 12.f; + trackParams[1].MinPt[0] = 1.f / 12.f; + trackParams[2].MinTrackLength = tracking::kCAMinTrackLength; + trackParams[2].MinPt[0] = 1.f / 12.f; + trackParams[2].MinPt[1] = 1.f / 5.f; + trackParams[2].MinPt[2] = 1.f; + trackParams[2].MinPt[3] = 1.f / 6.f; + trackParams[2].StartLayerMask = (1u << 6) | (1u << 3); + } else if (mode == Sync) { + trackParams.assign(1, defaults); + trackParams[0].MinTrackLength = tracking::kCAMinTrackLength; + } else { + LOGP(fatal, "ITS common-CA tracking mode '{}' is not supported yet; use 'sync' or 'async'", toString(mode)); + } + + plan.detector.ColBins = 64; + plan.detector.RowBins = 32; + plan.execution = {tc.maxMemory, tc.dropTFUponFailure}; + for (auto& p : trackParams) { + p.PassFlags.reset(); + } + trackParams.front().PassFlags.set(IterationStep::FirstPass, IterationStep::RebuildClusterLUT); + + const float bFactor = std::abs(o2::base::Propagator::Instance()->getNominalBz()) / 5.0066791f; + const float bFactorTracklets = bFactor < 0.01f ? 1.f : bFactor; + for (auto& p : trackParams) { + p.TrackletMinPt *= bFactorTracklets; + for (auto& minPt : p.MinPt) { + minPt *= bFactor; + } + p.UseDiamond = tc.useDiamond; + for (int iD = 0; iD < 3; ++iD) { + p.Diamond[iD] = tc.diamondPos[iD]; + } + p.PVres = tc.pvRes > 0 ? tc.pvRes : p.PVres; + } + return plan; + } + if (detId != detectors::DetID::MFT) { + LOGP(fatal, "Unsupported detector id {} in getTrackingPlan", static_cast(detId)); + } + + const auto& tc = TrackerParamConfig::Instance(); + + if (mode == Off) { + return plan; + } + if (mode == Unset) { + LOGP(fatal, "CA tracking mode is unset; set --tracking-mode or {}.trackingMode", TrackerParamConfig::getParamName()); + } + + if (mode != Async) { + if (std::any_of(std::begin(tc.minTrackLgtIter), std::end(tc.minTrackLgtIter), [](int value) { return value > 0; })) { + throw std::invalid_argument("MFTCATrackerParam.minTrackLgtIter overrides are implemented only for async mode"); + } + if (std::any_of(std::begin(tc.minPtIterLgt), std::end(tc.minPtIterLgt), [](float value) { return value > 0.f; })) { + throw std::invalid_argument("MFTCATrackerParam.minPtIterLgt overrides are implemented only for async mode"); + } + } + + if (mode == Async) { + trackParams.assign(3, defaults); + + trackParams[1].TrackletMinPt = 0.15f; + trackParams[2].TrackletMinPt = 0.08f; + + trackParams[0].MinPt[0] = 1.f / 12.f; // 10 clusters + trackParams[1].MinPt[0] = 1.f / 12.f; + + trackParams[2].MinTrackLength = TrackerParamConfig::MinTrackLength; + trackParams[2].MinPt[0] = 1.f / 12.f; // 10 clusters + trackParams[2].MinPt[1] = 1.f / 8.f; // 9 clusters + trackParams[2].MinPt[2] = 1.f / 5.f; // 8 clusters + trackParams[2].MinPt[3] = 1.f / 3.f; // 7 clusters + trackParams[2].MinPt[4] = 1.f / 2.f; // 6 clusters + trackParams[2].MinPt[5] = 1.f / 1.f; // 5 clusters + + for (int ip = 0; ip < static_cast(trackParams.size()); ip++) { + auto& param = trackParams[ip]; + if (ip < o2::its::constants::MaxIter) { + if (tc.minTrackLgtIter[ip] > 0) { + param.MinTrackLength = tc.minTrackLgtIter[ip]; + } + for (int ilg = tc.MaxTrackLength; ilg >= tc.MinTrackLength; ilg--) { + const int lslot0 = tc.MaxTrackLength - ilg; + const int lslot = lslot0 + ip * (tc.MaxTrackLength - tc.MinTrackLength + 1); + if (tc.minPtIterLgt[lslot] > 0.f) { + param.MinPt[lslot0] = tc.minPtIterLgt[lslot]; + } + } + } + } + } else if (mode == Sync) { + trackParams.assign(1, defaults); + trackParams[0].MinTrackLength = TrackerParamConfig::MinTrackLength; + } else if (mode == Cosmics) { + trackParams.assign(1, defaults); + trackParams[0].MinTrackLength = TrackerParamConfig::MinTrackLength; + plan.detector.ColBins = 32; + plan.detector.RowBins = 64; + trackParams[0].PVres = 1.e5f; + trackParams[0].MaxChi2ClusterAttachment = 60.f; + trackParams[0].MaxChi2NDF = 40.f; + } else { + LOGP(fatal, "Unsupported CA tracking mode {}", toString(mode)); + } + + if (tc.nIterations != -1 && (tc.nIterations <= 0 || static_cast(tc.nIterations) > trackParams.size())) { + throw std::invalid_argument(std::format("MFTCATrackerParam.nIterations={} is invalid for {}: use -1 or 1..{}", + tc.nIterations, toString(mode), trackParams.size())); + } + if (tc.nIterations > 0) { + trackParams.resize(tc.nIterations); + } + if (tc.materialModel != "nominal") { + throw std::invalid_argument("MFTCATrackerParam.materialModel='" + tc.materialModel + "' is unsupported; use nominal"); + } + if (tc.useMatCorrTGeo) { + throw std::invalid_argument("MFTCATrackerParam.useMatCorrTGeo requests unsupported TGeo material; use materialModel=nominal"); + } + if (!tc.useFastMaterial) { + throw std::invalid_argument("MFTCATrackerParam.useFastMaterial=false requests unsupported LUT material; use materialModel=nominal and useFastMaterial=true"); + } + constexpr uint32_t allowedStartLayers = (uint32_t{1} << tracking::MFTNLayers) - 1; + for (int iteration = 0; iteration < tracking::MaxIter; ++iteration) { + if (tc.startLayerMask[iteration] & ~allowedStartLayers) { + throw std::invalid_argument(std::format("MFTCATrackerParam.startLayerMask[{}]={} contains bits outside the {} MFT layers", + iteration, tc.startLayerMask[iteration], tracking::MFTNLayers)); + } + } + + plan.execution = {tc.maxMemory, tc.dropTFUponFailure}; + for (int i{0}; i < TrackerParamConfig::getNLayers(); ++i) { + plan.detector.SystError2Row[i] = tc.sysErr2Row[i] > 0 ? tc.sysErr2Row[i] : plan.detector.SystError2Row[i]; + plan.detector.SystError2Col[i] = tc.sysErr2Col[i] > 0 ? tc.sysErr2Col[i] : plan.detector.SystError2Col[i]; + plan.detector.AddTimeError[i] = tc.addTimeError[i]; + } + plan.detector.ColBins = tc.LUTbinsU > 0 ? tc.LUTbinsU : plan.detector.ColBins; + plan.detector.RowBins = tc.LUTbinsV > 0 ? tc.LUTbinsV : plan.detector.RowBins; + + for (auto& param : trackParams) { + param.PassFlags.reset(); + } + if (!trackParams.empty()) { + trackParams[0].PassFlags.set(IterationStep::FirstPass, IterationStep::RebuildClusterLUT); + } + + const float bFactor = std::abs(o2::base::Propagator::Instance()->getNominalBz()) / 5.0066791f; + const float bFactorTracklets = bFactor < 0.01f ? 1.f : bFactor; + + for (auto& p : trackParams) { + p.TrackletMinPt *= bFactorTracklets; + for (int ilg = tc.MaxTrackLength; ilg >= tc.MinTrackLength; ilg--) { + const int lslot = tc.MaxTrackLength - ilg; + if (lslot < static_cast(p.MinPt.size())) { + p.MinPt[lslot] *= bFactor; + } + } + + p.ReseedIfShorter = tc.reseedIfShorter; + p.RepeatRefitOut = tc.repeatRefitOut; + p.ShiftRefToCluster = tc.shiftRefToCluster; + p.CreateArtefactLabels = tc.createArtefactLabels; + p.AllowSharingFirstCluster = tc.allowSharingFirstCluster; + p.SharedClusterMaxDeltaPhi = tc.sharedClusterMaxDeltaPhi; + p.SharedClusterMaxDeltaEta = tc.sharedClusterMaxDeltaEta; + p.SharedClusterOppositeSign = tc.sharedClusterOppositeSign; + p.PerPrimaryVertexProcessing = tc.perPrimaryVertexProcessing; + + const auto iter = &p - trackParams.data(); + if (iter < o2::its::constants::MaxIter) { + p.MaxHoles = tc.maxHolesIter[iter]; + } + + // The legacy NONE tag disables external providers, not nominal material. + p.CorrType = o2::base::PropagatorImpl::MatCorrType::USEMatCorrNONE; + if (tc.startLayerMask[iter] != 0) { + p.StartLayerMask = tc.startLayerMask[iter]; + } + + p.MaxChi2ClusterAttachment = tc.maxChi2ClusterAttachment > 0 ? tc.maxChi2ClusterAttachment : p.MaxChi2ClusterAttachment; + p.MaxChi2NDF = tc.maxChi2NDF > 0 ? tc.maxChi2NDF : p.MaxChi2NDF; + p.PVres = tc.pvRes > 0 ? tc.pvRes : p.PVres; + p.NSigmaCut *= tc.nSigmaCut > 0 ? tc.nSigmaCut : 1.f; + p.TrackletMinPt *= tc.minPt > 0 ? tc.minPt : 1.f; + for (int iD{0}; iD < 3; ++iD) { + p.Diamond[iD] = tc.diamondPos[iD]; + } + p.UseDiamond = tc.useDiamond; + } + + LOGP(info, "MFT CA {}: {} passes, material model nominal, index=PhiR phiBins={} radiusBins={} (radians, cm)", + toString(mode), trackParams.size(), plan.detector.RowBins, plan.detector.ColBins); + if (tc.reseedIfShorter != 0) { + LOGP(warning, "MFTCATrackerParam.reseedIfShorter={} is reserved and has no effect in the current common refit", tc.reseedIfShorter); + } + for (size_t iteration = 0; iteration < trackParams.size(); ++iteration) { + const auto& p = trackParams[iteration]; + LOGP(info, "MFT CA pass {}: minTrackLength={} trackletMinPt={} maxChi2ClusterAttachment={} maxChi2NDF={} startLayerMask={}", + iteration, p.MinTrackLength, p.TrackletMinPt, p.MaxChi2ClusterAttachment, p.MaxChi2NDF, p.StartLayerMask.value()); + } + + return plan; +} + +} // namespace TrackingMode +} // namespace o2::itsmft diff --git a/Detectors/ITSMFT/common/tracking/src/FamilyMaterialOperations.cxx b/Detectors/ITSMFT/common/tracking/src/FamilyMaterialOperations.cxx new file mode 100644 index 0000000000000..4926db453e673 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/FamilyMaterialOperations.cxx @@ -0,0 +1,356 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// Defines both detail::barrel::correctForMaterial(state, material, direction) and +// detail::forward::correctForMaterial(state, material, direction): the PID/absCharge- +// aware composite cylinder/disk operations built on the detector-neutral +// scalar kernel in MaterialPhysics.h. Both overloads share the complete +// preflight-validation/momentum-derivation/scratch-and-commit orchestration +// below; only the coordinate-specific kinematics check and covariance +// projection formula differ between them. +// +// This translation unit is host-only, does not construct or delegate through +// TrackParCovF or TrackParCovFwd, and includes TrackParametrization.h solely +// to reuse its public kCY2max/kCZ2max/kCSnp2max/kCTgl2max/kC1Pt2max constants +// for the retained barrel covariance-range handling (no narrower public +// header declares them; the same reuse pattern is already used by +// MaterialPhysics.cxx for its own constants). + +#include "ITSMFTTracking/detail/SurfaceStateOperations.h" +#include "ITSMFTTracking/MaterialPhysics.h" + +#include +#include + +#include "ReconstructionDataFormats/PID.h" +#include "ReconstructionDataFormats/TrackParametrization.h" + +namespace o2::itsmft::tracking +{ +namespace +{ + +bool covarianceDiagonalsNonNegative(const SurfaceTrackState& state) noexcept +{ + for (uint8_t i = 0; i < 5; ++i) { + if (state.covariance[packedCovarianceIndex(i, i)] < 0.f) { + return false; + } + } + return true; +} + +// Physical-momentum derivation shared by both coordinate conventions. u = slot 4, t = slot 3. +bool derivePhysicalMomentum(const SurfaceTrackState& state, float& momentumGeV) noexcept +{ + const float t = state.parameters[3]; + const float u = state.parameters[4]; + const float absU = std::abs(u); + const float pT = (state.absCharge == 0) ? (1.f / absU) : (static_cast(state.absCharge) / absU); + if (pT <= 0.f) { + return false; + } + const float p = pT * std::sqrt(1.f + t * t); + if (p <= 0.f) { + return false; + } + momentumGeV = p; + return true; +} + +material::MaterialOperationResult makePreflightFailure(material::MaterialFailureReason reason) noexcept +{ + material::MaterialOperationResult result{}; + result.momentumBeforeGeV = 0.f; + result.momentumAfterGeV = 0.f; + result.signedEnergyChangeGeV = 0.f; + result.highlandTheta2Rad2 = 0.f; + result.relativeInverseMomentumVariance = 0.f; + result.energyLossSubsteps = 0; + result.flags = material::MaterialOperationFlags::None; + result.failure = reason; + result.reserved = 0; + return result; +} + +material::MaterialOperationResult makeProjectionFailure(const material::MaterialOperationResult& scalarResult, + material::MaterialFailureReason reason) noexcept +{ + material::MaterialOperationResult result{}; + result.momentumBeforeGeV = scalarResult.momentumBeforeGeV; + result.momentumAfterGeV = 0.f; + result.signedEnergyChangeGeV = 0.f; + result.highlandTheta2Rad2 = 0.f; + result.relativeInverseMomentumVariance = 0.f; + result.energyLossSubsteps = 0; + result.flags = material::MaterialOperationFlags::None; + result.failure = reason; + result.reserved = 0; + return result; +} + +// Barrel covariance-range upper bound, in (Y, Z, Snp, Tgl, Q2Pt) slot order: +// the retained TrackParametrizationWithError::checkCovariance() +// range-clamp values, and the same five constants +// PropagatorBarrelOperations.cxx's post-propagate/rotate/update +// sanitization (ADR 0008) enforces. +constexpr float kBarrelMaxDiagonal[5] = {o2::track::kCY2max, o2::track::kCZ2max, o2::track::kCSnp2max, + o2::track::kCTgl2max, o2::track::kC1Pt2max}; + +// Thin wrapper over the shared, detector-neutral sanitizeCovariance() +// (SurfaceTrackState.h): abs()'s each diagonal and, if it still exceeds +// the retained maximum, clamps it and rescales every off-diagonal entry +// involving that parameter by sqrt(max/diagonal). No legacy track object is +// constructed; this operates directly on the packed float covariance array. +// Formerly a private reimplementation of this exact behavior; now delegates to +// the one shared implementation also used by the barrel state operations' +// own post-propagate/rotate/update sanitization, with no behavioral change. +void limitBarrelCovariance(SurfaceTrackState& scratch) noexcept +{ + sanitizeCovariance(scratch, kBarrelMaxDiagonal); +} + +// Shared preflight validation, steps 1-6 of the required order. Step 3's +// kind-specific extra check (barrel |Snp|<1 / forward alpha==0) is +// supplied by the caller; the shared slot-4-nonzero part of step 3 is applied +// here for both kinds. +template +bool preflightValidate(const SurfaceTrackState& state, SurfaceKind expectedFamily, FamilyKinematicsCheck&& familyCheck, + material::MaterialFailureReason& failure) noexcept +{ + if (state.kind != expectedFamily) { + failure = material::MaterialFailureReason::SourceSurfaceKindMismatch; + return false; + } + const float u = state.parameters[4]; + if (!familyCheck(state) || u == 0.f) { + failure = material::MaterialFailureReason::InvalidStateKinematics; + return false; + } + if (state.pid.getID() >= o2::track::PID::NIDsTot) { + failure = material::MaterialFailureReason::InvalidPID; + return false; + } + if (state.absCharge != 0 && state.pid.getMass() == 0.f) { + failure = material::MaterialFailureReason::ChargedMasslessPID; + return false; + } + if (!covarianceDiagonalsNonNegative(state)) { + failure = material::MaterialFailureReason::InvalidCovariance; + return false; + } + return true; +} + +// Complete incidence-aware transactional operation shared by cylinder and disk +// states (Slice 2 "Transactional result contract"): validate the state and its +// incidence reference, derive physical momentum, scale the nominal material by +// the incidence path length, invoke the scalar kernel, project covariance on +// scratch only, validate the projected scratch, and commit exactly once. +// projectCovariance may additionally apply cylinder-specific covariance range +// handling (barrel only); it must not touch state.parameters[4], which this +// function updates uniformly for both kinds after projection. +// +// Unconditional no-op contract: once the scalar kernel succeeds, absCharge +// == 0 or an exactly-{0,0} materialBudget returns the scalar result +// immediately, before projectCovariance (and any barrel covariance-range +// limiting it applies) or the slot-4 update ever run. This holds even when +// the source state's barrel covariance diagonals already exceed the +// retained checkCovariance limits: those diagonals must not be silently +// clamped by an operation that has no material to apply. +template +material::MaterialOperationResult correctForMaterialImpl(SurfaceTrackState& state, SurfaceTrackParameters& incidenceReference, + SurfaceKind expectedFamily, + material::IntegratedMaterialBudget materialBudget, + material::MaterialTraversalDirection direction, + FamilyKinematicsCheck&& familyCheck, + ScaleMaterial&& scaleMaterial, + ProjectCovariance&& projectCovariance) noexcept +{ + material::MaterialFailureReason failure{}; + if (incidenceReference.kind != expectedFamily) { + return makePreflightFailure(material::MaterialFailureReason::SourceSurfaceKindMismatch); + } + if (!familyCheck(incidenceReference) || incidenceReference.parameters[4] == 0.f) { + return makePreflightFailure(material::MaterialFailureReason::InvalidStateKinematics); + } + if (!preflightValidate(state, expectedFamily, familyCheck, failure)) { + return makePreflightFailure(failure); + } + + float momentumBeforeGeV = 0.f; + if (!derivePhysicalMomentum(state, momentumBeforeGeV)) { + return makePreflightFailure(material::MaterialFailureReason::InvalidStateKinematics); + } + + SurfaceTrackState scratchState = state; + SurfaceTrackParameters scratchReference = incidenceReference; + scaleMaterial(materialBudget, scratchReference); + const auto scalarResult = material::calculateMaterialPhysics(momentumBeforeGeV, scratchState.pid, scratchState.absCharge, direction, materialBudget); + if (!scalarResult.ok()) { + return scalarResult; + } + + const bool isNoopMaterial = (materialBudget.xOverX0 == 0.f && materialBudget.arealDensityGPerCm2 == 0.f); + if (scratchState.absCharge == 0 || isNoopMaterial) { + return scalarResult; + } + + const float tBefore = scratchState.parameters[3]; + const float kBefore = scratchState.parameters[4]; + projectCovariance(scratchState, scalarResult, tBefore, kBefore); + + // The equality branch preserves the exact no-op invariant for the + // MCS-only-with-unchanged-momentum case (xOverX0 > 0, arealDensity == 0): + // x == y implies kAfter == kBefore bit-for-bit with no division rounding. + // The nonzero-change branch keeps the accepted/legacy left-to-right + // arithmetic (multiply, then divide) rather than dividing the momenta + // first, which would prematurely underflow for extreme momentum ratios + // and would not reproduce the retained nonzero-material rounding. + const float kAfter = (scalarResult.momentumBeforeGeV == scalarResult.momentumAfterGeV) + ? kBefore + : (kBefore * scalarResult.momentumBeforeGeV) / scalarResult.momentumAfterGeV; + scratchState.parameters[4] = kAfter; + + // Complete post-projection domain validation: the projected state must + // still satisfy every kind/kinematics precondition the source state was + // required to satisfy, and physical momentum must still be re-derivable. + if (scratchState.parameters[4] == 0.f || !familyCheck(scratchState)) { + return makeProjectionFailure(scalarResult, material::MaterialFailureReason::InvalidStateKinematics); + } + float momentumAfterDerived = 0.f; + if (!derivePhysicalMomentum(scratchState, momentumAfterDerived)) { + return makeProjectionFailure(scalarResult, material::MaterialFailureReason::InvalidStateKinematics); + } + if (!covarianceDiagonalsNonNegative(scratchState)) { + return makeProjectionFailure(scalarResult, material::MaterialFailureReason::InvalidCovariance); + } + + // Energy loss changes q/pT in the covariance-bearing state and its + // incidence reference by the same pBefore/pAfter factor. The equality + // branch keeps MCS-only corrections bit-exact. + const float referenceKBefore = scratchReference.parameters[4]; + scratchReference.parameters[4] = (scalarResult.momentumBeforeGeV == scalarResult.momentumAfterGeV) + ? referenceKBefore + : (referenceKBefore * scalarResult.momentumBeforeGeV) / scalarResult.momentumAfterGeV; + if (scratchReference.parameters[4] == 0.f || !std::isfinite(scratchReference.parameters[4])) { + return makeProjectionFailure(scalarResult, material::MaterialFailureReason::InvalidStateKinematics); + } + + state = scratchState; + incidenceReference = scratchReference; + return scalarResult; +} + +} // namespace +} // namespace o2::itsmft::tracking + +namespace o2::itsmft::tracking::detail::barrel +{ +material::MaterialOperationResult correctForMaterial(SurfaceTrackState& state, SurfaceTrackParameters& linRef, + material::IntegratedMaterialBudget materialBudget, + material::MaterialTraversalDirection direction) noexcept +{ + auto familyCheck = [](const auto& s) noexcept { + return std::abs(s.parameters[2]) < 1.f; + }; + // ITS layer budgets describe a normal crossing of the cylindrical layer. + // Match TrackParametrizationWithError::correctForMaterial(..., true), which + // the legacy ITS tracker uses at every layer: lengthen both material + // quantities by the path of the incident track before evaluating energy + // loss and multiple scattering. + auto scaleMaterial = [](material::IntegratedMaterialBudget& material, const SurfaceTrackParameters& incidence) noexcept { + const float snp = incidence.parameters[2]; + const float tgl = incidence.parameters[3]; + const float cosPhi2 = (1.f - snp) * (1.f + snp); + const float inverseCosLambda2 = 1.f + tgl * tgl; + const float incidenceScale = std::sqrt(inverseCosLambda2 / cosPhi2); + material.xOverX0 *= incidenceScale; + material.arealDensityGPerCm2 *= incidenceScale; + }; + // Barrel parameters are (Y, Z, Snp, Tgl, Q2Pt). The accepted Jacobian + // requires q/pT unconditionally in slots 13/14, fixing the retained + // TrackParametrizationWithError::correctForMaterial() unit-charge + // conditional that omits q/pT there (see the module doc comment). + auto projectCovariance = [](SurfaceTrackState& scratch, const material::MaterialOperationResult& scalarResult, + float t, float k) noexcept { + const float A = 1.f + t * t; + const float snp = scratch.parameters[2]; + const float c2 = 1.f - snp * snp; + const float h = scalarResult.highlandTheta2Rad2; + const float R = scalarResult.relativeInverseMomentumVariance; + scratch.covariance[packedCovarianceIndex(2, 2)] += h * A * c2; + scratch.covariance[packedCovarianceIndex(3, 3)] += h * A * A; + scratch.covariance[packedCovarianceIndex(4, 3)] += h * A * t * k; + scratch.covariance[packedCovarianceIndex(4, 4)] += h * (t * k) * (t * k) + k * k * R; + limitBarrelCovariance(scratch); + }; + return correctForMaterialImpl(state, linRef, SurfaceKind::Cylinder, materialBudget, direction, + familyCheck, scaleMaterial, projectCovariance); +} + +material::MaterialOperationResult correctForMaterial(SurfaceTrackState& state, material::IntegratedMaterialBudget materialBudget, + material::MaterialTraversalDirection direction) noexcept +{ + SurfaceTrackParameters incidenceReference{state}; + return correctForMaterial(state, incidenceReference, materialBudget, direction); +} + +} // namespace o2::itsmft::tracking::detail::barrel + +namespace o2::itsmft::tracking::detail::forward +{ +material::MaterialOperationResult correctForMaterial(SurfaceTrackState& state, SurfaceTrackParameters& linRef, + material::IntegratedMaterialBudget materialBudget, + material::MaterialTraversalDirection direction) noexcept +{ + auto familyCheck = [](const auto& s) noexcept { + return s.alpha == 0.f && s.parameters[3] != 0.f; + }; + // MFT layer budgets describe a normal crossing of a disk. Match + // TrackParCovFwd::addMCSEffect(), which lengthens x/X0 by csc(lambda), and + // apply the same path-length scaling to the areal density used for energy + // loss. For a linearized propagation the incidence comes from the + // reference trajectory, exactly as for the barrel operation above. + auto scaleMaterial = [](material::IntegratedMaterialBudget& material, const SurfaceTrackParameters& incidence) noexcept { + const float tgl = incidence.parameters[3]; + const float incidenceScale = std::sqrt(1.f + tgl * tgl) / std::abs(tgl); + material.xOverX0 *= incidenceScale; + material.arealDensityGPerCm2 *= incidenceScale; + }; + // Forward parameters are (X, Y, Phi, Tanl, Q2Pt); unlike barrel there is no + // cos(phi)-like factor on the angular diagonal term, and forward does not + // inherit barrel-only covariance range limiting. Slot 13 (the Q2Pt/Tanl + // cross term) and the k^2*R straggling contribution to slot 14 are new + // physics: the legacy TrackParCovFwd::addMCSEffect() never populates + // slot 13 and has no charge/PID/energy-loss awareness at all. + auto projectCovariance = [](SurfaceTrackState& scratch, const material::MaterialOperationResult& scalarResult, + float t, float k) noexcept { + const float A = 1.f + t * t; + const float h = scalarResult.highlandTheta2Rad2; + const float R = scalarResult.relativeInverseMomentumVariance; + scratch.covariance[packedCovarianceIndex(2, 2)] += h * A; + scratch.covariance[packedCovarianceIndex(3, 3)] += h * A * A; + scratch.covariance[packedCovarianceIndex(4, 3)] += h * A * t * k; + scratch.covariance[packedCovarianceIndex(4, 4)] += h * (t * k) * (t * k) + k * k * R; + }; + return correctForMaterialImpl(state, linRef, SurfaceKind::Disk, materialBudget, direction, + familyCheck, scaleMaterial, projectCovariance); +} + +material::MaterialOperationResult correctForMaterial(SurfaceTrackState& state, material::IntegratedMaterialBudget materialBudget, + material::MaterialTraversalDirection direction) noexcept +{ + SurfaceTrackParameters incidenceReference{state}; + return correctForMaterial(state, incidenceReference, materialBudget, direction); +} + +} // namespace o2::itsmft::tracking::detail::forward diff --git a/Detectors/ITSMFT/common/tracking/src/IOUtils.cxx b/Detectors/ITSMFT/common/tracking/src/IOUtils.cxx new file mode 100644 index 0000000000000..23987496b28df --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/IOUtils.cxx @@ -0,0 +1,646 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSMFTTracking/IOUtils.h" + +#include +#include +#include +#include +#include +#include + +#include "ITSMFTTracking/TimeFrame.h" +#include "Framework/Logger.h" +#include "GPUCommonMath.h" +#include "ITSBase/GeometryTGeo.h" +#include "MFTBase/GeometryTGeo.h" +#include "MathUtils/Utils.h" + +namespace +{ + +using o2::itsmft::ioutils::detail::addSysErrors; +using o2::itsmft::ioutils::detail::shouldApplySysErrors; + +template +o2::itsmft::tracking::ClusterDecodeResult decodeClusterBounded( + GeomT* geom, const o2::itsmft::CompClusterExt& cluster, + o2::itsmft::tracking::BoundedPatternCursor& patterns, + const o2::itsmft::TopologyDictionary* dict, bool applySysErrors) +{ + using o2::itsmft::tracking::ClusterDecodeError; + o2::itsmft::tracking::ClusterDecodeResult result; + if (dict == nullptr) { + result.error = ClusterDecodeError::MissingDictionary; + return result; + } + if (geom == nullptr) { + result.error = ClusterDecodeError::GeometryUnavailable; + return result; + } + + const auto sensorID = cluster.getSensorID(); + if (!o2::itsmft::ioutils::detail::isSensorInGeometry(sensorID, geom->getSize())) { + result.error = ClusterDecodeError::InvalidSensor; + return result; + } + const int layer = geom->getLayer(sensorID); + if (!o2::itsmft::ioutils::detail::isLayerInDetector(layer, o2::itsmft::tracking::TrackerParamRef::nLayers())) { + result.error = ClusterDecodeError::InvalidLayer; + return result; + } + + const auto clusterData = o2::itsmft::ioutils::extractClusterDataBounded(cluster, patterns, dict); + if (!clusterData.ok()) { + result.error = clusterData.error; + return result; + } + float sigma2Row = clusterData.sig2Row; + float sigma2Col = clusterData.sig2Col; + if (applySysErrors && shouldApplySysErrors()) { + addSysErrors(layer, sigma2Row, sigma2Col); + } + + if constexpr (DetId == o2::detectors::DetID::ITS) { + const auto trkXYZ = geom->getMatrixT2L(sensorID) ^ clusterData.coordinates; + const auto gloXYZ = geom->getMatrixL2G(sensorID) * clusterData.coordinates; + result.decoded = {{gloXYZ.x(), gloXYZ.y(), gloXYZ.z()}, + {trkXYZ.x(), trkXYZ.y(), trkXYZ.z(), geom->getSensorRefAlpha(sensorID)}, + {sigma2Row, 0.f, sigma2Col}, + clusterData.shape, + layer}; + } else { + if (!geom->getCacheL2G().isFilled() || geom->getCacheL2G().getSize() <= sensorID) { + result.error = ClusterDecodeError::GeometryUnavailable; + return result; + } + const auto gloXYZ = geom->getMatrixL2G(sensorID) * clusterData.coordinates; + result.decoded = {{gloXYZ.x(), gloXYZ.y(), gloXYZ.z()}, {}, {sigma2Row, 0.f, sigma2Col}, clusterData.shape, layer}; + } + return result; +} + +} // namespace + +namespace o2::itsmft::ioutils +{ + +void fillMatrixCache(o2::detectors::DetID::ID detId) +{ + const auto mask = o2::math_utils::bit2Mask(o2::math_utils::TransformType::T2L, o2::math_utils::TransformType::L2G); + if (detId == o2::detectors::DetID::ITS) { + o2::its::GeometryTGeo::Instance()->fillMatrixCache(mask); + } else if (detId == o2::detectors::DetID::MFT) { + o2::mft::GeometryTGeo::Instance()->fillMatrixCache(mask); + } else { + LOGP(fatal, "Unsupported detector id {} in fillMatrixCache", static_cast(detId)); + } +} + +template +o2::itsmft::tracking::ClusterDecodeResult decodeCluster( + const CompClusterExt& cluster, o2::itsmft::tracking::BoundedPatternCursor& patterns, + const TopologyDictionary* dict, bool applySysErrors) +{ + if constexpr (DetId == o2::detectors::DetID::ITS) { + return decodeClusterBounded(o2::its::GeometryTGeo::Instance(), cluster, patterns, dict, applySysErrors); + } else { + return decodeClusterBounded(o2::mft::GeometryTGeo::Instance(), cluster, patterns, dict, applySysErrors); + } +} + +template o2::itsmft::tracking::ClusterDecodeResult decodeCluster( + const CompClusterExt&, o2::itsmft::tracking::BoundedPatternCursor&, const TopologyDictionary*, bool); +template o2::itsmft::tracking::ClusterDecodeResult decodeCluster( + const CompClusterExt&, o2::itsmft::tracking::BoundedPatternCursor&, const TopologyDictionary*, bool); + +} // namespace o2::itsmft::ioutils + +namespace o2::itsmft::tracking +{ + +namespace +{ +class FailedTimeFrameLoadGuard +{ + public: + explicit FailedTimeFrameLoadGuard(TimeFrame& frame) noexcept : mFrame{&frame} {} + ~FailedTimeFrameLoadGuard() + { + if (mFrame != nullptr) { + mFrame->resetTimeFrame(); + } + } + void release() noexcept { mFrame = nullptr; } + + private: + TimeFrame* mFrame; +}; + +void clearFrameAndSidecars(TimeFrame& frame, + std::vector>* externalIndicesBySurface, + std::vector>* clusterSizesBySurface) noexcept +{ + frame.resetTimeFrame(); + if (externalIndicesBySurface != nullptr) { + externalIndicesBySurface->clear(); + } + if (clusterSizesBySurface != nullptr) { + clusterSizesBySurface->clear(); + } +} + +LoadSourcesResult decodeSources(TimeFrame& frame, const SurfaceCatalogView& catalog, + gsl::span sources, + const o2::InteractionRecord& origin, + std::vector>* externalIndicesBySurface, + std::vector>* clusterSizesBySurface); +} // namespace + +LoadSourcesResult loadTimeFrameSources(TimeFrame& frame, gsl::span sources, + SurfaceCatalogView catalog, const o2::InteractionRecord& origin, + std::vector>* externalIndicesBySurface, + std::vector>* clusterSizesBySurface) +{ + clearFrameAndSidecars(frame, externalIndicesBySurface, clusterSizesBySurface); + if (!frame.isConfigured()) { + return {MultiSourceLoadError::FrameNotConfigured}; + } + if (sources.empty()) { + return {MultiSourceLoadError::OtherMalformedInput}; + } + FailedTimeFrameLoadGuard failedLoad{frame}; + std::vector> loadedExternalIndices; + std::vector> loadedClusterSizes; + const auto loadResult = decodeSources(frame, catalog, sources, origin, + &loadedExternalIndices, &loadedClusterSizes); + if (!loadResult.ok()) { + return loadResult; + } + + const auto& layout = frame.getLayout(); + if (layout.empty()) { + return {MultiSourceLoadError::FrameNotConfigured}; + } + std::array configuredSurfaces{}; + for (std::size_t position = 0; position < layout.size(); ++position) { + configuredSurfaces[position] = true; + } + std::array mappedSurfaces{}; + for (const auto& source : sources) { + for (const auto surface : source.layerToSurface) { + if (!surface.isValid() || surface.value() >= MaxLayoutSurfaces || + mappedSurfaces[surface.value()] || !configuredSurfaces[surface.value()]) { + return {MultiSourceLoadError::InvalidLayerMapping, source.id}; + } + if (catalog.getSurface(surface).detectorId != static_cast(source.detector)) { + return {MultiSourceLoadError::DetectorSurfaceMismatch, source.id}; + } + mappedSurfaces[surface.value()] = true; + } + } + if (mappedSurfaces != configuredSurfaces) { + // Attribute an omitted surface only when one source owns its detector. + for (uint16_t position = 0; position < layout.size(); ++position) { + const auto surface = LayerId{position}; + if (mappedSurfaces[surface.value()]) { + continue; + } + ClusterSourceId owner; + for (const auto& source : sources) { + if (static_cast(source.detector) != catalog.getSurface(surface).detectorId) { + continue; + } + if (owner.isValid()) { + return {MultiSourceLoadError::InvalidLayerMapping}; + } + owner = source.id; + } + return {MultiSourceLoadError::InvalidLayerMapping, owner}; + } + return {MultiSourceLoadError::InvalidLayerMapping}; + } + + frame.setROFViews(sources.front().rofViews); + for (uint16_t position = 0; position < layout.size(); ++position) { + const auto surface = LayerId{position}; + const ClusterSourceInput* owner = nullptr; + uint16_t localLayer = 0; + for (const auto& source : sources) { + const auto it = std::find(source.layerToSurface.begin(), source.layerToSurface.end(), surface); + if (it == source.layerToSurface.end()) { + continue; + } + if (owner != nullptr) { + return {MultiSourceLoadError::InvalidLayerMapping, source.id}; + } + owner = &source; + localLayer = static_cast(std::distance(source.layerToSurface.begin(), it)); + } + if (owner == nullptr) { + return {MultiSourceLoadError::InvalidLayerMapping}; + } + + const auto globals = frame.getGlobalMeasurements(surface); + std::vector boundaries; + boundaries.assign(owner->rofs.size() + 1, 0); + std::size_t measurement = 0; + for (std::size_t rof = 0; rof < owner->rofs.size(); ++rof) { + const auto firstEntry = static_cast(owner->rofs[rof].getFirstEntry()); + const auto endEntry = firstEntry + static_cast(owner->rofs[rof].getNEntries()); + while (measurement < globals.size()) { + const auto clusterId = globals[measurement].clusterId; + if (surface.value() >= loadedExternalIndices.size() || + clusterId >= loadedExternalIndices[surface.value()].size()) { + return {MultiSourceLoadError::InconsistentDecoderMetadata, owner->id, + static_cast(rof), clusterId}; + } + const auto externalIndex = loadedExternalIndices[surface.value()][clusterId]; + if (externalIndex >= endEntry) { + break; + } + if (externalIndex < firstEntry) { + return {MultiSourceLoadError::InconsistentDecoderMetadata, owner->id, + static_cast(rof), externalIndex}; + } + ++measurement; + } + boundaries[rof + 1] = static_cast(measurement); + } + if (measurement != globals.size()) { + return {MultiSourceLoadError::InconsistentDecoderMetadata, owner->id}; + } + frame.setROFNavigation(position, boundaries, owner->rofViews, localLayer); + } + if (externalIndicesBySurface != nullptr) { + *externalIndicesBySurface = std::move(loadedExternalIndices); + } + if (clusterSizesBySurface != nullptr) { + *clusterSizesBySurface = std::move(loadedClusterSizes); + } + failedLoad.release(); + return {}; +} + +LoadSourcesResult loadTimeFrameSource( + TimeFrame& frame, + const ClusterDecoder& decoder, + const o2::InteractionRecord& origin, + const ROFTimingConfig& timing, + gsl::span clusters, + gsl::span patterns, + gsl::span rofs, + const itsmft::TopologyDictionary* dictionary, + const dataformats::MCTruthContainer* labels, + o2::detectors::DetID::ID detector, + gsl::span layerToSurface, + SurfaceCatalogView catalog, + bool applySysErrors, + std::vector>* externalIndicesBySurface, + std::vector>* clusterSizesBySurface) +{ + constexpr ClusterSourceId sourceId{0}; + if (detector != o2::detectors::DetID::ITS && detector != o2::detectors::DetID::MFT) { + clearFrameAndSidecars(frame, externalIndicesBySurface, clusterSizesBySurface); + return {MultiSourceLoadError::UnsupportedDetector, sourceId}; + } + if (catalog.surfaces == nullptr || catalog.nSurfaces == 0) { + clearFrameAndSidecars(frame, externalIndicesBySurface, clusterSizesBySurface); + return {MultiSourceLoadError::SurfaceCatalogNotConfigured, sourceId}; + } + ClusterSourceInput source; + source.id = sourceId; + source.detector = detector; + source.clusters = clusters; + source.patterns = patterns; + source.rofs = rofs; + source.dictionary = dictionary; + source.labels = labels; + source.layerToSurface = layerToSurface; + source.timing = timing; + source.decoder = &decoder; + source.applySysErrors = applySysErrors; + source.rofViews = frame.getROFViews(); + return loadTimeFrameSources(frame, gsl::span{&source, 1}, catalog, origin, + externalIndicesBySurface, clusterSizesBySurface); +} + +namespace +{ +bool isSupportedDetector(o2::detectors::DetID::ID det) noexcept +{ + return det == o2::detectors::DetID::ITS || det == o2::detectors::DetID::MFT; +} + +bool covariance2DIsPositiveSemidefinite(float varianceFirst, float covariance, + float varianceSecond) noexcept +{ + if (!o2::gpu::GPUCommonMath::Finite(varianceFirst) || !o2::gpu::GPUCommonMath::Finite(covariance) || + !o2::gpu::GPUCommonMath::Finite(varianceSecond) || varianceFirst < 0.f || varianceSecond < 0.f) { + return false; + } + const double diagonalProduct = static_cast(varianceFirst) * varianceSecond; + const double covarianceSquared = static_cast(covariance) * covariance; + const double tolerance = 16. * std::numeric_limits::epsilon() * + std::max(diagonalProduct, covarianceSquared); + return diagonalProduct - covarianceSquared >= -tolerance; +} + +bool globalCovarianceIsPositiveSemidefinite(const GlobalCovariance3F& covariance) noexcept +{ + const float xx = covariance[GlobalMeasurement::XX]; + const float xy = covariance[GlobalMeasurement::XY]; + const float xz = covariance[GlobalMeasurement::XZ]; + const float yy = covariance[GlobalMeasurement::YY]; + const float yz = covariance[GlobalMeasurement::YZ]; + const float zz = covariance[GlobalMeasurement::ZZ]; + if (!covariance2DIsPositiveSemidefinite(xx, xy, yy) || + !covariance2DIsPositiveSemidefinite(xx, xz, zz) || + !covariance2DIsPositiveSemidefinite(yy, yz, zz)) { + return false; + } + const double determinant = + static_cast(xx) * yy * zz + 2. * static_cast(xy) * xz * yz - + static_cast(xx) * yz * yz - static_cast(yy) * xz * xz - + static_cast(zz) * xy * xy; + const double scale = std::max({std::abs(static_cast(xx) * yy * zz), + std::abs(2. * static_cast(xy) * xz * yz), + std::abs(static_cast(xx) * yz * yz), + std::abs(static_cast(yy) * xz * xz), + std::abs(static_cast(zz) * xy * xy)}); + return o2::gpu::GPUCommonMath::Finite(static_cast(determinant)) && + determinant >= -32. * std::numeric_limits::epsilon() * scale; +} + +bool decodedMeasurementIsValid(const GlobalMeasurement& global, + const SurfaceMeasurement& local) noexcept +{ + return o2::gpu::GPUCommonMath::Finite(global.x) && o2::gpu::GPUCommonMath::Finite(global.y) && + o2::gpu::GPUCommonMath::Finite(global.z) && + globalCovarianceIsPositiveSemidefinite(global.covariance) && + o2::gpu::GPUCommonMath::Finite(local.frame.q) && o2::gpu::GPUCommonMath::Finite(local.frame.u) && + o2::gpu::GPUCommonMath::Finite(local.frame.v) && o2::gpu::GPUCommonMath::Finite(local.frame.frameAngle) && + covariance2DIsPositiveSemidefinite(local.covariance.uu, + local.covariance.uv, + local.covariance.vv); +} + +MultiSourceLoadError mapDecodeError(ClusterDecodeError error) noexcept +{ + switch (error) { + case ClusterDecodeError::None: + return MultiSourceLoadError::None; + case ClusterDecodeError::MissingDictionary: + return MultiSourceLoadError::MissingDictionary; + case ClusterDecodeError::TruncatedExplicitPattern: + return MultiSourceLoadError::TruncatedExplicitPattern; + case ClusterDecodeError::MalformedExplicitPattern: + return MultiSourceLoadError::MalformedExplicitPattern; + case ClusterDecodeError::InvalidPatternId: + return MultiSourceLoadError::InvalidPatternId; + case ClusterDecodeError::InvalidSensor: + return MultiSourceLoadError::InvalidSensor; + case ClusterDecodeError::InvalidLayer: + return MultiSourceLoadError::InvalidDecodedLayer; + case ClusterDecodeError::GeometryUnavailable: + return MultiSourceLoadError::GeometryUnavailable; + case ClusterDecodeError::OtherMalformedInput: + return MultiSourceLoadError::OtherMalformedInput; + } + return MultiSourceLoadError::OtherMalformedInput; +} +} // namespace + +namespace +{ +LoadSourcesResult decodeSources(TimeFrame& frame, + const SurfaceCatalogView& catalog, + gsl::span sources, + const o2::InteractionRecord& origin, + std::vector>* externalIndicesBySurface, + std::vector>* clusterSizesBySurface) +{ + const auto nSources = static_cast(sources.size()); + + std::vector seen(nSources, false); + std::vector sourceBySurface(catalog.nSurfaces, ClusterSourceId::invalid()); + for (const auto& src : sources) { + if (!src.id.isValid() || src.id.value() >= nSources) { + return {MultiSourceLoadError::NonDenseSourceIds, src.id}; + } + if (seen[src.id.value()]) { + return {MultiSourceLoadError::DuplicateSourceId, src.id}; + } + seen[src.id.value()] = true; + if (!isSupportedDetector(src.detector)) { + return {MultiSourceLoadError::UnsupportedDetector, src.id}; + } + if (src.decoder == nullptr) { + return {MultiSourceLoadError::MissingDecoder, src.id}; + } + if (!src.clusters.empty() && src.dictionary == nullptr) { + return {MultiSourceLoadError::MissingDictionary, src.id, 0, 0}; + } + for (const auto surface : src.layerToSurface) { + if (!surface.isValid() || surface.value() >= catalog.nSurfaces) { + return {MultiSourceLoadError::InvalidLayerMapping, src.id}; + } + if (sourceBySurface[surface.value()].isValid()) { + return {MultiSourceLoadError::InvalidLayerMapping, src.id}; + } + if (catalog.getSurface(surface).detectorId != static_cast(src.detector)) { + return {MultiSourceLoadError::DetectorSurfaceMismatch, src.id}; + } + sourceBySurface[surface.value()] = src.id; + } + } + + std::vector> perSurfaceClusterSizes(catalog.nSurfaces); + std::vector> stagedExternalIndices(catalog.nSurfaces); + bool hasMCInformation = false; + + for (const auto& src : sources) { + hasMCInformation |= src.labels != nullptr; + + int64_t expectedNext = 0; + for (uint32_t r = 0; r < src.rofs.size(); ++r) { + const auto& rof = src.rofs[r]; + const int64_t first = rof.getFirstEntry(); + const int64_t n = rof.getNEntries(); + if (n < 0 || first != expectedNext) { + return {MultiSourceLoadError::InvalidROFRange, src.id, r}; + } + expectedNext = first + n; + if (expectedNext > static_cast(src.clusters.size())) { + return {MultiSourceLoadError::InvalidROFRange, src.id, r}; + } + } + if (expectedNext != static_cast(src.clusters.size())) { + return {MultiSourceLoadError::InvalidROFRange, src.id, static_cast(src.rofs.size())}; + } + + for (uint32_t r = 0; r < src.rofs.size(); ++r) { + const auto built = computeROFIntervalBC(src.rofs[r].getBCData(), origin, src.timing, r); + if (!built.ok()) { + return LoadSourcesResult{.error = MultiSourceLoadError::TimingError, .source = src.id, .rof = r, .timingDetail = built.error}; + } + } + + src.decoder->prepare(); + BoundedPatternCursor patterns{src.patterns}; + for (uint32_t r = 0; r < src.rofs.size(); ++r) { + const auto& rof = src.rofs[r]; + const auto firstEntry = rof.getFirstEntry(); + const auto nEntries = rof.getNEntries(); + for (int32_t clusterId = firstEntry; clusterId < firstEntry + nEntries; ++clusterId) { + const auto& cluster = src.clusters[clusterId]; + const auto externalIndex = static_cast(clusterId); + const auto decodeResult = src.decoder->decode(cluster, patterns, src.dictionary, + externalIndex, src.applySysErrors); + if (!decodeResult.ok()) { + return {mapDecodeError(decodeResult.error), src.id, r, externalIndex}; + } + const auto& decoded = decodeResult.decoded; + if (decoded.layer < 0 || static_cast(decoded.layer) >= src.layerToSurface.size()) { + return {MultiSourceLoadError::InvalidLayerMapping, src.id, r, externalIndex}; + } + const auto expectedSurface = src.layerToSurface[decoded.layer]; + if (!expectedSurface.isValid() || expectedSurface.value() >= catalog.nSurfaces) { + return {MultiSourceLoadError::InvalidLayerMapping, src.id, r, externalIndex}; + } + const auto& surfaceDescriptor = catalog.getSurface(expectedSurface); + if (surfaceDescriptor.detectorId != static_cast(src.detector)) { + return {MultiSourceLoadError::DetectorSurfaceMismatch, src.id, r, externalIndex}; + } + const auto localClusterId = static_cast(frame.getGlobalMeasurements(expectedSurface).size()); + GlobalMeasurement global; + SurfaceMeasurement measurement; + if (surfaceDescriptor.kind == SurfaceKind::Cylinder) { + global = makeCylinderGlobalMeasurement(decoded, localClusterId); + measurement = makeCylinderSurfaceMeasurement(decoded); + } else { + global = makeDiskGlobalMeasurement(decoded, localClusterId); + measurement = makeDiskSurfaceMeasurement(decoded); + } + if (!decodedMeasurementIsValid(global, measurement)) { + return {MultiSourceLoadError::OtherMalformedInput, src.id, r, externalIndex}; + } + global.x -= frame.getBeamX(); + global.y -= frame.getBeamY(); + global.radius = std::hypot(global.x, global.y); + global.phi = o2::its::math_utils::computePhi(global.x, global.y); + if (src.labels != nullptr) { + frame.addMeasurement(expectedSurface, global, measurement, src.labels->getLabels(externalIndex)); + } else { + frame.addMeasurement(expectedSurface, global, measurement); + } + perSurfaceClusterSizes[expectedSurface.value()].push_back(decoded.shape.nPixels); + stagedExternalIndices[expectedSurface.value()].push_back(externalIndex); + } + } + if (!patterns.empty()) { + return {MultiSourceLoadError::TrailingPatternData, src.id, + static_cast(src.rofs.size()), static_cast(src.clusters.size())}; + } + } + + frame.setHasMCInformation(hasMCInformation); + if (externalIndicesBySurface != nullptr) { + *externalIndicesBySurface = std::move(stagedExternalIndices); + } + if (clusterSizesBySurface != nullptr) { + *clusterSizesBySurface = std::move(perSurfaceClusterSizes); + } + return {}; +} +} // namespace + +LoadSourcesResult loadSources(TimeFrame& frame, const SurfaceCatalogView& catalog, + gsl::span sources, + const o2::InteractionRecord& origin, + std::vector>* externalIndicesBySurface, + std::vector>* clusterSizesBySurface) +{ + clearFrameAndSidecars(frame, externalIndicesBySurface, clusterSizesBySurface); + if (!frame.isConfigured()) { + return {MultiSourceLoadError::FrameNotConfigured}; + } + FailedTimeFrameLoadGuard failedLoad{frame}; + const auto result = decodeSources(frame, catalog, sources, origin, + externalIndicesBySurface, clusterSizesBySurface); + if (result.ok()) { + failedLoad.release(); + } + return result; +} + +namespace +{ +std::string formatLoadSourcesResult(const char* label, const LoadSourcesResult& result) +{ + return std::format("{}: error={} source={} rof={} clusterIndex={} timingDetail={}", + label, static_cast(result.error), result.source.value(), + result.rof, result.clusterIndex, static_cast(result.timingDetail)); +} +} // namespace + +RecoverableLoadFailure::RecoverableLoadFailure(const LoadSourcesResult& result) + : std::runtime_error(formatLoadSourcesResult("TimeFrame loading boundary: recoverable data failure", result)), + mResult(result) +{ +} + +TimeFrameLoadException::TimeFrameLoadException(TimeFrameLoadFailureReason reason, std::string message) + : std::runtime_error(std::move(message)), mReason(reason) +{ +} + +TimeFrameLoadException::TimeFrameLoadException(const LoadSourcesResult& result) + : std::runtime_error(formatLoadSourcesResult("TimeFrame loading boundary: structural failure", result)), + mReason(TimeFrameLoadFailureReason::LoadSourcesFailure), + mLoadResult(result) +{ +} + +bool isRecoverableLoadError(MultiSourceLoadError error, TimingBuildError timingDetail) noexcept +{ + switch (error) { + case MultiSourceLoadError::InvalidROFRange: + case MultiSourceLoadError::TruncatedExplicitPattern: + case MultiSourceLoadError::MalformedExplicitPattern: + case MultiSourceLoadError::InvalidPatternId: + case MultiSourceLoadError::InvalidSensor: + case MultiSourceLoadError::InvalidDecodedLayer: + case MultiSourceLoadError::OtherMalformedInput: + case MultiSourceLoadError::TrailingPatternData: + return true; + case MultiSourceLoadError::TimingError: + return timingDetail == TimingBuildError::Overflow; + case MultiSourceLoadError::None: + case MultiSourceLoadError::NonDenseSourceIds: + case MultiSourceLoadError::DuplicateSourceId: + case MultiSourceLoadError::UnsupportedDetector: + case MultiSourceLoadError::MissingDecoder: + case MultiSourceLoadError::InvalidLayerMapping: + case MultiSourceLoadError::DetectorSurfaceMismatch: + case MultiSourceLoadError::InconsistentDecoderMetadata: + case MultiSourceLoadError::SurfaceCatalogNotConfigured: + case MultiSourceLoadError::SurfaceCatalogStale: + case MultiSourceLoadError::MissingDictionary: + case MultiSourceLoadError::GeometryUnavailable: + case MultiSourceLoadError::FrameNotConfigured: + return false; + } + return false; +} + +} // namespace o2::itsmft::tracking diff --git a/Detectors/ITSMFT/common/tracking/src/ITSMFTTrackingLinkDef.h b/Detectors/ITSMFT/common/tracking/src/ITSMFTTrackingLinkDef.h new file mode 100644 index 0000000000000..bd7b773e7ec75 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/ITSMFTTrackingLinkDef.h @@ -0,0 +1,26 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifdef __CLING__ + +#pragma link off all globals; +#pragma link off all classes; +#pragma link off all functions; + +#pragma link C++ class o2::itsmft::TrackerParamConfig < o2::detectors::DetID::MFT> + ; +#pragma link C++ class o2::conf::ConfigurableParamHelper < o2::itsmft::TrackerParamConfig < o2::detectors::DetID::MFT>> + ; + +// String-keyed workflow configuration requires ROOT dictionaries for both +// common-CA parameter record. +#pragma link C++ class o2::itsmft::ITSCommonCATrackerParam + ; +#pragma link C++ class o2::conf::ConfigurableParamHelper < o2::itsmft::ITSCommonCATrackerParam> + ; + +#endif diff --git a/Detectors/ITSMFT/common/tracking/src/IndexTableConfiguration.cxx b/Detectors/ITSMFT/common/tracking/src/IndexTableConfiguration.cxx new file mode 100644 index 0000000000000..ac3e802ac6b24 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/IndexTableConfiguration.cxx @@ -0,0 +1,74 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSMFTTracking/IndexTableConfiguration.h" + +#include +#include +#include + +#include "CommonConstants/MathConstants.h" +#include "GPUCommonMath.h" + +namespace o2::itsmft::tracking +{ + +using o2::itsmft::IndexTableCoordType; + +IndexTableConfigError bindIndexTableConfiguration(o2::itsmft::IndexTableUtilsCore& staged, + const DetectorParameters& params, + int activeSurfaceCount, + SurfaceKind kind, + gsl::span chartRanges) noexcept +{ + if (kind != SurfaceKind::Cylinder && kind != SurfaceKind::Disk) { + return IndexTableConfigError::InvalidSurfaceKind; + } + if (!(activeSurfaceCount > 0 && activeSurfaceCount <= o2::itsmft::IndexTableUtilsCore::MaxLayers)) { + return IndexTableConfigError::InvalidActiveLayerCount; + } + if (params.RowBins <= 0) { + return IndexTableConfigError::NonPositiveRowBins; + } + if (params.ColBins <= 0) { + return IndexTableConfigError::NonPositiveColBins; + } + + const std::uint64_t binCount = static_cast(params.RowBins) * static_cast(params.ColBins); + if (binCount > static_cast(std::numeric_limits::max())) { + return IndexTableConfigError::RowColBinCountExceedsIndexRange; + } + + if (chartRanges.size() < static_cast(activeSurfaceCount)) { + return IndexTableConfigError::InsufficientChartRanges; + } + std::array colMin{}; + std::array colMax{}; + for (int iLayer = 0; iLayer < activeSurfaceCount; ++iLayer) { + if (!o2::gpu::GPUCommonMath::Finite(chartRanges[iLayer].min) || + !o2::gpu::GPUCommonMath::Finite(chartRanges[iLayer].max)) { + return IndexTableConfigError::NonFiniteChartRange; + } + if (!(chartRanges[iLayer].max > chartRanges[iLayer].min)) { + return IndexTableConfigError::InvalidChartRange; + } + colMin[iLayer] = chartRanges[iLayer].min; + colMax[iLayer] = chartRanges[iLayer].max; + } + + staged.setIndexTableParams(kind == SurfaceKind::Disk ? IndexTableCoordType::PhiR : IndexTableCoordType::PhiZ, + params.RowBins, params.ColBins, 0.f, o2::constants::math::TwoPI, + gsl::span{colMin.data(), static_cast(activeSurfaceCount)}, + gsl::span{colMax.data(), static_cast(activeSurfaceCount)}); + return IndexTableConfigError::None; +} + +} // namespace o2::itsmft::tracking diff --git a/Detectors/ITSMFT/common/tracking/src/MaterialPhysics.cxx b/Detectors/ITSMFT/common/tracking/src/MaterialPhysics.cxx new file mode 100644 index 0000000000000..fd4a3c32bfe0e --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/MaterialPhysics.cxx @@ -0,0 +1,177 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSMFTTracking/MaterialPhysics.h" + +#include + +// Reuse the public energy-loss constants and Bethe-Bloch helper. These +// headers are implementation details of this translation unit. +#include "ReconstructionDataFormats/TrackParametrization.h" +#include "ReconstructionDataFormats/TrackUtils.h" + +namespace o2::itsmft::tracking::material +{ + +namespace +{ +constexpr float kHighlandConst2 = 0.0136f * 0.0136f; +constexpr float kStragglingConst = 0.0007f; +constexpr float kMinMomentumGeV = 0.01f; + +MaterialOperationResult makeFailureResult(float momentumGeV, MaterialFailureReason reason) noexcept +{ + MaterialOperationResult result{}; + result.momentumBeforeGeV = momentumGeV; + result.momentumAfterGeV = 0.f; + result.signedEnergyChangeGeV = 0.f; + result.highlandTheta2Rad2 = 0.f; + result.relativeInverseMomentumVariance = 0.f; + result.energyLossSubsteps = 0; + result.flags = MaterialOperationFlags::None; + result.failure = reason; + result.reserved = 0; + return result; +} + +MaterialOperationResult makeSuccessResult(float momentumBeforeGeV, float momentumAfterGeV, + float signedEnergyChangeGeV, float highlandTheta2Rad2, + float relativeInverseMomentumVariance, + uint8_t energyLossSubsteps, + MaterialOperationFlags flags) noexcept +{ + MaterialOperationResult result{}; + result.momentumBeforeGeV = momentumBeforeGeV; + result.momentumAfterGeV = momentumAfterGeV; + result.signedEnergyChangeGeV = signedEnergyChangeGeV; + result.highlandTheta2Rad2 = highlandTheta2Rad2; + result.relativeInverseMomentumVariance = relativeInverseMomentumVariance; + result.energyLossSubsteps = energyLossSubsteps; + result.flags = flags; + result.failure = MaterialFailureReason::None; + result.reserved = 0; + return result; +} + +// Compute the capped substep count without an out-of-range float-to-int +// conversion. Also report whether the requested count exceeded the cap. +void classifySubsteps(float fullStepEnergyLossGeV, float kineticEnergyGeV, uint8_t& substeps, bool& clamped) noexcept +{ + const float ratio = std::fabs(fullStepEnergyLossGeV) / kineticEnergyGeV * o2::track::ELoss2EKinThreshInv; + if (ratio >= static_cast(o2::track::MaxELossIter)) { + substeps = static_cast(o2::track::MaxELossIter); + clamped = true; + return; + } + // Keep the conversion in range even when ratio is unordered. Subsequent + // arithmetic remains responsible for propagating invalid inputs. + const float boundedRatio = ratio < static_cast(o2::track::MaxELossIter) ? ratio : 0.f; + const int requested = 1 + static_cast(boundedRatio); + substeps = static_cast(requested); + clamped = false; +} +} // namespace + +MaterialOperationResult calculateMaterialPhysics( + float momentumGeV, + o2::track::PID pid, + uint8_t absCharge, + MaterialTraversalDirection direction, + IntegratedMaterialBudget material) noexcept +{ + if (direction != MaterialTraversalDirection::AlongMomentum && direction != MaterialTraversalDirection::OppositeMomentum) { + return makeFailureResult(momentumGeV, MaterialFailureReason::InvalidDirection); + } + if (material.xOverX0 < 0.f || material.arealDensityGPerCm2 < 0.f) { + return makeFailureResult(momentumGeV, MaterialFailureReason::InvalidMaterial); + } + if (momentumGeV <= 0.f) { + return makeFailureResult(momentumGeV, MaterialFailureReason::MomentumBelowMinimum); + } + if (pid.getID() >= o2::track::PID::NIDsTot) { + return makeFailureResult(momentumGeV, MaterialFailureReason::InvalidPID); + } + const float mass = pid.getMass(); + if (absCharge != 0 && mass == 0.f) { + return makeFailureResult(momentumGeV, MaterialFailureReason::ChargedMasslessPID); + } + + if (absCharge == 0) { + return makeSuccessResult(momentumGeV, momentumGeV, 0.f, 0.f, 0.f, 0, MaterialOperationFlags::None); + } + + const float q2 = static_cast(absCharge) * static_cast(absCharge); + const float p0 = momentumGeV; + const float p0Squared = p0 * p0; + const float e0 = std::sqrt(p0Squared + mass * mass); + const float beta2 = p0Squared / (e0 * e0); + if (beta2 <= 0.f) { + return makeFailureResult(momentumGeV, MaterialFailureReason::NonFiniteResult); + } + + float e = e0; + float p = p0; + uint8_t substeps = 0; + MaterialOperationFlags flags = MaterialOperationFlags::None; + + if (material.arealDensityGPerCm2 > 0.f) { + const float ekin = e0 - mass; + const float bg0 = p0 / mass; + const float dedx0 = o2::track::BetheBlochSolidOpt(bg0) * q2; + const float fullStepEnergyLoss = dedx0 * material.arealDensityGPerCm2; + + bool clamped = false; + classifySubsteps(fullStepEnergyLoss, ekin, substeps, clamped); + if (clamped) { + flags = MaterialOperationFlags::SubstepCountClamped; + } + + const float arealDensityStep = material.arealDensityGPerCm2 / static_cast(substeps); + MaterialFailureReason loopFailure = MaterialFailureReason::None; + for (uint8_t i = 0; i < substeps; ++i) { + const float bg = p / mass; + const float dedx = o2::track::BetheBlochSolidOpt(bg) * q2; + const float dE = dedx * arealDensityStep; + e = (direction == MaterialTraversalDirection::AlongMomentum) ? (e - dE) : (e + dE); + if (e <= mass) { + loopFailure = MaterialFailureReason::StoppedInMaterial; + break; + } + p = std::sqrt(e * e - mass * mass); + } + if (loopFailure != MaterialFailureReason::None) { + return makeFailureResult(momentumGeV, loopFailure); + } + } + + if (p < kMinMomentumGeV) { + return makeFailureResult(momentumGeV, MaterialFailureReason::MomentumBelowMinimum); + } + const float signedEnergyChangeGeV = e - e0; + + float highlandTheta2Rad2 = 0.f; + if (material.xOverX0 > 0.f) { + highlandTheta2Rad2 = kHighlandConst2 / (beta2 * p0 * p0) * material.xOverX0 * q2; + if (highlandTheta2Rad2 > o2::constants::math::PI * o2::constants::math::PI) { + return makeFailureResult(momentumGeV, MaterialFailureReason::ExcessiveScattering); + } + } + + float relativeInverseMomentumVariance = 0.f; + if (signedEnergyChangeGeV != 0.f) { + relativeInverseMomentumVariance = kStragglingConst * kStragglingConst * std::fabs(signedEnergyChangeGeV) * e0 * e0 / (p0 * p0 * p0 * p0); + } + + return makeSuccessResult(momentumGeV, p, signedEnergyChangeGeV, highlandTheta2Rad2, + relativeInverseMomentumVariance, substeps, flags); +} + +} // namespace o2::itsmft::tracking::material diff --git a/Detectors/ITSMFT/common/tracking/src/Propagator.cxx b/Detectors/ITSMFT/common/tracking/src/Propagator.cxx new file mode 100644 index 0000000000000..9eba7c1236a33 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/Propagator.cxx @@ -0,0 +1,462 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSMFTTracking/Propagator.h" + +#include + +#include "ITSMFTTracking/detail/SurfaceStateOperations.h" +#include "ReconstructionDataFormats/TrackParametrization.h" + +namespace o2::itsmft::tracking +{ + +namespace +{ + +// Remove tiny negative diagonal values caused by floating-point cancellation +// during covariance transport. Larger negative values remain errors. +void clampNegligibleCovarianceNoise(SurfaceTrackState& state) noexcept +{ + constexpr float kNoiseFloor = 1.e-3f; + for (uint8_t i = 0; i < 5; ++i) { + const uint8_t index = packedCovarianceIndex(i, i); + if (state.covariance[index] < 0.f && state.covariance[index] > -kNoiseFloor) { + state.covariance[index] = 0.f; + } + } +} + +// Apply outCov = J * inCov * J^T to a packed-symmetric 5x5 covariance. +void congruenceTransform(const float (&inCov)[15], const float (&jacobian)[5][5], float (&outCov)[15]) noexcept +{ + float full[5][5]; + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t col = 0; col < 5; ++col) { + full[row][col] = inCov[packedCovarianceIndex(row, col)]; + } + } + float tmp[5][5]; + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t col = 0; col < 5; ++col) { + float sum = 0.f; + for (uint8_t k = 0; k < 5; ++k) { + sum += jacobian[row][k] * full[k][col]; + } + tmp[row][col] = sum; + } + } + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t col = 0; col <= row; ++col) { + float sum = 0.f; + for (uint8_t k = 0; k < 5; ++k) { + sum += tmp[row][k] * jacobian[col][k]; + } + outCov[packedCovarianceIndex(row, col)] = sum; + } + } +} + +// Convert Barrel (bY, bZ, Snp, Tgl, Q2Pt) to Forward +// (X, Y, Phi, Tanl, InvQPt) on the fixed-z plane through the nominal point. +bool barrelToForward(SurfaceTrackState& state, float bz, OperationFailureReason& reason) noexcept +{ + const float snp = state.parameters[2]; + const float tanl = state.parameters[3]; + if (!(std::abs(snp) < 1.f) || tanl == 0.f) { + reason = OperationFailureReason::SurfaceKindConversionFailure; + return false; + } + const float csA = std::cos(state.alpha); + const float snA = std::sin(state.alpha); + const float csp = std::sqrt((1.f - snp) * (1.f + snp)); + const float bX = state.referenceCoordinate; + const float bY = state.parameters[0]; + + const float xGlo = bX * csA - bY * snA; + const float yGlo = bX * snA + bY * csA; + const float zGlo = state.parameters[1]; + float phi = std::remainder(state.alpha + std::asin(snp), o2::constants::math::TwoPI); + // Match the library's (-pi, pi] angle convention. + if (phi <= -o2::constants::math::PI) { + phi += o2::constants::math::TwoPI; + } + + // A displaced source z reaches the fixed target plane after transverse + // path -deltaZ/tanl. Include both position and direction along that path. + const float curvature = state.absCharge == 0 ? 0.f : state.parameters[4] * bz * o2::constants::math::B2C; + const float jacobian[5][5] = { + {-snA, -(csA * csp - snA * snp) / tanl, 0.f, 0.f, 0.f}, + {csA, -(snA * csp + csA * snp) / tanl, 0.f, 0.f, 0.f}, + {0.f, -curvature / tanl, 1.f / csp, 0.f, 0.f}, + {0.f, 0.f, 0.f, 1.f, 0.f}, + {0.f, 0.f, 0.f, 0.f, 1.f}}; + float newCov[15]; + congruenceTransform(state.covariance, jacobian, newCov); + + const float newParameters[5] = {xGlo, yGlo, phi, state.parameters[3], state.parameters[4]}; + for (uint8_t i = 0; i < 5; ++i) { + state.parameters[i] = newParameters[i]; + } + for (uint8_t i = 0; i < 15; ++i) { + state.covariance[i] = newCov[i]; + } + state.referenceCoordinate = zGlo; + state.alpha = 0.f; + state.kind = SurfaceKind::Disk; + return true; +} + +// Convert Forward (X, Y, Phi, Tanl, InvQPt) to Barrel +// (bY, bZ, Snp, Tgl, Q2Pt) on the fixed local-x plane through the nominal +// point. Both target alpha and local x are held fixed in the Jacobian. +bool forwardToBarrel(SurfaceTrackState& state, float bz, OperationFailureReason& reason) noexcept +{ + const float x = state.parameters[0]; + const float y = state.parameters[1]; + const float r = std::sqrt(x * x + y * y); + if (!(r > 1.e-6f)) { + reason = OperationFailureReason::SurfaceKindConversionFailure; + return false; + } + const float alpha = std::atan2(y, x); + const float csA = std::cos(alpha); + const float snA = std::sin(alpha); + const float phi = state.parameters[2]; + const float csp = std::cos(phi - alpha); + const float snp = std::sin(phi - alpha); + // The barrel convention encodes only the positive-cosine branch at alpha. + // Reject inward/tangent directions rather than silently reversing them. + if (!(csp > 0.f && std::abs(snp) < 1.f)) { + reason = OperationFailureReason::SurfaceKindConversionFailure; + return false; + } + + const float bX = x * csA + y * snA; + const float bY = -x * snA + y * csA; + const float bZ = state.referenceCoordinate; + + // A displacement along the plane normal shifts the intersection by + // transverse path -deltaX/csp, inducing local-y, z and direction errors. + const float curvature = state.absCharge == 0 ? 0.f : state.parameters[4] * bz * o2::constants::math::B2C; + const float tanlOverCsp = state.parameters[3] / csp; + const float jacobian[5][5] = { + {-snA - snp * csA / csp, csA - snp * snA / csp, 0.f, 0.f, 0.f}, + {-tanlOverCsp * csA, -tanlOverCsp * snA, 0.f, 0.f, 0.f}, + {-curvature * csA, -curvature * snA, csp, 0.f, 0.f}, + {0.f, 0.f, 0.f, 1.f, 0.f}, + {0.f, 0.f, 0.f, 0.f, 1.f}}; + float newCov[15]; + congruenceTransform(state.covariance, jacobian, newCov); + + const float newParameters[5] = {bY, bZ, snp, state.parameters[3], state.parameters[4]}; + for (uint8_t i = 0; i < 5; ++i) { + state.parameters[i] = newParameters[i]; + } + for (uint8_t i = 0; i < 15; ++i) { + state.covariance[i] = newCov[i]; + } + state.referenceCoordinate = bX; + state.alpha = alpha; + state.kind = SurfaceKind::Cylinder; + return true; +} + +// Both attachment algorithms work on a candidate and commit only after every +// fallible operation succeeds. Linearized attachment also keeps a local reference. +struct AttachmentTransaction { + SurfaceTrackState state; + float chi2; + + void commit(SurfaceTrackState& destination, float& destinationChi2) const noexcept + { + destination = state; + destinationChi2 = chi2; + } +}; + +bool acceptsAttachmentChi2(float predictedChi2, bool gateEnabled, float maxChi2, + OperationFailureReason& reason) noexcept +{ + if (predictedChi2 < 0.f || (gateEnabled && predictedChi2 > maxChi2)) { + reason = OperationFailureReason::PredictedChi2Failure; + return false; + } + return true; +} + +} // namespace + +bool Propagator::attachMeasurement(SurfaceTrackState& state, const SurfaceDescriptor& targetSurface, + const SurfaceMeasurement& measurement, float bz, + material::MaterialTraversalDirection direction, + bool chi2GateEnabled, float maxChi2, float& chi2, + OperationFailureReason& reason) noexcept +{ + if (!acceptsAttachmentChi2(0.f, chi2GateEnabled, maxChi2, reason)) { + return false; + } + + AttachmentTransaction transaction{state, chi2}; + auto& scratch = transaction.state; + if (!convertKind(scratch, targetSurface.kind, bz, reason)) { + return false; + } + const auto materialBudget = targetSurface.material; + float predictedChi2 = 0.f; + float updateChi2 = 0.f; + const material::IntegratedMaterialBudget integratedMaterial{materialBudget.xOverX0, materialBudget.arealDensityGPerCm2}; + if (scratch.kind == SurfaceKind::Cylinder) { + if (!detail::barrel::rotate(scratch, measurement.frame.frameAngle, reason) || + !detail::barrel::propagate(scratch, measurement.frame.q, bz, reason)) { + return false; + } + const auto materialResult = detail::barrel::correctForMaterial(scratch, integratedMaterial, direction); + if (!materialResult.ok()) { + reason = OperationFailureReason::MaterialFailure; + return false; + } + if (!detail::barrel::predictedChi2(scratch, measurement, predictedChi2, reason)) { + return false; + } + if (!acceptsAttachmentChi2(predictedChi2, chi2GateEnabled, maxChi2, reason)) { + return false; + } + if (!detail::barrel::update(scratch, measurement, updateChi2, reason)) { + return false; + } + } else if (scratch.kind == SurfaceKind::Disk) { + if (!propagateToReference(scratch, measurement.frame.q, bz, reason)) { + return false; + } + const auto materialResult = detail::forward::correctForMaterial(scratch, integratedMaterial, direction); + if (!materialResult.ok()) { + reason = OperationFailureReason::MaterialFailure; + return false; + } + if (!detail::forward::predictedChi2(scratch, measurement, predictedChi2, reason)) { + return false; + } + if (!acceptsAttachmentChi2(predictedChi2, chi2GateEnabled, maxChi2, reason)) { + return false; + } + if (!detail::forward::update(scratch, measurement, updateChi2, reason)) { + return false; + } + } else { + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; + } + transaction.chi2 += updateChi2; + transaction.commit(state, chi2); + return true; +} + +bool Propagator::stateChi2(const SurfaceTrackState& reference, const SurfaceTrackState& candidate, + float& chi2, OperationFailureReason& reason) noexcept +{ + if (reference.kind != candidate.kind) { + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; + } + if (reference.kind == SurfaceKind::Cylinder) { + return detail::barrel::stateChi2(reference, candidate, chi2, reason); + } + if (reference.kind == SurfaceKind::Disk) { + return detail::forward::stateChi2(reference, candidate, chi2, reason); + } + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; +} + +bool Propagator::propagateToReference(SurfaceTrackState& state, float targetReferenceCoordinate, float bz, + OperationFailureReason& reason) noexcept +{ + if (state.kind == SurfaceKind::Cylinder) { + return detail::barrel::propagate(state, targetReferenceCoordinate, bz, reason); + } + if (state.kind == SurfaceKind::Disk) { + return detail::forward::propagate(state, targetReferenceCoordinate, bz, reason); + } + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; +} + +bool Propagator::propagateToReference(SurfaceTrackState& state, SurfaceTrackParameters& linRef, + float targetReferenceCoordinate, float bz, + OperationFailureReason& reason) noexcept +{ + if (state.kind != linRef.kind) { + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; + } + if (state.kind == SurfaceKind::Cylinder) { + return detail::barrel::propagate(state, linRef, targetReferenceCoordinate, bz, reason); + } + if (state.kind == SurfaceKind::Disk) { + return detail::forward::propagate(state, linRef, targetReferenceCoordinate, bz, reason); + } + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; +} + +bool Propagator::convertKind(SurfaceTrackState& state, SurfaceKind targetKind, float bz, + OperationFailureReason& reason) noexcept +{ + if (targetKind != SurfaceKind::Cylinder && targetKind != SurfaceKind::Disk) { + reason = OperationFailureReason::SurfaceKindConversionFailure; + return false; + } + if (state.kind != SurfaceKind::Cylinder && state.kind != SurfaceKind::Disk) { + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; + } + if (state.kind == targetKind) { + return true; + } + auto finiteState = [](const SurfaceTrackState& value) { + if (!std::isfinite(value.referenceCoordinate) || !std::isfinite(value.alpha)) { + return false; + } + for (float parameter : value.parameters) { + if (!std::isfinite(parameter)) { + return false; + } + } + for (float covariance : value.covariance) { + if (!std::isfinite(covariance)) { + return false; + } + } + return true; + }; + if (!std::isfinite(bz) || !finiteState(state)) { + reason = OperationFailureReason::SurfaceKindConversionFailure; + return false; + } + SurfaceTrackState scratch = state; + const bool converted = targetKind == SurfaceKind::Disk ? barrelToForward(scratch, bz, reason) + : forwardToBarrel(scratch, bz, reason); + if (!converted || !finiteState(scratch)) { + reason = OperationFailureReason::SurfaceKindConversionFailure; + return false; + } + state = scratch; + return true; +} + +bool Propagator::propagateToMeasurement(SurfaceTrackState& state, SurfaceTrackParameters& linRef, + const SurfaceDescriptor& targetSurface, const SurfaceMeasurement& targetMeasurement, + float bz, material::MaterialTraversalDirection direction, + bool chi2GateEnabled, float maxChi2, float& chi2, + bool shiftReferenceToMeasurement, OperationFailureReason& reason) noexcept +{ + if (chi2 < 0.f) { + reason = OperationFailureReason::PredictedChi2Failure; + return false; + } + if (!acceptsAttachmentChi2(0.f, chi2GateEnabled, maxChi2, reason)) { + return false; + } + + const SurfaceKind targetKind = targetSurface.kind; + if (targetKind == SurfaceKind::Undefined) { + reason = OperationFailureReason::SurfaceKindConversionFailure; + return false; + } + + AttachmentTransaction transaction{state, chi2}; + auto& scratchState = transaction.state; + SurfaceTrackParameters scratchRef = linRef; + + if (scratchState.kind != targetKind) { + if (!convertKind(scratchState, targetKind, bz, reason)) { + return false; + } + // Changing parameter conventions is also a relinearization boundary. + // The conversion Jacobian is evaluated at scratchState, so begin the + // target-kind propagation from that same point. + scratchRef = SurfaceTrackParameters{scratchState}; + } + + const material::IntegratedMaterialBudget materialBudget{targetSurface.material.xOverX0, targetSurface.material.arealDensityGPerCm2}; + auto& scratchChi2 = transaction.chi2; + float predChi2 = 0.f; + float updateChi2 = 0.f; + + if (targetKind == SurfaceKind::Cylinder) { + if (!detail::barrel::rotate(scratchState, scratchRef, targetMeasurement.frame.frameAngle, bz, reason)) { + return false; + } + if (!detail::barrel::propagate(scratchState, scratchRef, targetMeasurement.frame.q, bz, reason)) { + return false; + } + clampNegligibleCovarianceNoise(scratchState); + const auto materialResult = detail::barrel::correctForMaterial(scratchState, scratchRef, materialBudget, direction); + if (!materialResult.ok()) { + reason = OperationFailureReason::MaterialFailure; + return false; + } + if (!detail::barrel::predictedChi2(scratchState, targetMeasurement, predChi2, reason)) { + return false; + } + } else { + if (!Propagator::propagateToReference(scratchState, scratchRef, targetMeasurement.frame.q, bz, reason)) { + return false; + } + clampNegligibleCovarianceNoise(scratchState); + const auto materialResult = detail::forward::correctForMaterial(scratchState, scratchRef, materialBudget, direction); + if (!materialResult.ok()) { + reason = OperationFailureReason::MaterialFailure; + return false; + } + if (!detail::forward::predictedChi2(scratchState, targetMeasurement, predChi2, reason)) { + return false; + } + } + + if (!acceptsAttachmentChi2(predChi2, chi2GateEnabled, maxChi2, reason)) { + return false; + } + + if (targetKind == SurfaceKind::Cylinder) { + if (!detail::barrel::update(scratchState, targetMeasurement, updateChi2, reason)) { + return false; + } + } else { + if (!detail::forward::update(scratchState, targetMeasurement, updateChi2, reason)) { + return false; + } + } + scratchChi2 += updateChi2; + if (scratchChi2 < 0.f) { + reason = OperationFailureReason::NonFiniteOutput; + return false; + } + + if (shiftReferenceToMeasurement) { + if (targetKind == SurfaceKind::Cylinder) { + if (!detail::barrel::shiftReferenceToMeasurement(scratchRef, targetMeasurement, reason)) { + return false; + } + } else { + if (!detail::forward::shiftReferenceToMeasurement(scratchRef, targetMeasurement, reason)) { + return false; + } + } + } + + transaction.commit(state, chi2); + linRef = scratchRef; + return true; +} + +} // namespace o2::itsmft::tracking diff --git a/Detectors/ITSMFT/common/tracking/src/PropagatorBarrelOperations.cxx b/Detectors/ITSMFT/common/tracking/src/PropagatorBarrelOperations.cxx new file mode 100644 index 0000000000000..68688fde56495 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/PropagatorBarrelOperations.cxx @@ -0,0 +1,727 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSMFTTracking/detail/SurfaceStateOperations.h" + +#include +#include +#include + +#include "CommonConstants/MathConstants.h" +#include "GPUROOTSMatrixFwd.h" +#include + +// Provides covariance/curvature constants for device-visible operations; +// no track object is constructed here. +#include "ReconstructionDataFormats/TrackParametrization.h" + +namespace o2::itsmft::tracking::detail::barrel +{ +namespace +{ + +using DenseMatrix5 = float[5][5]; + +// Packed symmetric 5x5 covariance for stateChi2. MatRepSym::offset() matches +// packedCovarianceIndex exactly, so the combined covariance is built directly +// in packed storage. +using CombinedCovariance = o2::math_utils::SMatrix>; +static_assert(o2::math_utils::MatRepSym::kSize == 15, "packed symmetric 5x5 representation must hold exactly 15 floats"); +static_assert(sizeof(CombinedCovariance) == 15 * sizeof(float), "combined covariance must occupy exactly 15 floats"); + +// sanitizeCovariance() upper bounds in (Y, Z, Snp, Tgl, Q2Pt) order. These +// match the barrel limits used by FamilyMaterialOperations. +constexpr float kBarrelMaxDiagonal[5] = {o2::track::kCY2max, o2::track::kCZ2max, o2::track::kCSnp2max, + o2::track::kCTgl2max, o2::track::kC1Pt2max}; + +bool validateSource(const SurfaceTrackState& state, OperationFailureReason& reason) noexcept +{ + if (state.kind != SurfaceKind::Cylinder) { + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; + } + return true; +} + +void unpackCovariance(const SurfaceTrackState& state, DenseMatrix5& covariance) noexcept +{ + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column < 5; ++column) { + covariance[row][column] = state.covariance[packedCovarianceIndex(row, column)]; + } + } +} + +void packCovariance(const DenseMatrix5& covariance, SurfaceTrackState& state) noexcept +{ + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column <= row; ++column) { + state.covariance[packedCovarianceIndex(row, column)] = covariance[row][column]; + } + } +} + +void identity(DenseMatrix5& matrix) noexcept +{ + for (uint8_t i = 0; i < 5; ++i) { + matrix[i][i] = 1.f; + } +} + +void transportCovariance(SurfaceTrackState& state, const DenseMatrix5& jacobian) noexcept +{ + DenseMatrix5 covariance{}; + DenseMatrix5 product{}; + DenseMatrix5 transported{}; + unpackCovariance(state, covariance); + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column < 5; ++column) { + for (uint8_t inner = 0; inner < 5; ++inner) { + product[row][column] += jacobian[row][inner] * covariance[inner][column]; + } + } + } + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column < 5; ++column) { + for (uint8_t inner = 0; inner < 5; ++inner) { + transported[row][column] += product[row][inner] * jacobian[column][inner]; + } + } + } + packCovariance(transported, state); +} + +// Shared commit point for non-linRef rotate() and propagate(). It validates +// and sanitizes the covariance (ADR 0008) on every exit, including dx == 0. +bool commit(SurfaceTrackState& destination, SurfaceTrackState& scratch) noexcept +{ + sanitizeCovariance(scratch, kBarrelMaxDiagonal); + destination = scratch; + return true; +} + +bool residualInverse(const SurfaceTrackState& state, const SurfaceMeasurement& measurement, + float& inverse00, float& inverse01, float& inverse11, + OperationFailureReason& reason) noexcept +{ + const float s00 = state.covariance[packedCovarianceIndex(0, 0)] + measurement.covariance.uu; + const float s01 = state.covariance[packedCovarianceIndex(1, 0)] + measurement.covariance.uv; + const float s11 = state.covariance[packedCovarianceIndex(1, 1)] + measurement.covariance.vv; + const float determinant = s00 * s11 - s01 * s01; + if (determinant == 0.f) { + reason = OperationFailureReason::InvalidCovariance; + return false; + } + const float inverseDeterminant = 1.f / determinant; + inverse00 = s11 * inverseDeterminant; + inverse01 = -s01 * inverseDeterminant; + inverse11 = s00 * inverseDeterminant; + return true; +} + +} // namespace + +bool rotate(SurfaceTrackState& state, float targetAlpha, OperationFailureReason& reason) noexcept +{ + if (!validateSource(state, reason)) { + return false; + } + SurfaceTrackState scratch = state; + const float canonicalTargetAlpha = std::remainder(targetAlpha, 2.f * o2::constants::math::PI); + const float delta = std::remainder(canonicalTargetAlpha - scratch.alpha, 2.f * o2::constants::math::PI); + const float sine = std::sin(delta); + const float cosine = std::cos(delta); + const float snp = scratch.parameters[2]; + if (std::abs(snp) >= 1.f) { + reason = OperationFailureReason::RotationFailure; + return false; + } + const float csp = std::sqrt((1.f - snp) * (1.f + snp)); + const float rotatedCosine = csp * cosine + snp * sine; + const float rotatedSnp = snp * cosine - csp * sine; + if (rotatedCosine < 0.f || std::abs(rotatedSnp) >= 1.f || csp == 0.f) { + reason = OperationFailureReason::RotationFailure; + return false; + } + const float x = scratch.referenceCoordinate; + const float y = scratch.parameters[0]; + scratch.referenceCoordinate = x * cosine + y * sine; + scratch.parameters[0] = -x * sine + y * cosine; + scratch.parameters[2] = rotatedSnp; + scratch.alpha = canonicalTargetAlpha; + const float ratio = cosine + snp / csp * sine; + scratch.covariance[packedCovarianceIndex(0, 0)] *= cosine * cosine; + scratch.covariance[packedCovarianceIndex(1, 0)] *= cosine; + scratch.covariance[packedCovarianceIndex(2, 0)] *= cosine * ratio; + scratch.covariance[packedCovarianceIndex(2, 1)] *= ratio; + scratch.covariance[packedCovarianceIndex(2, 2)] *= ratio * ratio; + scratch.covariance[packedCovarianceIndex(3, 0)] *= cosine; + scratch.covariance[packedCovarianceIndex(3, 2)] *= ratio; + scratch.covariance[packedCovarianceIndex(4, 0)] *= cosine; + scratch.covariance[packedCovarianceIndex(4, 2)] *= ratio; + return commit(state, scratch); +} + +bool propagate(SurfaceTrackState& state, float targetX, float bz, OperationFailureReason& reason) noexcept +{ + if (!validateSource(state, reason)) { + return false; + } + SurfaceTrackState scratch = state; + const float dx = targetX - scratch.referenceCoordinate; + if (dx == 0.f) { + scratch.referenceCoordinate = targetX; + return commit(state, scratch); + } + const float snp = scratch.parameters[2]; + const float curvature = scratch.absCharge == 0 ? 0.f : scratch.parameters[4] * bz * o2::constants::math::B2C; + const float propagatedSnp = snp + curvature * dx; + if (std::abs(snp) >= 1.f || std::abs(propagatedSnp) >= 1.f) { + reason = OperationFailureReason::UnreachableTarget; + return false; + } + const float csp = std::sqrt((1.f - snp) * (1.f + snp)); + const float propagatedCsp = std::sqrt((1.f - propagatedSnp) * (1.f + propagatedSnp)); + if (csp == 0.f || propagatedCsp == 0.f) { + reason = OperationFailureReason::UnreachableTarget; + return false; + } + const float reciprocalCosines = 1.f / (csp + propagatedCsp); + const float dyOverDx = (snp + propagatedSnp) * reciprocalCosines; + const float x2r = curvature * dx; + const bool arcZ = std::abs(x2r) > 0.05f; + float dz = 0.f; + if (arcZ) { + const float argument = csp * propagatedSnp - propagatedCsp * snp; + if (std::abs(argument) > 1.f || curvature == 0.f) { + reason = OperationFailureReason::PropagationFailure; + return false; + } + float angle = std::asin(argument); + if (snp * snp + propagatedSnp * propagatedSnp > 1.f && snp * propagatedSnp < 0.f) { + angle = propagatedSnp > 0.f ? o2::constants::math::PI - angle : -o2::constants::math::PI - angle; + } + dz = scratch.parameters[3] / curvature * angle; + } else { + dz = dx * (propagatedCsp + propagatedSnp * dyOverDx) * scratch.parameters[3]; + } + scratch.referenceCoordinate = targetX; + scratch.parameters[0] += dx * dyOverDx; + scratch.parameters[1] += dz; + scratch.parameters[2] = propagatedSnp; + + const float propagatedCspInverse = 1.f / propagatedCsp; + const float dxOverCosines = dx * reciprocalCosines; + const float hh = dxOverCosines * propagatedCspInverse * (1.f + csp * propagatedCsp + snp * propagatedSnp); + const float jj = dx * (dyOverDx - propagatedSnp * propagatedCspInverse); + DenseMatrix5 jacobian{}; + identity(jacobian); + jacobian[0][2] = hh / csp; + jacobian[0][4] = hh * dxOverCosines * bz * o2::constants::math::B2C; + jacobian[1][2] = scratch.parameters[3] * (jacobian[0][2] * propagatedSnp + jj); + jacobian[1][3] = dx * (propagatedCsp + propagatedSnp * dyOverDx); + jacobian[1][4] = scratch.parameters[3] * (jacobian[0][4] * propagatedSnp + jj * dx * bz * o2::constants::math::B2C); + jacobian[2][4] = dx * bz * o2::constants::math::B2C; + transportCovariance(scratch, jacobian); + return commit(state, scratch); +} + +bool predictedChi2(const SurfaceTrackState& state, const SurfaceMeasurement& measurement, float& chi2, + OperationFailureReason& reason) noexcept +{ + if (!validateSource(state, reason)) { + return false; + } + float inverse00 = 0.f; + float inverse01 = 0.f; + float inverse11 = 0.f; + if (!residualInverse(state, measurement, inverse00, inverse01, inverse11, reason)) { + return false; + } + const float residualY = measurement.frame.u - state.parameters[0]; + const float residualZ = measurement.frame.v - state.parameters[1]; + const float scratchChi2 = residualY * (inverse00 * residualY + inverse01 * residualZ) + + residualZ * (inverse01 * residualY + inverse11 * residualZ); + chi2 = scratchChi2; + return true; +} + +bool update(SurfaceTrackState& state, const SurfaceMeasurement& measurement, float& chi2, + OperationFailureReason& reason) noexcept +{ + if (!validateSource(state, reason)) { + return false; + } + float inverse00 = 0.f; + float inverse01 = 0.f; + float inverse11 = 0.f; + if (!residualInverse(state, measurement, inverse00, inverse01, inverse11, reason)) { + return false; + } + DenseMatrix5 covariance{}; + DenseMatrix5 josephTransform{}; + DenseMatrix5 transformedCovariance{}; + DenseMatrix5 updatedCovariance{}; + float gain[5][2]{}; + unpackCovariance(state, covariance); + const float residual[2] = {measurement.frame.u - state.parameters[0], measurement.frame.v - state.parameters[1]}; + SurfaceTrackState scratch = state; + for (uint8_t row = 0; row < 5; ++row) { + gain[row][0] = covariance[row][0] * inverse00 + covariance[row][1] * inverse01; + gain[row][1] = covariance[row][0] * inverse01 + covariance[row][1] * inverse11; + scratch.parameters[row] += gain[row][0] * residual[0] + gain[row][1] * residual[1]; + } + + // Joseph covariance update: (I - K H) P (I - K H)^T + K R K^T. + // The surface measurement matrix H selects state parameters 0 and 1. + identity(josephTransform); + for (uint8_t row = 0; row < 5; ++row) { + josephTransform[row][0] -= gain[row][0]; + josephTransform[row][1] -= gain[row][1]; + } + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column < 5; ++column) { + for (uint8_t inner = 0; inner < 5; ++inner) { + transformedCovariance[row][column] += josephTransform[row][inner] * covariance[inner][column]; + } + } + } + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column < 5; ++column) { + for (uint8_t inner = 0; inner < 5; ++inner) { + updatedCovariance[row][column] += transformedCovariance[row][inner] * josephTransform[column][inner]; + } + updatedCovariance[row][column] += + gain[row][0] * (measurement.covariance.uu * gain[column][0] + measurement.covariance.uv * gain[column][1]) + + gain[row][1] * (measurement.covariance.uv * gain[column][0] + measurement.covariance.vv * gain[column][1]); + } + } + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column < row; ++column) { + const float symmetric = 0.5f * (updatedCovariance[row][column] + updatedCovariance[column][row]); + updatedCovariance[row][column] = symmetric; + updatedCovariance[column][row] = symmetric; + } + } + packCovariance(updatedCovariance, scratch); + const float scratchChi2 = residual[0] * (inverse00 * residual[0] + inverse01 * residual[1]) + + residual[1] * (inverse01 * residual[0] + inverse11 * residual[1]); + // Preserve the established covariance bounds after the Joseph update. + sanitizeCovariance(scratch, kBarrelMaxDiagonal); + state = scratch; + chi2 = scratchChi2; + return true; +} + +bool stateChi2(const SurfaceTrackState& reference, const SurfaceTrackState& candidate, float& chi2, + OperationFailureReason& reason) noexcept +{ + if (reference.kind != SurfaceKind::Cylinder || candidate.kind != SurfaceKind::Cylinder) { + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; + } + if (std::abs(reference.alpha - candidate.alpha) > o2::constants::math::Epsilon) { + reason = OperationFailureReason::AlphaMismatch; + return false; + } + if (std::abs(reference.referenceCoordinate - candidate.referenceCoordinate) > o2::constants::math::Epsilon) { + reason = OperationFailureReason::ReferenceCoordinateMismatch; + return false; + } + + CombinedCovariance combined; + float* packed = combined.Array(); + for (uint8_t i = 0; i < 15; ++i) { + packed[i] = reference.covariance[i] + candidate.covariance[i]; + } + if (!combined.Invert()) { + reason = OperationFailureReason::InvalidCovariance; + return false; + } + + float diff[5]; + for (uint8_t i = 0; i < 5; ++i) { + diff[i] = reference.parameters[i] - candidate.parameters[i]; + } + float chi2diag = 0.f; + float chi2ndiag = 0.f; + for (uint8_t i = 0; i < 5; ++i) { + chi2diag += diff[i] * diff[i] * packed[packedCovarianceIndex(i, i)]; + for (uint8_t j = 0; j < i; ++j) { + chi2ndiag += diff[i] * diff[j] * packed[packedCovarianceIndex(i, j)]; + } + } + const float scratchChi2 = chi2diag + 2.f * chi2ndiag; + chi2 = scratchChi2; + return true; +} + +#ifndef GPUCA_GPUCODE + +namespace +{ + +// Covariance-free propagation of SurfaceTrackParameters using the +// TrackParametrization::propagateParamTo formula. stateAbsCharge supplies the +// charge absent from SurfaceTrackParameters; it matches the paired +// state's particle hypothesis. +bool propagateReferenceParams(SurfaceTrackParameters& ref, uint8_t stateAbsCharge, float targetX, float bz, + OperationFailureReason& reason) noexcept +{ + const float dx = targetX - ref.referenceCoordinate; + if (dx == 0.f) { + ref.referenceCoordinate = targetX; + return true; + } + const float snp = ref.parameters[2]; + const float curvature = stateAbsCharge == 0 ? 0.f : ref.parameters[4] * bz * o2::constants::math::B2C; + const float propagatedSnp = snp + curvature * dx; + if (std::abs(snp) >= 1.f || std::abs(propagatedSnp) >= 1.f) { + reason = OperationFailureReason::UnreachableTarget; + return false; + } + const float csp = std::sqrt((1.f - snp) * (1.f + snp)); + const float propagatedCsp = std::sqrt((1.f - propagatedSnp) * (1.f + propagatedSnp)); + if (csp == 0.f || propagatedCsp == 0.f) { + reason = OperationFailureReason::UnreachableTarget; + return false; + } + const float reciprocalCosines = 1.f / (csp + propagatedCsp); + const float dyOverDx = (snp + propagatedSnp) * reciprocalCosines; + const float x2r = curvature * dx; + const bool arcZ = std::abs(x2r) > 0.05f; + float dz = 0.f; + if (arcZ) { + const float argument = csp * propagatedSnp - propagatedCsp * snp; + if (std::abs(argument) > 1.f || curvature == 0.f) { + reason = OperationFailureReason::PropagationFailure; + return false; + } + float angle = std::asin(argument); + if (snp * snp + propagatedSnp * propagatedSnp > 1.f && snp * propagatedSnp < 0.f) { + angle = propagatedSnp > 0.f ? o2::constants::math::PI - angle : -o2::constants::math::PI - angle; + } + dz = ref.parameters[3] / curvature * angle; + } else { + dz = dx * (propagatedCsp + propagatedSnp * dyOverDx) * ref.parameters[3]; + } + ref.referenceCoordinate = targetX; + ref.parameters[0] += dx * dyOverDx; + ref.parameters[1] += dz; + ref.parameters[2] = propagatedSnp; + return true; +} + +} // namespace + +bool rotate(SurfaceTrackState& state, SurfaceTrackParameters& linRef, float targetAlpha, float bz, + OperationFailureReason& reason) noexcept +{ + if (!validateSource(state, reason)) { + return false; + } + if (linRef.kind != SurfaceKind::Cylinder) { + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; + } + // Pairing requires exact referenceCoordinate/alpha equality. Parameters may + // differ because linRef is a linearization reference. + if (state.referenceCoordinate != linRef.referenceCoordinate) { + reason = OperationFailureReason::ReferenceCoordinateMismatch; + return false; + } + if (state.alpha != linRef.alpha) { + reason = OperationFailureReason::AlphaMismatch; + return false; + } + const float stateSnp = state.parameters[2]; + if (std::abs(stateSnp) >= 1.f) { + reason = OperationFailureReason::RotationFailure; + return false; + } + + SurfaceTrackState scratchState = state; + SurfaceTrackParameters scratchRef = linRef; + + const float canonicalAlpha = std::remainder(targetAlpha, 2.f * o2::constants::math::PI); + + // Rotate the reference using its own pre-rotation snp. + const float refSnpBefore = scratchRef.parameters[2]; + if (std::abs(refSnpBefore) >= 1.f) { + reason = OperationFailureReason::RotationFailure; + return false; + } + const float delta = std::remainder(canonicalAlpha - scratchRef.alpha, 2.f * o2::constants::math::PI); + const float sa = std::sin(delta); + const float ca = std::cos(delta); + const float refCsp0 = std::sqrt((1.f - refSnpBefore) * (1.f + refSnpBefore)); + if (refCsp0 * ca + refSnpBefore * sa < 0.f) { + reason = OperationFailureReason::RotationFailure; + return false; + } + const float refSnpRotated = refSnpBefore * ca - refCsp0 * sa; + if (std::abs(refSnpRotated) >= 1.f) { + reason = OperationFailureReason::RotationFailure; + return false; + } + const float refXOld = scratchRef.referenceCoordinate; + const float refYOld = scratchRef.parameters[0]; + scratchRef.alpha = canonicalAlpha; + scratchRef.referenceCoordinate = refXOld * ca + refYOld * sa; + scratchRef.parameters[0] = -refXOld * sa + refYOld * ca; + scratchRef.parameters[2] = refSnpRotated; + + // Rotate the state's pre-rotation X,Y by the reference delta. + const float trackX = scratchState.referenceCoordinate * ca + scratchState.parameters[0] * sa; + + if (!propagateReferenceParams(scratchRef, state.absCharge, trackX, bz, reason)) { + reason = OperationFailureReason::RotationFailure; + return false; + } + + // Rotate the state using its own snp and post-rotation validity. + const float csp = std::sqrt((1.f - stateSnp) * (1.f + stateSnp)); + if (csp * ca + stateSnp * sa < 0.f) { + reason = OperationFailureReason::RotationFailure; + return false; + } + const float updatedSnp = stateSnp * ca - csp * sa; + if (std::abs(updatedSnp) >= 1.f) { + reason = OperationFailureReason::RotationFailure; + return false; + } + const float stateXOld = scratchState.referenceCoordinate; + const float stateYOld = scratchState.parameters[0]; + scratchState.parameters[0] = -stateXOld * sa + stateYOld * ca; + scratchState.referenceCoordinate = trackX; + scratchState.parameters[2] = updatedSnp; + scratchState.alpha = canonicalAlpha; + + // Evaluate the covariance Jacobian at the reference, not the state's snp. + // Compute cspRef1 algebraically to match the legacy formula. + const float cspRef1 = ca * refCsp0 + sa * refSnpBefore; + if (cspRef1 == 0.f) { + reason = OperationFailureReason::RotationFailure; + return false; + } + const float rr = cspRef1 / refCsp0; + + // Compute the extra lower-triangle row before the plane-rotation multiplies, + // matching the legacy evaluation order. + const float cXSigY = scratchState.covariance[packedCovarianceIndex(0, 0)] * ca * sa; + const float cXSigZ = scratchState.covariance[packedCovarianceIndex(1, 0)] * sa; + const float cXSigSnp = scratchState.covariance[packedCovarianceIndex(2, 0)] * rr * sa; + const float cXSigTgl = scratchState.covariance[packedCovarianceIndex(3, 0)] * sa; + const float cXSigQ2Pt = scratchState.covariance[packedCovarianceIndex(4, 0)] * sa; + const float cSigX2 = scratchState.covariance[packedCovarianceIndex(0, 0)] * sa * sa; + + scratchState.covariance[packedCovarianceIndex(0, 0)] *= ca * ca; + scratchState.covariance[packedCovarianceIndex(1, 0)] *= ca; + scratchState.covariance[packedCovarianceIndex(2, 0)] *= ca * rr; + scratchState.covariance[packedCovarianceIndex(2, 1)] *= rr; + scratchState.covariance[packedCovarianceIndex(2, 2)] *= rr * rr; + scratchState.covariance[packedCovarianceIndex(3, 0)] *= ca; + scratchState.covariance[packedCovarianceIndex(3, 2)] *= rr; + scratchState.covariance[packedCovarianceIndex(4, 0)] *= ca; + scratchState.covariance[packedCovarianceIndex(4, 2)] *= rr; + + const float cspRef1Inv = 1.f / cspRef1; + const float j3 = -refSnpRotated * cspRef1Inv; + const float j4 = -scratchRef.parameters[3] * cspRef1Inv; + const float j5 = state.absCharge != 0 ? scratchRef.parameters[4] * bz * o2::constants::math::B2C : 0.f; + + const float hXSigY = cXSigY + cSigX2 * j3; + const float hXSigZ = cXSigZ + cSigX2 * j4; + const float hXSigSnp = cXSigSnp + cSigX2 * j5; + + scratchState.covariance[packedCovarianceIndex(0, 0)] += j3 * (cXSigY + hXSigY); + scratchState.covariance[packedCovarianceIndex(1, 1)] += j4 * (cXSigZ + hXSigZ); + scratchState.covariance[packedCovarianceIndex(2, 0)] += cXSigSnp * j3 + hXSigY * j5; + scratchState.covariance[packedCovarianceIndex(2, 2)] += j5 * (cXSigSnp + hXSigSnp); + scratchState.covariance[packedCovarianceIndex(3, 1)] += cXSigTgl * j4; + scratchState.covariance[packedCovarianceIndex(4, 0)] += cXSigQ2Pt * j3; + scratchState.covariance[packedCovarianceIndex(4, 2)] += cXSigQ2Pt * j5; + + scratchState.covariance[packedCovarianceIndex(1, 0)] += cXSigZ * j3 + hXSigY * j4; + scratchState.covariance[packedCovarianceIndex(2, 1)] += cXSigSnp * j4 + hXSigZ * j5; + scratchState.covariance[packedCovarianceIndex(3, 0)] += cXSigTgl * j3; + scratchState.covariance[packedCovarianceIndex(3, 2)] += cXSigTgl * j5; + scratchState.covariance[packedCovarianceIndex(4, 1)] += cXSigQ2Pt * j4; + + sanitizeCovariance(scratchState, kBarrelMaxDiagonal); + state = scratchState; + linRef = scratchRef; + return true; +} + +bool propagate(SurfaceTrackState& state, SurfaceTrackParameters& linRef, float targetX, float bz, + OperationFailureReason& reason) noexcept +{ + if (!validateSource(state, reason)) { + return false; + } + if (linRef.kind != SurfaceKind::Cylinder) { + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; + } + // Pairing requires exact referenceCoordinate/alpha equality; parameters may + // differ. + if (state.referenceCoordinate != linRef.referenceCoordinate) { + reason = OperationFailureReason::ReferenceCoordinateMismatch; + return false; + } + if (state.alpha != linRef.alpha) { + reason = OperationFailureReason::AlphaMismatch; + return false; + } + + const float effectiveBz = state.absCharge == 0 ? 0.f : bz; + const float dx = targetX - state.referenceCoordinate; + if (std::abs(dx) < o2::constants::math::Almost0) { + SurfaceTrackState scratchState = state; + SurfaceTrackParameters scratchRef = linRef; + scratchState.referenceCoordinate = targetX; + scratchRef.referenceCoordinate = targetX; + state = scratchState; + linRef = scratchRef; + return true; + } + + SurfaceTrackParameters scratchRef = linRef; + const float snpRef0 = scratchRef.parameters[2]; + const float cspRef0 = std::sqrt((1.f - snpRef0) * (1.f + snpRef0)); + const float tglRef0 = scratchRef.parameters[3]; + + if (!propagateReferenceParams(scratchRef, state.absCharge, targetX, effectiveBz, reason)) { + return false; + } + const float snpRef1 = scratchRef.parameters[2]; + const float cspRef1 = std::sqrt((1.f - snpRef1) * (1.f + snpRef1)); + if (cspRef0 == 0.f || cspRef1 == 0.f) { + reason = OperationFailureReason::PropagationFailure; + return false; + } + + const float kb = effectiveBz * o2::constants::math::B2C; + const float cspRef0Inv = 1.f / cspRef0; + const float cspRef1Inv = 1.f / cspRef1; + const float cc = cspRef0 + cspRef1; + const float ccInv = 1.f / cc; + const float dy2dx = (snpRef0 + snpRef1) * ccInv; + const float dxccInv = dx * ccInv; + const float hh = dxccInv * cspRef1Inv * (1.f + cspRef0 * cspRef1 + snpRef0 * snpRef1); + const float jj = dx * (dy2dx - snpRef1 * cspRef1Inv); + + const float f02 = hh * cspRef0Inv; + const float f04 = hh * dxccInv * kb; + const float f24 = dx * kb; + const float f12 = tglRef0 * (f02 * snpRef1 + jj); + const float f13 = dx * (cspRef1 + snpRef1 * dy2dx); + const float f14 = tglRef0 * (f04 * snpRef1 + jj * f24); + + float diff[5]; + for (uint8_t i = 0; i < 5; ++i) { + diff[i] = state.parameters[i] - linRef.parameters[i]; + } + const float snpUpd = snpRef1 + diff[2] + f24 * diff[4]; + if (std::abs(snpUpd) >= 1.f) { + reason = OperationFailureReason::PropagationFailure; + return false; + } + + SurfaceTrackState scratchState = state; + scratchState.referenceCoordinate = targetX; + scratchState.parameters[0] = scratchRef.parameters[0] + diff[0] + f02 * diff[2] + f04 * diff[4]; + scratchState.parameters[1] = scratchRef.parameters[1] + diff[1] + f13 * diff[3] + f14 * diff[4]; + scratchState.parameters[2] = snpUpd; + scratchState.parameters[3] = scratchRef.parameters[3] + diff[3]; + scratchState.parameters[4] = scratchRef.parameters[4] + diff[4]; + + const float c00 = state.covariance[packedCovarianceIndex(0, 0)]; + const float c10 = state.covariance[packedCovarianceIndex(1, 0)]; + const float c11 = state.covariance[packedCovarianceIndex(1, 1)]; + const float c20 = state.covariance[packedCovarianceIndex(2, 0)]; + const float c21 = state.covariance[packedCovarianceIndex(2, 1)]; + const float c22 = state.covariance[packedCovarianceIndex(2, 2)]; + const float c30 = state.covariance[packedCovarianceIndex(3, 0)]; + const float c31 = state.covariance[packedCovarianceIndex(3, 1)]; + const float c32 = state.covariance[packedCovarianceIndex(3, 2)]; + const float c33 = state.covariance[packedCovarianceIndex(3, 3)]; + const float c40 = state.covariance[packedCovarianceIndex(4, 0)]; + const float c41 = state.covariance[packedCovarianceIndex(4, 1)]; + const float c42 = state.covariance[packedCovarianceIndex(4, 2)]; + const float c43 = state.covariance[packedCovarianceIndex(4, 3)]; + const float c44 = state.covariance[packedCovarianceIndex(4, 4)]; + + const float b00 = f02 * c20 + f04 * c40; + const float b01 = f12 * c20 + f14 * c40 + f13 * c30; + const float b02 = f24 * c40; + const float b10 = f02 * c21 + f04 * c41; + const float b11 = f12 * c21 + f14 * c41 + f13 * c31; + const float b12 = f24 * c41; + const float b20 = f02 * c22 + f04 * c42; + const float b21 = f12 * c22 + f14 * c42 + f13 * c32; + const float b22 = f24 * c42; + const float b40 = f02 * c42 + f04 * c44; + const float b41 = f12 * c42 + f14 * c44 + f13 * c43; + const float b42 = f24 * c44; + const float b30 = f02 * c32 + f04 * c43; + const float b31 = f12 * c32 + f14 * c43 + f13 * c33; + const float b32 = f24 * c43; + + const float a00 = f02 * b20 + f04 * b40; + const float a01 = f02 * b21 + f04 * b41; + const float a02 = f02 * b22 + f04 * b42; + const float a11 = f12 * b21 + f14 * b41 + f13 * b31; + const float a12 = f12 * b22 + f14 * b42 + f13 * b32; + const float a22 = f24 * b42; + + scratchState.covariance[packedCovarianceIndex(0, 0)] = c00 + b00 + b00 + a00; + scratchState.covariance[packedCovarianceIndex(1, 0)] = c10 + b10 + b01 + a01; + scratchState.covariance[packedCovarianceIndex(2, 0)] = c20 + b20 + b02 + a02; + scratchState.covariance[packedCovarianceIndex(3, 0)] = c30 + b30; + scratchState.covariance[packedCovarianceIndex(4, 0)] = c40 + b40; + scratchState.covariance[packedCovarianceIndex(1, 1)] = c11 + b11 + b11 + a11; + scratchState.covariance[packedCovarianceIndex(2, 1)] = c21 + b21 + b12 + a12; + scratchState.covariance[packedCovarianceIndex(3, 1)] = c31 + b31; + scratchState.covariance[packedCovarianceIndex(4, 1)] = c41 + b41; + scratchState.covariance[packedCovarianceIndex(2, 2)] = c22 + b22 + b22 + a22; + scratchState.covariance[packedCovarianceIndex(3, 2)] = c32 + b32; + scratchState.covariance[packedCovarianceIndex(4, 2)] = c42 + b42; + scratchState.covariance[packedCovarianceIndex(3, 3)] = c33; + scratchState.covariance[packedCovarianceIndex(4, 3)] = c43; + scratchState.covariance[packedCovarianceIndex(4, 4)] = c44; + + // A large Jacobian step can invalidate covariance through an off-diagonal + // term even when all diagonals look valid. Sanitize before committing. + sanitizeCovariance(scratchState, kBarrelMaxDiagonal); + state = scratchState; + linRef = scratchRef; + return true; +} + +bool shiftReferenceToMeasurement(SurfaceTrackParameters& linRef, const SurfaceMeasurement& measurement, + OperationFailureReason& reason) noexcept +{ + if (linRef.kind != SurfaceKind::Cylinder) { + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; + } + SurfaceTrackParameters scratch = linRef; + scratch.parameters[0] = measurement.frame.u; + scratch.parameters[1] = measurement.frame.v; + linRef = scratch; + return true; +} + +#endif // GPUCA_GPUCODE + +} // namespace o2::itsmft::tracking::detail::barrel diff --git a/Detectors/ITSMFT/common/tracking/src/PropagatorForwardOperations.cxx b/Detectors/ITSMFT/common/tracking/src/PropagatorForwardOperations.cxx new file mode 100644 index 0000000000000..4760b9af91990 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/PropagatorForwardOperations.cxx @@ -0,0 +1,614 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSMFTTracking/detail/SurfaceStateOperations.h" + +#include +#include +#include +#include + +#include "CommonConstants/MathConstants.h" +#include "GPUROOTSMatrixFwd.h" +#include + +namespace o2::itsmft::tracking::detail::forward +{ +namespace +{ + +using DenseMatrix5 = float[5][5]; + +// Packed symmetric 5x5 covariance for stateChi2. MatRepSym::offset matches +// packedCovarianceIndex (row*(row+1)/2+column), enabling direct construction. +using CombinedCovariance = o2::math_utils::SMatrix>; +static_assert(o2::math_utils::MatRepSym::kSize == 15, "packed symmetric 5x5 representation must hold exactly 15 floats"); +static_assert(sizeof(CombinedCovariance) == 15 * sizeof(float), "combined covariance must occupy exactly 15 floats"); + +// Forward diagonals have no finite ceiling; non-negativity and correlations +// are still checked. +constexpr float kForwardNoRangeLimit = std::numeric_limits::max(); +constexpr float kForwardMaxDiagonal[5] = {kForwardNoRangeLimit, kForwardNoRangeLimit, kForwardNoRangeLimit, + kForwardNoRangeLimit, kForwardNoRangeLimit}; + +void unpackCovariance(const SurfaceTrackState& state, DenseMatrix5& covariance) noexcept +{ + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column < 5; ++column) { + covariance[row][column] = state.covariance[packedCovarianceIndex(row, column)]; + } + } +} + +void packCovariance(const DenseMatrix5& covariance, SurfaceTrackState& state) noexcept +{ + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column <= row; ++column) { + state.covariance[packedCovarianceIndex(row, column)] = covariance[row][column]; + } + } +} + +void transportCovariance(SurfaceTrackState& state, const DenseMatrix5& jacobian) noexcept +{ + DenseMatrix5 covariance{}; + DenseMatrix5 product{}; + DenseMatrix5 transported{}; + unpackCovariance(state, covariance); + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column < 5; ++column) { + for (uint8_t inner = 0; inner < 5; ++inner) { + product[row][column] += jacobian[row][inner] * covariance[inner][column]; + } + } + } + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column < 5; ++column) { + for (uint8_t inner = 0; inner < 5; ++inner) { + transported[row][column] += product[row][inner] * jacobian[column][inner]; + } + } + } + packCovariance(transported, state); +} + +void identity(DenseMatrix5& matrix) noexcept +{ + for (uint8_t i = 0; i < 5; ++i) { + matrix[i][i] = 1.f; + } +} + +bool validateSource(const SurfaceTrackState& state, OperationFailureReason& reason) noexcept +{ + if (state.kind != SurfaceKind::Disk) { + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; + } + return true; +} + +// Sanitize covariance once, at the propagation commit point. +bool commitPropagation(SurfaceTrackState& destination, SurfaceTrackState& scratch) noexcept +{ + sanitizeCovariance(scratch, kForwardMaxDiagonal); + destination = scratch; + return true; +} + +bool propagateLinear(SurfaceTrackState& state, float targetZ, OperationFailureReason& reason) noexcept +{ + const float dz = targetZ - state.referenceCoordinate; + const float tanl = state.parameters[3]; + if (tanl == 0.f && dz != 0.f) { + reason = OperationFailureReason::UnreachableTarget; + return false; + } + if (dz == 0.f) { + return true; + } + const float inverseTanl = 1.f / tanl; + const float n = dz * inverseTanl; + const float m = n * inverseTanl; + const float sinPhi = std::sin(state.parameters[2]); + const float cosPhi = std::cos(state.parameters[2]); + state.parameters[0] += n * cosPhi; + state.parameters[1] += n * sinPhi; + state.referenceCoordinate = targetZ; + + DenseMatrix5 jacobian{}; + identity(jacobian); + jacobian[0][2] = -n * sinPhi; + jacobian[0][3] = -m * cosPhi; + jacobian[1][2] = n * cosPhi; + jacobian[1][3] = -m * sinPhi; + transportCovariance(state, jacobian); + return true; +} + +bool propagateHelixParameters(SurfaceTrackState& state, float targetZ, float bz, + OperationFailureReason& reason) noexcept +{ + const float dz = targetZ - state.referenceCoordinate; + if (dz == 0.f) { + return true; + } + const float tanl = state.parameters[3]; + const float inverseQPt = state.parameters[4]; + if (tanl == 0.f) { + reason = OperationFailureReason::UnreachableTarget; + return false; + } + if (bz == 0.f || inverseQPt == 0.f) { + reason = OperationFailureReason::PropagationFailure; + return false; + } + const float inverseTanl = 1.f / tanl; + const float qPt = 1.f / inverseQPt; + const float phi = state.parameters[2]; + const float sinPhi = std::sin(phi); + const float cosPhi = std::cos(phi); + const float k = std::abs(o2::constants::math::B2C * bz); + const float inverseK = 1.f / k; + const float theta = -inverseQPt * dz * k * inverseTanl; + const float sinTheta = std::sin(theta); + const float cosTheta = std::cos(theta); + const float fieldSign = std::copysign(1.f, bz); + const float y = sinPhi * qPt * inverseK; + const float x = cosPhi * qPt * inverseK; + state.parameters[0] += fieldSign * (y - y * cosTheta) - x * sinTheta; + state.parameters[1] += fieldSign * (-x + x * cosTheta) - y * sinTheta; + state.parameters[2] += fieldSign * theta; + state.referenceCoordinate = targetZ; + return true; +} + +bool propagateHelix(SurfaceTrackState& state, float targetZ, float bz, + OperationFailureReason& reason) noexcept +{ + const float originalZ = state.referenceCoordinate; + const float dz = targetZ - originalZ; + if (dz == 0.f) { + return true; + } + const float phi = state.parameters[2]; + const float tanl = state.parameters[3]; + const float inverseQPt = state.parameters[4]; + if (!propagateHelixParameters(state, targetZ, bz, reason)) { + return false; + } + const float inverseTanl = 1.f / tanl; + const float qPt = 1.f / inverseQPt; + const float sinPhi = std::sin(phi); + const float cosPhi = std::cos(phi); + const float k = std::abs(o2::constants::math::B2C * bz); + const float inverseK = 1.f / k; + const float theta = -inverseQPt * dz * k * inverseTanl; + const float sinTheta = std::sin(theta); + const float cosTheta = std::cos(theta); + const float fieldSign = std::copysign(1.f, bz); + const float n = dz * inverseTanl; + const float m = n * inverseTanl; + const float o = sinTheta * cosPhi; + const float p = sinPhi * cosTheta; + const float r = sinPhi * sinTheta; + const float s = cosPhi * cosTheta; + const float y = sinPhi * qPt * inverseK; + const float x = cosPhi * qPt * inverseK; + const float t = qPt * cosTheta; + const float u = qPt * sinTheta; + const float v = qPt; + const float nn = dz * inverseTanl * qPt; + + DenseMatrix5 jacobian{}; + identity(jacobian); + jacobian[0][2] = fieldSign * x - fieldSign * x * cosTheta + y * sinTheta; + jacobian[0][3] = fieldSign * r * m - s * m; + jacobian[0][4] = -fieldSign * nn * r + fieldSign * t * y - fieldSign * v * y + nn * s + u * x; + jacobian[1][2] = fieldSign * y - fieldSign * y * cosTheta - x * sinTheta; + jacobian[1][3] = -fieldSign * o * m - p * m; + jacobian[1][4] = fieldSign * nn * o - fieldSign * t * x + fieldSign * v * x + nn * p + u * y; + jacobian[2][3] = -fieldSign * theta * inverseTanl; + jacobian[2][4] = -fieldSign * k * n; + transportCovariance(state, jacobian); + return true; +} + +bool propagateAccepted(SurfaceTrackState& destination, float targetZ, float bz, + OperationFailureReason& reason) noexcept +{ + if (!validateSource(destination, reason)) { + return false; + } + SurfaceTrackState scratch = destination; + const bool success = std::abs(bz) > 0.01f ? propagateHelix(scratch, targetZ, bz, reason) + : propagateLinear(scratch, targetZ, reason); + return success && commitPropagation(destination, scratch); +} + +bool residualInverse(const SurfaceTrackState& state, const SurfaceMeasurement& measurement, + float& inverse00, float& inverse01, float& inverse11, + OperationFailureReason& reason) noexcept +{ + const float s00 = state.covariance[packedCovarianceIndex(0, 0)] + measurement.covariance.uu; + const float s01 = state.covariance[packedCovarianceIndex(1, 0)] + measurement.covariance.uv; + const float s11 = state.covariance[packedCovarianceIndex(1, 1)] + measurement.covariance.vv; + const float determinant = s00 * s11 - s01 * s01; + if (determinant == 0.f) { + reason = OperationFailureReason::InvalidCovariance; + return false; + } + const float inverseDeterminant = 1.f / determinant; + inverse00 = s11 * inverseDeterminant; + inverse01 = -s01 * inverseDeterminant; + inverse11 = s00 * inverseDeterminant; + return true; +} + +} // namespace + +bool predictedChi2(const SurfaceTrackState& state, const SurfaceMeasurement& measurement, float& chi2, + OperationFailureReason& reason) noexcept +{ + if (!validateSource(state, reason)) { + return false; + } + float inverse00 = 0.f; + float inverse01 = 0.f; + float inverse11 = 0.f; + if (!residualInverse(state, measurement, inverse00, inverse01, inverse11, reason)) { + return false; + } + const float residualX = measurement.frame.u - state.parameters[0]; + const float residualY = measurement.frame.v - state.parameters[1]; + const float scratchChi2 = residualX * (inverse00 * residualX + inverse01 * residualY) + + residualY * (inverse01 * residualX + inverse11 * residualY); + chi2 = scratchChi2; + return true; +} + +bool update(SurfaceTrackState& state, const SurfaceMeasurement& measurement, float& chi2, + OperationFailureReason& reason) noexcept +{ + if (!validateSource(state, reason)) { + return false; + } + float inverse00 = 0.f; + float inverse01 = 0.f; + float inverse11 = 0.f; + if (!residualInverse(state, measurement, inverse00, inverse01, inverse11, reason)) { + return false; + } + + DenseMatrix5 covariance{}; + DenseMatrix5 josephTransform{}; + DenseMatrix5 transformedCovariance{}; + DenseMatrix5 updatedCovariance{}; + float gain[5][2]{}; + unpackCovariance(state, covariance); + const float residual[2] = {measurement.frame.u - state.parameters[0], measurement.frame.v - state.parameters[1]}; + SurfaceTrackState scratch = state; + for (uint8_t row = 0; row < 5; ++row) { + gain[row][0] = covariance[row][0] * inverse00 + covariance[row][1] * inverse01; + gain[row][1] = covariance[row][0] * inverse01 + covariance[row][1] * inverse11; + scratch.parameters[row] += gain[row][0] * residual[0] + gain[row][1] * residual[1]; + } + + // Joseph covariance update: (I - K H) P (I - K H)^T + K R K^T. + // The surface measurement matrix H selects state parameters 0 and 1. + identity(josephTransform); + for (uint8_t row = 0; row < 5; ++row) { + josephTransform[row][0] -= gain[row][0]; + josephTransform[row][1] -= gain[row][1]; + } + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column < 5; ++column) { + for (uint8_t inner = 0; inner < 5; ++inner) { + transformedCovariance[row][column] += josephTransform[row][inner] * covariance[inner][column]; + } + } + } + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column < 5; ++column) { + for (uint8_t inner = 0; inner < 5; ++inner) { + updatedCovariance[row][column] += transformedCovariance[row][inner] * josephTransform[column][inner]; + } + updatedCovariance[row][column] += + gain[row][0] * (measurement.covariance.uu * gain[column][0] + measurement.covariance.uv * gain[column][1]) + + gain[row][1] * (measurement.covariance.uv * gain[column][0] + measurement.covariance.vv * gain[column][1]); + } + } + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column < row; ++column) { + const float symmetric = 0.5f * (updatedCovariance[row][column] + updatedCovariance[column][row]); + updatedCovariance[row][column] = symmetric; + updatedCovariance[column][row] = symmetric; + } + } + packCovariance(updatedCovariance, scratch); + const float scratchChi2 = residual[0] * (inverse00 * residual[0] + inverse01 * residual[1]) + + residual[1] * (inverse01 * residual[0] + inverse11 * residual[1]); + // Preserve the established covariance bounds after the Joseph update. + sanitizeCovariance(scratch, kForwardMaxDiagonal); + state = scratch; + chi2 = scratchChi2; + return true; +} + +bool correctForMaterial(SurfaceTrackState& state, float xOverX0, OperationFailureReason& reason) noexcept +{ + if (!validateSource(state, reason)) { + return false; + } + if (xOverX0 == 0.f) { + return true; + } + const float tanl = state.parameters[3]; + if (tanl == 0.f) { + reason = OperationFailureReason::MaterialFailure; + return false; + } + const float inverseQPt = state.parameters[4]; + const float onePlusTanl2 = 1.f + tanl * tanl; + const float inverseMomentum = std::abs(inverseQPt) / std::sqrt(onePlusTanl2); + const float pathLengthOverX0 = xOverX0 * std::abs(std::sqrt(onePlusTanl2) / tanl); + const float theta2 = highlandTheta2(inverseMomentum, pathLengthOverX0); + SurfaceTrackState scratch = state; + scratch.covariance[packedCovarianceIndex(2, 2)] += theta2 * onePlusTanl2; + scratch.covariance[packedCovarianceIndex(3, 3)] += theta2 * onePlusTanl2 * onePlusTanl2; + scratch.covariance[packedCovarianceIndex(4, 4)] += theta2 * tanl * tanl * inverseQPt * inverseQPt; + state = scratch; + return true; +} + +bool stateChi2(const SurfaceTrackState& reference, const SurfaceTrackState& candidate, float& chi2, + OperationFailureReason& reason) noexcept +{ + if (reference.kind != SurfaceKind::Disk || candidate.kind != SurfaceKind::Disk) { + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; + } + if (std::abs(reference.referenceCoordinate - candidate.referenceCoordinate) > o2::constants::math::Epsilon) { + reason = OperationFailureReason::ReferenceCoordinateMismatch; + return false; + } + + CombinedCovariance combined; + float* packed = combined.Array(); + for (uint8_t i = 0; i < 15; ++i) { + packed[i] = reference.covariance[i] + candidate.covariance[i]; + } + if (!combined.Invert()) { + reason = OperationFailureReason::InvalidCovariance; + return false; + } + + // Use direct (unwrapped) differences of (X, Y, Phi, Tanl, InvQPt). + float diff[5]; + for (uint8_t i = 0; i < 5; ++i) { + diff[i] = reference.parameters[i] - candidate.parameters[i]; + } + float chi2diag = 0.f; + float chi2ndiag = 0.f; + for (uint8_t i = 0; i < 5; ++i) { + chi2diag += diff[i] * diff[i] * packed[packedCovarianceIndex(i, i)]; + for (uint8_t j = 0; j < i; ++j) { + chi2ndiag += diff[i] * diff[j] * packed[packedCovarianceIndex(i, j)]; + } + } + const float scratchChi2 = chi2diag + 2.f * chi2ndiag; + chi2 = scratchChi2; + return true; +} + +#ifndef GPUCA_GPUCODE + +namespace +{ + +// Reference-only position update with the Jacobian at the original parameters. +bool referencePropagateLinear(SurfaceTrackParameters& ref, float targetZ, DenseMatrix5& jacobian, + OperationFailureReason& reason) noexcept +{ + identity(jacobian); + const float dz = targetZ - ref.referenceCoordinate; + const float tanl = ref.parameters[3]; + if (tanl == 0.f && dz != 0.f) { + reason = OperationFailureReason::UnreachableTarget; + return false; + } + if (dz == 0.f) { + return true; + } + const float inverseTanl = 1.f / tanl; + const float n = dz * inverseTanl; + const float m = n * inverseTanl; + const float sinPhi = std::sin(ref.parameters[2]); + const float cosPhi = std::cos(ref.parameters[2]); + ref.parameters[0] += n * cosPhi; + ref.parameters[1] += n * sinPhi; + ref.referenceCoordinate = targetZ; + + jacobian[0][2] = -n * sinPhi; + jacobian[0][3] = -m * cosPhi; + jacobian[1][2] = n * cosPhi; + jacobian[1][3] = -m * sinPhi; + return true; +} + +// Position-only helix step, matching propagateHelixParameters. +bool referencePropagateHelixParameters(SurfaceTrackParameters& ref, float targetZ, float bz, + OperationFailureReason& reason) noexcept +{ + const float dz = targetZ - ref.referenceCoordinate; + if (dz == 0.f) { + return true; + } + const float tanl = ref.parameters[3]; + const float inverseQPt = ref.parameters[4]; + if (tanl == 0.f) { + reason = OperationFailureReason::UnreachableTarget; + return false; + } + if (bz == 0.f || inverseQPt == 0.f) { + reason = OperationFailureReason::PropagationFailure; + return false; + } + const float inverseTanl = 1.f / tanl; + const float qPt = 1.f / inverseQPt; + const float phi = ref.parameters[2]; + const float sinPhi = std::sin(phi); + const float cosPhi = std::cos(phi); + const float k = std::abs(o2::constants::math::B2C * bz); + const float inverseK = 1.f / k; + const float theta = -inverseQPt * dz * k * inverseTanl; + const float sinTheta = std::sin(theta); + const float cosTheta = std::cos(theta); + const float fieldSign = std::copysign(1.f, bz); + const float y = sinPhi * qPt * inverseK; + const float x = cosPhi * qPt * inverseK; + ref.parameters[0] += fieldSign * (y - y * cosTheta) - x * sinTheta; + ref.parameters[1] += fieldSign * (-x + x * cosTheta) - y * sinTheta; + ref.parameters[2] += fieldSign * theta; + ref.referenceCoordinate = targetZ; + return true; +} + +bool referencePropagateHelix(SurfaceTrackParameters& ref, float targetZ, float bz, DenseMatrix5& jacobian, + OperationFailureReason& reason) noexcept +{ + identity(jacobian); + const float originalZ = ref.referenceCoordinate; + const float dz = targetZ - originalZ; + if (dz == 0.f) { + return true; + } + const float phi = ref.parameters[2]; + const float tanl = ref.parameters[3]; + const float inverseQPt = ref.parameters[4]; + if (!referencePropagateHelixParameters(ref, targetZ, bz, reason)) { + return false; + } + const float inverseTanl = 1.f / tanl; + const float qPt = 1.f / inverseQPt; + const float sinPhi = std::sin(phi); + const float cosPhi = std::cos(phi); + const float k = std::abs(o2::constants::math::B2C * bz); + const float inverseK = 1.f / k; + const float theta = -inverseQPt * dz * k * inverseTanl; + const float sinTheta = std::sin(theta); + const float cosTheta = std::cos(theta); + const float fieldSign = std::copysign(1.f, bz); + const float n = dz * inverseTanl; + const float m = n * inverseTanl; + const float o = sinTheta * cosPhi; + const float p = sinPhi * cosTheta; + const float r = sinPhi * sinTheta; + const float s = cosPhi * cosTheta; + const float y = sinPhi * qPt * inverseK; + const float x = cosPhi * qPt * inverseK; + const float t = qPt * cosTheta; + const float u = qPt * sinTheta; + const float v = qPt; + const float nn = dz * inverseTanl * qPt; + + jacobian[0][2] = fieldSign * x - fieldSign * x * cosTheta + y * sinTheta; + jacobian[0][3] = fieldSign * r * m - s * m; + jacobian[0][4] = -fieldSign * nn * r + fieldSign * t * y - fieldSign * v * y + nn * s + u * x; + jacobian[1][2] = fieldSign * y - fieldSign * y * cosTheta - x * sinTheta; + jacobian[1][3] = -fieldSign * o * m - p * m; + jacobian[1][4] = fieldSign * nn * o - fieldSign * t * x + fieldSign * v * x + nn * p + u * y; + jacobian[2][3] = -fieldSign * theta * inverseTanl; + jacobian[2][4] = -fieldSign * k * n; + return true; +} + +bool propagateAccepted(SurfaceTrackState& state, SurfaceTrackParameters& linRef, float targetZ, float bz, + OperationFailureReason& reason) noexcept +{ + if (!validateSource(state, reason)) { + return false; + } + if (linRef.kind != SurfaceKind::Disk) { + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; + } + // The fitted state and linearization reference must share the exact anchor; + // their parameters may differ. Forward alpha is always 0/unused. + if (state.referenceCoordinate != linRef.referenceCoordinate) { + reason = OperationFailureReason::ReferenceCoordinateMismatch; + return false; + } + + SurfaceTrackParameters scratchRef = linRef; + DenseMatrix5 jacobian{}; + const bool ok = std::abs(bz) > 0.01f ? referencePropagateHelix(scratchRef, targetZ, bz, jacobian, reason) + : referencePropagateLinear(scratchRef, targetZ, jacobian, reason); + if (!ok) { + return false; + } + + float diff[5]; + for (uint8_t i = 0; i < 5; ++i) { + diff[i] = state.parameters[i] - linRef.parameters[i]; + } + + SurfaceTrackState scratchState = state; + scratchState.referenceCoordinate = targetZ; + for (uint8_t row = 0; row < 5; ++row) { + float value = scratchRef.parameters[row]; + for (uint8_t column = 0; column < 5; ++column) { + value += jacobian[row][column] * diff[column]; + } + scratchState.parameters[row] = value; + } + transportCovariance(scratchState, jacobian); + + // ADR 0008: a large Jacobian step can break positive semidefiniteness via + // an off-diagonal term even when diagonals look valid. Sanitize before the + // next operation receives the covariance. + sanitizeCovariance(scratchState, kForwardMaxDiagonal); + state = scratchState; + linRef = scratchRef; + return true; +} + +} // namespace + +bool shiftReferenceToMeasurement(SurfaceTrackParameters& linRef, const SurfaceMeasurement& measurement, + OperationFailureReason& reason) noexcept +{ + if (linRef.kind != SurfaceKind::Disk) { + reason = OperationFailureReason::SourceSurfaceKindMismatch; + return false; + } + SurfaceTrackParameters scratch = linRef; + scratch.parameters[0] = measurement.frame.u; + scratch.parameters[1] = measurement.frame.v; + linRef = scratch; + return true; +} + +#endif // GPUCA_GPUCODE + +bool propagate(SurfaceTrackState& state, float targetZ, float bz, + OperationFailureReason& reason) noexcept +{ + return propagateAccepted(state, targetZ, bz, reason); +} + +bool propagate(SurfaceTrackState& state, SurfaceTrackParameters& linRef, + float targetZ, float bz, OperationFailureReason& reason) noexcept +{ + return propagateAccepted(state, linRef, targetZ, bz, reason); +} + +} // namespace o2::itsmft::tracking::detail::forward diff --git a/Detectors/ITSMFT/common/tracking/src/TimeFrame.cxx b/Detectors/ITSMFT/common/tracking/src/TimeFrame.cxx new file mode 100644 index 0000000000000..838c60e2b0064 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/TimeFrame.cxx @@ -0,0 +1,482 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file TimeFrame.cxx +/// \brief +/// + +#include "ITSMFTTracking/TimeFrame.h" +#include +#include +#include +#include + +#include "ITSMFTTracking/IndexTableConfiguration.h" +#include "ITSMFTTracking/MathUtils.h" + +namespace o2::itsmft::tracking +{ + +void TimeFrame::addPrimaryVertex(const Vertex& vert) +{ + mPrimaryVertices.emplace_back(vert); + if (!isBeamPositionOverridden) { + const float w = vert.getNContributors(); + mBeamPos[0] = (mBeamPos[0] * mBeamPosWeight + vert.getX() * w) / (mBeamPosWeight + w); + mBeamPos[1] = (mBeamPos[1] * mBeamPosWeight + vert.getY() * w) / (mBeamPosWeight + w); + mBeamPosWeight += w; + } +} + +void TimeFrame::resetBeamXY(const float x, const float y, const float w) +{ + mBeamPos[0] = x; + mBeamPos[1] = y; + mBeamPosWeight = w; +} + +gsl::span TimeFrame::getGlobalMeasurements(LayerId surface) const +{ + return surface.isValid() && surface.value() < mLayerGlobalMeasurements.size() ? gsl::make_span(mLayerGlobalMeasurements[surface.value()]) : gsl::span{}; +} + +gsl::span TimeFrame::getGlobalMeasurements(LayerId surface) +{ + return surface.isValid() && surface.value() < mLayerGlobalMeasurements.size() ? gsl::make_span(mLayerGlobalMeasurements[surface.value()]) : gsl::span{}; +} + +void TimeFrame::addMeasurement(LayerId surface, GlobalMeasurement global, + const SurfaceMeasurement& measurement) +{ + if (!mConfigurationValid || !surface.isValid() || surface.value() >= mLayerGlobalMeasurements.size()) { + throw std::logic_error{"TimeFrame::addMeasurement(): invalid or unconfigured surface"}; + } + const auto position = surface.value(); + const auto clusterId = static_cast(mLayerSurfaceMeasurements[position].size()); + global.clusterId = clusterId; + mLayerGlobalMeasurements[position].push_back(global); + mLayerSurfaceMeasurements[position].push_back(measurement); + mLayerUsedClusters[position].push_back(uint8_t{0}); +} + +void TimeFrame::addMeasurement(LayerId surface, GlobalMeasurement global, + const SurfaceMeasurement& measurement, + gsl::span labels) +{ + addMeasurement(surface, global, measurement); + const auto clusterId = static_cast(mLayerSurfaceMeasurements[surface.value()].size() - 1); + mLayerClusterLabels[surface.value()].addElements(clusterId, labels); +} + +const SurfaceMeasurement* TimeFrame::getSurfaceMeasurement(LayerId layer, uint32_t clusterId) const noexcept +{ + if (!layer.isValid() || layer.value() >= mLayerSurfaceMeasurements.size()) { + return nullptr; + } + const auto& measurements = mLayerSurfaceMeasurements[layer.value()]; + return clusterId < measurements.size() ? &measurements[clusterId] : nullptr; +} + +gsl::span TimeFrame::getLabels(LayerId layer, uint32_t clusterId) const +{ + if (!layer.isValid() || layer.value() >= mLayerClusterLabels.size()) { + return {}; + } + return mLayerClusterLabels[layer.value()].getLabels(clusterId); +} + +std::size_t TimeFrame::getTotalMeasurements() const noexcept +{ + std::size_t total = 0; + for (const auto& measurements : mLayerGlobalMeasurements) { + total += measurements.size(); + } + return total; +} + +gsl::span TimeFrame::getClustersOnLayer(int rofId, int layer) +{ + if (rofId < 0 || rofId >= getNrof(layer)) { + return {}; + } + const int first = mROFramesClusters[layer][rofId]; + return {mLayerGlobalMeasurements[layer].data() + first, + static_cast::size_type>(mROFramesClusters[layer][rofId + 1] - first)}; +} + +gsl::span TimeFrame::getClustersOnLayer(int rofId, int layer) const +{ + if (rofId < 0 || rofId >= getNrof(layer)) { + return {}; + } + const int first = mROFramesClusters[layer][rofId]; + return {mLayerGlobalMeasurements[layer].data() + first, + static_cast::size_type>(mROFramesClusters[layer][rofId + 1] - first)}; +} + +gsl::span TimeFrame::getClustersPerROFrange(int rofMin, int range, int layer) const +{ + if (rofMin < 0 || rofMin >= getNrof(layer)) { + return {}; + } + const int first = mROFramesClusters[layer][rofMin]; + const int last = mROFramesClusters[layer][o2::gpu::CAMath::Min(rofMin + range, getNrof(layer))]; + return {mLayerGlobalMeasurements[layer].data() + first, static_cast::size_type>(last - first)}; +} + +gsl::span TimeFrame::getROFramesClustersPerROFrange(int rofMin, int range, int layer) const +{ + const int checkedRange = o2::gpu::CAMath::Min(range, getNrof(layer) - rofMin); + return {mROFramesClusters[layer].data() + rofMin, static_cast::size_type>(checkedRange)}; +} + +gsl::span TimeFrame::getROFrameClusters(int layer) const +{ + return gsl::make_span(mROFramesClusters[layer]); +} + +gsl::span TimeFrame::getIndexTable(int rofId, int layer) +{ + if (rofId < 0 || rofId >= getNrof(layer)) { + return {}; + } + const int tableSize = mIndexTableUtils[layer].getNrowBins() * mIndexTableUtils[layer].getNcolBins() + 1; + return {mIndexTables[layer].data() + rofId * tableSize, static_cast::size_type>(tableSize)}; +} + +int TimeFrame::getClusterROF(int layer, int cluster) const +{ + return static_cast(std::lower_bound(mROFramesClusters[layer].begin(), mROFramesClusters[layer].end(), cluster + 1) - + mROFramesClusters[layer].begin() - 1); +} + +int TimeFrame::getTotalClustersPerROFrange(int rofMin, int range, int layer) const +{ + const int last = o2::gpu::CAMath::Min(rofMin + range, getNrof(layer)); + return mROFramesClusters[layer][last] - mROFramesClusters[layer][rofMin]; +} + +gsl::span TimeFrame::getUsedClusters(int layer) +{ + return layer >= 0 && static_cast(layer) < mLayerUsedClusters.size() ? gsl::make_span(mLayerUsedClusters[layer]) : gsl::span{}; +} + +bool TimeFrame::isClusterUsed(int layer, uint32_t clusterId) const +{ + return layer >= 0 && static_cast(layer) < mLayerUsedClusters.size() && clusterId < mLayerUsedClusters[layer].size() && mLayerUsedClusters[layer][clusterId] != 0; +} + +void TimeFrame::markUsedCluster(int layer, uint32_t clusterId) +{ + if (layer >= 0 && static_cast(layer) < mLayerUsedClusters.size() && clusterId < mLayerUsedClusters[layer].size()) { + mLayerUsedClusters[layer][clusterId] = 1; + } +} + +std::size_t TimeFrame::getNumberOfClusters() const +{ + return std::accumulate(mLayerGlobalMeasurements.begin(), mLayerGlobalMeasurements.end(), std::size_t{0}, + [](std::size_t total, const auto& layer) { return total + layer.size(); }); +} + +std::size_t TimeFrame::getNumberOfUsedClusters() const +{ + return std::accumulate(mLayerUsedClusters.begin(), mLayerUsedClusters.end(), std::size_t{0}, [](std::size_t total, const auto& layer) { + return total + static_cast(std::count(layer.begin(), layer.end(), uint8_t{1})); + }); +} + +void TimeFrame::setROFViews(RuntimeROFViews views) noexcept +{ + mROFViews = views; + mROFViewsBySurface.assign(mLayout.size(), views); + mROFLocalLayerBySurface.resize(mROFViewsBySurface.size()); + std::iota(mROFLocalLayerBySurface.begin(), mROFLocalLayerBySurface.end(), uint16_t{0}); + mUseUPC = false; +} + +void TimeFrame::setROFNavigation(std::size_t position, gsl::span boundaries, + RuntimeROFViews views, uint16_t localLayer) +{ + if (!mConfigurationValid || position >= mROFramesClusters.size()) { + throw std::logic_error{"TimeFrame::setROFNavigation(): invalid or unconfigured surface position"}; + } + mROFramesClusters[position].assign(boundaries.begin(), boundaries.end()); + mROFViewsBySurface[position] = views; + mROFLocalLayerBySurface[position] = localLayer; + mUseUPC = false; +} + +const RuntimeROFTableEntry& TimeFrame::getROFOverlap(int fromLayer, int toLayer, int rof) const noexcept +{ + return getROFViews(fromLayer).overlap.getOverlap(getROFLocalLayer(fromLayer), getROFLocalLayer(toLayer), rof); +} + +bool TimeFrame::isROFEnabled(int layer, int rof) const noexcept +{ + const auto& views = getROFViews(layer); + return (mUseUPC ? views.upcMask : views.mask).isROFEnabled(getROFLocalLayer(layer), rof); +} + +bool TimeFrame::isVertexCompatible(int layer, int rof, const Vertex& vertex) const noexcept +{ + return getROFViews(layer).vertexLookup.isVertexCompatible(getROFLocalLayer(layer), rof, vertex); +} + +o2::its::TimeEstBC TimeFrame::getROFTimeStamp(int fromLayer, int fromROF, int toLayer, int toROF) const noexcept +{ + return getROFViews(fromLayer).overlap.getTimeStamp(getROFLocalLayer(fromLayer), fromROF, + getROFLocalLayer(toLayer), toROF); +} + +int TimeFrame::getMaxVerticesPerROF() const noexcept +{ + if (mROFViewsBySurface.empty()) { + return mROFViews.vertexLookup.getMaxVerticesPerROF(); + } + int result = 0; + for (const auto& views : mROFViewsBySurface) { + result = std::max(result, views.vertexLookup.getMaxVerticesPerROF()); + } + return result; +} + +gsl::span TimeFrame::getPrimaryVertices(int layer, int rofId) const +{ + if (rofId < 0 || rofId >= getNrof(layer)) { + return {}; + } + const auto& entry = getROFViews(layer).vertexLookup.getVertices(getROFLocalLayer(layer), rofId); + return {mPrimaryVertices.data() + entry.getFirstEntry(), + static_cast::size_type>(entry.getEntries())}; +} + +bool TimeFrame::hasMCinformation() const noexcept +{ + return mHasMCInformation; +} + +gsl::span TimeFrame::getClusterLabels(int layer, int cluster) const +{ + if (layer < 0 || static_cast(layer) >= mLayerGlobalMeasurements.size() || cluster < 0 || static_cast(cluster) >= mLayerGlobalMeasurements[layer].size()) { + return {}; + } + return getLabels(LayerId{static_cast(layer)}, mLayerGlobalMeasurements[layer][cluster].clusterId); +} + +bool TimeFrame::configure(DetectorLayout&& layout, std::size_t maxEdges, std::size_t maxCells, + std::shared_ptr memoryPool) +{ + if (mConfigurationValid || !memoryPool || !layout.valid() || layout.empty()) { + return false; + } + const auto nOwnedSurfaces = layout.size(); + const auto nMeasurementSurfaces = layout.size(); + mScratch.setMemoryPool(memoryPool); + setMemoryPool(std::move(memoryPool)); + try { + mScratch.configureStorage(maxEdges, maxCells); + mROFramesClusters.resize(nOwnedSurfaces); + mROFViewsBySurface.resize(nOwnedSurfaces); + mROFLocalLayerBySurface.resize(nOwnedSurfaces); + mLayerGlobalMeasurements.resize(nMeasurementSurfaces); + mLayerSurfaceMeasurements.resize(nMeasurementSurfaces); + mLayerUsedClusters.resize(nMeasurementSurfaces); + mLayerClusterLabels.resize(nMeasurementSurfaces); + clearResizeBoundedVector(mIndexTables, nOwnedSurfaces, mMemoryPool.get()); + mIndexTableUtils.reset(layout.getSurfaceCatalog()); + mMinR.assign(nOwnedSurfaces, std::numeric_limits::max()); + mMaxR.assign(nOwnedSurfaces, std::numeric_limits::lowest()); + mMinZ.assign(nOwnedSurfaces, std::numeric_limits::max()); + mMaxZ.assign(nOwnedSurfaces, std::numeric_limits::lowest()); + } catch (const std::bad_alloc&) { + resetTimeFrame(); + mScratch.clearStorage(); + mROFramesClusters.clear(); + mROFViewsBySurface.clear(); + mROFLocalLayerBySurface.clear(); + mLayerGlobalMeasurements.clear(); + mLayerSurfaceMeasurements.clear(); + mLayerUsedClusters.clear(); + mLayerClusterLabels.clear(); + mIndexTables.clear(); + mIndexTableUtils.clear(); + mMinR.clear(); + mMaxR.clear(); + mMinZ.clear(); + mMaxZ.clear(); + return false; + } + mLayout = std::move(layout); + mCapacityEstimator.reset(); + mConfigurationValid = true; + return true; +} + +TimeFrameScratch& TimeFrame::getScratch() +{ + return mScratch; +} + +const TimeFrameScratch& TimeFrame::getScratch() const +{ + return mScratch; +} + +void TimeFrame::resetTimeFrame() noexcept +{ + mScratch.reset(); + deepVectorClear(mPrimaryVertices); + deepVectorClear(mPrimaryVerticesLabels); + // Common tracks, labels, and cluster references are valid only for the + // current TimeFrame measurements, so clear them together. + deepVectorClear(mGenericTracks); + deepVectorClear(mTrackLabels); + deepVectorClear(mTrackClusterIndices); + for (auto& measurements : mLayerGlobalMeasurements) { + measurements.clear(); + } + for (auto& measurements : mLayerSurfaceMeasurements) { + measurements.clear(); + } + for (auto& used : mLayerUsedClusters) { + used.clear(); + } + for (auto& labels : mLayerClusterLabels) { + labels.clear(); + } + mHasMCInformation = false; + mROFViews = {}; + std::fill(mROFViewsBySurface.begin(), mROFViewsBySurface.end(), RuntimeROFViews{}); + std::fill(mROFLocalLayerBySurface.begin(), mROFLocalLayerBySurface.end(), uint16_t{0}); + mUseUPC = false; + for (auto& boundaries : mROFramesClusters) { + boundaries.clear(); + } + deepVectorClear(mIndexTables); + std::fill(mMinR.begin(), mMinR.end(), std::numeric_limits::max()); + std::fill(mMaxR.begin(), mMaxR.end(), std::numeric_limits::lowest()); + std::fill(mMinZ.begin(), mMinZ.end(), std::numeric_limits::max()); + std::fill(mMaxZ.begin(), mMaxZ.end(), std::numeric_limits::lowest()); +} + +void TimeFrame::setMemoryPool(std::shared_ptr pool) +{ + mMemoryPool = pool; + + auto initVector = [&](bounded_vector& vec) { + deepVectorClear(vec, mMemoryPool.get()); + }; + + initVector(mPrimaryVertices); + initVector(mPrimaryVerticesLabels); + initVector(mGenericTracks); + initVector(mTrackLabels); + initVector(mTrackClusterIndices); + for (auto& table : mIndexTables) { + initVector(table); + } +} + +void TimeFrame::prepareIndexTables(const IndexTableConfigurationSet& indexTableConfigs) +{ + if (indexTableConfigs.size() != mIndexTables.size()) { + throw std::logic_error{"TimeFrame::prepareIndexTables(): configuration extent mismatch"}; + } + mIndexTableUtils = indexTableConfigs; + for (std::size_t layer = 0; layer < mIndexTables.size(); ++layer) { + std::size_t stride = 0; + if (!checkedIndexTableSizeProduct(static_cast(mIndexTableUtils[layer].getNrowBins()), + static_cast(mIndexTableUtils[layer].getNcolBins()), stride) || + stride == std::numeric_limits::max()) { + throw std::bad_alloc{}; + } + ++stride; + std::size_t tableSize = 0; + if (!checkedIndexTableSizeProduct(static_cast(getNrof(static_cast(layer))), stride, tableSize)) { + throw std::bad_alloc{}; + } + clearResizeBoundedVector(mIndexTables[layer], tableSize, mMemoryPool.get()); + } + std::fill(mMinR.begin(), mMinR.end(), std::numeric_limits::max()); + std::fill(mMaxR.begin(), mMaxR.end(), std::numeric_limits::lowest()); + std::fill(mMinZ.begin(), mMinZ.end(), std::numeric_limits::max()); + std::fill(mMaxZ.begin(), mMaxZ.end(), std::numeric_limits::lowest()); +} + +void TimeFrame::prepareClusters(int maxLayers) +{ + struct SortingHelper { + int bin; + int indexWithinBin; + int measurementIndex; + }; + + const int stopLayer = std::min(maxLayers, static_cast(mLayerGlobalMeasurements.size())); + for (int layer = 0; layer < stopLayer; ++layer) { + const auto& utils = mIndexTableUtils[layer]; + const int colBinsCount = utils.getNcolBins(); + std::size_t numBins = 0; + if (!checkedIndexTableSizeProduct(static_cast(utils.getNrowBins()), + static_cast(colBinsCount), numBins) || + numBins == std::numeric_limits::max()) { + throw std::bad_alloc{}; + } + const std::size_t stride = numBins + 1; + bounded_vector helpers(mMemoryPool.get()); + bounded_vector sortedMeasurements(mMemoryPool.get()); + bounded_vector counts(numBins, 0, mMemoryPool.get()); + bounded_vector offsets(numBins, 0, mMemoryPool.get()); + + for (int rof = 0; rof < getNrof(layer); ++rof) { + if (!isROFEnabled(layer, rof)) { + continue; + } + const int first = mROFramesClusters[layer][rof]; + const int last = mROFramesClusters[layer][rof + 1]; + const int count = last - first; + auto* tableBase = mIndexTables[layer].data() + rof * stride; + helpers.resize(count); + sortedMeasurements.resize(count); + const bool usePhiRBinning = utils.getCoordType() == o2::itsmft::IndexTableCoordType::PhiR; + + for (int local = 0; local < count; ++local) { + const int measurementIndex = first + local; + const auto& measurement = mLayerGlobalMeasurements[layer][measurementIndex]; + auto& helper = helpers[local]; + int colBin = utils.getColBinIndex(layer, usePhiRBinning ? measurement.radius : measurement.z); + if (colBin < 0 || colBin >= colBinsCount) { + colBin = std::clamp(colBin, 0, colBinsCount - 1); + } + helper.bin = utils.getBinIndex(colBin, utils.getRowBinIndex(measurement.phi)); + helper.indexWithinBin = counts[helper.bin]++; + helper.measurementIndex = measurementIndex; + mMinR[layer] = o2::gpu::GPUCommonMath::Min(measurement.radius, mMinR[layer]); + mMaxR[layer] = o2::gpu::GPUCommonMath::Max(measurement.radius, mMaxR[layer]); + mMinZ[layer] = o2::gpu::GPUCommonMath::Min(measurement.z, mMinZ[layer]); + mMaxZ[layer] = o2::gpu::GPUCommonMath::Max(measurement.z, mMaxZ[layer]); + } + std::exclusive_scan(counts.begin(), counts.end(), offsets.begin(), 0); + + for (const auto& helper : helpers) { + sortedMeasurements[offsets[helper.bin] + helper.indexWithinBin] = mLayerGlobalMeasurements[layer][helper.measurementIndex]; + } + std::copy(sortedMeasurements.begin(), sortedMeasurements.end(), mLayerGlobalMeasurements[layer].begin() + first); + std::copy_n(offsets.data(), counts.size(), tableBase); + std::fill_n(tableBase + counts.size(), stride - counts.size(), count); + std::fill(counts.begin(), counts.end(), 0); + helpers.clear(); + sortedMeasurements.clear(); + } + } +} + +} // namespace o2::itsmft::tracking diff --git a/Detectors/ITSMFT/common/tracking/src/TimeFrameScratch.cxx b/Detectors/ITSMFT/common/tracking/src/TimeFrameScratch.cxx new file mode 100644 index 0000000000000..1907f8aaf571d --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/TimeFrameScratch.cxx @@ -0,0 +1,125 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSMFTTracking/detail/TimeFrameScratch.h" + +#include + +namespace o2::itsmft::tracking +{ + +namespace +{ +template +void applyToContainers(Operation&& operation, Containers&... containers) +{ + (operation(containers), ...); +} +} // namespace + +void TimeFrameScratch::clearResizeEdgeStorage(std::size_t nEdges) +{ + auto clearResize = [this, nEdges](auto& container) { + clearResizeBoundedVector(container, nEdges, mMemoryPool.get()); + }; + applyToContainers(clearResize, mTracklets, mTrackletsLookupTable, mTrackletLabels, + mEdgePhiCuts, mEdgeMSAngles); +} + +void TimeFrameScratch::clearResizeCellStorage(std::size_t nCells) +{ + auto clearResize = [this, nCells](auto& container) { + clearResizeBoundedVector(container, nCells, mMemoryPool.get()); + }; + applyToContainers(clearResize, mCells, mCellsLookupTable, mCellsNeighbours, + mCellsNeighboursTopology, mCellsNeighboursLUT, mCellLabels); +} + +void TimeFrameScratch::configureStorage(std::size_t nEdges, std::size_t nCells) +{ + mNEdges = nEdges; + mNCells = nCells; + clearResizeEdgeStorage(nEdges); + clearResizeCellStorage(nCells); +} + +void TimeFrameScratch::reset() +{ + applyToContainers([](auto& container) { deepVectorClear(container); }, + mTracklets, mTrackletsLookupTable, mTrackletLabels, mCells, + mCellsLookupTable, mCellsNeighbours, mCellsNeighboursTopology, + mCellsNeighboursLUT, mCellLabels, mEdgePhiCuts, mEdgeMSAngles); +} + +void TimeFrameScratch::clearStorage() noexcept +{ + applyToContainers([](auto& container) { container.clear(); }, + mTracklets, mTrackletsLookupTable, mTrackletLabels, mCells, + mCellsLookupTable, mCellsNeighbours, mCellsNeighboursTopology, + mCellsNeighboursLUT, mCellLabels); + deepVectorClear(mEdgePhiCuts); + deepVectorClear(mEdgeMSAngles); + mNEdges = 0; + mNCells = 0; +} + +void TimeFrameScratch::setMemoryPool(std::shared_ptr pool) +{ + mMemoryPool = std::move(pool); + applyToContainers([this](auto& container) { deepVectorClear(container, mMemoryPool.get()); }, + mEdgePhiCuts, mEdgeMSAngles, mTracklets, mTrackletsLookupTable, + mTrackletLabels, mCells, mCellsLookupTable, mCellsNeighbours, + mCellsNeighboursTopology, mCellsNeighboursLUT, mCellLabels); +} + +std::size_t TimeFrameScratch::getNumberOfCells() const +{ + std::size_t result = 0; + for (const auto& cells : mCells) { + result += cells.size(); + } + return result; +} + +std::size_t TimeFrameScratch::getNumberOfTracklets() const +{ + std::size_t result = 0; + for (const auto& tracklets : mTracklets) { + result += tracklets.size(); + } + return result; +} + +std::size_t TimeFrameScratch::getNumberOfNeighbours() const +{ + std::size_t result = 0; + for (const auto& neighbours : mCellsNeighbours) { + result += neighbours.size(); + } + return result; +} + +void TimeFrameScratch::beginIteration(std::size_t nEdges, std::size_t nCells, + gsl::span trackletLookupSizes) +{ + if (nEdges > mNEdges || nCells > mNCells || trackletLookupSizes.size() != nEdges) { + throw std::logic_error{"TimeFrameScratch::beginIteration(): requested storage exceeds configured capacity"}; + } + + clearResizeCellStorage(nCells); + clearResizeEdgeStorage(nEdges); + + for (std::size_t edge = 0; edge < nEdges; ++edge) { + mTrackletsLookupTable[edge].resize(trackletLookupSizes[edge] + 1, 0); + } +} + +} // namespace o2::itsmft::tracking diff --git a/Detectors/ITSMFT/common/tracking/src/Tracker.cxx b/Detectors/ITSMFT/common/tracking/src/Tracker.cxx new file mode 100644 index 0000000000000..2fcb9fa7a74ec --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/Tracker.cxx @@ -0,0 +1,620 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file Tracker.cxx +/// \brief +/// + +#include "ITSMFTTracking/Tracker.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Framework/Logger.h" +#include "GPUCommonMath.h" +#include "ITSMFTTracking/BoundedAllocator.h" +#include "ITSMFTTracking/IndexTableConfiguration.h" +#include "ITSMFTTracking/MaterialPhysics.h" +#include "ITSMFTTracking/detail/TrackerTraversalPreparation.h" + +namespace o2::itsmft::tracking +{ + +namespace +{ +constexpr std::size_t kindIndex(SurfaceKind kind) noexcept +{ + return kind == SurfaceKind::Cylinder ? 0u : 1u; +} + +TrackingKernelParameters bindTrackingKernelParameters(const IterationParameters& params) noexcept +{ + TrackingKernelParameters out; + out.trackletMinPt = params.TrackletMinPt; + out.nSigmaCut = params.NSigmaCut; + out.maxChi2ClusterAttachment = params.MaxChi2ClusterAttachment; + out.maxChi2NDF = params.MaxChi2NDF; + out.pvResolution = params.PVres; + return out; +} + +} // namespace + +namespace +{ +void validateSparsePlan(const IterationConfiguration& configuration, int iteration, const TraversalTopologyView& layout) +{ + const auto fail = [iteration]() { throw TraversalException{iteration, TraversalFailureReason::SparseTopologyMismatch}; }; + const auto& topology = layout; + if (layout.catalog.surfaces == nullptr || layout.catalog.nSurfaces == 0 || + (topology.nEdges != 0 && (topology.edges == nullptr || topology.pathsByFirstEdgeOffsets == nullptr)) || + (topology.nPaths != 0 && (topology.paths == nullptr || topology.pathsByFirstEdge == nullptr))) { + fail(); + } + + const auto edges = configuration.edgeIds(); + const auto cells = configuration.cellIds(); + if (edges.empty() || edges.size() > topology.nEdges || cells.size() > topology.nPaths) { + fail(); + } + for (const auto id : edges) { + if (!id.isValid() || id.value() >= topology.nEdges || !configuration.getEdgeSlot(id)) { + fail(); + } + const auto& edge = topology.getEdge(id); + if (!configuration.hasLayer(edge.from) || !configuration.hasLayer(edge.to)) { + fail(); + } + } + for (const auto id : cells) { + if (!id.isValid() || id.value() >= topology.nPaths || !configuration.getCellSlot(id)) { + fail(); + } + const auto& path = topology.getPath(id); + const auto& firstEdge = topology.getEdge(path.first); + const auto& secondEdge = topology.getEdge(path.second); + if (!configuration.getEdgeSlot(path.first) || !configuration.getEdgeSlot(path.second) || + !configuration.hasLayer(firstEdge.from) || !configuration.hasLayer(firstEdge.to) || + !configuration.hasLayer(secondEdge.to) || + !configuration.topology.activeLayers.has(firstEdge.from.value()) || + !configuration.topology.activeLayers.has(firstEdge.to.value()) || + !configuration.topology.activeLayers.has(secondEdge.to.value())) { + fail(); + } + } + for (const auto id : configuration.topology.scheduledPaths) { + if (!configuration.getCellSlot(id)) { + fail(); + } + } + for (const auto id : configuration.topology.roadStartPaths) { + if (!configuration.getCellSlot(id)) { + fail(); + } + } +} + +DetectorConfiguration prepareDetectorConfiguration(const DetectorLayout& layout, const DetectorParameters& parameters) +{ + DetectorConfiguration configuration; + const auto catalog = layout.getSurfaceCatalog(); + const auto surfaceCount = layout.size(); + if (surfaceCount == 0 || surfaceCount > MaxLayoutSurfaces || + parameters.LayerRadii.size() < surfaceCount || + parameters.AddTimeError.size() < surfaceCount || + parameters.SystError2Col.size() < surfaceCount || + parameters.SystError2Row.size() < surfaceCount || + parameters.LayerResolution.size() < surfaceCount) { + throw TraversalException{-1, TraversalFailureReason::InvalidSurfaceParameters}; + } + configuration.layerRadii.assign(parameters.LayerRadii.begin(), parameters.LayerRadii.begin() + surfaceCount); + configuration.addTimeError.assign(parameters.AddTimeError.begin(), parameters.AddTimeError.begin() + surfaceCount); + configuration.layerResolution.assign(parameters.LayerResolution.begin(), parameters.LayerResolution.begin() + surfaceCount); + configuration.systError2Row.assign(parameters.SystError2Row.begin(), parameters.SystError2Row.begin() + surfaceCount); + configuration.systError2Col.assign(parameters.SystError2Col.begin(), parameters.SystError2Col.begin() + surfaceCount); + configuration.positionResolutions.resize(surfaceCount); + std::array chartRanges{}; + for (std::size_t position = 0; position < surfaceCount; ++position) { + const auto surface = LayerId{static_cast(position)}; + const auto& descriptor = catalog.getSurface(surface); + chartRanges[position] = descriptor.chartRange; + configuration.positionResolutions[position] = o2::gpu::CAMath::Sqrt( + 0.5f * (parameters.SystError2Col[position] + parameters.SystError2Row[position]) + + parameters.LayerResolution[position] * parameters.LayerResolution[position]); + } + if (!configuration.indexTableConfigs.reset(catalog)) { + throw TraversalException{-1, TraversalFailureReason::InvalidIndexTableConfiguration}; + } + const gsl::span chartRangeView{chartRanges.data(), surfaceCount}; + for (const auto kind : {SurfaceKind::Cylinder, SurfaceKind::Disk}) { + if (configuration.indexTableConfigs.hasKind(kind) && + bindIndexTableConfiguration(configuration.indexTableConfigs.forKind(kind), parameters, + static_cast(surfaceCount), kind, chartRangeView) != IndexTableConfigError::None) { + throw TraversalException{-1, TraversalFailureReason::InvalidIndexTableConfiguration}; + } + } + return configuration; +} + +void prepareIterationConfiguration(const DetectorLayout& layout, const DetectorConfiguration& detector, + IterationConfiguration& configuration, int iteration) +{ + const auto topology = configuration.getTopologyView(layout.getSurfaceCatalog()); + const auto& parameters = configuration.parameters; + const auto layerCount = configuration.topology.nLayers; + if (layerCount == 0 || layerCount > MaxLayoutSurfaces || + parameters.NLayers != static_cast(layerCount)) { + throw TraversalException{iteration, TraversalFailureReason::LegacyMaterialMismatch}; + } + + for (uint16_t position = 0; position < layerCount; ++position) { + const auto surface = LayerId{position}; + const auto& descriptor = topology.getSurface(surface); + if (materialCorrectionModeSupport(descriptor.kind, parameters.CorrType) == MaterialCorrectionModeSupport::Unsupported) { + throw TraversalException{iteration, TraversalFailureReason::UnsupportedMaterialCorrectionMode}; + } + } + + if (!bindAttachHitConfig(topology.catalog, parameters).isValid(static_cast(layerCount)) || + detector.layerRadii.size() < layerCount || + detector.positionResolutions.size() < layerCount || + detector.indexTableConfigs.size() < layerCount) { + throw TraversalException{iteration, TraversalFailureReason::InvalidSurfaceParameters}; + } + configuration.kernelParameters = bindTrackingKernelParameters(parameters); + if (!configuration.kernelParameters.isValid()) { + throw TraversalException{iteration, TraversalFailureReason::InvalidSurfaceParameters}; + } + validateSparsePlan(configuration, iteration, topology); +} + +void prepareTraversalEdgeTolerances( + IterationContext& context, + int iteration) +{ + auto& scratch = context.scratch; + const auto& graph = context.topology; + const auto& trkParam = context.configuration.parameters; + const auto& topology = graph; + + const int layerCount = context.configuration.topology.nLayers; + std::array msAngles{}; + for (int iLayer{0}; iLayer < layerCount; ++iLayer) { + const auto surface = LayerId{static_cast(iLayer)}; + if (topology.getSurface(surface).kind == SurfaceKind::Cylinder) { + msAngles[iLayer] = cylinderLayerMultipleScatteringAngle( + CylinderLayerScatteringInputs{topology.getSurface(surface).material.xOverX0}, trkParam.TrackletMinPt); + } else { + msAngles[iLayer] = diskLayerMultipleScatteringAngle( + DiskLayerScatteringInputs{topology.getSurface(surface).material.xOverX0, + context.detectorConfiguration.layerRadii[iLayer], + topology.getSurface(surface).referenceCoordinate}, + trkParam.TrackletMinPt); + } + } + + auto& edgeMSAngles = scratch.getEdgeMSAngles(); + auto& edgePhiCuts = scratch.getEdgePhiCuts(); + const float oneOverR{0.001f * 0.3f * std::abs(context.bz) / trkParam.TrackletMinPt}; + for (const auto edgeId : context.configuration.edgeIds()) { + const auto edgeSlot = context.configuration.getEdgeSlot(edgeId); + if (!edgeSlot) { + throw TraversalException{iteration, TraversalFailureReason::TraversalBindingMismatch}; + } + const auto& edge = topology.getEdge(edgeId); + if (!context.configuration.hasLayer(edge.from) || !context.configuration.hasLayer(edge.to)) { + throw TraversalException{iteration, TraversalFailureReason::TraversalBindingMismatch}; + } + const int fromLayer = edge.from.value(); + const int toLayer = edge.to.value(); + const float r1 = std::min(context.detectorConfiguration.layerRadii[fromLayer], context.detectorConfiguration.layerRadii[toLayer]); + const float r2 = std::max(context.detectorConfiguration.layerRadii[fromLayer], context.detectorConfiguration.layerRadii[toLayer]); + const float edgeOneOverR = clampEdgeCurvature(oneOverR, r2); + const float res1 = o2::gpu::CAMath::Hypot(trkParam.PVres, context.detectorConfiguration.positionResolutions[fromLayer]); + const float res2 = o2::gpu::CAMath::Hypot(trkParam.PVres, context.detectorConfiguration.positionResolutions[toLayer]); + const auto prep = ::o2::itsmft::tracking::prepareEdgeScatteringAndBending( + gsl::span(msAngles.data(), static_cast(layerCount)), fromLayer, toLayer, r1, r2, edgeOneOverR, res1, res2); + edgeMSAngles[*edgeSlot] = prep.msAngle; + edgePhiCuts[*edgeSlot] = prep.phiCut; + } +} +} // namespace + +void Tracker::initializeIteration(IterationContext& context) const +{ + const int iteration = context.iteration; + if (iteration < 0 || static_cast(iteration) >= mIterations.size()) { + throw TraversalException{iteration, TraversalFailureReason::IterationOutOfRange}; + } + const auto& configuration = context.configuration; + const auto& parameters = configuration.parameters; + auto& frame = context.frame; + auto& scratch = context.scratch; + const auto layerCount = configuration.topology.nLayers; + + if (parameters.PassFlags[IterationStep::FirstPass]) { + frame.prepareIndexTables(context.detectorConfiguration.indexTableConfigs); + } else { + for (std::size_t position = 0; position < layerCount; ++position) { + if (!indexTableConfigurationsMatch(context.detectorConfiguration.indexTableConfigs[position], + frame.getIndexTableUtils(static_cast(position)), + static_cast(layerCount))) { + throw TraversalException{iteration, TraversalFailureReason::IndexTableConfigurationMismatch}; + } + } + } + if (parameters.PassFlags[IterationStep::RebuildClusterLUT]) { + frame.prepareClusters(static_cast(layerCount)); + } + + const auto edgeIds = context.configuration.edgeIds(); + const auto cellIds = context.configuration.cellIds(); + std::array trackletLookupSizes; + for (const auto edgeId : edgeIds) { + const auto from = context.topology.getEdge(edgeId).from; + if (!configuration.hasLayer(from) || from.value() >= context.layerGlobalMeasurements.size()) { + throw TraversalException{iteration, TraversalFailureReason::TraversalBindingMismatch}; + } + trackletLookupSizes[edgeId.value()] = context.layerGlobalMeasurements[from.value()].size(); + } + scratch.beginIteration(edgeIds.size(), cellIds.size(), {trackletLookupSizes.data(), edgeIds.size()}); + + // Sorted clusters are a locator cache. Validate every enabled ROF that can + // participate in a configured edge, including LUT-reuse paths. + // Keep spans local until validation and kind setup complete. + std::array candidateReachableLayers{}; + for (const auto edgeId : edgeIds) { + const auto& edge = context.topology.getEdge(edgeId); + if (!configuration.hasLayer(edge.from) || !configuration.hasLayer(edge.to)) { + throw TraversalException{iteration, TraversalFailureReason::SparseTopologyMismatch}; + } + candidateReachableLayers[edge.from.value()] = true; + candidateReachableLayers[edge.to.value()] = true; + } + for (std::size_t layer = 0; layer < layerCount; ++layer) { + if (!candidateReachableLayers[layer]) { + continue; + } + const auto measurements = context.layerGlobalMeasurements[layer]; + const auto rofBoundaries = frame.getROFrameClusters(static_cast(layer)); + const auto rofMask = frame.getROFViews(static_cast(layer)).mask; + // Orchestration-only users may omit the mask; without it no ROF is reachable. + if (rofMask.mFlatMask == nullptr || rofMask.mLayerROFOffsets == nullptr) { + continue; + } + for (int rof = 0; rof < frame.getNrof(static_cast(layer)); ++rof) { + const auto sorted = frame.getClustersOnLayer(rof, static_cast(layer)); + if (sorted.empty()) { + continue; + } + if (!frame.isROFEnabled(static_cast(layer), rof)) { + continue; + } + const int first = rofBoundaries[rof]; + const int last = rofBoundaries[rof + 1]; + if (first < 0 || last < first || last > static_cast(measurements.size()) || + sorted.size() != static_cast(last - first)) { + throw TraversalException{iteration, TraversalFailureReason::NormalizedMeasurementMismatch}; + } + std::vector seen; + seen.reserve(sorted.size()); + for (const auto& measurement : sorted) { + if (!measurement.hasValidClusterId() || + frame.getSurfaceMeasurement(LayerId{static_cast(layer)}, measurement.clusterId) == nullptr) { + throw TraversalException{iteration, TraversalFailureReason::NormalizedMeasurementMismatch}; + } + seen.push_back(measurement.clusterId); + } + std::sort(seen.begin(), seen.end()); + if (std::adjacent_find(seen.begin(), seen.end()) != seen.end()) { + throw TraversalException{iteration, TraversalFailureReason::NormalizedMeasurementMismatch}; + } + } + } + + prepareTraversalEdgeTolerances(context, iteration); +} + +gsl::span> Tracker::prepareTimeFrame( + TimeFrame& frame, std::array, MaxLayoutSurfaces>& measurements) const +{ + const auto layerCount = mIterations.front().topology.nLayers; + for (uint16_t position = 0; position < layerCount; ++position) { + const auto surface = LayerId{position}; + const auto globals = frame.getGlobalMeasurements(surface); + if (globals.size() > static_cast(std::numeric_limits::max())) { + throw TraversalException{-1, TraversalFailureReason::NormalizedMeasurementMismatch}; + } + for (const auto& global : globals) { + if (!global.hasValidClusterId() || global.clusterId > static_cast(std::numeric_limits::max()) || + frame.getSurfaceMeasurement(surface, global.clusterId) == nullptr) { + throw TraversalException{-1, TraversalFailureReason::NormalizedMeasurementMismatch}; + } + } + const auto rofBoundaries = frame.getROFrameClusters(static_cast(position)); + if (rofBoundaries.empty() || rofBoundaries.front() != 0 || + rofBoundaries.back() != static_cast(globals.size())) { + throw TraversalException{-1, TraversalFailureReason::NormalizedMeasurementMismatch}; + } + for (std::size_t rof = 0; rof + 1 < rofBoundaries.size(); ++rof) { + const int first = rofBoundaries[rof]; + const int last = rofBoundaries[rof + 1]; + if (first < 0 || last < first || last > static_cast(globals.size())) { + throw TraversalException{-1, TraversalFailureReason::NormalizedMeasurementMismatch}; + } + } + measurements[position] = globals; + } + return {measurements.data(), layerCount}; +} + +TrackerInitializationResult Tracker::initialize(TimeFrame& frame, const TrackerInitialization& configuration) +{ + TrackerInitializationResult result; + if (frame.isConfigured()) { + result.error = TrackerInitializationError::FrameAlreadyConfigured; + return result; + } + if (configuration.plan.iterations.empty()) { + result.error = TrackerInitializationError::EmptyConfiguration; + return result; + } + if (configuration.catalog.surfaces == nullptr || configuration.catalog.nSurfaces == 0) { + result.error = TrackerInitializationError::MissingCatalog; + return result; + } + if (!configuration.memoryPool) { + result.error = TrackerInitializationError::MissingMemoryPool; + return result; + } + + DetectorLayout layout{gsl::span{configuration.catalog.surfaces, + configuration.catalog.nSurfaces}, + configuration.layout}; + if (!layout.valid()) { + result.error = TrackerInitializationError::LayoutInvalid; + result.layoutError = layout.getError(); + return result; + } + DetectorConfiguration detectorConfiguration; + try { + detectorConfiguration = prepareDetectorConfiguration(layout, configuration.plan.detector); + } catch (const TraversalException&) { + result.error = TrackerInitializationError::TraversalPlanBuildFailed; + return result; + } + + std::vector iterations; + std::size_t maxEdges = 0; + std::size_t maxCells = 0; + iterations.reserve(configuration.plan.iterations.size()); + + for (std::size_t iteration = 0; iteration < configuration.plan.iterations.size(); ++iteration) { + const auto& input = configuration.plan.iterations[iteration]; + if (input.NLayers != 0 && input.NLayers != layout.size()) { + result.error = TrackerInitializationError::CapacityMismatch; + result.failedIteration = iteration; + return result; + } + const auto topology = deriveTraversalTopology(layout, input); + if (!topology.ok()) { + result.error = TrackerInitializationError::TraversalPlanBuildFailed; + result.failedIteration = iteration; + return result; + } + IterationConfiguration iterationConfiguration; + iterationConfiguration.parameters = input; + iterationConfiguration.parameters.NLayers = static_cast(layout.size()); + iterationConfiguration.topology = *topology.topology; + try { + prepareIterationConfiguration(layout, detectorConfiguration, iterationConfiguration, static_cast(iteration)); + } catch (const TraversalException&) { + result.error = TrackerInitializationError::TraversalPlanBuildFailed; + result.failedIteration = iteration; + return result; + } + maxEdges = std::max(maxEdges, iterationConfiguration.topology.edges.size()); + maxCells = std::max(maxCells, iterationConfiguration.topology.paths.size()); + iterations.push_back(std::move(iterationConfiguration)); + } + + if (!frame.configure(std::move(layout), maxEdges, maxCells, configuration.memoryPool)) { + result.error = TrackerInitializationError::CapacityMismatch; + return result; + } + mExecutionPolicy = configuration.plan.execution; + mDetectorConfiguration = std::move(detectorConfiguration); + mIterations = std::move(iterations); + mFrame = &frame; + return result; +} + +bool Tracker::isConfiguredFor(const TimeFrame& frame) const noexcept +{ + return mFrame == &frame && !mIterations.empty() && frame.isConfigured(); +} + +void Tracker::computeTracksMClabels(TimeFrame& frame) const +{ + bounded_vector trackLabels(frame.getMemoryPool().get()); + if (!frame.hasMCinformation()) { + frame.getTrackLabels().swap(trackLabels); + return; + } + + const auto& tracks = frame.getGenericTracks(); + const auto& references = frame.getTrackClusterIndices(); + trackLabels.reserve(tracks.size()); + + struct Candidate { + MCCompLabel representative; + std::size_t count{0}; + std::size_t lastSeenCluster{0}; + }; + + for (const auto& track : tracks) { + if (!isValidTrackRange(track, static_cast(references.size()))) { + throw std::logic_error{"Tracker::computeTracksMClabels(): invalid track cluster-reference range"}; + } + + std::vector candidates; + std::size_t attachedClusters = 0; + for (uint32_t index = track.firstClusterRef; index < track.clusterRefEnd; ++index) { + const auto& reference = references[index]; + if (!reference.isValid() || frame.getSurfaceMeasurement(reference.layer, reference.clusterId) == nullptr) { + throw std::logic_error{"Tracker::computeTracksMClabels(): unresolved track cluster reference"}; + } + + ++attachedClusters; + for (const auto& label : frame.getLabels(reference.layer, reference.clusterId)) { + const auto candidate = std::find_if(candidates.begin(), candidates.end(), [&label](const auto& current) { + return label == current.representative; + }); + if (candidate == candidates.end()) { + candidates.push_back({label, 1, attachedClusters}); + } else if (candidate->lastSeenCluster != attachedClusters) { + ++candidate->count; + candidate->lastSeenCluster = attachedClusters; + } + } + } + + MCCompLabel winner; + if (candidates.empty()) { + winner.setFakeFlag(); + } else { + const auto best = std::max_element(candidates.begin(), candidates.end(), [](const auto& left, const auto& right) { + return left.count < right.count; + }); + winner = best->representative; + // A single attached cluster without the winning identity makes the + // reconstructed track fake. + if (best->count != attachedClusters) { + winner.setFakeFlag(); + } + } + trackLabels.push_back(winner); + } + + frame.getTrackLabels().swap(trackLabels); +} + +void Tracker::configureBeamPosition(TimeFrame& frame) const +{ + const auto& params = mIterations.front().parameters; + if (!params.UseDiamond) { + return; + } + const float systErrY2 = mDetectorConfiguration.systError2Row.empty() ? 0.f : mDetectorConfiguration.systError2Row[0]; + const float layerRes = mDetectorConfiguration.layerResolution.empty() ? 0.f : mDetectorConfiguration.layerResolution[0]; + frame.setBeamPosition(params.Diamond[0], params.Diamond[1], params.DiamondCov[3], layerRes, systErrY2); +} + +TrackingResult Tracker::run(TimeFrame& frame, TrackerTraits& traits) +{ + if (!isConfiguredFor(frame)) { + throw TraversalException{-1, TraversalFailureReason::MissingLayout}; + } + float total{0.f}; + std::vector acceptedTrackCounts; + auto& estimator = frame.getCapacityEstimator(); + bool estimatorTransactionStarted{false}; + const auto rollbackEstimator = [&] { + if (estimatorTransactionStarted) { + estimator.rollbackTransaction(); + estimatorTransactionStarted = false; + } + }; + try { + estimator.beginTransaction(); + estimatorTransactionStarted = true; + configureBeamPosition(frame); + auto& scratch = frame.getScratch(); + acceptedTrackCounts.reserve(mIterations.size()); + std::array, MaxLayoutSurfaces> measurementSpans; + const auto layerGlobalMeasurements = prepareTimeFrame(frame, measurementSpans); + const auto& memoryPool = frame.getMemoryPool(); + // Apply a tighter event-local limit when configured; this also lets + // workflows and tests inject a resource failure after loading. + if (mExecutionPolicy.MaxMemory != std::numeric_limits::max() && + memoryPool->getMaxMemory() > mExecutionPolicy.MaxMemory) { + memoryPool->setMaxMemory(mExecutionPolicy.MaxMemory); + } + for (int iteration = 0; iteration < static_cast(mIterations.size()); ++iteration) { + const auto& configuration = mIterations[iteration]; + const auto& trkParam = configuration.parameters; + if (trkParam.PassFlags[IterationStep::UseUPCMask]) { + frame.useUPCMask(); + } + + const auto acceptedTrackBegin = frame.getGenericTracks().size(); + IterationContext context{iteration, frame, scratch, + configuration.getTopologyView(frame.getLayout().getSurfaceCatalog()), + configuration, mDetectorConfiguration, layerGlobalMeasurements, + frame.getBz()}; + initializeIteration(context); + traits.runTraversal(context); + acceptedTrackCounts.push_back(frame.getGenericTracks().size() - acceptedTrackBegin); + } + computeTracksMClabels(frame); + if (std::getenv("O2_ITSMFT_PRINT_SLAB_STATS") != nullptr) { + estimator.print(); + } + estimator.commitTransaction(); + estimatorTransactionStarted = false; + } catch (const TraversalException& err) { + // Structural/configuration failures are not per-TF data failures, so + // DropTFUponFailure does not apply. Reset before propagating. + LOGP(error, "CA tracker hit a structural traversal failure: {}", err.what()); + rollbackEstimator(); + frame.resetTimeFrame(); + throw; + } catch (const BoundedMemoryResource::MemoryLimitExceeded& err) { + // Recoverable per-TF resource failure: the bounded pool budget was + // exceeded for this TimeFrame. + LOGP(error, "CA tracker exceeded memory limit: {}", err.what()); + rollbackEstimator(); + frame.resetTimeFrame(); + if (mExecutionPolicy.DropTFUponFailure) { + return TrackingResult{TrackingOutcome::RecoverableDropped, 0.f}; + } + throw; + } catch (const std::bad_alloc& err) { + // Some CA scratch containers use the plain heap instead of the bounded + // pool, so memory pressure can surface as bad_alloc. Handle it likewise. + LOGP(error, "CA tracker allocation failed: {}", err.what()); + rollbackEstimator(); + frame.resetTimeFrame(); + if (mExecutionPolicy.DropTFUponFailure) { + return TrackingResult{TrackingOutcome::RecoverableDropped, 0.f}; + } + throw; + } catch (const std::exception& err) { + // Unclassified exceptions are treated as structural and always propagate; + // recoverability is not inferred from std::exception alone. + LOGP(error, "CA tracker failed with an unclassified exception; treating as structural: {}", err.what()); + rollbackEstimator(); + frame.resetTimeFrame(); + throw; + } + + return TrackingResult{TrackingOutcome::Success, total, std::move(acceptedTrackCounts)}; +} + +} // namespace o2::itsmft::tracking diff --git a/Detectors/ITSMFT/common/tracking/src/TrackerTraits.cxx b/Detectors/ITSMFT/common/tracking/src/TrackerTraits.cxx new file mode 100644 index 0000000000000..b84f0d6f24355 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/TrackerTraits.cxx @@ -0,0 +1,1177 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file TrackerTraits.cxx +/// \brief +/// + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "CommonConstants/MathConstants.h" +#include "Framework/Logger.h" +#include "GPUCommonMath.h" +#include "ITSMFTTracking/BoundedAllocator.h" +#include "ITSMFTTracking/Cell.h" +#include "ITSMFTTracking/CapacityEstimator.h" +#include "ITSMFTTracking/SlabBumpAllocator.h" +#include "ITSMFTTracking/Constants.h" +#include "ITSMFTTracking/MathUtils.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/IndexTableConfiguration.h" +#include "ITSMFTTracking/RefitDriver.h" +#include "ITSMFTTracking/Propagator.h" +#include "ITSMFTTracking/MaterialPhysics.h" +#include "ITSMFTTracking/detail/MFTFwdTrackHelpers.h" +#include "ITSMFTTracking/IndexTableUtils.h" +#include "ITSMFTTracking/LayerMask.h" +#include "ITSMFTTracking/TripletFitting.h" +#include "ITSMFTTracking/detail/TimeFrameScratch.h" +#include "ITSMFTTracking/TrackerTraits.h" +#include "ITSMFTTracking/detail/CandidateFinding.h" +#include "ReconstructionDataFormats/TrackParametrization.h" +#include "SimulationDataFormat/MCCompLabel.h" + +namespace o2::itsmft::tracking +{ + +namespace math_utils = o2::its::math_utils; +using o2::its::TimeEstBC; + +namespace +{ +constexpr uint8_t kCompatibilityAbsCharge = 1; +const o2::track::PID kCompatibilityPID = o2::track::PID::Pion; + +struct RoadSeedEmission { + TrackSeed seed; + int cellId{-1}; + int cellPathId{-1}; +}; + +void reserveGenericTrackPublication(TimeFrame& frame, std::size_t candidateCount, std::size_t maxReferencesPerTrack) +{ + auto& tracks = frame.getGenericTracks(); + auto& references = frame.getTrackClusterIndices(); + if (candidateCount > tracks.max_size() - tracks.size() || + (maxReferencesPerTrack != 0 && candidateCount > (references.max_size() - references.size()) / maxReferencesPerTrack)) { + throw std::length_error{"GenericTrack publication exceeds the output container capacity"}; + } + tracks.reserve(tracks.size() + candidateCount); + references.reserve(references.size() + candidateCount * maxReferencesPerTrack); +} + +bool appendGenericTrack(TimeFrame& frame, + const TrackingCandidate& candidate, + gsl::span> layerMeasurements) +{ + GenericTrack track = candidate.track; + track.hitLayers = {}; + std::vector resolvedReferences; + resolvedReferences.reserve(layerMeasurements.size()); + for (std::size_t position = 0; position < layerMeasurements.size(); ++position) { + const int localIndex = candidate.getClusterIndex(static_cast(position)); + if (localIndex == o2::its::constants::UnusedIndex) { + continue; + } + if (localIndex < 0 || static_cast(localIndex) >= layerMeasurements[position].size()) { + return false; + } + const auto& measurement = layerMeasurements[position][localIndex]; + const TrackClusterReference reference{LayerId{static_cast(position)}, 0, measurement.clusterId}; + if (!reference.isValid()) { + return false; + } + resolvedReferences.push_back(reference); + track.hitLayers.set(static_cast(position)); + } + if (!track.innerState.hasRecognizedKind() || !track.outerState.hasRecognizedKind() || + !track.timestamp.isValid() || resolvedReferences.empty()) { + return false; + } + + auto& tracks = frame.getGenericTracks(); + auto& references = frame.getTrackClusterIndices(); + const auto oldTrackSize = tracks.size(); + const auto oldReferenceSize = references.size(); + if (oldTrackSize > std::numeric_limits::max() || oldReferenceSize > std::numeric_limits::max() || + resolvedReferences.size() > std::numeric_limits::max() - oldReferenceSize) { + return false; + } + + try { + for (const auto& reference : resolvedReferences) { + references.push_back(reference); + } + track.firstClusterRef = static_cast(oldReferenceSize); + track.clusterRefEnd = static_cast(references.size()); + tracks.push_back(track); + } catch (...) { + references.resize(oldReferenceSize); + tracks.resize(oldTrackSize); + throw; + } + return true; +} + +// A static diamond vertex represents all primary vertices and has no event +// timestamp. Derive its envelope from the tested ROF's configured bounds; +// TimeEstBC cannot represent a full TimeFrame. The resulting timestamp is +// compatible by construction with that ROF. +template +Vertex diamondVertexForROF(const Vertex& base, const ROFOverlapView& rofOverlapView, int layer, int rofId) +{ + Vertex v = base; + v.setTimeStamp(rofOverlapView.getLayer(layer).getROFTimeBounds(rofId, true)); + return v; +} + +// Convert ROOT-visible parameters to the device-portable record once per iteration. +} // namespace + +void TrackerTraits::runTraversal(IterationContext& view) +{ + if (view.iteration < 0) { + throw TraversalException{view.iteration, TraversalFailureReason::IterationOutOfRange}; + } + int maxNvertices{-1}; + if (view.configuration.parameters.PerPrimaryVertexProcessing) { + maxNvertices = view.frame.getMaxVerticesPerROF(); + } + int iVertex = std::min(maxNvertices, 0); + do { + computeLayerTracklets(view, view.iteration, iVertex); + computeLayerCells(view, view.iteration); + findCellsNeighbours(view, view.iteration); + findRoads(view, view.iteration); + } while (++iVertex < maxNvertices); +} + +void TrackerTraits::computeLayerTracklets(IterationContext& context, const int iteration, int iVertex) +{ + auto& scratch = context.scratch; + const auto scratchEdgeCount = scratch.getTracklets().size(); + for (size_t edgeId = 0; edgeId < scratchEdgeCount; ++edgeId) { + scratch.getTracklets()[edgeId].clear(); + scratch.getTrackletsLabel(edgeId).clear(); + std::fill(scratch.getTrackletsLookupTable()[edgeId].begin(), scratch.getTrackletsLookupTable()[edgeId].end(), 0); + } + + const auto edgeIds = context.configuration.edgeIds(); + const auto& mMemoryPool = scratch.getMemoryPool(); + auto* mFrame = &context.frame; + const auto& trkParam = context.configuration.parameters; + const auto& mTraversalGraph = context.topology; + const auto& mKernelParameters = context.configuration.kernelParameters; + const auto& mLayerGlobalMeasurements = context.layerGlobalMeasurements; + const auto& topology = mTraversalGraph; + const Vertex diamondVert(trkParam.Diamond, trkParam.DiamondCov, 1, 1.f); + + mTaskArena->execute([&] { + auto forTracklets = [&](int fromLayer, int toLayer, SurfaceKind kind, + const TrackletProjectionCache& edgeCache, int pivotROF, auto&& emit) { + if (!mFrame->isROFEnabled(fromLayer, pivotROF)) { + return; + } + // Derive a diamond vertex for this pivot ROF; each invocation owns its + // stack frame, so this is safe inside the parallel dispatch. + Vertex diamondForROF{}; + gsl::span primaryVertices; + if (trkParam.UseDiamond) { + diamondForROF = diamondVertexForROF(diamondVert, mFrame->getROFViews(fromLayer).overlap, + mFrame->getROFLocalLayer(fromLayer), pivotROF); + primaryVertices = gsl::span(&diamondForROF, 1); + } else { + primaryVertices = mFrame->getPrimaryVertices(fromLayer, pivotROF); + } + if (primaryVertices.empty()) { + return; + } + const int startVtx = iVertex >= 0 ? iVertex : 0; + const int endVtx = iVertex >= 0 ? o2::gpu::CAMath::Min(iVertex + 1, int(primaryVertices.size())) : int(primaryVertices.size()); + if (endVtx <= startVtx || (iVertex + 1) > primaryVertices.size()) { + return; + } + + const auto& rofOverlap = mFrame->getROFOverlap(fromLayer, toLayer, pivotROF); + if (!rofOverlap.getEntries()) { + return; + } + + auto layer0 = mFrame->getClustersOnLayer(pivotROF, fromLayer); + if (layer0.empty()) { + return; + } + + for (int iCluster = 0; iCluster < int(layer0.size()); ++iCluster) { + const GlobalMeasurement& sourceMeasurement = layer0[iCluster]; + const int currentSortedIndex = mFrame->getSortedIndex(pivotROF, fromLayer, iCluster); + if (mFrame->isClusterUsed(fromLayer, sourceMeasurement.clusterId)) { + continue; + } + + for (int iV = startVtx; iV < endVtx; ++iV) { + const auto& pv = primaryVertices[iV]; + if (!mFrame->isVertexCompatible(fromLayer, pivotROF, pv)) { + continue; + } + if (pv.isFlagSet(Vertex::Flags::UPCMode) != trkParam.PassFlags[IterationStep::SelectUPCVertices]) { + continue; + } + const auto& indexTableUtils = mFrame->getIndexTableUtils(toLayer); + TrackletSearchWindow window{}; + if (!projectTrackletSearchWindow(sourceMeasurement, pv, mFrame->getBeamPositionVariance(), + kind, edgeCache, indexTableUtils, + mKernelParameters.nSigmaCut, window)) { + continue; + } + const auto bins = window.bins; + int rowBinsNum = bins.w - bins.y + 1; + if (rowBinsNum < 0) { + rowBinsNum += indexTableUtils.getNrowBins(); + } + rowBinsNum = std::max(0, rowBinsNum); + + for (int targetROF = rofOverlap.getFirstEntry(); targetROF < rofOverlap.getEntriesBound(); ++targetROF) { + if (!mFrame->isROFEnabled(toLayer, targetROF)) { + continue; + } + auto layer1 = mFrame->getClustersOnLayer(targetROF, toLayer); + if (layer1.empty()) { + continue; + } + const auto ts = mFrame->getROFTimeStamp(fromLayer, pivotROF, toLayer, targetROF); + if (!ts.isCompatible(pv.getTimeStamp())) { + continue; + } + const auto& targetIndexTable = mFrame->getIndexTable(targetROF, toLayer); + const int colBinRange = (bins.z - bins.x) + 1; + for (int iRow = 0; iRow < rowBinsNum; ++iRow) { + int iRowBin = bins.y + iRow; + iRowBin %= indexTableUtils.getNrowBins(); + if (iRowBin < 0 || iRowBin >= indexTableUtils.getNrowBins()) { + break; + } + const int firstBinIdx = indexTableUtils.getBinIndex(bins.x, iRowBin); + const int maxBinIdx = firstBinIdx + colBinRange; + const int firstRow = targetIndexTable[firstBinIdx]; + const int lastRow = targetIndexTable[maxBinIdx]; + for (int iNext = firstRow; iNext < lastRow; ++iNext) { + if (iNext >= int(layer1.size())) { + break; + } + const GlobalMeasurement& targetMeasurement = layer1[iNext]; + if (mFrame->isClusterUsed(toLayer, targetMeasurement.clusterId)) { + continue; + } + + const float targetReferenceCoordinate = kind == SurfaceKind::Cylinder ? targetMeasurement.radius : targetMeasurement.z; + const float targetProjectedCoordinate = kind == SurfaceKind::Cylinder ? targetMeasurement.z : targetMeasurement.radius; + const float referenceDelta = targetReferenceCoordinate - window.sourceReferenceCoordinate; + const float candidatePrediction = window.sourceProjectedCoordinate + window.slope * referenceDelta; + const float candidateVariance = window.varianceConstant + + referenceDelta * (window.varianceLinear + referenceDelta * window.varianceQuadratic); + const float projectedResidual = candidatePrediction - targetProjectedCoordinate; + const float phiResidual = std::remainder(window.phiPrediction - targetMeasurement.phi, o2::constants::math::TwoPI); + + if (!(candidateVariance > 0.f && window.phiVariance > 0.f)) { + continue; + } + const float chi2 = o2::its::math_utils::Sq(projectedResidual) / candidateVariance + + o2::its::math_utils::Sq(phiResidual) / window.phiVariance; + if (chi2 >= o2::its::math_utils::Sq(mKernelParameters.nSigmaCut)) { + continue; + } + const float deltaR = sourceMeasurement.radius - targetMeasurement.radius; + const float deltaZ = sourceMeasurement.z - targetMeasurement.z; + const float tanL = o2::its::math_utils::Sq(deltaR) > o2::constants::math::Almost0 ? deltaZ / deltaR : std::copysign(o2::constants::math::VeryBig, deltaZ); + const float phi{o2::gpu::GPUCommonMath::ATan2(sourceMeasurement.y - targetMeasurement.y, + sourceMeasurement.x - targetMeasurement.x)}; + emit(currentSortedIndex, mFrame->getSortedIndex(targetROF, toLayer, iNext), tanL, phi, ts); + } + } + } + } + } + }; + + const int maxConcurrency = std::max(1, mTaskArena->max_concurrency()); + const int nConcurrentSinks = std::min(static_cast(edgeIds.size()), maxConcurrency); + tbb::parallel_for(0, static_cast(edgeIds.size()), [&](const int edgeIndex) { + const auto edgeId = edgeIds[edgeIndex]; + const auto& edge = topology.getEdge(edgeId); + const int fromLayer = edge.from.value(); + const int toLayer = edge.to.value(); + const auto kind = topology.getSurface(edge.from).kind; + const auto& layerRadii = context.detectorConfiguration.layerRadii; + const TrackletProjectionCache edgeCache{ + fromLayer, toLayer, layerRadii[fromLayer], layerRadii[toLayer], + mFrame->getMinR(toLayer), mFrame->getMaxR(toLayer), + mFrame->getMinZ(toLayer), mFrame->getMaxZ(toLayer), + context.detectorConfiguration.positionResolutions[fromLayer], + scratch.getEdgeMSAngle(edgeId.value()), scratch.getEdgePhiCut(edgeId.value())}; + const int endROF = mFrame->getROFTiming(fromLayer).mNROFsTF; + const auto key = CapacityEstimator::makeKey(SlabSite::Tracklets, iteration, iVertex + 1, edgeId); + const auto scale = static_cast(mFrame->getClusters()[fromLayer].size()); + const auto capacity = mFrame->getCapacityEstimator().capacity(key, scale); + UnorderedSlabSink sink{{.capacity = capacity, .nThreads = maxConcurrency, .nConcurrentSinks = nConcurrentSinks}, mMemoryPool.get()}; + tbb::parallel_for(0, endROF, [&](const int pivotROF) { + auto& handle = sink.local(); + forTracklets(fromLayer, toLayer, kind, edgeCache, pivotROF, + [&handle](auto&&... args) { handle.emplace(std::forward(args)...); }); + }); + const auto stats = sink.stats(); + sink.finalizeUnordered(scratch.getTracklets()[edgeId.value()]); + mFrame->getCapacityEstimator().update(key, scale, stats.requested, stats.capacity, stats.emitted, + stats.spilled, stats.overflowed, stats.memoryLimited); + }); + + tbb::parallel_for(0, static_cast(edgeIds.size()), [&](const int edgeIndex) { + const auto edgeId = edgeIds[edgeIndex]; + /// Sort tracklets & remove duplicates + // duplicates can exist simply since we evaluate per vertex + auto& trkl{scratch.getTracklets()[edgeId.value()]}; + std::sort(trkl.begin(), trkl.end()); + trkl.erase(std::unique(trkl.begin(), trkl.end()), trkl.end()); + trkl.shrink_to_fit(); + auto& lut{scratch.getTrackletsLookupTable()[edgeId.value()]}; + if (!trkl.empty()) { + for (const auto& tkl : trkl) { + lut[tkl.firstClusterIndex + 1]++; + } + std::inclusive_scan(lut.begin(), lut.end(), lut.begin()); + } + }); + + /// Create tracklets labels + if (mFrame->hasMCinformation() && trkParam.CreateArtefactLabels) { + tbb::parallel_for(0, static_cast(edgeIds.size()), [&](const int edgeIndex) { + const auto edgeId = edgeIds[edgeIndex]; + const auto& edge = topology.getEdge(edgeId); + const int fromLayer = edge.from.value(); + const int toLayer = edge.to.value(); + for (auto& trk : scratch.getTracklets()[edgeId.value()]) { + MCCompLabel label; + const auto currentId = mFrame->getClusters()[fromLayer][trk.firstClusterIndex].clusterId; + const auto nextId = mFrame->getClusters()[toLayer][trk.secondClusterIndex].clusterId; + for (const auto& lab1 : mFrame->getLabels(LayerId{static_cast(fromLayer)}, currentId)) { + for (const auto& lab2 : mFrame->getLabels(LayerId{static_cast(toLayer)}, nextId)) { + if (lab1 == lab2 && lab1.isValid()) { + label = lab1; + break; + } + } + if (label.isValid()) { + break; + } + } + scratch.getTrackletsLabel(edgeId.value()).emplace_back(label); + } + }); + } + }); +} + +void TrackerTraits::computeLayerCells(IterationContext& context, const int iteration) +{ + auto& scratch = context.scratch; + const auto scratchCellCount = scratch.getCells().size(); + for (size_t cellPathId = 0; cellPathId < scratchCellCount; ++cellPathId) { + deepVectorClear(scratch.getCells()[cellPathId]); + deepVectorClear(scratch.getCellsLookupTable()[cellPathId]); + if (context.frame.hasMCinformation() && context.configuration.parameters.CreateArtefactLabels) { + deepVectorClear(scratch.getCellsLabel(cellPathId)); + } + } + + const auto cellIds = context.configuration.cellIds(); + const auto& mMemoryPool = scratch.getMemoryPool(); + const auto& trkParam = context.configuration.parameters; + const auto mBz = context.bz; + const auto& mTraversalGraph = context.topology; + const auto& mKernelParameters = context.configuration.kernelParameters; + const auto& mLayerGlobalMeasurements = context.layerGlobalMeasurements; + const auto& topology = mTraversalGraph; + + mTaskArena->execute([&] { + auto forTrackletCells = [&](int firstEdgeId, int secondEdgeId, const std::array& hitLayers, int iTracklet, auto&& emit) { + const Tracklet& currentTracklet{scratch.getTracklets()[firstEdgeId][iTracklet]}; + const int nextLayerClusterIndex{currentTracklet.secondClusterIndex}; + const int nextLayerFirstTrackletIndex{scratch.getTrackletsLookupTable()[secondEdgeId][nextLayerClusterIndex]}; + const int nextLayerLastTrackletIndex{scratch.getTrackletsLookupTable()[secondEdgeId][nextLayerClusterIndex + 1]}; + for (int iNextTracklet{nextLayerFirstTrackletIndex}; iNextTracklet < nextLayerLastTrackletIndex; ++iNextTracklet) { + const Tracklet& nextTracklet{scratch.getTracklets()[secondEdgeId][iNextTracklet]}; + if (nextTracklet.firstClusterIndex != nextLayerClusterIndex) { + break; + } + if (!currentTracklet.getTimeStamp().isCompatible(nextTracklet.getTimeStamp())) { + continue; + } + + /// Prepare the track seed; clusters are numbered from inner to outer. + const int sortedId[3]{currentTracklet.firstClusterIndex, nextTracklet.firstClusterIndex, nextTracklet.secondClusterIndex}; + + const float edgeMSAngle = scratch.getEdgeMSAngle(secondEdgeId); + const float angularTolerance = mKernelParameters.nSigmaCut * edgeMSAngle; + const float lambda01 = std::atan(currentTracklet.tanLambda); + const float lambda12 = std::atan(nextTracklet.tanLambda); + const float deltaLambda = std::abs(lambda01 - lambda12); + if (deltaLambda > angularTolerance) { + continue; + } + + const auto& inner = mLayerGlobalMeasurements[hitLayers[0]][sortedId[0]]; + const auto& middle = mLayerGlobalMeasurements[hitLayers[1]][sortedId[1]]; + const auto& outer = mLayerGlobalMeasurements[hitLayers[2]][sortedId[2]]; + const float length01 = std::hypot(inner.x - middle.x, inner.y - middle.y); + const float length12 = std::hypot(middle.x - outer.x, middle.y - outer.y); + const float maximumCurvature = std::min({std::abs(o2::constants::math::B2C * mBz) / + mKernelParameters.trackletMinPt, + 2.f / length01, + 2.f / length12}); + const float maximumBending = + std::asin(std::clamp(0.5f * maximumCurvature * length01, 0.f, 1.f)) + + std::asin(std::clamp(0.5f * maximumCurvature * length12, 0.f, 1.f)); + const float deltaPhi = std::abs(std::remainder(currentTracklet.phi - nextTracklet.phi, + o2::constants::math::TwoPI)); + const float sinTheta = std::max(std::abs(std::cos(0.5f * (lambda01 + lambda12))), + o2::constants::math::Almost0); + const float azimuthalTolerance = angularTolerance / sinTheta; + if (deltaPhi > maximumBending + azimuthalTolerance) { + continue; + } + + const std::array measurements{inner, middle, outer}; + TripletFitFactor tripletFactor{}; + if (makeTripletFitFactor(measurements, tripletFactor)) { + TimeEstBC ts = currentTracklet.getTimeStamp(); + ts += nextTracklet.getTimeStamp(); + // Build directly from the resolved plan positions; plan validation + // already checked them against the cell's hit-surface mask. + const LayerMask hitLayerMask{hitLayers[0], hitLayers[1], hitLayers[2]}; + CellSeed seed{hitLayerMask, sortedId[0], sortedId[1], sortedId[2], iTracklet, iNextTracklet, ts}; + seed.tripletFactor() = tripletFactor; + emit(std::move(seed)); + } + } + }; + + const int maxConcurrency = std::max(1, mTaskArena->max_concurrency()); + for (const auto cellId : cellIds) { + const auto& cellTopology = topology.getPath(cellId); + const auto firstEdgeId = cellTopology.first; + const auto secondEdgeId = cellTopology.second; + if (scratch.getTracklets()[firstEdgeId.value()].empty() || + scratch.getTracklets()[secondEdgeId.value()].empty()) { + continue; + } + + const auto& firstEdge = topology.getEdge(cellTopology.first); + const auto& secondEdge = topology.getEdge(cellTopology.second); + const std::array layers{firstEdge.from.value(), firstEdge.to.value(), secondEdge.to.value()}; + + auto& layerCells = scratch.getCells()[cellId.value()]; + auto& lut = scratch.getCellsLookupTable()[cellId.value()]; + const int currentLayerTrackletsNum{static_cast(scratch.getTracklets()[firstEdgeId.value()].size())}; + const auto key = CapacityEstimator::makeKey(SlabSite::Cells, iteration, 0, cellId); + const auto scale = static_cast(currentLayerTrackletsNum); + const auto capacity = context.frame.getCapacityEstimator().capacity(key, scale); + GroupedSlabSink sink{{.capacity = capacity, .nThreads = maxConcurrency}, mMemoryPool.get()}; + tbb::parallel_for(0, currentLayerTrackletsNum, [&](const int iTracklet) { + auto& handle = sink.local(); + handle.beginProducer(iTracklet); + forTrackletCells(firstEdgeId.value(), secondEdgeId.value(), layers, iTracklet, + [&handle](CellSeed seed) { handle.emplace(std::move(seed)); }); + }); + const auto stats = sink.stats(); + sink.finalizeGrouped(static_cast(currentLayerTrackletsNum), lut, layerCells); + context.frame.getCapacityEstimator().update(key, scale, stats.requested, stats.capacity, stats.emitted, + stats.spilled, stats.overflowed, stats.memoryLimited); + + if (context.frame.hasMCinformation() && trkParam.CreateArtefactLabels) { + auto& labels = scratch.getCellsLabel(cellId.value()); + labels.reserve(layerCells.size()); + for (const auto& cell : layerCells) { + MCCompLabel currentLab{scratch.getTrackletsLabel(firstEdgeId.value())[cell.getFirstTrackletIndex()]}; + MCCompLabel nextLab{scratch.getTrackletsLabel(secondEdgeId.value())[cell.getSecondTrackletIndex()]}; + labels.emplace_back(currentLab == nextLab ? currentLab : MCCompLabel()); + } + } + } + }); + + const auto scratchEdgeCount = scratch.getTracklets().size(); + for (size_t edgeId = 0; edgeId < scratchEdgeCount; ++edgeId) { + deepVectorClear(scratch.getTracklets()[edgeId]); + deepVectorClear(scratch.getTrackletsLabel(edgeId)); + } +} + +void TrackerTraits::findCellsNeighbours(IterationContext& context, const int iteration) +{ + auto& scratch = context.scratch; + const auto& memoryPool = scratch.getMemoryPool(); + const auto& topology = context.topology; + const auto& globalMeasurements = context.layerGlobalMeasurements; + const auto& params = context.configuration.kernelParameters; + for (std::size_t slot = 0; slot < scratch.getCellsNeighbours().size(); ++slot) { + deepVectorClear(scratch.getCellsNeighbours()[slot]); + deepVectorClear(scratch.getCellsNeighboursTopology()[slot]); + deepVectorClear(scratch.getCellsNeighboursLUT()[slot]); + } + const auto& scheduledCells = context.configuration.topology.scheduledPaths; + const auto scratchCellCount = scratch.getCells().size(); + if (scratch.getCellsLookupTable().size() != scratchCellCount || + scratch.getCellsNeighbours().size() != scratchCellCount || + scratch.getCellsNeighboursTopology().size() != scratchCellCount || + scratch.getCellsNeighboursLUT().size() != scratchCellCount) { + throw TraversalException{iteration, TraversalFailureReason::SparseTopologyMismatch}; + } + mTaskArena->execute([&] { + std::vector> cellsNeighboursByTarget; + cellsNeighboursByTarget.reserve(scratchCellCount); + for (size_t cellPathId = 0; cellPathId < scratchCellCount; ++cellPathId) { + cellsNeighboursByTarget.emplace_back(memoryPool.get()); + } + + for (const auto cellId : scheduledCells) { + if (static_cast(cellId.value()) >= scratchCellCount || + static_cast(cellId.value()) >= scratch.getCellsLookupTable().size()) { + throw TraversalException{iteration, TraversalFailureReason::SparseTopologyMismatch}; + } + const auto& cellTopology = topology.getPath(cellId); + const float currentMSAngle = scratch.getEdgeMSAngle(cellTopology.second.value()); + const float currentAngularVariance = currentMSAngle * currentMSAngle; + if (scratch.getCells()[cellId.value()].empty()) { + continue; + } + const auto successors = topology.getPathsStartingWithEdge(cellTopology.second); + if (!successors.getEntries()) { + continue; + } + + struct SuccessorBinding { + CellPathId cellId; + float angularVariance; + }; + std::array successorBindings{}; + size_t successorBindingCount = 0; + if (successors.getEntries() > successorBindings.size()) { + throw TraversalException{iteration, TraversalFailureReason::SparseTopologyMismatch}; + } + for (uint32_t iSuccessor = 0; iSuccessor < successors.getEntries(); ++iSuccessor) { + const auto nextCellId = topology.pathsByFirstEdge[successors.getFirstEntry() + iSuccessor]; + if (static_cast(nextCellId.value()) >= scratch.getCells().size() || + static_cast(nextCellId.value()) >= scratch.getCellsLookupTable().size()) { + throw TraversalException{iteration, TraversalFailureReason::SparseTopologyMismatch}; + } + if (scratch.getCells()[nextCellId.value()].empty() || + scratch.getCellsLookupTable()[nextCellId.value()].empty()) { + continue; + } + const auto& nextCellTopology = topology.getPath(nextCellId); + const float nextMSAngle = scratch.getEdgeMSAngle(nextCellTopology.second.value()); + successorBindings[successorBindingCount++] = {nextCellId, nextMSAngle * nextMSAngle}; + } + + const int maxConcurrency = std::max(1, mTaskArena->max_concurrency()); + const auto key = CapacityEstimator::makeKey(SlabSite::Neighbours, iteration, 0, cellId); + const auto scale = static_cast(scratch.getCells()[cellId.value()].size()); + const auto capacity = context.frame.getCapacityEstimator().capacity(key, scale); + UnorderedSlabSink sink{{.capacity = capacity, .nThreads = maxConcurrency}, memoryPool.get()}; + tbb::parallel_for(0, static_cast(scratch.getCells()[cellId.value()].size()), [&](const int iCell) { + auto& handle = sink.local(); + const auto& currentCellSeed{scratch.getCells()[cellId.value()][iCell]}; + const int nextLayerTrackletIndex{currentCellSeed.getSecondTrackletIndex()}; + for (size_t iSuccessor = 0; iSuccessor < successorBindingCount; ++iSuccessor) { + const auto& successor = successorBindings[iSuccessor]; + const auto& nextCellLUT = scratch.getCellsLookupTable()[successor.cellId.value()]; + if (nextLayerTrackletIndex < 0 || nextLayerTrackletIndex + 1 >= static_cast(nextCellLUT.size())) { + continue; + } + const int nextLayerFirstCellIndex{nextCellLUT[nextLayerTrackletIndex]}; + const int nextLayerLastCellIndex{nextCellLUT[nextLayerTrackletIndex + 1]}; + if (nextLayerFirstCellIndex < 0 || nextLayerLastCellIndex < nextLayerFirstCellIndex || + nextLayerLastCellIndex > static_cast(scratch.getCells()[successor.cellId.value()].size())) { + throw TraversalException{iteration, TraversalFailureReason::SparseTopologyMismatch}; + } + for (int iNextCell{nextLayerFirstCellIndex}; iNextCell < nextLayerLastCellIndex; ++iNextCell) { + const auto& nextCellSeedRef{scratch.getCells()[successor.cellId.value()][iNextCell]}; + if (nextCellSeedRef.getFirstTrackletIndex() != nextLayerTrackletIndex || !currentCellSeed.getTimeStamp().isCompatible(nextCellSeedRef.getTimeStamp())) { + break; + } + + const auto currentMiddle = currentCellSeed.getClusterReference(1); + const auto currentOuter = currentCellSeed.getClusterReference(2); + const auto nextInner = nextCellSeedRef.getClusterReference(0); + const auto nextMiddle = nextCellSeedRef.getClusterReference(1); + if (currentMiddle.surfacePosition != nextInner.surfacePosition || + currentMiddle.clusterIndex != nextInner.clusterIndex || + currentOuter.surfacePosition != nextMiddle.surfacePosition || + currentOuter.clusterIndex != nextMiddle.clusterIndex) { + continue; + } + + const std::array references{ + currentCellSeed.getClusterReference(0), currentMiddle, + currentOuter, nextCellSeedRef.getClusterReference(2)}; + std::array measurements{}; + bool measurementsValid = true; + for (std::size_t hit = 0; hit < references.size(); ++hit) { + const auto reference = references[hit]; + if (reference.surfacePosition < 0 || + static_cast(reference.surfacePosition) >= globalMeasurements.size() || + reference.clusterIndex < 0 || + static_cast(reference.clusterIndex) >= globalMeasurements[reference.surfacePosition].size()) { + measurementsValid = false; + break; + } + measurements[hit] = globalMeasurements[reference.surfacePosition][reference.clusterIndex]; + } + AdjacentTripletFitResult adjacentFit{}; + const bool fitValid = measurementsValid && + fitAdjacentTripletFactors( + currentCellSeed.tripletFactor(), nextCellSeedRef.tripletFactor(), measurements, + {currentAngularVariance, successor.angularVariance}, adjacentFit); + if (!fitValid || adjacentFit.chi2 > params.maxChi2ClusterAttachment) { + continue; + } + + const int nextLevel = currentCellSeed.getLevel() + 1; + handle.emplace(cellId.value(), iCell, successor.cellId.value(), iNextCell, nextLevel); + } + } + }); + + const auto stats = sink.stats(); + bounded_vector sourceNeighbours{memoryPool.get()}; + sink.finalizeUnordered(sourceNeighbours); + context.frame.getCapacityEstimator().update(key, scale, stats.requested, stats.capacity, stats.emitted, + stats.spilled, stats.overflowed, stats.memoryLimited); + std::sort(sourceNeighbours.begin(), sourceNeighbours.end(), [](const auto& a, const auto& b) { + return std::tie(a.nextCellTopology, a.nextCell, a.cellTopology, a.cell) < + std::tie(b.nextCellTopology, b.nextCell, b.cellTopology, b.cell); + }); + for (const auto& neighbour : sourceNeighbours) { + cellsNeighboursByTarget[neighbour.nextCellTopology].push_back(neighbour); + if (neighbour.level > scratch.getCells()[neighbour.nextCellTopology][neighbour.nextCell].getLevel()) { + scratch.getCells()[neighbour.nextCellTopology][neighbour.nextCell].setLevel(neighbour.level); + } + } + } + + for (size_t cellPathId = 0; cellPathId < scratchCellCount; ++cellPathId) { + auto& cellsNeighbours = cellsNeighboursByTarget[cellPathId]; + if (cellsNeighbours.empty()) { + continue; + } + + std::sort(cellsNeighbours.begin(), cellsNeighbours.end(), [](const auto& a, const auto& b) { + return std::tie(a.nextCell, a.cellTopology, a.cell) < std::tie(b.nextCell, b.cellTopology, b.cell); + }); + + auto& cellsNeighbourLUT = scratch.getCellsNeighboursLUT()[cellPathId]; + cellsNeighbourLUT.assign(scratch.getCells()[cellPathId].size(), 0); + for (const auto& neigh : cellsNeighbours) { + ++cellsNeighbourLUT[neigh.nextCell]; + } + std::inclusive_scan(cellsNeighbourLUT.begin(), cellsNeighbourLUT.end(), cellsNeighbourLUT.begin()); + + scratch.getCellsNeighbours()[cellPathId].reserve(cellsNeighbours.size()); + scratch.getCellsNeighboursTopology()[cellPathId].reserve(cellsNeighbours.size()); + std::ranges::transform(cellsNeighbours, std::back_inserter(scratch.getCellsNeighbours()[cellPathId]), [](const auto& neigh) { return neigh.cell; }); + std::ranges::transform(cellsNeighbours, std::back_inserter(scratch.getCellsNeighboursTopology()[cellPathId]), [](const auto& neigh) { return neigh.cellTopology; }); + } + }); + for (auto& cellLUT : scratch.getCellsLookupTable()) { + deepVectorClear(cellLUT); + } +} + +bool TrackerTraits::buildTrackSeed(IterationContext& context, int, + const CellSeed& cell, TrackSeed& output, + OperationFailureReason& reason) const +{ + std::array globals{}; + std::array measurements{}; + std::array surfaces{}; + for (int hit = 0; hit < 3; ++hit) { + const auto reference = cell.getClusterReference(hit); + const auto surface = LayerId{static_cast(reference.surfacePosition)}; + globals[hit] = &context.layerGlobalMeasurements[reference.surfacePosition][reference.clusterIndex]; + measurements[hit] = context.frame.getSurfaceMeasurement(surface, globals[hit]->clusterId); + surfaces[hit] = &context.topology.getSurface(surface); + } + + SurfaceTrackState state{}; + float chi2{0.f}; + const auto& outer = *measurements[2]; + const auto kind = surfaces[2]->kind; + + float sinPhi = 0.f, cosPhi = 0.f, tanLambda = 0.f, qOverPt = 1.f / o2::track::kMostProbablePt; + float curvatureSquared = 1.f; + + state.referenceCoordinate = outer.frame.q; + state.alpha = (kind == SurfaceKind::Cylinder) ? outer.frame.frameAngle : 0.f; + state.parameters[0] = outer.frame.u; + state.parameters[1] = outer.frame.v; + + float cosAlpha, sinAlpha, x[3], y[3]; + o2::math_utils::detail::sincos(state.alpha, sinAlpha, cosAlpha); + for (int i{0}; i < 3; ++i) { + const auto& pos = globals[i]->position; + x[i] = pos.x * cosAlpha + pos.y * sinAlpha; + y[i] = -pos.x * sinAlpha + pos.y * cosAlpha; + } + const float dx = x[2] - x[1]; + const float dy = y[2] - y[1]; + const float chordLength = std::hypot(dx, dy); + const float inverseLength = 1.f / chordLength; + + const float chordCos = dx * inverseLength; + const float chordSin = dy * inverseLength; + tanLambda = -0.5f * + (math_utils::computeTanDipAngle(x[0], y[0], x[1], y[1], globals[0]->position.z, globals[1]->position.z) + + math_utils::computeTanDipAngle(x[1], y[1], x[2], y[2], globals[1]->position.z, globals[2]->position.z)); + + if (std::abs(context.bz) < 0.01f) { + cosPhi = chordCos; + sinPhi = chordSin; + } else { + const float curvature = + math_utils::computeCurvature( + x[2], y[2], x[1], y[1], x[0], y[0]); + + const float halfSin = 0.5f * curvature * chordLength; + const float halfCos = + std::sqrt((1.f - halfSin) * (1.f + halfSin)); + + cosPhi = chordCos * halfCos - chordSin * halfSin; + sinPhi = chordSin * halfCos + chordCos * halfSin; + qOverPt = curvature / + (context.bz * o2::constants::math::B2C); + curvatureSquared = curvature * curvature; + } + + float phi = o2::gpu::GPUCommonMath::ASin(sinPhi); + if (cosPhi < 0.f) { + phi = o2::constants::math::PI - phi; + } else if (phi < 0.f) { + phi += o2::constants::math::TwoPI; + } + + state.parameters[2] = (kind == SurfaceKind::Cylinder) ? sinPhi : phi; + state.parameters[3] = tanLambda; + state.parameters[4] = qOverPt; + state.covariance[packedCovarianceIndex(0, 0)] = outer.covariance.uu; + state.covariance[packedCovarianceIndex(1, 0)] = outer.covariance.uv; + state.covariance[packedCovarianceIndex(1, 1)] = outer.covariance.vv; + state.covariance[packedCovarianceIndex(2, 2)] = (kind == SurfaceKind::Cylinder) ? o2::track::kCSnp2max : o2::track::kCSnp2max / (cosPhi * cosPhi); + state.covariance[packedCovarianceIndex(3, 3)] = o2::track::kCTgl2max; + state.covariance[packedCovarianceIndex(4, 4)] = o2::track::kC1Pt2max * std::clamp(curvatureSquared, 0.0005f, 1.f); + + state.kind = kind; + state.flags = 0; + state.absCharge = kCompatibilityAbsCharge; + state.pid = kCompatibilityPID; + + const std::array attachmentMeasurements{measurements[1], measurements[0]}; + const std::array attachmentSurfaces{surfaces[1], surfaces[0]}; + for (int step = 0; step < 2; ++step) { + const auto& targetSurface = *attachmentSurfaces[step]; + if (!Propagator::attachMeasurement( + state, targetSurface, *attachmentMeasurements[step], context.bz, + material::MaterialTraversalDirection::OppositeMomentum, + step == 1, + context.configuration.kernelParameters.maxChi2ClusterAttachment, + chi2, reason)) { + return false; + } + } + + output = TrackSeed{cell, state, chi2}; + return true; +} + +template +void TrackerTraits::processNeighbours(IterationContext& context, int iteration, CellPathId startingPath, + int defaultCellPathId, int startLevel, int currentLevel, + const bounded_vector& currentCellSeed, + const bounded_vector& currentCellId, + const bounded_vector& currentCellPathId, + bounded_vector& updatedCellSeeds, + bounded_vector& updatedCellsIds, + bounded_vector& updatedCellsPathIds, + const TrackingKernelParameters& params) +{ + auto* scratch = &context.scratch; + const auto& mMemoryPool = scratch->getMemoryPool(); + const auto mBz = context.bz; + const auto& mLayerGlobalMeasurements = context.layerGlobalMeasurements; + const int activeSurfaceCount = context.configuration.topology.nLayers; + + mTaskArena->execute([&] { + auto forCellNeighbours = [&](int iCell, auto&& emit) { + const auto& currentCell{currentCellSeed[iCell]}; + const int cellPathId = currentCellPathId.empty() ? defaultCellPathId : currentCellPathId[iCell]; + + if (currentCell.getLevel() != currentLevel) { + return; + } + if (currentCellId.empty()) { + for (int layer = 0; layer < activeSurfaceCount; ++layer) { + const int clusterIndex = currentCell.getCluster(layer); + if (clusterIndex != o2::its::constants::UnusedIndex && + context.frame.isClusterUsed(layer, mLayerGlobalMeasurements[layer][clusterIndex].clusterId)) { + return; + } + } + } + + const int cellId = currentCellId.empty() ? iCell : currentCellId[iCell]; + if (cellPathId < 0 || scratch->getCellsNeighboursLUT()[cellPathId].empty()) { + return; + } + const int startNeighbourId{cellId ? scratch->getCellsNeighboursLUT()[cellPathId][cellId - 1] : 0}; + const int endNeighbourId{scratch->getCellsNeighboursLUT()[cellPathId][cellId]}; + TrackSeed baseSeed{}; + if constexpr (std::is_same_v) { + OperationFailureReason buildReason{}; + if (!buildTrackSeed(context, cellPathId, currentCell, baseSeed, buildReason)) { + return; + } + } else { + baseSeed = currentCell; + } + for (int iNeighbourCell{startNeighbourId}; iNeighbourCell < endNeighbourId; ++iNeighbourCell) { + const int neighbourCellPathId = scratch->getCellsNeighboursTopology()[cellPathId][iNeighbourCell]; + const int neighbourCellId = scratch->getCellsNeighbours()[cellPathId][iNeighbourCell]; + const auto& neighbourCell = scratch->getCells()[neighbourCellPathId][neighbourCellId]; + if (neighbourCell.getSecondTrackletIndex() != currentCell.getFirstTrackletIndex()) { + continue; + } + if (!currentCell.getTimeStamp().isCompatible(neighbourCell.getTimeStamp())) { + continue; + } + if (currentCell.getLevel() - 1 != neighbourCell.getLevel()) { + continue; + } + const int neighbourLayer = neighbourCell.getInnerLayer(); + if (neighbourLayer < 0 || neighbourLayer >= activeSurfaceCount) { + throw TraversalException{iteration, TraversalFailureReason::SparseTopologyMismatch}; + } + const int neighbourCluster = neighbourCell.getFirstClusterIndex(); + const auto& neighbourGlobal = mLayerGlobalMeasurements[neighbourLayer][neighbourCluster]; + if (context.frame.isClusterUsed(neighbourLayer, neighbourGlobal.clusterId)) { + continue; + } + + /// Let's start the fitting procedure + TrackSeed seed{baseSeed}; + seed.getTimeStamp() = currentCell.getTimeStamp(); + seed.getTimeStamp() += neighbourCell.getTimeStamp(); + + const auto* measurement = context.frame.getSurfaceMeasurement(LayerId{static_cast(neighbourLayer)}, neighbourGlobal.clusterId); + if (measurement == nullptr) { + continue; + } + float chi2 = seed.getChi2(); + OperationFailureReason attachReason{}; + const bool attached = Propagator::attachMeasurement(seed.state(), context.topology.getSurface(LayerId{static_cast(neighbourLayer)}), *measurement, mBz, + material::MaterialTraversalDirection::OppositeMomentum, true, + params.maxChi2ClusterAttachment, chi2, attachReason); + if (!attached) { + continue; + } + seed.setChi2(chi2); + + seed.setCluster(neighbourLayer, neighbourCluster); + auto hitLayerMask = seed.getHitLayerMask(); + hitLayerMask.set(neighbourLayer); + seed.setHitLayerMask(hitLayerMask); + seed.setLevel(neighbourCell.getLevel()); + seed.setFirstTrackletIndex(neighbourCell.getFirstTrackletIndex()); + seed.setSecondTrackletIndex(neighbourCell.getSecondTrackletIndex()); + emit(RoadSeedEmission{std::move(seed), neighbourCellId, neighbourCellPathId}); + } + }; + + const int nCells = static_cast(currentCellSeed.size()); + const auto key = CapacityEstimator::makeKey(SlabSite::Roads, iteration, + CapacityEstimator::makeVariant(startLevel, currentLevel), + startingPath); + const auto scale = static_cast(nCells); + const auto capacity = context.frame.getCapacityEstimator().capacity(key, scale); + GroupedSlabSink sink{{.capacity = capacity, .nThreads = std::max(1, mTaskArena->max_concurrency())}, mMemoryPool.get()}; + tbb::parallel_for(0, nCells, [&](const int iCell) { + auto& handle = sink.local(); + handle.beginProducer(iCell); + forCellNeighbours(iCell, [&handle](RoadSeedEmission emission) { handle.emplace(std::move(emission)); }); + }); + const auto stats = sink.stats(); + bounded_vector lut{mMemoryPool.get()}; + bounded_vector emissions{mMemoryPool.get()}; + sink.finalizeGrouped(static_cast(nCells), lut, emissions); + context.frame.getCapacityEstimator().update(key, scale, stats.requested, stats.capacity, stats.emitted, + stats.spilled, stats.overflowed, stats.memoryLimited); + updatedCellSeeds.reserve(emissions.size()); + updatedCellsIds.reserve(emissions.size()); + updatedCellsPathIds.reserve(emissions.size()); + for (auto& emission : emissions) { + updatedCellSeeds.push_back(std::move(emission.seed)); + updatedCellsIds.push_back(emission.cellId); + updatedCellsPathIds.push_back(emission.cellPathId); + } + }); +} + +void TrackerTraits::findRoads(IterationContext& context, const int iteration) +{ + auto* scratch = &context.scratch; + const auto& mMemoryPool = scratch->getMemoryPool(); + const auto& trkParam = context.configuration.parameters; + const auto mBz = context.bz; + const auto& mTraversalGraph = context.topology; + const auto& mKernelParameters = context.configuration.kernelParameters; + const auto& mLayerGlobalMeasurements = context.layerGlobalMeasurements; + const gsl::span roadStartCells = context.configuration.topology.roadStartPaths; + const int activeSurfaceCount = context.configuration.topology.nLayers; + bounded_vector> firstClusters(activeSurfaceCount, bounded_vector(mMemoryPool.get()), mMemoryPool.get()); + firstClusters.resize(activeSurfaceCount); + // Road starts are the binding's seeding-eligible sparse-plan subsequence. + // CellPathId values use compact slots; LayerId directly indexes layout-owned + // layer data. + // Filter roads by absolute q/pT in parameters[4]'s units, identically for + // both families. Non-finite values fail the finite-bound comparison. + constexpr float maxAbsQOverPt = 1.e3f; + const auto seedingLayerMask = context.topology.seedingLayers; + const auto nonSeedingLayerMask = ~seedingLayerMask; + const int cellsPerRoad = seedingLayerMask.count() - 2; + const auto& componentOffsets = context.configuration.topology.roadStartComponentOffsets; + const auto holeLayerMask = context.frame.getLayout().getHoleLayers(); + if (componentOffsets.empty() || componentOffsets.front() != 0 || componentOffsets.back() != roadStartCells.size()) { + throw TraversalException{iteration, TraversalFailureReason::SparseTopologyMismatch}; + } + for (size_t component = 0; component + 1 < componentOffsets.size(); ++component) { + const auto componentRoadStarts = roadStartCells.subspan(componentOffsets[component], + componentOffsets[component + 1] - componentOffsets[component]); + for (int startLevel{cellsPerRoad}; startLevel >= trkParam.CellMinimumLevel(); --startLevel) { + + auto seedFilter = [&](const auto& seed) { + const auto hitLayerMask = seed.getHitLayerMask(); + const int effectiveTrackLength = hitLayerMask.empty() + ? 0 + : hitLayerMask.length() - (LayerMask::span(hitLayerMask.first(), hitLayerMask.last()) & nonSeedingLayerMask).count(); + const auto effectiveHoleMask = hitLayerMask.holeMask() & ~nonSeedingLayerMask; + return effectiveHoleMask.isAllowedHoleMask(trkParam.MaxHoles, holeLayerMask) && + effectiveTrackLength >= trkParam.getMinSeedingClusters() && + std::abs(seed.getQOverPt()) <= maxAbsQOverPt && seed.getChi2() <= trkParam.MaxChi2NDF * ((startLevel + 2) * 2 - 5); + }; + + bounded_vector trackSeeds(mMemoryPool.get()); + // The binding supplies the ownership-filtered road-start span. + for (const auto startId : componentRoadStarts) { + // Cell population is per-event/per-vertex data, so check it against + // the current vertex rather than caching it in the pass plan. + if (scratch->getCells()[startId.value()].empty()) { + continue; + } + + bounded_vector lastCellId(mMemoryPool.get()), updatedCellId(mMemoryPool.get()); + bounded_vector lastCellPathId(mMemoryPool.get()), updatedCellPathId(mMemoryPool.get()); + bounded_vector lastCellSeed(mMemoryPool.get()), updatedCellSeed(mMemoryPool.get()); + + processNeighbours(context, iteration, startId, startId.value(), startLevel, startLevel, + scratch->getCells()[startId.value()], lastCellId, lastCellPathId, + updatedCellSeed, updatedCellId, updatedCellPathId, mKernelParameters); + + int level = startLevel; + while (level > 2 && !updatedCellSeed.empty()) { + lastCellSeed.swap(updatedCellSeed); + lastCellId.swap(updatedCellId); + lastCellPathId.swap(updatedCellPathId); + deepVectorClear(updatedCellSeed); /// tame the memory peaks + deepVectorClear(updatedCellId); /// tame the memory peaks + deepVectorClear(updatedCellPathId); + --level; + processNeighbours(context, iteration, startId, o2::its::constants::UnusedIndex, startLevel, level, + lastCellSeed, lastCellId, lastCellPathId, + updatedCellSeed, updatedCellId, updatedCellPathId, mKernelParameters); + } + deepVectorClear(lastCellId); /// tame the memory peaks + deepVectorClear(lastCellPathId); /// tame the memory peaks + deepVectorClear(lastCellSeed); /// tame the memory peaks + + if (!updatedCellSeed.empty()) { + trackSeeds.reserve(trackSeeds.size() + std::count_if(updatedCellSeed.begin(), updatedCellSeed.end(), seedFilter)); + std::copy_if(updatedCellSeed.begin(), updatedCellSeed.end(), std::back_inserter(trackSeeds), seedFilter); + } + } + + if (trackSeeds.empty()) { + continue; + } + + bounded_vector tracks(mMemoryPool.get()); + mTaskArena->execute([&] { + const int nSeeds = static_cast(trackSeeds.size()); + const auto key = CapacityEstimator::makeKey(SlabSite::Tracks, iteration, + CapacityEstimator::makeVariant(startLevel, static_cast(component)), 0); + const auto scale = static_cast(nSeeds); + const auto capacity = context.frame.getCapacityEstimator().capacity(key, scale); + GroupedSlabSink sink{{.capacity = capacity, .nThreads = std::max(1, mTaskArena->max_concurrency())}, mMemoryPool.get()}; + tbb::parallel_for(0, nSeeds, [&](const int iSeed) { + SurfaceTrackState innerState{}; + SurfaceTrackState outerState{}; + float chi2 = 0.f; + OperationFailureReason reason{}; + if (!fitTrackSeedLegs(trackSeeds[iSeed], context.frame, mLayerGlobalMeasurements, + mTraversalGraph.getSurfaceCatalogView(), mBz, + trkParam.ShiftRefToCluster, trkParam.MaxChi2ClusterAttachment, trkParam.MaxChi2NDF, + trkParam.RepeatRefitOut, gsl::span(trkParam.MinPt), + innerState, outerState, chi2, reason)) { + return; + } + TrackingCandidate temporaryTrack; + temporaryTrack.seed = trackSeeds[iSeed]; + temporaryTrack.track.innerState = innerState; + temporaryTrack.track.outerState = outerState; + temporaryTrack.track.chi2 = chi2; + temporaryTrack.charge = innerState.parameters[4] < 0.f ? -1 : 1; + temporaryTrack.phi = innerState.kind == SurfaceKind::Cylinder ? std::asin(innerState.parameters[2]) + innerState.alpha : innerState.parameters[2]; + temporaryTrack.eta = std::asinh(innerState.parameters[3]); + auto& handle = sink.local(); + handle.beginProducer(iSeed); + handle.emplace(std::move(temporaryTrack)); + }); + const auto stats = sink.stats(); + bounded_vector lut{mMemoryPool.get()}; + sink.finalizeGrouped(static_cast(nSeeds), lut, tracks); + context.frame.getCapacityEstimator().update(key, scale, stats.requested, stats.capacity, stats.emitted, + stats.spilled, stats.overflowed, stats.memoryLimited); + deepVectorClear(trackSeeds); + }); + + // Same ordering as o2::its::track::isBetter (longer track, then lower chi2). + std::sort(tracks.begin(), tracks.end(), [](const TrackingCandidate& a, const TrackingCandidate& b) { + const auto ncla = a.getNumberOfClusters(); + const auto nclb = b.getNumberOfClusters(); + return (ncla == nclb) ? (a.track.chi2 < b.track.chi2) : ncla > nclb; + }); + acceptTracks(context, iteration, tracks, firstClusters); + } + } +} + +void TrackerTraits::acceptTracks(IterationContext& context, int iteration, + bounded_vector& tracks, + bounded_vector>& firstClusters) +{ + auto* scratch = &context.scratch; + auto* mFrame = &context.frame; + const auto& trkParam = context.configuration.parameters; + const auto& mLayerGlobalMeasurements = context.layerGlobalMeasurements; + const int activeSurfaceCount = context.configuration.topology.nLayers; + reserveGenericTrackPublication(*mFrame, tracks.size(), static_cast(activeSurfaceCount)); + for (auto& track : tracks) { + int nShared = 0; + bool isFirstShared{false}; + int firstLayer{-1}, firstCluster{-1}; + for (int iLayer{0}; iLayer < activeSurfaceCount; ++iLayer) { + if (track.getClusterIndex(iLayer) == o2::its::constants::UnusedIndex) { + continue; + } + const auto clusterId = mLayerGlobalMeasurements[iLayer][track.getClusterIndex(iLayer)].clusterId; + bool isShared = mFrame->isClusterUsed(iLayer, clusterId); + nShared += int(isShared); + if (firstLayer < 0) { + firstCluster = track.getClusterIndex(iLayer); + isFirstShared = isShared && trkParam.AllowSharingFirstCluster && std::find(firstClusters[iLayer].begin(), firstClusters[iLayer].end(), firstCluster) != firstClusters[iLayer].end(); + firstLayer = iLayer; + } + } + + /// do not account for the first cluster in the shared clusters number if it is allowed + if (nShared - int(isFirstShared && trkParam.AllowSharingFirstCluster) > trkParam.SharedMaxClusters) { + continue; + } + + bool firstCls{true}, nominalCompatible{true}; + TimeEstBC nominalTS, expandedTS; + float smallestROFHalf = std::numeric_limits::max(); + for (int iLayer{0}; iLayer < activeSurfaceCount; ++iLayer) { + if (track.getClusterIndex(iLayer) == o2::its::constants::UnusedIndex) { + continue; + } + smallestROFHalf = std::min(smallestROFHalf, mFrame->getROFTiming(iLayer).mROFLength * 0.5f); + const auto clusterId = mLayerGlobalMeasurements[iLayer][track.getClusterIndex(iLayer)].clusterId; + mFrame->markUsedCluster(iLayer, clusterId); + int currentROF = mFrame->getClusterROF(iLayer, track.getClusterIndex(iLayer)); + const auto nominalROFTS = mFrame->getROFTiming(iLayer).getROFTimeBounds(currentROF); + const auto expandedROFTS = mFrame->getROFTiming(iLayer).getROFTimeBounds(currentROF, true); + if (firstCls) { + firstCls = false; + nominalTS = nominalROFTS; + expandedTS = expandedROFTS; + } else { + if (nominalCompatible) { + if (nominalTS.isCompatible(nominalROFTS)) { + nominalTS += nominalROFTS; + } else { + nominalCompatible = false; + } + } + if (!expandedTS.isCompatible(expandedROFTS)) { + LOGP(fatal, "TS {}+/-{} are incompatible with {}+/-{}, this should not happen!", expandedROFTS.getTimeStamp(), expandedROFTS.getTimeStampError(), expandedTS.getTimeStamp(), expandedTS.getTimeStampError()); + } + expandedTS += expandedROFTS; + } + } + const auto selectedTimestamp = nominalCompatible ? nominalTS : expandedTS; + const auto selectedTimestampSymmetric = selectedTimestamp.makeSymmetrical(); + // This is the same sanity clamp as the legacy symmetric timestamp, but + // committed directly to the detector-neutral GenericTrack interval. + const float selectedTimestampError = std::min(selectedTimestampSymmetric.getTimeStampError(), smallestROFHalf); + track.track.timestamp = {static_cast(selectedTimestampSymmetric.getTimeStamp() - selectedTimestampError), + static_cast(selectedTimestampSymmetric.getTimeStamp() + selectedTimestampError)}; + if (!appendGenericTrack(*mFrame, track, mLayerGlobalMeasurements)) { + LOGP(fatal, "GenericTrack publication failed for an accepted CA track"); + } + + if (trkParam.AllowSharingFirstCluster) { + firstClusters[firstLayer].push_back(firstCluster); + } + } +} + +void TrackerTraits::setNThreads(int n, std::shared_ptr& arena) +{ +#if defined(OPTIMISATION_OUTPUT) + mTaskArena = std::make_shared(1); +#else + if (arena == nullptr) { + mTaskArena = std::make_shared(std::abs(n)); + LOGP(info, "Setting tracker with {} threads.", n); + } else { + mTaskArena = arena; + } +#endif +} + +} // namespace o2::itsmft::tracking diff --git a/Detectors/ITSMFT/common/tracking/src/TrackerTraversalPreparation.cxx b/Detectors/ITSMFT/common/tracking/src/TrackerTraversalPreparation.cxx new file mode 100644 index 0000000000000..e9c256accf2f1 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/TrackerTraversalPreparation.cxx @@ -0,0 +1,68 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSMFTTracking/detail/TrackerTraversalPreparation.h" + +#include + +#include "CommonConstants/MathConstants.h" +#include "GPUCommonMath.h" +#include "ITSMFTTracking/MathUtils.h" + +namespace o2::itsmft::tracking +{ + +float cylinderLayerMultipleScatteringAngle( + const CylinderLayerScatteringInputs& inputs, float trackletMinPt) +{ + return o2::its::math_utils::MSangle(0.14f, trackletMinPt, inputs.layerxX0); +} + +float diskLayerMultipleScatteringAngle(const DiskLayerScatteringInputs& inputs, float trackletMinPt) +{ + const float invP = 1.f / trackletMinPt; + const float tanlRef = (std::abs(inputs.layerRadius) > 1e-6f) + ? inputs.referenceCoordinate / inputs.layerRadius + : 0.f; + const float absTanl = std::abs(tanlRef); + const float cscLambda = (absTanl > 1e-6f) + ? std::sqrt(1.f + tanlRef * tanlRef) / absTanl + : 1e6f; + return 0.0136f * invP * std::sqrt(inputs.layerxX0 * cscLambda); +} + +float clampEdgeCurvature(float oneOverR, float outerRadius) noexcept +{ + return (outerRadius > 0.f && 0.5f * oneOverR >= 1.f / outerRadius) + ? (2.f / outerRadius) - o2::constants::math::Almost0 + : oneOverR; +} + +EdgeScatteringBendingPrep prepareEdgeScatteringAndBending( + gsl::span perLayerMSAngle, int fromLayer, int toLayer, + float r1, float r2, float clampedOneOverR, float res1, float res2) noexcept +{ + float ms2 = 0.f; + for (int layer = fromLayer; layer < toLayer; ++layer) { + ms2 += o2::its::math_utils::Sq(perLayerMSAngle[layer]); + } + const float msAngle = o2::gpu::CAMath::Sqrt(ms2); + const float cosTheta1half = o2::gpu::CAMath::Sqrt(1.f - o2::its::math_utils::Sq(0.5f * r1 * clampedOneOverR)); + const float cosTheta2half = o2::gpu::CAMath::Sqrt(1.f - o2::its::math_utils::Sq(0.5f * r2 * clampedOneOverR)); + const float x = (r2 * cosTheta1half) - (r1 * cosTheta2half); + const float delta = o2::gpu::CAMath::Sqrt(1.f / (1.f - 0.25f * o2::its::math_utils::Sq(x * clampedOneOverR)) * + (o2::its::math_utils::Sq((0.25f * r1 * r2 * o2::its::math_utils::Sq(clampedOneOverR) / cosTheta2half) + cosTheta1half) * o2::its::math_utils::Sq(res1) + + o2::its::math_utils::Sq((0.25f * r1 * r2 * o2::its::math_utils::Sq(clampedOneOverR) / cosTheta1half) + cosTheta2half) * o2::its::math_utils::Sq(res2))); + const float phiCut = o2::gpu::CAMath::Min(o2::gpu::CAMath::ASin(0.5f * x * clampedOneOverR) + 2.f * msAngle + delta, o2::constants::math::PI * 0.5f); + return {msAngle, phiCut}; +} + +} // namespace o2::itsmft::tracking diff --git a/Detectors/ITSMFT/common/tracking/src/TrackingConfigParam.cxx b/Detectors/ITSMFT/common/tracking/src/TrackingConfigParam.cxx new file mode 100644 index 0000000000000..d3b483a508d9f --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/TrackingConfigParam.cxx @@ -0,0 +1,23 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSMFTTracking/TrackingConfigParam.h" + +namespace o2::itsmft +{ +// Register MFT CA parameters in the global parameter database. +// ITS production tracking uses the legacy o2::its::TrackerParamConfig. +static auto& sMFTCATrackerParam = TrackerParamConfig::Instance(); +} // namespace o2::itsmft + +// Register the dedicated ITS common-CA configuration. +// The registered legacy ITS tracker and vertexer remain in ITSTrackingConfigParam.cxx. +O2ParamImpl(o2::itsmft::ITSCommonCATrackerParam); diff --git a/Detectors/ITSMFT/common/tracking/src/TraversalTopology.cxx b/Detectors/ITSMFT/common/tracking/src/TraversalTopology.cxx new file mode 100644 index 0000000000000..13cd2a9f11408 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/TraversalTopology.cxx @@ -0,0 +1,168 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSMFTTracking/TraversalTopology.h" +#include "ITSMFTTracking/Configuration.h" + +#include +#include + +#include + +namespace o2::itsmft::tracking +{ + +namespace +{ +uint16_t componentForPosition(gsl::span componentOffsets, uint16_t position) noexcept +{ + const auto upper = std::upper_bound(componentOffsets.begin(), componentOffsets.end(), position); + return static_cast(std::distance(componentOffsets.begin(), upper) - 1); +} + +LayerMask skippedBetween(uint16_t fromPosition, uint16_t toPosition) noexcept +{ + return LayerMask::skipped(fromPosition, toPosition); +} +} // namespace + +TraversalTopologyBuildResult deriveTraversalTopology(const DetectorLayout& layout, + const o2::itsmft::IterationParameters& parameters) +{ + TraversalTopologyBuildResult result; + if (!layout.valid()) { + result.error = TraversalTopologyError::InvalidLayout; + return result; + } + + if (parameters.NLayers != 0 && parameters.NLayers != layout.size()) { + result.error = TraversalTopologyError::LayerCountMismatch; + return result; + } + if (parameters.MaxHoles < 0) { + result.error = TraversalTopologyError::NegativeMaxHoles; + return result; + } + const auto seedingLayers = parameters.SeedingLayers; + const auto roadStartLayers = parameters.StartLayerMask; + const auto disabledLayers = parameters.InactiveLayerMask; + + TraversalTopology topology; + topology.nLayers = static_cast(layout.size()); + for (uint16_t position = 0; position < layout.size(); ++position) { + if (!disabledLayers.has(position)) { + topology.activeLayers.set(position); + topology.activeSurfaceList.push_back(LayerId{position}); + } + } + if (topology.activeSurfaceList.empty()) { + result.error = TraversalTopologyError::NoActiveSurfaces; + return result; + } + topology.seedingLayers = seedingLayers.empty() ? topology.activeLayers : (seedingLayers & topology.activeLayers); + + const auto componentOffsets = layout.getComponentOffsets(); + const auto holeLayers = layout.getHoleLayers(); + const auto componentOf = [componentOffsets](uint16_t position) { + return componentForPosition(componentOffsets, position); + }; + + for (uint16_t fromPosition = 0; fromPosition + 1 < layout.size(); ++fromPosition) { + if (!topology.seedingLayers.has(fromPosition)) { + continue; + } + for (uint16_t toPosition = fromPosition + 1; toPosition < layout.size(); ++toPosition) { + if (!topology.seedingLayers.has(toPosition) || + componentOf(fromPosition) != componentOf(toPosition)) { + continue; + } + const auto skipped = skippedBetween(fromPosition, toPosition) & topology.seedingLayers; + if (skipped.count() > parameters.MaxHoles || !skipped.isSubsetOf(holeLayers)) { + continue; + } + if (topology.edges.size() >= MaxLayoutEdges) { + result.error = TraversalTopologyError::TooManyEdges; + return result; + } + topology.edges.push_back(Edge{LayerId{fromPosition}, LayerId{toPosition}}); + } + } + + for (uint32_t first = 0; first < topology.edges.size(); ++first) { + for (uint32_t second = 0; second < topology.edges.size(); ++second) { + const auto& firstEdge = topology.edges[first]; + const auto& secondEdge = topology.edges[second]; + if (firstEdge.to != secondEdge.from || firstEdge.from == secondEdge.to) { + continue; + } + const auto skipped = (skippedBetween(firstEdge.from.value(), firstEdge.to.value()) | + skippedBetween(secondEdge.from.value(), secondEdge.to.value())) & + topology.seedingLayers; + if (skipped.count() > parameters.MaxHoles || !skipped.isSubsetOf(holeLayers)) { + continue; + } + if (topology.paths.size() >= MaxLayoutPaths) { + result.error = TraversalTopologyError::TooManyPaths; + return result; + } + topology.paths.push_back(CellPath{EdgeId{static_cast(first)}, EdgeId{static_cast(second)}}); + } + } + + topology.pathsByFirstEdgeOffsets.assign(topology.edges.size() + 1, 0); + for (const auto& path : topology.paths) { + ++topology.pathsByFirstEdgeOffsets[path.first.value() + 1]; + } + for (size_t offset = 1; offset < topology.pathsByFirstEdgeOffsets.size(); ++offset) { + topology.pathsByFirstEdgeOffsets[offset] += topology.pathsByFirstEdgeOffsets[offset - 1]; + } + topology.pathsByFirstEdge.resize(topology.paths.size()); + auto cursor = topology.pathsByFirstEdgeOffsets; + for (uint32_t path = 0; path < topology.paths.size(); ++path) { + topology.pathsByFirstEdge[cursor[topology.paths[path].first.value()]++] = CellPathId{static_cast(path)}; + } + + topology.scheduledPaths.reserve(topology.paths.size()); + for (uint32_t path = 0; path < topology.paths.size(); ++path) { + topology.scheduledPaths.push_back(CellPathId{static_cast(path)}); + } + const auto pathOrder = [&](CellPathId lhs, CellPathId rhs) { + const auto lhsTarget = topology.edges[topology.paths[lhs.value()].second.value()].to; + const auto rhsTarget = topology.edges[topology.paths[rhs.value()].second.value()].to; + return lhsTarget != rhsTarget ? lhsTarget < rhsTarget : lhs < rhs; + }; + std::sort(topology.scheduledPaths.begin(), topology.scheduledPaths.end(), pathOrder); + + topology.roadStartPaths.reserve(topology.paths.size()); + for (const auto path : topology.scheduledPaths) { + const auto target = topology.edges[topology.paths[path.value()].second.value()].to; + if (roadStartLayers.has(target.value())) { + topology.roadStartPaths.push_back(path); + } + } + topology.roadStartComponentOffsets.push_back(0); + uint16_t previousComponent = std::numeric_limits::max(); + for (uint32_t index = 0; index < topology.roadStartPaths.size(); ++index) { + const auto path = topology.roadStartPaths[index]; + const auto target = topology.edges[topology.paths[path.value()].second.value()].to; + const auto component = componentOf(target.value()); + if (component != previousComponent && index != 0) { + topology.roadStartComponentOffsets.push_back(index); + } + previousComponent = component; + } + topology.roadStartComponentOffsets.push_back(static_cast(topology.roadStartPaths.size())); + + result.topology.emplace(std::move(topology)); + return result; +} + +} // namespace o2::itsmft::tracking diff --git a/Detectors/ITSMFT/common/tracking/src/TripletFitting.cxx b/Detectors/ITSMFT/common/tracking/src/TripletFitting.cxx new file mode 100644 index 0000000000000..c82114547df3a --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/src/TripletFitting.cxx @@ -0,0 +1,433 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSMFTTracking/TripletFitting.h" + +#include +#include +#include +#include +#include + +namespace o2::itsmft::tracking +{ +namespace +{ + +constexpr std::size_t NMeasurementCoordinates = 9; +constexpr std::size_t NCoordinates = NMeasurementCoordinates; +constexpr std::size_t NAdjacentKinks = 4; + +using KinkVector = std::array; +using KinkCovariance = std::array, NAdjacentKinks>; + +struct DualNumber { + float value{0.}; + std::array derivative{}; + + static DualNumber variable(float val, std::size_t index) noexcept + { + DualNumber result{val}; + result.derivative[index] = 1.; + return result; + } +}; + +DualNumber operator+(const DualNumber& lhs, const DualNumber& rhs) noexcept +{ + DualNumber result{lhs.value + rhs.value}; + for (std::size_t i = 0; i < NCoordinates; ++i) { + result.derivative[i] = lhs.derivative[i] + rhs.derivative[i]; + } + return result; +} + +DualNumber operator-(const DualNumber& lhs, const DualNumber& rhs) noexcept +{ + DualNumber result{lhs.value - rhs.value}; + for (std::size_t i = 0; i < NCoordinates; ++i) { + result.derivative[i] = lhs.derivative[i] - rhs.derivative[i]; + } + return result; +} + +DualNumber operator-(const DualNumber& value) noexcept +{ + DualNumber result{-value.value}; + for (std::size_t i = 0; i < NCoordinates; ++i) { + result.derivative[i] = -value.derivative[i]; + } + return result; +} + +DualNumber operator*(const DualNumber& lhs, const DualNumber& rhs) noexcept +{ + DualNumber result{lhs.value * rhs.value}; + for (std::size_t i = 0; i < NCoordinates; ++i) { + result.derivative[i] = lhs.derivative[i] * rhs.value + lhs.value * rhs.derivative[i]; + } + return result; +} + +DualNumber operator/(const DualNumber& lhs, const DualNumber& rhs) noexcept +{ + const float inverse = 1. / rhs.value; + DualNumber result{lhs.value * inverse}; + for (std::size_t i = 0; i < NCoordinates; ++i) { + result.derivative[i] = (lhs.derivative[i] - result.value * rhs.derivative[i]) * inverse; + } + return result; +} + +DualNumber squareRoot(const DualNumber& argument) noexcept +{ + const float root = std::sqrt(argument.value); + DualNumber result{root}; + const float scale = 0.5 / root; + for (std::size_t i = 0; i < NCoordinates; ++i) { + result.derivative[i] = scale * argument.derivative[i]; + } + return result; +} + +DualNumber arcSine(const DualNumber& argument) noexcept +{ + DualNumber result{std::asin(argument.value)}; + const float scale = 1. / std::sqrt(1. - argument.value * argument.value); + for (std::size_t i = 0; i < NCoordinates; ++i) { + result.derivative[i] = scale * argument.derivative[i]; + } + return result; +} + +DualNumber arcTangent2(const DualNumber& y, const DualNumber& x) noexcept +{ + DualNumber result{std::atan2(y.value, x.value)}; + const float denominator = x.value * x.value + y.value * y.value; + for (std::size_t i = 0; i < NCoordinates; ++i) { + result.derivative[i] = (x.value * y.derivative[i] - y.value * x.derivative[i]) / denominator; + } + return result; +} + +struct SegmentGeometry { + DualNumber bendingAngle; + DualNumber transverseArcLength; + DualNumber cotangentTheta; + DualNumber sineTheta; + DualNumber cosineTheta; + DualNumber index; +}; + +bool makeSegmentGeometry(const DualNumber& transverseCurvature, const DualNumber& chordLength, + const DualNumber& deltaZ, SegmentGeometry& result) noexcept +{ + const DualNumber halfSine = DualNumber{0.5} * transverseCurvature * chordLength; + if (std::abs(halfSine.value) >= 1.) { + return false; + } + + const DualNumber halfSine2 = halfSine * halfSine; + const DualNumber halfSine4 = halfSine2 * halfSine2; + DualNumber asinOverArgument; + DualNumber angleCotangent; + const DualNumber halfAngle = arcSine(halfSine); + if (std::abs(halfSine.value) < 1.e-4) { + asinOverArgument = DualNumber{1.} + halfSine2 * DualNumber{1. / 6.} + halfSine4 * DualNumber{3. / 40.}; + angleCotangent = DualNumber{1.} - halfSine2 * DualNumber{1. / 3.} - halfSine4 * DualNumber{2. / 15.}; + } else { + asinOverArgument = halfAngle / halfSine; + angleCotangent = halfAngle * squareRoot(DualNumber{1.} - halfSine2) / halfSine; + } + + const DualNumber bendingAngle = DualNumber{2.} * halfAngle; + const DualNumber transverseArcLength = chordLength * asinOverArgument; + const DualNumber cotangentTheta = deltaZ / transverseArcLength; + const DualNumber sineTheta = DualNumber{1.} / squareRoot(DualNumber{1.} + cotangentTheta * cotangentTheta); + const DualNumber cosineTheta = cotangentTheta * sineTheta; + const DualNumber index = DualNumber{1.} / + (angleCotangent * sineTheta * sineTheta + cosineTheta * cosineTheta); + if (transverseArcLength.value <= 0. || sineTheta.value <= 0. || index.value <= 0.) { + return false; + } + result = {bendingAngle, transverseArcLength, cotangentTheta, sineTheta, cosineTheta, index}; + return true; +} + +struct TripletGeometry { + DualNumber phiTilde; + DualNumber thetaTilde; + DualNumber rhoPhi; + DualNumber rhoTheta; +}; + +bool makeTripletGeometry(const std::array& measurements, + TripletGeometry& result) noexcept +{ + std::array, 3> point{}; + for (std::size_t hit = 0; hit < measurements.size(); ++hit) { + const std::array position{ + measurements[hit].x, measurements[hit].y, measurements[hit].z}; + for (std::size_t coordinate = 0; coordinate < 3; ++coordinate) { + const std::size_t index = 3 * hit + coordinate; + point[hit][coordinate] = DualNumber::variable(position[coordinate], index); + } + } + + const DualNumber dx01 = point[1][0] - point[0][0]; + const DualNumber dy01 = point[1][1] - point[0][1]; + const DualNumber dz01 = point[1][2] - point[0][2]; + const DualNumber dx12 = point[2][0] - point[1][0]; + const DualNumber dy12 = point[2][1] - point[1][1]; + const DualNumber dz12 = point[2][2] - point[1][2]; + const DualNumber dx02 = point[2][0] - point[0][0]; + const DualNumber dy02 = point[2][1] - point[0][1]; + const DualNumber length01 = squareRoot(dx01 * dx01 + dy01 * dy01); + const DualNumber length12 = squareRoot(dx12 * dx12 + dy12 * dy12); + const DualNumber length02 = squareRoot(dx02 * dx02 + dy02 * dy02); + if (length01.value <= 0. || + length12.value <= 0. || length02.value <= 0.) { + return false; + } + + const DualNumber cross = dx01 * dy12 - dy01 * dx12; + const DualNumber transverseCurvature = DualNumber{2.} * cross / (length01 * length12 * length02); + SegmentGeometry firstSegment; + SegmentGeometry secondSegment; + if (!makeSegmentGeometry(transverseCurvature, length01, dz01, firstSegment) || + !makeSegmentGeometry(transverseCurvature, length12, dz12, secondSegment)) { + return false; + } + + const DualNumber theta01 = arcTangent2(firstSegment.transverseArcLength, dz01); + const DualNumber theta12 = arcTangent2(secondSegment.transverseArcLength, dz12); + const DualNumber phiTilde = DualNumber{0.5} * + (firstSegment.bendingAngle * firstSegment.index + + secondSegment.bendingAngle * secondSegment.index); + const DualNumber thetaTilde = theta12 - theta01 + + (DualNumber{1.} - secondSegment.index) * secondSegment.cotangentTheta - + (DualNumber{1.} - firstSegment.index) * firstSegment.cotangentTheta; + const DualNumber rhoPhi = DualNumber{-0.5} * + (firstSegment.transverseArcLength * firstSegment.index / firstSegment.sineTheta + + secondSegment.transverseArcLength * secondSegment.index / secondSegment.sineTheta); + + DualNumber rhoTheta; + const float maximumHalfSine = 0.5 * std::abs(transverseCurvature.value) * + std::max(length01.value, length12.value); + if (maximumHalfSine < 1.e-4) { + rhoTheta = transverseCurvature * + (length12 * length12 * secondSegment.cosineTheta - + length01 * length01 * firstSegment.cosineTheta) / + DualNumber{12.}; + } else { + rhoTheta = ((DualNumber{1.} - firstSegment.index) * firstSegment.cotangentTheta / firstSegment.sineTheta - + (DualNumber{1.} - secondSegment.index) * secondSegment.cotangentTheta / secondSegment.sineTheta) / + transverseCurvature; + } + + if (rhoPhi.value == 0.) { + return false; + } + result = {phiTilde, thetaTilde, rhoPhi, rhoTheta}; + return true; +} + +float covarianceContraction(const std::array& left, + const GlobalCovariance3F& covariance, + const std::array& right) noexcept +{ + return left[0] * (covariance.xx * right[0] + covariance.xy * right[1] + covariance.xz * right[2]) + + left[1] * (covariance.xy * right[0] + covariance.yy * right[1] + covariance.yz * right[2]) + + left[2] * (covariance.xz * right[0] + covariance.yz * right[1] + covariance.zz * right[2]); +} + +bool choleskyDecompose(const KinkCovariance& covariance, + KinkCovariance& lower) noexcept +{ + for (std::size_t row = 0; row < NAdjacentKinks; ++row) { + for (std::size_t column = 0; column <= row; ++column) { + float value = covariance[row][column]; + for (std::size_t k = 0; k < column; ++k) { + value -= lower[row][k] * lower[column][k]; + } + if (row == column) { + if (value <= 0.) { + return false; + } + lower[row][column] = std::sqrt(value); + } else { + lower[row][column] = value / lower[column][column]; + } + } + } + return true; +} + +bool choleskySolve(const KinkCovariance& lower, const KinkVector& right, + KinkVector& solution) noexcept +{ + KinkVector intermediate{}; + for (std::size_t row = 0; row < NAdjacentKinks; ++row) { + float value = right[row]; + for (std::size_t column = 0; column < row; ++column) { + value -= lower[row][column] * intermediate[column]; + } + intermediate[row] = value / lower[row][row]; + } + for (int row = static_cast(NAdjacentKinks) - 1; row >= 0; --row) { + float value = intermediate[row]; + for (std::size_t column = static_cast(row) + 1; + column < NAdjacentKinks; ++column) { + value -= lower[column][row] * solution[column]; + } + solution[row] = value / lower[row][row]; + } + return true; +} + +float dotProduct(const KinkVector& left, const KinkVector& right) noexcept +{ + float result = 0.; + for (std::size_t i = 0; i < NAdjacentKinks; ++i) { + result += left[i] * right[i]; + } + return result; +} + +bool referenceSinTheta(const GlobalMeasurement& first, + const GlobalMeasurement& third, + float& sineTheta) noexcept +{ + const float dx = third.x - first.x; + const float dy = third.y - first.y; + const float dz = third.z - first.z; + const float transverse = std::hypot(dx, dy); + const float length = std::hypot(transverse, dz); + sineTheta = transverse / length; + return sineTheta > 0. && sineTheta <= 1.; +} + +} // namespace + +bool makeTripletFitFactor( + const std::array& measurements, + TripletFitFactor& result) noexcept +{ + TripletGeometry geometry; + if (!makeTripletGeometry(measurements, geometry)) { + return false; + } + const float kappaReference = -geometry.phiTilde.value / geometry.rhoPhi.value; + TripletFitFactor scratch{ + {geometry.thetaTilde.value, geometry.phiTilde.value}, + {geometry.rhoTheta.value, geometry.rhoPhi.value}, + {}}; + + for (std::size_t hit = 0; hit < measurements.size(); ++hit) { + for (std::size_t coordinate = 0; coordinate < 3; ++coordinate) { + const std::size_t index = 3 * hit + coordinate; + const float gradientTheta = geometry.thetaTilde.derivative[index] + + kappaReference * geometry.rhoTheta.derivative[index]; + const float gradientPhi = geometry.phiTilde.derivative[index] + + kappaReference * geometry.rhoPhi.derivative[index]; + scratch.h[hit].theta[coordinate] = gradientTheta; + scratch.h[hit].phi[coordinate] = gradientPhi; + } + } + if (!scratch.isValid()) { + return false; + } + result = scratch; + return true; +} + +bool fitAdjacentTripletFactors( + const TripletFitFactor& firstFactor, + const TripletFitFactor& secondFactor, + const std::array& measurements, + const std::array& angularVariance, + AdjacentTripletFitResult& result) noexcept +{ + std::array sineTheta{}; + if (!referenceSinTheta(measurements[0], measurements[2], sineTheta[0]) || + !referenceSinTheta(measurements[1], measurements[3], sineTheta[1])) { + return false; + } + + const KinkVector psi{ + firstFactor.psi.theta, firstFactor.psi.phi, + secondFactor.psi.theta, secondFactor.psi.phi}; + const KinkVector rho{ + firstFactor.rho.theta, firstFactor.rho.phi, + secondFactor.rho.theta, secondFactor.rho.phi}; + KinkCovariance covariance{}; + covariance[0][0] = angularVariance[0]; + covariance[1][1] = angularVariance[0] / (sineTheta[0] * sineTheta[0]); + covariance[2][2] = angularVariance[1]; + covariance[3][3] = angularVariance[1] / (sineTheta[1] * sineTheta[1]); + + // Build H for four unique hits. The factors use slots (0,1,2) and (1,2,3), + // so shared hits contribute to the cross-triplet covariance. + std::array, NAdjacentKinks>, 4> gradients{}; + for (std::size_t coordinate = 0; coordinate < 3; ++coordinate) { + for (std::size_t hit = 0; hit < 3; ++hit) { + gradients[hit][0][coordinate] = firstFactor.h[hit].theta[coordinate]; + gradients[hit][1][coordinate] = firstFactor.h[hit].phi[coordinate]; + gradients[hit + 1][2][coordinate] = secondFactor.h[hit].theta[coordinate]; + gradients[hit + 1][3][coordinate] = secondFactor.h[hit].phi[coordinate]; + } + } + for (std::size_t hit = 0; hit < measurements.size(); ++hit) { + for (std::size_t row = 0; row < NAdjacentKinks; ++row) { + for (std::size_t column = 0; column <= row; ++column) { + const float contribution = covarianceContraction( + gradients[hit][row], measurements[hit].covariance, gradients[hit][column]); + covariance[row][column] += contribution; + if (row != column) { + covariance[column][row] += contribution; + } + } + } + } + + KinkCovariance lower{}; + KinkVector precisionPsi{}; + KinkVector precisionRho{}; + if (!choleskyDecompose(covariance, lower) || + !choleskySolve(lower, psi, precisionPsi) || + !choleskySolve(lower, rho, precisionRho)) { + return false; + } + const float rhoPrecisionPsi = dotProduct(rho, precisionPsi); + const float rhoPrecisionRho = dotProduct(rho, precisionRho); + const float psiPrecisionPsi = dotProduct(psi, precisionPsi); + if (rhoPrecisionRho <= 0.) { + return false; + } + + const float curvature = -rhoPrecisionPsi / rhoPrecisionRho; + const float curvatureVariance = 1. / rhoPrecisionRho; + const float removedCurvatureTerm = rhoPrecisionPsi * rhoPrecisionPsi / rhoPrecisionRho; + float chi2 = psiPrecisionPsi - removedCurvatureTerm; + const float chi2Tolerance = 128. * std::numeric_limits::epsilon() * + std::max(std::abs(psiPrecisionPsi), std::abs(removedCurvatureTerm)); + if (chi2 < 0. && chi2 >= -chi2Tolerance) { + chi2 = 0.; + } + if (curvatureVariance <= 0. || chi2 < 0.) { + return false; + } + + result = {curvature, curvatureVariance, chi2}; + return true; +} + +} // namespace o2::itsmft::tracking diff --git a/Detectors/ITSMFT/common/tracking/test/CMakeLists.txt b/Detectors/ITSMFT/common/tracking/test/CMakeLists.txt index e7f6d20e32773..b7f92fc87249e 100644 --- a/Detectors/ITSMFT/common/tracking/test/CMakeLists.txt +++ b/Detectors/ITSMFT/common/tracking/test/CMakeLists.txt @@ -28,3 +28,40 @@ o2_add_test(boundedmemoryresource COMPONENT_NAME itsmft-tracking LABELS "itsmft;tracking" PUBLIC_LINK_LIBRARIES O2::ITSMFTTracking) + +function(o2_add_common_tracking_test name source) + o2_add_test(${name} + SOURCES ${source} + COMPONENT_NAME itsmft-tracking + LABELS "itsmft;tracking" + PUBLIC_LINK_LIBRARIES O2::ITSMFTTracking O2::ITStracking O2::MFTTracking) +endfunction() + +o2_add_common_tracking_test(detectorlayout testDetectorLayout.cxx) +o2_add_common_tracking_test(traversal-topology testTraversalTopology.cxx) +o2_add_common_tracking_test(mft-normalized-refit testMFTNormalizedRefit.cxx) +o2_add_common_tracking_test(tracklet-finding testTrackletFinding.cxx) +o2_add_common_tracking_test(cell-finding testCellFinding.cxx) +o2_add_common_tracking_test(triplet-fitting testTripletFitting.cxx) +o2_add_common_tracking_test(its-mft-surfacespec-projection testITSMFTSurfaceSpecProjection.cxx) +o2_add_common_tracking_test(generictrack testGenericTrack.cxx) +o2_add_common_tracking_test(propagator testPropagator.cxx) +o2_add_common_tracking_test(material-physics testMaterialPhysics.cxx) +o2_add_common_tracking_test(covariance-sanitization testCovarianceSanitization.cxx) +o2_add_common_tracking_test(surfacetiming testSurfaceTiming.cxx) +o2_add_common_tracking_test(multisourceloading testMultiSourceLoading.cxx) +o2_add_common_tracking_test(timeframe-load-failure testTimeFrameLoadFailure.cxx) +o2_add_common_tracking_test(timeframe-lifecycle testTimeFrameLifecycle.cxx) +o2_add_common_tracking_test(tracker-failure-contract testTrackerFailureContract.cxx) +o2_add_common_tracking_test(computelayercells-orchestration testComputeLayerCellsOrchestration.cxx) +o2_add_common_tracking_test(computelayertracklets-orchestration testComputeLayerTrackletsOrchestration.cxx) +o2_add_common_tracking_test(its-common-ca-tracking-mode-configuration testITSCommonCATrackingModeConfiguration.cxx) +o2_add_test(combined-tracking-composition + SOURCES testCombinedTrackingComposition.cxx + COMPONENT_NAME itsmft-tracking + LABELS "itsmft;tracking" + PUBLIC_LINK_LIBRARIES O2::ITSMFTTracking O2::ITStracking O2::MFTTracking O2::ITSCAWorkflow) + +o2_add_common_tracking_test(mft-ca-tracking-configuration testMFTCATrackingConfiguration.cxx) + +o2_add_common_tracking_test(workflow-session testWorkflowSession.cxx) diff --git a/Detectors/ITSMFT/common/tracking/test/CombinedTrackingTestSupport.h b/Detectors/ITSMFT/common/tracking/test/CombinedTrackingTestSupport.h new file mode 100644 index 0000000000000..45dc1a9a4d04c --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/CombinedTrackingTestSupport.h @@ -0,0 +1,265 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_TEST_COMBINEDTRACKINGTESTSUPPORT_H_ +#define ALICEO2_ITSMFT_TRACKING_TEST_COMBINEDTRACKINGTESTSUPPORT_H_ + +#include "TrackingParameterTestSupport.h" +#include +#include +#include +#include +#include +#include +#include + +#include "ITSMFTTracking/Configuration.h" +#include "ITSCAWorkflow/PublicationAdapter.h" +#include "ITSMFTTracking/detail/ITSSharedClusterCompatibility.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/Tracker.h" +#include "ITSMFTTracking/TrackerTraits.h" +#include "ITSMFTTracking/ROFLookupTables.h" + +namespace o2::itsmft::tracking::test +{ + +using CombinedSurfaceSpec = ConcatenatedSurfaceSpec; +inline constexpr auto CombinedSurfaceCatalog = projectStaticSurfaceCatalog(); + +inline SurfaceCatalogView combinedCatalogView() +{ + return {CombinedSurfaceCatalog.data(), static_cast(CombinedSurfaceCatalog.size())}; +} + +inline std::vector orderedSurfaceRange(uint16_t first, uint16_t count) +{ + std::vector result; + result.reserve(count); + for (uint16_t i = 0; i < count; ++i) { + result.push_back(LayerId{static_cast(first + i)}); + } + return result; +} + +inline TrackerInitialization makeCombinedConfiguration(const TrackingParameters& itsParams, + const TrackingParameters& mftParams) +{ + DetectorLayoutDefinition definition; + definition.componentOffsets = {0, ITSNLayers}; + const auto combine = [&] { + auto parameters = itsParams; + parameters.NLayers = ITSNLayers + MFTNLayers; + const auto concatenate = [](auto& output, const auto& prefix, const auto& suffix) { + output = prefix; + output.insert(output.end(), suffix.begin(), suffix.end()); + }; + concatenate(parameters.AddTimeError, itsParams.AddTimeError, mftParams.AddTimeError); + concatenate(parameters.LayerZ, itsParams.LayerZ, mftParams.LayerZ); + concatenate(parameters.LayerRadii, itsParams.LayerRadii, mftParams.LayerRadii); + concatenate(parameters.LayerResolution, itsParams.LayerResolution, mftParams.LayerResolution); + concatenate(parameters.SystError2Row, itsParams.SystError2Row, mftParams.SystError2Row); + concatenate(parameters.SystError2Col, itsParams.SystError2Col, mftParams.SystError2Col); + parameters.LayerColHalfExtent = itsParams.LayerColHalfExtent.empty() ? itsParams.LayerZ : itsParams.LayerColHalfExtent; + const auto& mftColExtent = mftParams.LayerColHalfExtent.empty() ? mftParams.LayerZ : mftParams.LayerColHalfExtent; + parameters.LayerColHalfExtent.insert(parameters.LayerColHalfExtent.end(), mftColExtent.begin(), mftColExtent.end()); + const auto configuredSeedingLayers = [](const auto& input) { + return input.SeedingLayers.empty() ? LayerMask::span(0, input.NLayers - 1) : input.SeedingLayers; + }; + parameters.InactiveLayerMask = itsParams.InactiveLayerMask.value() | + (mftParams.InactiveLayerMask.value() << itsParams.NLayers); + parameters.SeedingLayers = configuredSeedingLayers(itsParams).value() | + (configuredSeedingLayers(mftParams).value() << itsParams.NLayers); + parameters.StartLayerMask = LayerMask{(uint32_t{1} << (ITSNLayers + MFTNLayers)) - 1u}; + return parameters; + }; + return {combinedCatalogView(), std::move(definition), makeTrackingPlan(combine()), + std::make_shared()}; +} + +class CombinedTrackingPlan +{ + public: + CombinedTrackingPlan(std::vector itsParams, std::vector mftParams) + { + if (itsParams.size() != 1 || mftParams.size() != 1) { + throw std::invalid_argument{"combined test application plan requires one iteration per detector"}; + } + + mConfiguration = makeCombinedConfiguration(itsParams[0], mftParams[0]); + mITSPublicationAdapter.adoptITSSharedClusterCompatibility(&mITSCompatibility); + mTracker = std::make_unique(); + mTraits = std::make_unique(); + } + + CombinedTrackingPlan(const CombinedTrackingPlan&) = delete; + CombinedTrackingPlan& operator=(const CombinedTrackingPlan&) = delete; + + void adoptFrame(TimeFrame& frame) + { + mFrame = &frame; + const auto result = mTracker->initialize(frame, mConfiguration); + if (!result.ok()) { + throw std::runtime_error{"combined test application plan failed to configure the TimeFrame"}; + } + } + void setBz(float bz) + { + mFrame->setBz(bz); + } + void setNThreads(int n) + { + mTraits->setNThreads(n, mArena); + } + + Tracker& itsTracker() noexcept { return *mTracker; } + Tracker& mftTracker() noexcept { return *mTracker; } + TrackingResult runITS() + { + auto result = mTracker->run(*mFrame, *mTraits); + mLastResult = result; + if (result.outcome == TrackingOutcome::Success) { + const auto configurations = mTracker->getIterationConfigurations(); + std::size_t firstTrack = 0; + for (std::size_t i = 0; i < configurations.size(); ++i) { + if (i >= result.acceptedTrackCounts.size() || + result.acceptedTrackCounts[i] > mFrame->getGenericTracks().size() - firstTrack) { + throw std::runtime_error{"failed to seal ITS tracking compatibility"}; + } + std::vector selected; + for (std::size_t index = 0; index < result.acceptedTrackCounts[i]; ++index) { + const auto globalIndex = firstTrack + index; + if (mFrame->getGenericTracks()[globalIndex].innerState.kind == SurfaceKind::Cylinder) { + selected.push_back(static_cast(globalIndex)); + } + } + if (!mITSPublicationAdapter.completeAccepted( + selected, configurations[i].parameters, *mFrame, i + 1 == configurations.size())) { + throw std::runtime_error{"failed to seal ITS tracking compatibility"}; + } + firstTrack += result.acceptedTrackCounts[i]; + } + } else { + mITSPublicationAdapter.reset(); + } + return result; + } + TrackingResult runMFT() + { + if (!mLastResult) { + runITS(); + } + auto result = *mLastResult; + return result; + } + RuntimeROFViews getITSROFViews() const noexcept { return {mITSROFOverlapTable.getView(), mITSROFVertexLookupTable.getView(), mITSMultiplicityMask.getView(), mITSUPCMask.getView()}; } + RuntimeROFViews getMFTROFViews() const noexcept { return {mMFTROFOverlapTable.getView(), mMFTROFVertexLookupTable.getView(), mMFTMultiplicityMask.getView(), mMFTUPCMask.getView()}; } + void clearPublicationSidecars() noexcept + { + mITSPublicationAdapter.reset(); + mLastResult.reset(); + } + + std::optional validateSources(const ClusterSourceInput& itsSource, + const ClusterSourceInput& mftSource) const noexcept + { + if (itsSource.id != ClusterSourceId{0} || itsSource.detector != o2::detectors::DetID::ITS) { + return LoadSourcesResult{MultiSourceLoadError::UnsupportedDetector, itsSource.id}; + } + if (mftSource.id != ClusterSourceId{1} || mftSource.detector != o2::detectors::DetID::MFT) { + return LoadSourcesResult{MultiSourceLoadError::UnsupportedDetector, mftSource.id}; + } + return std::nullopt; + } + + SurfaceCatalogView catalogView() const noexcept { return combinedCatalogView(); } + std::optional dropTFUponFailureFor(ClusterSourceId source) const noexcept + { + if (source == ClusterSourceId{0}) { + return mTracker != nullptr && !mTracker->getIterationConfigurations().empty() + ? std::optional{mTracker->getExecutionPolicy().DropTFUponFailure} + : std::nullopt; + } + if (source == ClusterSourceId{1}) { + return mTracker != nullptr && !mTracker->getIterationConfigurations().empty() + ? std::optional{mTracker->getExecutionPolicy().DropTFUponFailure} + : std::nullopt; + } + return std::nullopt; + } + void configureRofTables(const ClusterSourceInput& itsSource, const ClusterSourceInput& mftSource) + { + auto configure = [](auto& overlap, auto& vertex, auto& mask, const auto& timing, uint32_t nROFs, int layers) { + o2::its::LayerTiming layerTiming{}; + layerTiming.mNROFsTF = nROFs; + layerTiming.mROFLength = timing.rofLength; + layerTiming.mROFDelay = timing.rofDelay; + layerTiming.mROFBias = timing.rofBias; + layerTiming.mROFAddTimeErr = timing.rofAddTimeErr; + for (int layer = 0; layer < layers; ++layer) { + overlap.defineLayer(layer, layerTiming); + vertex.defineLayer(layer, layerTiming); + } + overlap.init(); + vertex.init(); + mask = std::remove_cvref_t{overlap}; + mask.resetMask(); + for (int layer = 0; layer < layers; ++layer) { + mask.setROFsEnabled(layer, 0, static_cast(nROFs), 1); + } + }; + configure(mITSROFOverlapTable, mITSROFVertexLookupTable, mITSMultiplicityMask, itsSource.timing, static_cast(itsSource.rofs.size()), ITSNLayers); + configure(mMFTROFOverlapTable, mMFTROFVertexLookupTable, mMFTMultiplicityMask, mftSource.timing, static_cast(mftSource.rofs.size()), MFTNLayers); + } + + const TimeFrameScratch& getITSScratch() const noexcept { return mFrame->getScratch(); } + const TimeFrameScratch& getMFTScratch() const noexcept { return mFrame->getScratch(); } + gsl::span getITSLayerMapping() const noexcept { return mITSLayerMapping; } + gsl::span getMFTLayerMapping() const noexcept { return mMFTLayerMapping; } + const ITSSharedClusterCompatibility& getITSSharedClusterCompatibility() const noexcept + { + return mITSCompatibility; + } + TraversalTopologyView getITSLayoutView() const noexcept + { + const auto* configuration = mTracker == nullptr ? nullptr : mTracker->getIterationConfiguration(0); + return mFrame != nullptr && configuration != nullptr && mTracker->isConfiguredFor(*mFrame) + ? configuration->getTopologyView(mFrame->getLayout().getSurfaceCatalog()) + : TraversalTopologyView{}; + } + TraversalTopologyView getMFTLayoutView() const noexcept { return getITSLayoutView(); } + + private: + const std::vector mITSLayerMapping = orderedSurfaceRange(0, ITSNLayers); + const std::vector mMFTLayerMapping = orderedSurfaceRange(ITSNLayers, MFTNLayers); + TrackerInitialization mConfiguration; + TimeFrame* mFrame = nullptr; + std::unique_ptr mTracker; + std::unique_ptr mTraits; + std::optional mLastResult; + o2::its::ca::PublicationAdapter mITSPublicationAdapter; + ITSSharedClusterCompatibility mITSCompatibility; + o2::its::ROFOverlapTable mITSROFOverlapTable; + o2::its::ROFVertexLookupTable mITSROFVertexLookupTable; + o2::its::ROFMaskTable mITSMultiplicityMask; + o2::its::ROFMaskTable mITSUPCMask; + o2::its::ROFOverlapTable mMFTROFOverlapTable; + o2::its::ROFVertexLookupTable mMFTROFVertexLookupTable; + o2::its::ROFMaskTable mMFTMultiplicityMask; + o2::its::ROFMaskTable mMFTUPCMask; + std::shared_ptr mArena; +}; + +} // namespace o2::itsmft::tracking::test + +#endif // ALICEO2_ITSMFT_TRACKING_TEST_COMBINEDTRACKINGTESTSUPPORT_H_ diff --git a/Detectors/ITSMFT/common/tracking/test/TrackingParameterTestSupport.h b/Detectors/ITSMFT/common/tracking/test/TrackingParameterTestSupport.h new file mode 100644 index 0000000000000..dc378680a6baf --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/TrackingParameterTestSupport.h @@ -0,0 +1,93 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_PARAMETER_TEST_SUPPORT_H_ +#define ALICEO2_ITSMFT_TRACKING_PARAMETER_TEST_SUPPORT_H_ +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "ITSMFTTracking/detail/MFTFwdTrackHelpers.h" + +namespace o2::itsmft::tracking::test +{ +template +concept HasDetectorRadii = requires(T value) { value.LayerRadii; }; +template +concept HasMemoryPolicy = requires(T value) { value.MaxMemory; }; +template +concept HasFailurePolicy = requires(T value) { value.DropTFUponFailure; }; +static_assert(!HasDetectorRadii); +static_assert(!HasMemoryPolicy); +static_assert(!HasFailurePolicy); + +// Retain the old input shape only for independent numerical reference fixtures. +struct ReferenceTrackingParameters : TrackingParameters { + std::vector LayerxX0{kNominalITSLayerX0.begin(), kNominalITSLayerX0.end()}; +}; +inline void resetDetectorDefaults(ReferenceTrackingParameters& parameters, o2::detectors::DetID::ID detector) +{ + o2::itsmft::resetDetectorDefaults(parameters, detector); + parameters.LayerxX0.clear(); + const auto catalog = detector == o2::detectors::DetID::ITS + ? SurfaceCatalogView{kITSStaticSurfaceCatalog.data(), kITSStaticSurfaceCatalog.size()} + : SurfaceCatalogView{kMFTStaticSurfaceCatalog.data(), kMFTStaticSurfaceCatalog.size()}; + for (uint32_t layer = 0; layer < catalog.nSurfaces; ++layer) { + parameters.LayerxX0.push_back(catalog.surfaces[layer].material.xOverX0); + } +} +inline TrackingPlan makeTrackingPlan(const TrackingParameters& parameters) +{ + return {parameters, parameters, {parameters}}; +} +inline TrackingPlan makeTrackingPlan(TrackingParameters&& parameters) +{ + TrackingPlan plan{std::move(static_cast(parameters)), parameters, {}}; + plan.iterations.push_back(std::move(static_cast(parameters))); + return plan; +} +template +TrackingPlan makeTrackingPlan(const std::vector& parameters) +{ + if (parameters.empty()) { + return {}; + } + auto plan = makeTrackingPlan(parameters.front()); + plan.iterations.assign(parameters.begin(), parameters.end()); + return plan; +} +// Expand the split result solely to keep pre-refactor preset assertions intact. +inline std::vector expandTrackingPlan(const TrackingPlan& plan) +{ + std::vector result; + for (const auto& iteration : plan.iterations) { + result.push_back({iteration, plan.detector, plan.execution}); + } + return result; +} +inline std::vector referenceTrackingParameters(o2::detectors::DetID::ID detector, TrackingMode::Type mode) +{ + return expandTrackingPlan(TrackingMode::getTrackingPlan(detector, mode)); +} +} // namespace o2::itsmft::tracking::test +namespace o2::itsmft::tracking::detail +{ +inline float mftLayerMSAngle(int layer, const test::ReferenceTrackingParameters& params) +{ + const float invP = 1.f / params.TrackletMinPt; + const float zLayer = mftLayerZ(layer); + const float rRef = params.LayerRadii[layer]; + const float tanlRef = (std::abs(rRef) > 1e-6f) ? zLayer / rRef : 0.f; + const float absTanl = std::abs(tanlRef); + const float cscLambda = (absTanl > 1e-6f) ? std::sqrt(1.f + tanlRef * tanlRef) / absTanl : 1e6f; + return 0.0136f * invP * std::sqrt(params.LayerxX0[layer] * cscLambda); +} + +} // namespace o2::itsmft::tracking::detail +#endif diff --git a/Detectors/ITSMFT/common/tracking/test/TraversalTestSupport.h b/Detectors/ITSMFT/common/tracking/test/TraversalTestSupport.h new file mode 100644 index 0000000000000..73e7199a951a9 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/TraversalTestSupport.h @@ -0,0 +1,86 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef ALICEO2_ITSMFT_TRACKING_TEST_TRAVERSALTESTSUPPORT_H_ +#define ALICEO2_ITSMFT_TRACKING_TEST_TRAVERSALTESTSUPPORT_H_ + +#include + +#include "ITSMFTTracking/Tracker.h" + +namespace o2::itsmft::tracking +{ + +// Test-only access to the Tracker-owned initialization transaction and +// explicit backend stages. The caller owns the span buffer for the returned view. +struct TrackerTestAccess { + static IterationContext prepare(Tracker& tracker, TimeFrame& frame, int iteration, + std::array, MaxLayoutSurfaces>& measurementSpans) + { + const auto* configuration = iteration < 0 ? nullptr : tracker.getIterationConfiguration(static_cast(iteration)); + if (configuration == nullptr || !tracker.isConfiguredFor(frame)) { + throw std::out_of_range{"test traversal iteration"}; + } + auto& scratch = frame.getScratch(); + auto layerGlobalMeasurements = tracker.prepareTimeFrame(frame, measurementSpans); + IterationContext view{iteration, + frame, + scratch, + configuration->getTopologyView(frame.getLayout().getSurfaceCatalog()), + *configuration, + tracker.mDetectorConfiguration, + layerGlobalMeasurements, + frame.getBz()}; + tracker.initializeIteration(view); + return view; + } + + static void computeTracklets(TrackerTraits& traits, IterationContext& view, int vertex) + { + traits.computeLayerTracklets(view, view.iteration, vertex); + } + + static void computeCells(TrackerTraits& traits, IterationContext& view) + { + traits.computeLayerCells(view, view.iteration); + } + + static void findNeighbours(TrackerTraits& traits, IterationContext& view) + { + traits.findCellsNeighbours(view, view.iteration); + } + + static bool buildTrackSeed(TrackerTraits& traits, IterationContext& view, + int cellPathId, const CellSeed& cell, + TrackSeed& output, OperationFailureReason& reason) + { + return traits.buildTrackSeed(view, cellPathId, cell, output, reason); + } + + static void findRoads(TrackerTraits& traits, IterationContext& view) + { + traits.findRoads(view, view.iteration); + } + + static void computeTracksMClabels(Tracker& tracker, TimeFrame& frame) + { + tracker.computeTracksMClabels(frame); + } + + static void configureBeamPosition(Tracker& tracker, TimeFrame& frame) + { + tracker.configureBeamPosition(frame); + } +}; + +} // namespace o2::itsmft::tracking + +#endif diff --git a/Detectors/ITSMFT/common/tracking/test/testCellFinding.cxx b/Detectors/ITSMFT/common/tracking/test/testCellFinding.cxx new file mode 100644 index 0000000000000..d16f5afb0119d --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testCellFinding.cxx @@ -0,0 +1,495 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE ITSMFT CellFindingNative +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK + +#include +#include +#include + +#include + +#include "ITSMFTTracking/detail/SurfaceStateOperations.h" +#include "ITSMFTTracking/MaterialPhysics.h" +#include "ITSMFTTracking/Propagator.h" +#include "ITSMFTTracking/detail/TrackingKernelParameters.h" +#include "ITStracking/Cluster.h" + +/// Focused coverage for the explicit cylinder/disk SurfaceTrackState +/// compatibility and hit-attachment leaves. The leaves are production-wired +/// through TrackerTraits; this test exercises their numerical contracts +/// directly, while the separate orchestration tests cover their callers. +/// +/// Oracle strategy: +/// - Formula-preserving evidence (rotation/propagation, predicted +/// chi2/update, state compatibility) is checked by an +/// independent, hand-written re-transcription of each operation's own +/// documented call sequence, built directly on the already-oracle-tested +/// detail::barrel::/detail::forward:: primitives (BarrelSurfaceStateOperations.h, +/// ForwardSurfaceStateOperations.h -- each already characterized against +/// its own legacy oracle in testBarrelSurfaceStateOperations.cxx / +/// testForwardSurfaceStateOperations.cxx). A bit-identical match against +/// this independent replay is strong evidence that the production +/// orchestration (step order, material-slot selection, chi2-cut +/// placement, measurement projection) is correct, without re-deriving +/// the already-tested primitive formulas themselves. +/// - Intentional material-physics differences (PID/absCharge-aware barrel +/// covariance correction; newly active forward energy loss/straggling) +/// are validated by observing that charge/PID and areal-density inputs +/// measurably change the result, not by comparing to legacy MCS-only +/// output. +using namespace o2::itsmft::tracking; + +namespace +{ + +template +bool bitEqual(const T& lhs, const T& rhs) +{ + return std::memcmp(&lhs, &rhs, sizeof(T)) == 0; +} + +bool attachMeasurement(SurfaceTrackState& state, const SurfaceMeasurement& measurement, + NominalSurfaceMaterial material, float bz, float& chi2, + const TrackingKernelParameters& parameters, + OperationFailureReason& reason) +{ + SurfaceDescriptor target{}; + target.kind = state.kind; + target.material = material; + return Propagator::attachMeasurement(state, target, measurement, bz, + material::MaterialTraversalDirection::OppositeMomentum, true, + parameters.maxChi2ClusterAttachment, chi2, reason); +} + +SurfaceMeasurement barrelMeasurementFromHit(const o2::its::TrackingFrameInfo& hit) +{ + SurfaceMeasurement measurement{}; + measurement.frame.q = hit.xTrackingFrame; + measurement.frame.frameAngle = hit.alphaTrackingFrame; + measurement.frame.u = hit.positionTrackingFrame[0]; + measurement.frame.v = hit.positionTrackingFrame[1]; + measurement.covariance.uu = hit.covarianceTrackingFrame[0]; + measurement.covariance.uv = hit.covarianceTrackingFrame[1]; + measurement.covariance.vv = hit.covarianceTrackingFrame[2]; + return measurement; +} + +SurfaceMeasurement diskMeasurementFromHit(const o2::its::TrackingFrameInfo& hit) +{ + SurfaceMeasurement measurement{}; + measurement.frame = {hit.zCoordinate, hit.xCoordinate, hit.yCoordinate, 0.f}; + measurement.covariance.uu = hit.covarianceTrackingFrame[0]; + measurement.covariance.uv = 0.f; + measurement.covariance.vv = hit.covarianceTrackingFrame[2]; + return measurement; +} + +// --- attachHit fixtures ----------------------------------------------- + +SurfaceTrackState barrelAttachState() +{ + SurfaceTrackState state{}; + state.parameters[0] = 1.25f; + state.parameters[1] = -0.75f; + state.parameters[2] = 0.2f; + state.parameters[3] = -0.35f; + state.parameters[4] = 0.8f; + state.referenceCoordinate = 4.f; + state.alpha = 0.3f; + state.kind = SurfaceKind::Cylinder; + state.absCharge = 1; + state.pid = o2::track::PID::Kaon; + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column <= row; ++column) { + state.covariance[packedCovarianceIndex(row, column)] = row == column ? 0.01f * (row + 1) : 0.0002f * (row + column + 1); + } + } + return state; +} + +o2::its::TrackingFrameInfo barrelAttachHit() +{ + return o2::its::TrackingFrameInfo{0.f, 0.f, 0.f, 2.5f, 0.3f, {0.8f, -0.45f}, {0.04f, 0.012f, 0.09f}}; +} + +constexpr float BarrelAttachBz = 5.f; + +NominalSurfaceMaterial barrelAttachMaterial() { return NominalSurfaceMaterial{0.01f, 0.001f}; } + +SurfaceTrackState diskAttachState() +{ + SurfaceTrackState state{}; + state.parameters[0] = 1.25f; + state.parameters[1] = -0.75f; + state.parameters[2] = 0.35f; + state.parameters[3] = -2.5f; + state.parameters[4] = 0.8f; + state.referenceCoordinate = -45.f; + state.kind = SurfaceKind::Disk; + state.absCharge = 2; + state.pid = o2::track::PID::Pion; + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column <= row; ++column) { + state.covariance[packedCovarianceIndex(row, column)] = row == column ? 0.01f * (row + 1) : 0.0002f * (row + column + 1); + } + } + return state; +} + +o2::its::TrackingFrameInfo diskAttachHit() +{ + return o2::its::TrackingFrameInfo{0.8f, -0.45f, -50.f, 0.f, 0.f, {0.f, 0.f}, {0.04f, 0.f, 0.09f}}; +} + +constexpr float DiskAttachBz = 5.f; + +NominalSurfaceMaterial diskAttachMaterial() { return NominalSurfaceMaterial{0.02f, 0.002f}; } + +// Test-local field-mapping helper: builds the SurfaceMeasurement +// attachDiskHit reads from a single legacy hit (Disk field mapping: +// global coordinates -> measurement.global, reference z -> measurement. +// frame.q [read as the propagate target, in place of the retired +// hit.zCoordinate], measured covariance -> measurement.covariance). +SurfaceMeasurement diskAttachMeasurementFrom(const o2::its::TrackingFrameInfo& hit) +{ + auto measurement = diskMeasurementFromHit(hit); + measurement.frame.q = hit.zCoordinate; + return measurement; +} + +SurfaceMeasurement diskAttachMeasurement() { return diskAttachMeasurementFrom(diskAttachHit()); } + +} // namespace + +// =========================================================================== +// Measurement attachment +// =========================================================================== + +BOOST_AUTO_TEST_CASE(AttachHitBarrelSuccessAndExactChi2Threshold) +{ + const auto state0 = barrelAttachState(); + const auto hit = barrelMeasurementFromHit(barrelAttachHit()); + const auto material = barrelAttachMaterial(); + + auto probe = state0; + float probeChi2 = 0.f; + OperationFailureReason reason{}; + TrackingKernelParameters permissive; + permissive.maxChi2ClusterAttachment = 1.e6f; + BOOST_REQUIRE(attachMeasurement(probe, hit, material, BarrelAttachBz, probeChi2, permissive, reason)); + BOOST_REQUIRE_GT(probeChi2, 0.f); + + auto accepted = state0; + float acceptedChi2 = 0.f; + TrackingKernelParameters accept; + accept.maxChi2ClusterAttachment = probeChi2; + BOOST_CHECK(attachMeasurement(accepted, hit, material, BarrelAttachBz, acceptedChi2, accept, reason)); + BOOST_CHECK(bitEqual(accepted, probe)); + BOOST_CHECK_EQUAL(acceptedChi2, probeChi2); + + auto rejected = state0; + float rejectedChi2 = -1.f; + const auto before = rejected; + const float chi2Before = rejectedChi2; + TrackingKernelParameters reject; + reject.maxChi2ClusterAttachment = std::nextafter(probeChi2, -std::numeric_limits::infinity()); + BOOST_CHECK(!attachMeasurement(rejected, hit, material, BarrelAttachBz, rejectedChi2, reject, reason)); + BOOST_CHECK(reason == OperationFailureReason::PredictedChi2Failure); + BOOST_CHECK(bitEqual(rejected, before)); + BOOST_CHECK_EQUAL(rejectedChi2, chi2Before); +} + +BOOST_AUTO_TEST_CASE(AttachHitBarrelEachFailureStagePreservesStateTransactionally) +{ + const auto state0 = barrelAttachState(); + TrackingKernelParameters permissive; + permissive.maxChi2ClusterAttachment = 1.e6f; + + auto checkFailure = [&](const SurfaceMeasurement& measurement, const NominalSurfaceMaterial& material, + OperationFailureReason expectedOneOf1, OperationFailureReason expectedOneOf2) { + auto state = state0; + float chi2 = -1.f; + const auto before = state; + const float chi2Before = chi2; + OperationFailureReason reason{}; + BOOST_CHECK(!attachMeasurement(state, measurement, material, BarrelAttachBz, chi2, permissive, reason)); + BOOST_CHECK(reason == expectedOneOf1 || reason == expectedOneOf2); + BOOST_CHECK(bitEqual(state, before)); + BOOST_CHECK_EQUAL(chi2, chi2Before); + }; + + // Rotation failure. + { + auto farHit = barrelAttachHit(); + farHit.alphaTrackingFrame = state0.alpha + 3.f; + checkFailure(barrelMeasurementFromHit(farHit), barrelAttachMaterial(), OperationFailureReason::RotationFailure, OperationFailureReason::RotationFailure); + } + + // Propagation failure. + { + auto farHit = barrelAttachHit(); + farHit.xTrackingFrame = -50000.f; + checkFailure(barrelMeasurementFromHit(farHit), barrelAttachMaterial(), OperationFailureReason::UnreachableTarget, OperationFailureReason::PropagationFailure); + } +} + +BOOST_AUTO_TEST_CASE(AttachHitBarrelNegativeChi2IsRejectedMatchingLegacyInclusiveCut) +{ + // No-op rotate (hit shares state's alpha) and no-op propagate (hit's + // target x equals state's own referenceCoordinate) plus a no-op material + // budget ({0,0} is the documented unconditional no-op) keep the state + // byte-identical to barrelAttachState() through predictedChi2, so its + // known covariance can be reasoned about directly: a pathologically large + // measurement cross-covariance (uv) makes the combined 2x2 determinant + // negative, which residualInverse's own gate does not reject outright + // (only exact-zero/non-finite determinants are), producing a negative + // predicted chi2 -- the same `< 0.f` established rejection + // attachCylinderHit already applies today. detail::barrel::update shares + // the identical residualInverse gate and therefore cannot independently + // fail once predictedChi2 has already succeeded with the same inputs; the + // two checks share one deterministic failure precedence (predictedChi2, + // evaluated before update, is always the one that observes a bad + // residualInverse first). + auto state = barrelAttachState(); + auto hit = barrelAttachHit(); + hit.alphaTrackingFrame = state.alpha; + hit.xTrackingFrame = state.referenceCoordinate; + hit.covarianceTrackingFrame[1] = 50.f; // huge uv cross term + const auto measurement = barrelMeasurementFromHit(hit); + const NominalSurfaceMaterial noopMaterial{0.f, 0.f}; + const auto before = state; + float chi2 = -1.f; + const float chi2Before = chi2; + OperationFailureReason reason{}; + TrackingKernelParameters permissive; + permissive.maxChi2ClusterAttachment = 1.e6f; + BOOST_CHECK(!attachMeasurement(state, measurement, noopMaterial, BarrelAttachBz, chi2, permissive, reason)); + BOOST_CHECK(reason == OperationFailureReason::PredictedChi2Failure); + BOOST_CHECK(bitEqual(state, before)); + BOOST_CHECK_EQUAL(chi2, chi2Before); +} + +BOOST_AUTO_TEST_CASE(AttachHitBarrelIsChargeAwareUnlikeNeutralMaterialCorrection) +{ + // PID/absCharge-aware behavior: a neutral state (absCharge == 0) takes the + // material kernel's documented unconditional no-op path, while a charged + // state with identical kinematics picks up a Highland covariance + // contribution -- the results must differ. + auto neutral = barrelAttachState(); + neutral.absCharge = 0; + auto charged = barrelAttachState(); + charged.absCharge = 1; + const auto hit = barrelMeasurementFromHit(barrelAttachHit()); + const auto material = barrelAttachMaterial(); + + float neutralChi2 = 0.f; + float chargedChi2 = 0.f; + OperationFailureReason reason{}; + TrackingKernelParameters permissive; + permissive.maxChi2ClusterAttachment = 1.e6f; + BOOST_REQUIRE(attachMeasurement(neutral, hit, material, BarrelAttachBz, neutralChi2, permissive, reason)); + BOOST_REQUIRE(attachMeasurement(charged, hit, material, BarrelAttachBz, chargedChi2, permissive, reason)); + BOOST_CHECK(!bitEqual(neutral, charged)); +} + +BOOST_AUTO_TEST_CASE(AttachHitBarrelIsByteDeterministic) +{ + auto first = barrelAttachState(); + auto second = barrelAttachState(); + float chi2First = 0.f; + float chi2Second = 0.f; + OperationFailureReason reason{}; + TrackingKernelParameters permissive; + permissive.maxChi2ClusterAttachment = 1.e6f; + BOOST_REQUIRE(attachMeasurement(first, barrelMeasurementFromHit(barrelAttachHit()), barrelAttachMaterial(), BarrelAttachBz, chi2First, permissive, reason)); + BOOST_REQUIRE(attachMeasurement(second, barrelMeasurementFromHit(barrelAttachHit()), barrelAttachMaterial(), BarrelAttachBz, chi2Second, permissive, reason)); + BOOST_CHECK(bitEqual(first, second)); + BOOST_CHECK_EQUAL(chi2First, chi2Second); +} + +// =========================================================================== +// attachDiskHit +// =========================================================================== + +BOOST_AUTO_TEST_CASE(AttachHitDiskSuccessAndExactChi2Threshold) +{ + const auto state0 = diskAttachState(); + const auto hit = diskAttachMeasurement(); + const auto material = diskAttachMaterial(); + + auto probe = state0; + float probeChi2 = 0.f; + OperationFailureReason reason{}; + TrackingKernelParameters permissive; + permissive.maxChi2ClusterAttachment = 1.e6f; + BOOST_REQUIRE(attachMeasurement(probe, hit, material, DiskAttachBz, probeChi2, permissive, reason)); + BOOST_REQUIRE_GT(probeChi2, 0.f); + + auto accepted = state0; + float acceptedChi2 = 0.f; + TrackingKernelParameters accept; + accept.maxChi2ClusterAttachment = probeChi2; + BOOST_CHECK(attachMeasurement(accepted, hit, material, DiskAttachBz, acceptedChi2, accept, reason)); + BOOST_CHECK(bitEqual(accepted, probe)); + BOOST_CHECK_EQUAL(acceptedChi2, probeChi2); + + auto rejected = state0; + float rejectedChi2 = -1.f; + const auto before = rejected; + const float chi2Before = rejectedChi2; + TrackingKernelParameters reject; + reject.maxChi2ClusterAttachment = std::nextafter(probeChi2, -std::numeric_limits::infinity()); + BOOST_CHECK(!attachMeasurement(rejected, hit, material, DiskAttachBz, rejectedChi2, reject, reason)); + BOOST_CHECK(reason == OperationFailureReason::PredictedChi2Failure); + BOOST_CHECK(bitEqual(rejected, before)); + BOOST_CHECK_EQUAL(rejectedChi2, chi2Before); +} + +BOOST_AUTO_TEST_CASE(AttachHitDiskEachFailureStagePreservesStateTransactionally) +{ + const auto state0 = diskAttachState(); + TrackingKernelParameters permissive; + permissive.maxChi2ClusterAttachment = 1.e6f; + + // Propagation failure: tanl == 0 at zero field rejects with + // UnreachableTarget. + { + auto zeroTanl = state0; + zeroTanl.parameters[3] = 0.f; + auto hit = diskAttachHit(); + hit.zCoordinate = -60.f; // dz != 0 + const auto measurement = diskAttachMeasurementFrom(hit); + auto state = zeroTanl; + float chi2 = -1.f; + const auto before = state; + const float chi2Before = chi2; + OperationFailureReason reason{}; + BOOST_CHECK(!attachMeasurement(state, measurement, diskAttachMaterial(), 0.f, chi2, permissive, reason)); + BOOST_CHECK(reason == OperationFailureReason::UnreachableTarget); + BOOST_CHECK(bitEqual(state, before)); + BOOST_CHECK_EQUAL(chi2, chi2Before); + } +} + +BOOST_AUTO_TEST_CASE(AttachHitDiskIsChargeAwareUnlikeNeutralMaterialCorrection) +{ + auto neutral = diskAttachState(); + neutral.absCharge = 0; + auto charged = diskAttachState(); + charged.absCharge = 1; + const auto hit = diskAttachMeasurement(); + const auto material = diskAttachMaterial(); + + float neutralChi2 = 0.f; + float chargedChi2 = 0.f; + OperationFailureReason reason{}; + TrackingKernelParameters permissive; + permissive.maxChi2ClusterAttachment = 1.e6f; + BOOST_REQUIRE(attachMeasurement(neutral, hit, material, DiskAttachBz, neutralChi2, permissive, reason)); + BOOST_REQUIRE(attachMeasurement(charged, hit, material, DiskAttachBz, chargedChi2, permissive, reason)); + BOOST_CHECK(!bitEqual(neutral, charged)); +} + +BOOST_AUTO_TEST_CASE(AttachHitDiskActivatesEnergyLossUnlikeLegacyMcsOnlyPath) +{ + auto noLossMaterial = diskAttachMaterial(); + noLossMaterial.arealDensityGPerCm2 = 0.f; + auto withLossMaterial = diskAttachMaterial(); + + auto stateNoLoss = diskAttachState(); + auto stateWithLoss = diskAttachState(); + const auto hit = diskAttachMeasurement(); + float chi2NoLoss = 0.f; + float chi2WithLoss = 0.f; + OperationFailureReason reason{}; + TrackingKernelParameters permissive; + permissive.maxChi2ClusterAttachment = 1.e6f; + BOOST_REQUIRE(attachMeasurement(stateNoLoss, hit, noLossMaterial, DiskAttachBz, chi2NoLoss, permissive, reason)); + BOOST_REQUIRE(attachMeasurement(stateWithLoss, hit, withLossMaterial, DiskAttachBz, chi2WithLoss, permissive, reason)); + BOOST_CHECK_NE(stateNoLoss.parameters[4], stateWithLoss.parameters[4]); +} + +BOOST_AUTO_TEST_CASE(AttachHitDiskIsByteDeterministic) +{ + auto first = diskAttachState(); + auto second = diskAttachState(); + float chi2First = 0.f; + float chi2Second = 0.f; + OperationFailureReason reason{}; + TrackingKernelParameters permissive; + permissive.maxChi2ClusterAttachment = 1.e6f; + BOOST_REQUIRE(attachMeasurement(first, diskAttachMeasurement(), diskAttachMaterial(), DiskAttachBz, chi2First, permissive, reason)); + BOOST_REQUIRE(attachMeasurement(second, diskAttachMeasurement(), diskAttachMaterial(), DiskAttachBz, chi2Second, permissive, reason)); + BOOST_CHECK(bitEqual(first, second)); + BOOST_CHECK_EQUAL(chi2First, chi2Second); +} + +// =========================================================================== +// Compatibility-projection coverage (private TrackingFrameInfo -> +// SurfaceMeasurement boundary, exercised indirectly through attachHit). +// =========================================================================== + +BOOST_AUTO_TEST_CASE(BarrelProjectionUsesFullCovarianceIncludingCrossTerm) +{ + const auto state0 = barrelAttachState(); + auto lowCrossTerm = barrelAttachHit(); + lowCrossTerm.covarianceTrackingFrame[1] = 0.f; + auto highCrossTerm = barrelAttachHit(); + highCrossTerm.covarianceTrackingFrame[1] = 0.03f; + + auto stateLow = state0; + auto stateHigh = state0; + float chi2Low = 0.f; + float chi2High = 0.f; + OperationFailureReason reason{}; + TrackingKernelParameters permissive; + permissive.maxChi2ClusterAttachment = 1.e6f; + BOOST_REQUIRE(attachMeasurement(stateLow, barrelMeasurementFromHit(lowCrossTerm), barrelAttachMaterial(), BarrelAttachBz, chi2Low, permissive, reason)); + BOOST_REQUIRE(attachMeasurement(stateHigh, barrelMeasurementFromHit(highCrossTerm), barrelAttachMaterial(), BarrelAttachBz, chi2High, permissive, reason)); + BOOST_CHECK_NE(chi2Low, chi2High); +} + +BOOST_AUTO_TEST_CASE(ForwardProjectionIsDiagonalOnlyAndIgnoresUnreadTrackingFrameFields) +{ + const auto state0 = diskAttachState(); + const auto baseline = diskAttachHit(); + + auto varyingCrossTerm = baseline; + varyingCrossTerm.covarianceTrackingFrame[1] = 999.f; // forward never reads this slot + + auto varyingUnreadFields = baseline; + varyingUnreadFields.xTrackingFrame = 12345.f; + varyingUnreadFields.alphaTrackingFrame = 6.7f; + varyingUnreadFields.positionTrackingFrame = {-999.f, 999.f}; + + TrackingKernelParameters permissive; + permissive.maxChi2ClusterAttachment = 1.e6f; + OperationFailureReason reason{}; + + auto stateBaseline = state0; + float chi2Baseline = 0.f; + BOOST_REQUIRE(attachMeasurement(stateBaseline, diskAttachMeasurementFrom(baseline), diskAttachMaterial(), DiskAttachBz, chi2Baseline, permissive, reason)); + + auto stateCrossTerm = state0; + float chi2CrossTerm = 0.f; + BOOST_REQUIRE(attachMeasurement(stateCrossTerm, diskAttachMeasurementFrom(varyingCrossTerm), diskAttachMaterial(), DiskAttachBz, chi2CrossTerm, permissive, reason)); + BOOST_CHECK(bitEqual(stateBaseline, stateCrossTerm)); + BOOST_CHECK_EQUAL(chi2Baseline, chi2CrossTerm); + + auto stateUnreadFields = state0; + float chi2UnreadFields = 0.f; + BOOST_REQUIRE(attachMeasurement(stateUnreadFields, diskAttachMeasurementFrom(varyingUnreadFields), diskAttachMaterial(), DiskAttachBz, chi2UnreadFields, permissive, reason)); + BOOST_CHECK(bitEqual(stateBaseline, stateUnreadFields)); + BOOST_CHECK_EQUAL(chi2Baseline, chi2UnreadFields); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testCombinedTrackingComposition.cxx b/Detectors/ITSMFT/common/tracking/test/testCombinedTrackingComposition.cxx new file mode 100644 index 0000000000000..46b414895b702 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testCombinedTrackingComposition.cxx @@ -0,0 +1,1365 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE ITSMFT CombinedTrackingComposition +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK + +#include "TrackingParameterTestSupport.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include "Field/MagneticField.h" + +#include "CommonDataFormat/InteractionRecord.h" +#include "CombinedTrackingTestSupport.h" +#include "DataFormatsITSMFT/CompCluster.h" +#include "DataFormatsITSMFT/ROFRecord.h" +#include "DataFormatsITSMFT/TopologyDictionary.h" +#include "DetectorsCommonDataFormats/DetID.h" +#include "ITSMFTTracking/Tracker.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/detail/ITSSharedClusterCompatibility.h" +#include "ITSMFTTracking/detail/TimeFrameScratch.h" +#include "ITSMFTTracking/detail/MFTFwdTrackHelpers.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/ClusterDecoding.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/TrackerTraits.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "ITSMFTTracking/GenericTrackOutputAdapter.h" +#include "ITSMFTTracking/Constants.h" +#include "ReconstructionDataFormats/Track.h" + +using namespace o2::itsmft; +using namespace o2::itsmft::tracking; + +namespace +{ + +struct GenericTrackPublicationExport { + o2::detectors::DetID::ID detector{}; + ClusterSourceId source{}; + ClockTimingPublicationView clock; + gsl::span layerMapping; +}; + +constexpr float Bz = 0.5f; +constexpr std::array OnePixelPattern{1, 1, 0x80}; + +const TopologyDictionary& dict() +{ + static const TopologyDictionary d; + return d; +} + +void ensureTrivialMagneticFieldIsSet() +{ + static const bool done = [] { + TGeoGlobalMagField::Instance()->SetField(new o2::field::MagneticField()); + TGeoGlobalMagField::Instance()->Lock(); + return true; + }(); + (void)done; +} + +std::vector ordered(uint16_t first, uint16_t count) +{ + std::vector result; + result.reserve(count); + for (uint16_t i = 0; i < count; ++i) { + result.push_back(LayerId{static_cast(first + i)}); + } + return result; +} + +class PrescribedDecoder final : public ClusterDecoder +{ + public: + PrescribedDecoder(o2::detectors::DetID::ID detector, SurfaceKind kind, std::vector clusters) + : mDetector{detector}, mKind{kind}, mClusters{std::move(clusters)} + { + } + + o2::itsmft::tracking::ClusterDecodeResult decode( + const CompClusterExt& cluster, + BoundedPatternCursor& patterns, + const TopologyDictionary* dictionary, + uint32_t externalIndex, + bool) const final + { + const auto clusterData = o2::itsmft::ioutils::extractClusterDataBounded(cluster, patterns, dictionary); + if (!clusterData.ok()) { + o2::itsmft::tracking::ClusterDecodeResult result; + result.error = clusterData.error; + return result; + } + + o2::itsmft::tracking::ClusterDecodeResult result; + if (externalIndex >= mClusters.size()) { + return result; + } + auto decoded = mClusters[externalIndex]; + decoded.shape = clusterData.shape; + result.decoded = decoded; + return result; + } + + private: + o2::detectors::DetID::ID mDetector; + SurfaceKind mKind; + std::vector mClusters; +}; + +DecodedCluster diskCluster(float x, float y, float z, int layer) +{ + DecodedCluster cluster{}; + cluster.global = {x, y, z}; + cluster.rowColumnCovariance = {1.e-2f, 0.f, 1.e-2f}; + cluster.layer = layer; + return cluster; +} + +DecodedCluster cylinderCluster(float radius, float phi, float tanLambda, int layer) +{ + DecodedCluster cluster{}; + cluster.global = {radius * std::cos(phi), radius * std::sin(phi), radius * tanLambda}; + cluster.cylinderFrame = {cluster.global.x, cluster.global.y, cluster.global.z, 0.f}; + cluster.rowColumnCovariance = {1.e-2f, 0.f, 1.e-2f}; + cluster.layer = layer; + return cluster; +} + +/// Same chained-projection construction as +/// testComputeLayerTrackletsOrchestration.cxx's buildMftChainClusters() / +/// the former traversal-binding orchestration test's identically-named helper: +/// each hop's target is a genuine geometric match via +/// detail::mftTrackletProject, so every adjacent pair in the chain produces +/// a real tracklet, and a full-length chain reaches acceptance. +std::vector buildMftChainClusters(const TrackingParameters& params, float bz, int nHops) +{ + std::vector clusters; + // Keep the synthetic trajectory on the descriptor-owned MFT radial chart. + // The former (1, 0.5) seed was inside the legacy square LUT but below the + // physical inner radius of every MFT disk. + float x = 3.f, y = 1.5f; + float z = detail::mftLayerZ(0); + clusters.push_back(diskCluster(x, y, z, 0)); + for (int hop = 0; hop < nHops; ++hop) { + const float nextZ = detail::mftLayerZ(hop + 1); + float targetX = 0.f, targetY = 0.f; + detail::mftTrackletProject(x, y, z, params.Diamond[0], params.Diamond[1], params.Diamond[2], + hop, hop + 1, bz, params.TrackletMinPt, targetX, targetY); + clusters.push_back(diskCluster(targetX, targetY, nextZ, hop + 1)); + x = targetX; + y = targetY; + z = nextZ; + } + return clusters; +} + +/// A genuine, low-but-nonzero-curvature helical ITS barrel trajectory, +/// sampled at each nominal layer radius via the same standard O2 barrel- +/// propagation utility production ITS/TPC-matching code already uses +/// (o2::track::TrackPar::getXatLabR() to find the local x where the helix +/// crosses a given lab radius, then getXYZGloAt() to read the global point +/// there -- both const, no incremental state mutation between layers). +/// +/// A perfectly collinear ("infinite pT" / zero-curvature) triple does not +/// define the linearized triplet factor used by cell construction. This +/// helix construction therefore supplies a deliberately non-degenerate ITS +/// road fixture. +std::vector buildItsHelixChainClusters(const std::vector& radii, float bz, float pt, float phi0, float tanl) +{ + const float px = pt * std::cos(phi0); + const float py = pt * std::sin(phi0); + const float pz = pt * tanl; + o2::track::TrackPar seed(std::array{0.f, 0.f, 0.f}, std::array{px, py, pz}, 1, true); + + std::vector clusters; + clusters.reserve(radii.size()); + for (size_t layer = 0; layer < radii.size(); ++layer) { + float xAtR = 0.f; + if (!seed.getXatLabR(radii[layer], xAtR, bz, o2::track::DirType::DirOutward)) { + return {}; + } + bool ok = false; + const auto point = seed.getXYZGloAt(xAtR, bz, ok); + if (!ok) { + return {}; + } + DecodedCluster cluster{}; + cluster.global = {static_cast(point.X()), static_cast(point.Y()), static_cast(point.Z())}; + cluster.cylinderFrame = {cluster.global.x, cluster.global.y, cluster.global.z, 0.f}; + cluster.rowColumnCovariance = {1.e-2f, 0.f, 1.e-2f}; + cluster.layer = static_cast(layer); + clusters.push_back(cluster); + } + return clusters; +} + +TrackingParameters makeItsParams() +{ + TrackingParameters p; + resetDetectorDefaults(p, o2::detectors::DetID::ITS); + // Tracklet formation needs a primary vertex to seed the search window + // (TrackerTraits.cxx's forTracklets()): with UseDiamond=false (ITS's own + // default) that must come from TimeFrame::getPrimaryVertices(), which + // these focused fixtures never populate. UseDiamond=true instead uses the + // fixed Diamond{0,0,0} vertex every synthetic radial chain below is built + // through, with no TimeFrame vertex needed -- the same knob + // buildMftChainClusters()'s MFT fixtures already rely on. + p.UseDiamond = true; + return p; +} + +TrackingParameters makeMftParams() +{ + TrackingParameters p; + resetDetectorDefaults(p, o2::detectors::DetID::MFT); + p.UseDiamond = true; + p.CreateArtefactLabels = false; + return p; +} + +/// Encodes `decoded` as compact/pattern input and returns a +/// ClusterSourceInput referencing `decoder`/`compactOut`/`patternsOut`/ +/// `rofsOut` (kept alive by the caller for the lifetime of every process() +/// call that uses it). +ClusterSourceInput makeSource(ClusterSourceId id, o2::detectors::DetID::ID det, const std::vector& surfaces, + const PrescribedDecoder& decoder, std::vector& compactOut, + std::vector& patternsOut, std::vector& rofsOut, + const std::vector& decoded) +{ + compactOut.reserve(decoded.size()); + patternsOut.reserve(decoded.size() * OnePixelPattern.size()); + for (const auto& cluster : decoded) { + compactOut.emplace_back(0, 0, CompCluster::InvalidPatternID, cluster.layer); + patternsOut.insert(patternsOut.end(), OnePixelPattern.begin(), OnePixelPattern.end()); + } + rofsOut = {ROFRecord{{100, 5}, 0, 0, static_cast(compactOut.size())}}; + + ClusterSourceInput source{}; + source.id = id; + source.detector = det; + source.clusters = compactOut; + source.patterns = patternsOut; + source.rofs = rofsOut; + source.dictionary = &dict(); + source.layerToSurface = surfaces; + source.timing = ROFTimingConfig{40, 0, 0, 0}; + source.decoder = &decoder; + return source; +} + +/// A source that is valid (dense-empty ROF, zero clusters) but describes no +/// hits at all -- the composition's own required "the other detector may be +/// empty" shape, matching the standalone workflow's zero-cluster path. +ClusterSourceInput makeEmptySource(ClusterSourceId id, o2::detectors::DetID::ID det, const std::vector& surfaces, + const PrescribedDecoder& decoder) +{ + ClusterSourceInput source{}; + source.id = id; + source.detector = det; + source.dictionary = &dict(); + source.layerToSurface = surfaces; + source.timing = ROFTimingConfig{40, 0, 0, 0}; + source.decoder = &decoder; + return source; +} + +/// Independent, non-combined, single-detector reference run: the same shape +/// the standalone path already uses -- global +/// LayerIds equal compact scratch slots, with the same plan-driven binding +/// model as the combined path. Used as the "reproduce the standalone oracle +/// count" reference for the combined composition. +template +struct StandaloneRun { + TimeFrame frame; + std::vector params; + std::shared_ptr pool = std::make_shared(); + Tracker tracker; + TrackerTraits traits; + std::shared_ptr arena; + TimeFrameScratch* scratch = nullptr; + std::vector catalog; + TrackingResult result; + + StandaloneRun(o2::detectors::DetID::ID det, SurfaceKind kind, + const TrackingParameters& singleParams, const std::vector& decoded, + int rofLength = 40) + : params{singleParams} + { + const auto orderedSurfaces = ordered(0, NLayers); + catalog.reserve(NLayers); + for (uint16_t i = 0; i < NLayers; ++i) { + SurfaceDescriptor surface{i, static_cast(det), kind}; + surface.chartRange = kind == SurfaceKind::Disk ? SurfaceChartRange{kMFTLookupRMin[i], kMFTLookupRMax[i]} : SurfaceChartRange{-20.f, 20.f}; + surface.referenceCoordinate = kind == SurfaceKind::Cylinder + ? singleParams.LayerRadii[i] + : detail::mftLayerZ(i); + const float xOverX0 = det == o2::detectors::DetID::MFT ? kNominalMFTLayerX0[i] : kNominalITSLayerX0[i]; + surface.material.xOverX0 = xOverX0; + surface.material.arealDensityGPerCm2 = xOverX0 * o2::its::constants::Radl * o2::its::constants::Rho; + catalog.push_back(surface); + } + const SurfaceCatalogView catalogView{catalog.data(), static_cast(catalog.size())}; + TrackerInitialization configuration; + configuration.catalog = catalogView; + configuration.memoryPool = pool; + configuration.layout = makeDetectorLayout(); + configuration.plan = o2::itsmft::tracking::test::makeTrackingPlan(singleParams); + const auto configured = tracker.initialize(frame, configuration); + BOOST_REQUIRE(configured.ok()); + scratch = &frame.getScratch(); + traits.setNThreads(1, arena); + frame.setBz(Bz); + + std::vector compact; + std::vector patterns; + for (const auto& cluster : decoded) { + compact.emplace_back(0, 0, CompCluster::InvalidPatternID, cluster.layer); + patterns.insert(patterns.end(), OnePixelPattern.begin(), OnePixelPattern.end()); + } + const std::vector rofs{ROFRecord{{100, 5}, 0, 0, static_cast(compact.size())}}; + PrescribedDecoder decoder{det, kind, decoded}; + const auto layerMapping = ordered(0, NLayers); + const auto load = loadTimeFrameSource(frame, decoder, o2::InteractionRecord{50, 5}, ROFTimingConfig{rofLength, 0, 0, 0}, + compact, patterns, rofs, &dict(), nullptr, det, + gsl::span{layerMapping}, + frame.getLayout().getSurfaceCatalog()); + BOOST_REQUIRE(load.ok()); + + o2::its::LayerTiming layerTiming{}; + layerTiming.mNROFsTF = 1; + layerTiming.mROFLength = rofLength; + o2::its::ROFOverlapTable rofTable; + for (int layer = 0; layer < NLayers; ++layer) { + rofTable.defineLayer(layer, layerTiming); + } + rofTable.init(); + o2::its::ROFVertexLookupTable vtxTable; + for (int layer = 0; layer < NLayers; ++layer) { + vtxTable.defineLayer(layer, layerTiming); + } + vtxTable.init(); + o2::its::ROFMaskTable mask{rofTable}; + mask.resetMask(); + for (int layer = 0; layer < NLayers; ++layer) { + mask.setROFsEnabled(layer, 0, 1, 1); + } + frame.setROFViews(RuntimeROFViews{rofTable.getView(), vtxTable.getView(), mask.getView(), {}}); + const auto tracking = tracker.run(frame, traits); + result.outcome = tracking.outcome; + } +}; + +/// Test-only reproduction of the whole-event load/track/publish composition +/// the combined DPL task's own trackFrame() applies -- not a shipped +/// coordinator class (M3 deleted the last one of those), just this file's +/// own driver so these tests can exercise the workflow-owned application plan +/// plus Tracker + loadTimeFrameSources() together the same way the +/// DPL task does, without a DPL ProcessingContext. +struct CombinedTrackingComposer { + struct Result { + TrackingOutcome outcome{TrackingOutcome::Structural}; + size_t nITSTracks{0}; + size_t nMFTTracks{0}; + }; + + test::CombinedTrackingPlan plan; + TimeFrame* frame = nullptr; + std::optional itsClock; + std::optional mftClock; + bool publicationValid = false; + + CombinedTrackingComposer(std::vector itsParams, std::vector mftParams) + : plan(std::move(itsParams), std::move(mftParams)) + { + } + + void adoptFrame(TimeFrame& f) + { + frame = &f; + plan.adoptFrame(f); + } + void setBz(float bz) { plan.setBz(bz); } + void setNThreads(int n) { plan.setNThreads(n); } + + void clearPublicationSidecars() noexcept + { + plan.clearPublicationSidecars(); + } + void invalidatePublication() noexcept + { + itsClock.reset(); + mftClock.reset(); + publicationValid = false; + } + void markPublicationValid() noexcept + { + itsClock.emplace(plan.getITSROFViews().overlap.getClockLayer()); + mftClock.emplace(plan.getMFTROFViews().overlap.getClockLayer()); + publicationValid = true; + } + std::optional getITSPublicationExport() const + { + if (!publicationValid || !itsClock) { + return std::nullopt; + } + return GenericTrackPublicationExport{o2::detectors::DetID::ITS, ClusterSourceId{0}, *itsClock, + plan.getITSLayerMapping()}; + } + std::optional getMFTPublicationExport() const + { + if (!publicationValid || !mftClock) { + return std::nullopt; + } + return GenericTrackPublicationExport{o2::detectors::DetID::MFT, ClusterSourceId{1}, *mftClock, + plan.getMFTLayerMapping()}; + } + + Result process(const ClusterSourceInput& itsSource, const ClusterSourceInput& mftSource, const o2::InteractionRecord& origin) + { + invalidatePublication(); + clearPublicationSidecars(); + + plan.configureRofTables(itsSource, mftSource); + auto itsInput = itsSource; + auto mftInput = mftSource; + itsInput.rofViews = plan.getITSROFViews(); + mftInput.rofViews = plan.getMFTROFViews(); + LoadSourcesResult loadResult; + if (const auto rejected = plan.validateSources(itsSource, mftSource)) { + loadResult = *rejected; + } else { + const std::array sources{itsInput, mftInput}; + loadResult = loadTimeFrameSources(*frame, gsl::span{sources}, plan.catalogView(), origin); + } + if (!loadResult.ok()) { + const bool errorIsRecoverable = isRecoverableLoadError(loadResult.error, loadResult.timingDetail); + const auto dropAllowed = plan.dropTFUponFailureFor(loadResult.source); + const bool sourceRecognized = dropAllowed.has_value(); + const auto outcome = errorIsRecoverable && sourceRecognized && dropAllowed.value_or(false) + ? TrackingOutcome::RecoverableDropped + : TrackingOutcome::Structural; + plan.clearPublicationSidecars(); + frame->resetTimeFrame(); + invalidatePublication(); + return {outcome, 0, 0}; + } + + try { + const auto itsResult = plan.runITS(); + if (itsResult.outcome != TrackingOutcome::Success) { + plan.clearPublicationSidecars(); + frame->resetTimeFrame(); + invalidatePublication(); + return {itsResult.outcome, 0, 0}; + } + const auto mftResult = plan.runMFT(); + if (mftResult.outcome != TrackingOutcome::Success) { + plan.clearPublicationSidecars(); + frame->resetTimeFrame(); + invalidatePublication(); + return {mftResult.outcome, 0, 0}; + } + } catch (const std::exception&) { + plan.clearPublicationSidecars(); + frame->resetTimeFrame(); + invalidatePublication(); + return {TrackingOutcome::Structural, 0, 0}; + } + + markPublicationValid(); + const auto countFor = [this](int first) { + return static_cast(std::count_if(this->frame->getGenericTracks().begin(), this->frame->getGenericTracks().end(), + [first](const auto& track) { return track.hitLayers.has(first); })); + }; + return {TrackingOutcome::Success, countFor(0), countFor(ITSNLayers)}; + } + + const TimeFrameScratch& getITSScratch() const noexcept { return plan.getITSScratch(); } + const TimeFrameScratch& getMFTScratch() const noexcept { return plan.getMFTScratch(); } + const ITSSharedClusterCompatibility& getITSSharedClusterCompatibility() const noexcept { return plan.getITSSharedClusterCompatibility(); } + gsl::span getITSLayerMapping() const noexcept { return plan.getITSLayerMapping(); } + gsl::span getMFTLayerMapping() const noexcept { return plan.getMFTLayerMapping(); } +}; + +CombinedTrackingComposer makeComposer(const TrackingParameters& itsParams, const TrackingParameters& mftParams) +{ + return CombinedTrackingComposer{std::vector{itsParams}, std::vector{mftParams}}; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(CombinedLoadingBackfillsOneGlobalWorkspace) +{ + // TrackerTraits::findRoads() unconditionally touches the global + // o2::base::Propagator singleton on first use, regardless of whether any + // road is actually found -- required before any clustersToTracks() call. + ensureTrivialMagneticFieldIsSet(); + const auto itsSurfaces = ordered(0, ITSNLayers); + const auto mftSurfaces = ordered(ITSNLayers, MFTNLayers); + const auto itsClusters = std::vector{cylinderCluster(3.f, 0.2f, 0.1f, 0), cylinderCluster(4.f, 0.2f, 0.1f, 1)}; + const auto mftClusters = std::vector{diskCluster(1.f, 0.5f, detail::mftLayerZ(0), 0), diskCluster(1.f, 0.5f, detail::mftLayerZ(1), 1)}; + + PrescribedDecoder itsDecoder{o2::detectors::DetID::ITS, SurfaceKind::Cylinder, itsClusters}; + PrescribedDecoder mftDecoder{o2::detectors::DetID::MFT, SurfaceKind::Disk, mftClusters}; + std::vector itsCompact, mftCompact; + std::vector itsPatterns, mftPatterns; + std::vector itsRofs, mftRofs; + const auto itsSource = makeSource(ClusterSourceId{0}, o2::detectors::DetID::ITS, itsSurfaces, itsDecoder, itsCompact, itsPatterns, itsRofs, itsClusters); + const auto mftSource = makeSource(ClusterSourceId{1}, o2::detectors::DetID::MFT, mftSurfaces, mftDecoder, mftCompact, mftPatterns, mftRofs, mftClusters); + + auto itsParams = makeItsParams(); + auto mftParams = makeMftParams(); + itsParams.MinTrackLength = 4; + mftParams.MinTrackLength = 5; + auto composer = makeComposer(itsParams, mftParams); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + constexpr uint32_t allCombinedSurfaces = (uint32_t{1} << (ITSNLayers + MFTNLayers)) - 1u; + BOOST_REQUIRE_EQUAL(composer.plan.itsTracker().getIterationConfigurations().size(), 1u); + const auto& combined = composer.plan.itsTracker().getIterationConfigurations()[0].parameters; + const auto& detector = composer.plan.itsTracker().getDetectorConfiguration(); + BOOST_CHECK_EQUAL(combined.NLayers, ITSNLayers + MFTNLayers); + BOOST_CHECK_EQUAL(combined.StartLayerMask.value(), allCombinedSurfaces); + BOOST_CHECK(combined.PassFlags == itsParams.PassFlags); + BOOST_REQUIRE(detector.indexTableConfigs.size() > 0); + BOOST_CHECK_EQUAL(detector.indexTableConfigs[0].getNcolBins(), itsParams.ColBins); + BOOST_CHECK_EQUAL(detector.indexTableConfigs[0].getNrowBins(), itsParams.RowBins); + BOOST_CHECK_EQUAL(combined.UseDiamond, itsParams.UseDiamond); + BOOST_CHECK_EQUAL_COLLECTIONS(std::begin(combined.Diamond), std::end(combined.Diamond), + std::begin(itsParams.Diamond), std::end(itsParams.Diamond)); + BOOST_CHECK_EQUAL_COLLECTIONS(std::begin(combined.DiamondCov), std::end(combined.DiamondCov), + std::begin(itsParams.DiamondCov), std::end(itsParams.DiamondCov)); + BOOST_CHECK_EQUAL(combined.MinTrackLength, itsParams.MinTrackLength); + BOOST_CHECK_EQUAL(combined.MaxHoles, itsParams.MaxHoles); + BOOST_CHECK_EQUAL(combined.NSigmaCut, itsParams.NSigmaCut); + BOOST_CHECK_EQUAL(combined.PVres, itsParams.PVres); + BOOST_CHECK_EQUAL(combined.TrackletMinPt, itsParams.TrackletMinPt); + BOOST_CHECK(combined.CorrType == itsParams.CorrType); + BOOST_CHECK_EQUAL(combined.MaxChi2ClusterAttachment, itsParams.MaxChi2ClusterAttachment); + BOOST_CHECK_EQUAL(combined.MaxChi2NDF, itsParams.MaxChi2NDF); + BOOST_CHECK_EQUAL(combined.ReseedIfShorter, itsParams.ReseedIfShorter); + BOOST_CHECK_EQUAL_COLLECTIONS(combined.MinPt.begin(), combined.MinPt.end(), itsParams.MinPt.begin(), itsParams.MinPt.end()); + BOOST_CHECK_EQUAL(combined.RepeatRefitOut, itsParams.RepeatRefitOut); + BOOST_CHECK_EQUAL(combined.ShiftRefToCluster, itsParams.ShiftRefToCluster); + BOOST_CHECK_EQUAL(combined.PerPrimaryVertexProcessing, itsParams.PerPrimaryVertexProcessing); + BOOST_CHECK_EQUAL(combined.AllowSharingFirstCluster, itsParams.AllowSharingFirstCluster); + BOOST_CHECK_EQUAL(combined.SharedClusterMaxDeltaPhi, itsParams.SharedClusterMaxDeltaPhi); + BOOST_CHECK_EQUAL(combined.SharedClusterMaxDeltaEta, itsParams.SharedClusterMaxDeltaEta); + BOOST_CHECK_EQUAL(combined.SharedClusterOppositeSign, itsParams.SharedClusterOppositeSign); + BOOST_CHECK_EQUAL(combined.SharedMaxClusters, itsParams.SharedMaxClusters); + + const auto checkConcatenated = [](const auto& actual, const auto& itsValues, const auto& mftValues) { + BOOST_REQUIRE_EQUAL(actual.size(), itsValues.size() + mftValues.size()); + BOOST_CHECK_EQUAL_COLLECTIONS(actual.begin(), actual.begin() + itsValues.size(), itsValues.begin(), itsValues.end()); + BOOST_CHECK_EQUAL_COLLECTIONS(actual.begin() + itsValues.size(), actual.end(), mftValues.begin(), mftValues.end()); + }; + checkConcatenated(detector.addTimeError, itsParams.AddTimeError, mftParams.AddTimeError); + checkConcatenated(detector.layerRadii, itsParams.LayerRadii, mftParams.LayerRadii); + checkConcatenated(detector.layerResolution, itsParams.LayerResolution, mftParams.LayerResolution); + checkConcatenated(detector.systError2Row, itsParams.SystError2Row, mftParams.SystError2Row); + checkConcatenated(detector.systError2Col, itsParams.SystError2Col, mftParams.SystError2Col); + const auto catalog = frame.getLayout().getSurfaceCatalog(); + BOOST_REQUIRE_EQUAL(catalog.nSurfaces, ITSNLayers + MFTNLayers); + for (uint32_t layer = 0; layer < catalog.nSurfaces; ++layer) { + const auto expected = layer < ITSNLayers ? kITSStaticSurfaceCatalog[layer].material.xOverX0 : kMFTStaticSurfaceCatalog[layer - ITSNLayers].material.xOverX0; + BOOST_CHECK_EQUAL(catalog.surfaces[layer].material.xOverX0, expected); + } + + const auto result = composer.process(itsSource, mftSource, o2::InteractionRecord{50, 5}); + BOOST_REQUIRE(result.outcome == TrackingOutcome::Success); + + // The time frame owns two lookup records independently of the tracker cache. + BOOST_CHECK_EQUAL(&frame.getIndexTableUtils(0), &frame.getIndexTableUtils(ITSNLayers - 1)); + BOOST_CHECK_EQUAL(&frame.getIndexTableUtils(ITSNLayers), &frame.getIndexTableUtils(ITSNLayers + MFTNLayers - 1)); + BOOST_CHECK(&frame.getIndexTableUtils(0) != &detector.indexTableConfigs[0]); + BOOST_CHECK(frame.getIndexTableUtils(0).getCoordType() == IndexTableCoordType::PhiZ); + BOOST_CHECK(frame.getIndexTableUtils(ITSNLayers).getCoordType() == IndexTableCoordType::PhiR); + + const auto topology = composer.plan.itsTracker().getIterationConfigurations()[0].getTopologyView(frame.getLayout().getSurfaceCatalog()); + BOOST_CHECK_EQUAL(topology.seedingLayers.value(), allCombinedSurfaces); + BOOST_REQUIRE_EQUAL(topology.nEdges, static_cast(ITSNLayers + MFTNLayers - 2)); + for (uint16_t edgeId = 0; edgeId < topology.nEdges; ++edgeId) { + const auto& edge = topology.getEdge(EdgeId{edgeId}); + const bool fromITS = edge.from.value() < ITSNLayers; + const bool toITS = edge.to.value() < ITSNLayers; + BOOST_CHECK_EQUAL(fromITS, toITS); + BOOST_CHECK(!(edge.from == LayerId{ITSNLayers - 1} && edge.to == LayerId{ITSNLayers})); + } + + BOOST_CHECK_EQUAL(composer.frame->getTotalClusters(), + static_cast(itsClusters.size() + mftClusters.size())); + BOOST_CHECK_EQUAL(&composer.getITSScratch(), &composer.getMFTScratch()); + // The one workspace keeps source-local ROF numbering per global surface. + BOOST_CHECK_EQUAL(composer.frame->getNrof(0), 1); + BOOST_CHECK_EQUAL(composer.frame->getNrof(ITSNLayers), 1); +} + +BOOST_AUTO_TEST_CASE(MftGlobalIdsWorkEndToEndThroughRefitUnderCombinedPolicy) +{ + ensureTrivialMagneticFieldIsSet(); + const auto itsSurfaces = ordered(0, ITSNLayers); + const auto mftSurfaces = ordered(ITSNLayers, MFTNLayers); + + const auto mftParams = makeMftParams(); + const auto mftClusters = buildMftChainClusters(mftParams, Bz, MFTNLayers - 1); + BOOST_REQUIRE_EQUAL(mftClusters.size(), static_cast(MFTNLayers)); + + PrescribedDecoder itsDecoder{o2::detectors::DetID::ITS, SurfaceKind::Cylinder, {}}; + PrescribedDecoder mftDecoder{o2::detectors::DetID::MFT, SurfaceKind::Disk, mftClusters}; + std::vector mftCompact; + std::vector mftPatterns; + std::vector mftRofs; + const auto itsSource = makeEmptySource(ClusterSourceId{0}, o2::detectors::DetID::ITS, itsSurfaces, itsDecoder); + const auto mftSource = makeSource(ClusterSourceId{1}, o2::detectors::DetID::MFT, mftSurfaces, mftDecoder, mftCompact, mftPatterns, mftRofs, mftClusters); + + auto composer = makeComposer(makeItsParams(), mftParams); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + const auto result = composer.process(itsSource, mftSource, o2::InteractionRecord{50, 5}); + BOOST_REQUIRE(result.outcome == TrackingOutcome::Success); + BOOST_CHECK_EQUAL(result.nITSTracks, 0u); + // Global MFT LayerIds 7..16 plus source 1 work end to end through the + // disk leaves and refit while the one combined selection policy is active. + BOOST_CHECK_GT(result.nMFTTracks, 0u); +} + +BOOST_AUTO_TEST_CASE(CombinedComponentsUseOwnROFTimingInOneCombinedPass) +{ + ensureTrivialMagneticFieldIsSet(); + const auto itsSurfaces = ordered(0, ITSNLayers); + const auto mftSurfaces = ordered(ITSNLayers, MFTNLayers); + + const auto itsParams = makeItsParams(); + const auto mftParams = makeMftParams(); + const auto itsClusters = buildItsHelixChainClusters(itsParams.LayerRadii, Bz, 1.f, 0.4f, 0.3f); + BOOST_REQUIRE_EQUAL(itsClusters.size(), static_cast(ITSNLayers)); + const auto mftClusters = buildMftChainClusters(mftParams, Bz, MFTNLayers - 1); + BOOST_REQUIRE_EQUAL(mftClusters.size(), static_cast(MFTNLayers)); + + StandaloneRun standaloneIts{o2::detectors::DetID::ITS, SurfaceKind::Cylinder, itsParams, itsClusters}; + BOOST_REQUIRE(standaloneIts.result.outcome == TrackingOutcome::Success); + // A genuine full 7-layer road (MinTrackLength=7, MaxHoles=0): the helix + // fixture above is a real, non-degenerate curved trajectory, so this is a + // nonzero accepted-track oracle, not a 0==0 parity check. + BOOST_REQUIRE_GT(standaloneIts.frame.getGenericTracks().size(), 0u); + StandaloneRun standaloneMft{o2::detectors::DetID::MFT, SurfaceKind::Disk, mftParams, mftClusters, 80}; + BOOST_REQUIRE(standaloneMft.result.outcome == TrackingOutcome::Success); + BOOST_REQUIRE_GT(standaloneMft.frame.getGenericTracks().size(), 0u); + + PrescribedDecoder itsDecoder{o2::detectors::DetID::ITS, SurfaceKind::Cylinder, itsClusters}; + PrescribedDecoder mftDecoder{o2::detectors::DetID::MFT, SurfaceKind::Disk, mftClusters}; + std::vector itsCompact, mftCompact; + std::vector itsPatterns, mftPatterns; + std::vector itsRofs, mftRofs; + const auto itsSource = makeSource(ClusterSourceId{0}, o2::detectors::DetID::ITS, itsSurfaces, itsDecoder, itsCompact, itsPatterns, itsRofs, itsClusters); + auto mftSource = makeSource(ClusterSourceId{1}, o2::detectors::DetID::MFT, mftSurfaces, mftDecoder, mftCompact, mftPatterns, mftRofs, mftClusters); + // Make the disconnected MFT component's ROF longer than ITS. Reusing the + // first/global ITS view would incorrectly clamp MFT to an ITS half-ROF; + // the per-surface timing lookup must reproduce standalone MFT instead. + mftSource.timing.rofLength = 80; + + auto composer = makeComposer(itsParams, mftParams); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + const auto result = composer.process(itsSource, mftSource, o2::InteractionRecord{50, 5}); + BOOST_REQUIRE(result.outcome == TrackingOutcome::Success); + + // This full-chain fixture survives both standalone and combined selection, + // allowing timestamp behavior to be compared on nonzero tracks without + // making general standalone/combined population parity a requirement. + BOOST_CHECK_GT(result.nITSTracks, 0u); + BOOST_CHECK_GT(result.nMFTTracks, 0u); + BOOST_CHECK_EQUAL(result.nITSTracks, standaloneIts.frame.getGenericTracks().size()); + BOOST_CHECK_EQUAL(result.nMFTTracks, standaloneMft.frame.getGenericTracks().size()); + + // The one workspace contains the disjoint components' compact buffers in + // graph order. Their populated cell count is therefore the sum of the two + // standalone component counts; tracklets have already been consumed. + BOOST_CHECK_EQUAL(composer.getITSScratch().getNumberOfTracklets(), + standaloneIts.scratch->getNumberOfTracklets() + standaloneMft.scratch->getNumberOfTracklets()); + BOOST_CHECK_EQUAL(composer.getITSScratch().getNumberOfCells(), + standaloneIts.scratch->getNumberOfCells() + standaloneMft.scratch->getNumberOfCells()); + BOOST_CHECK_EQUAL(&composer.getITSScratch(), &composer.getMFTScratch()); + BOOST_CHECK_GT(composer.getITSScratch().getNumberOfCells(), 0u); + + // GenericTrack global references resolve correctly and ordering is ITS + // then MFT: every accepted track's hitLayers mask stays within exactly + // one detector's own global range, and every ITS-range entry precedes + // every MFT-range entry (shared TimeFrame, append-only, ITS run first). + const auto itsMask = LayerMask{uint32_t{(1u << ITSNLayers) - 1u}}; + const auto mftMask = LayerMask{static_cast(((1u << MFTNLayers) - 1u) << ITSNLayers)}; + const auto& commonTracks = frame.getGenericTracks(); + BOOST_REQUIRE_EQUAL(commonTracks.size(), result.nITSTracks + result.nMFTTracks); + for (size_t i = 0; i < result.nITSTracks; ++i) { + BOOST_CHECK_EQUAL(commonTracks[i].timestamp.begin, standaloneIts.frame.getGenericTracks()[i].timestamp.begin); + BOOST_CHECK_EQUAL(commonTracks[i].timestamp.end, standaloneIts.frame.getGenericTracks()[i].timestamp.end); + } + for (size_t i = 0; i < result.nMFTTracks; ++i) { + const auto& combinedTrack = commonTracks[result.nITSTracks + i]; + const auto& standaloneTrack = standaloneMft.frame.getGenericTracks()[i]; + BOOST_CHECK_EQUAL(combinedTrack.timestamp.begin, standaloneTrack.timestamp.begin); + BOOST_CHECK_EQUAL(combinedTrack.timestamp.end, standaloneTrack.timestamp.end); + BOOST_CHECK_GT(combinedTrack.timestamp.end - combinedTrack.timestamp.begin, + commonTracks.front().timestamp.end - commonTracks.front().timestamp.begin); + } + bool seenMft = false; + size_t nextReference = 0; + for (size_t i = 0; i < commonTracks.size(); ++i) { + const auto& track = commonTracks[i]; + BOOST_CHECK_EQUAL(track.firstClusterRef, nextReference); + BOOST_CHECK_LT(track.firstClusterRef, track.clusterRefEnd); + BOOST_CHECK(isValidTrackRange(track, static_cast(frame.getTrackClusterIndices().size()))); + BOOST_REQUIRE(track.hitLayers.isSubsetOf(itsMask) || track.hitLayers.isSubsetOf(mftMask)); + const bool isMft = track.hitLayers.isSubsetOf(mftMask) && !track.hitLayers.empty(); + if (isMft) { + seenMft = true; + } else { + BOOST_CHECK_MESSAGE(!seenMft, "ITS GenericTrack at index " << i << " appeared after an MFT one"); + } + for (uint32_t ref = track.firstClusterRef; ref < track.clusterRefEnd; ++ref) { + const auto& reference = frame.getTrackClusterIndices()[ref]; + const auto globals = frame.getGlobalMeasurements(reference.layer); + BOOST_CHECK(std::any_of(globals.begin(), globals.end(), [&](const auto& measurement) { + return measurement.clusterId == reference.clusterId; + })); + BOOST_CHECK(isMft ? mftMask.has(reference.layer.value()) : itsMask.has(reference.layer.value())); + } + nextReference = track.clusterRefEnd; + } + BOOST_CHECK_EQUAL(nextReference, frame.getTrackClusterIndices().size()); + BOOST_CHECK_EQUAL(seenMft, result.nMFTTracks > 0); + + const auto& itsCompatibility = composer.getITSSharedClusterCompatibility().entries(); + BOOST_REQUIRE_EQUAL(itsCompatibility.size(), result.nITSTracks); + for (size_t i = 0; i < itsCompatibility.size(); ++i) { + BOOST_CHECK_EQUAL(itsCompatibility[i].genericTrackIndex, i); + } + + // Publication exports are valid after success, source-qualified, and + // carry each detector's own ordered-surface span. + const auto itsExport = composer.getITSPublicationExport(); + const auto mftExport = composer.getMFTPublicationExport(); + BOOST_REQUIRE(itsExport.has_value()); + BOOST_REQUIRE(mftExport.has_value()); + BOOST_CHECK(itsExport->detector == o2::detectors::DetID::ITS); + BOOST_CHECK(itsExport->source == ClusterSourceId{0}); + BOOST_CHECK_EQUAL(itsExport->layerMapping.size(), static_cast(ITSNLayers)); + BOOST_CHECK(itsExport->layerMapping[0] == LayerId{0}); + BOOST_CHECK(mftExport->detector == o2::detectors::DetID::MFT); + BOOST_CHECK(mftExport->source == ClusterSourceId{1}); + BOOST_CHECK_EQUAL(mftExport->layerMapping.size(), static_cast(MFTNLayers)); + BOOST_CHECK(mftExport->layerMapping[0] == LayerId{ITSNLayers}); +} + +BOOST_AUTO_TEST_CASE(LoadFailureResetsWholeCombinedTFExactlyOnceAndInvalidatesPublication) +{ + ensureTrivialMagneticFieldIsSet(); + const auto itsSurfaces = ordered(0, ITSNLayers); + const auto mftSurfaces = ordered(ITSNLayers, MFTNLayers); + const auto itsClusters = std::vector{cylinderCluster(3.f, 0.2f, 0.1f, 0), cylinderCluster(4.f, 0.2f, 0.1f, 1)}; + const auto mftClusters = std::vector{diskCluster(1.f, 0.5f, detail::mftLayerZ(0), 0), diskCluster(1.f, 0.5f, detail::mftLayerZ(1), 1)}; + + PrescribedDecoder itsDecoder{o2::detectors::DetID::ITS, SurfaceKind::Cylinder, itsClusters}; + PrescribedDecoder mftDecoder{o2::detectors::DetID::MFT, SurfaceKind::Disk, mftClusters}; + std::vector itsCompact, mftCompact; + std::vector itsPatterns, mftPatterns; + std::vector itsRofs, mftRofs; + const auto itsSource = makeSource(ClusterSourceId{0}, o2::detectors::DetID::ITS, itsSurfaces, itsDecoder, itsCompact, itsPatterns, itsRofs, itsClusters); + auto mftSource = makeSource(ClusterSourceId{1}, o2::detectors::DetID::MFT, mftSurfaces, mftDecoder, mftCompact, mftPatterns, mftRofs, mftClusters); + + auto composer = makeComposer(makeItsParams(), makeMftParams()); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + // First pass genuinely succeeds, so there is real state (scratches, + // GenericTracks, publication exports) for the second, failing pass to + // actually have to clear. + const auto first = composer.process(itsSource, mftSource, o2::InteractionRecord{50, 5}); + BOOST_REQUIRE(first.outcome == TrackingOutcome::Success); + BOOST_REQUIRE(composer.getITSPublicationExport().has_value()); + BOOST_REQUIRE(composer.getMFTPublicationExport().has_value()); + + // Malformed MFT ROF partition (a gap before the second cluster): a + // structural load failure loadTimeFrameSources() must + // reject before touching either scratch or the shared TimeFrame. + std::vector malformedMftRofs{ROFRecord{{100, 5}, 0, 0, 1}, ROFRecord{{140, 5}, 0, 2, 1}}; + mftSource.rofs = malformedMftRofs; + + const auto second = composer.process(itsSource, mftSource, o2::InteractionRecord{50, 5}); + // MFT's own DropTFUponFailure defaults false (makeMftParams() never sets + // it), so this recoverable InvalidROFRange load error is still classified + // Structural. + BOOST_CHECK(second.outcome == TrackingOutcome::Structural); + BOOST_CHECK_EQUAL(second.nITSTracks, 0u); + BOOST_CHECK_EQUAL(second.nMFTTracks, 0u); + + BOOST_CHECK_EQUAL(composer.frame->getTotalClusters(), 0); + BOOST_CHECK_EQUAL(composer.frame->getTotalClusters(), 0); + BOOST_CHECK(frame.getGenericTracks().empty()); + BOOST_CHECK(frame.getTrackClusterIndices().empty()); + BOOST_CHECK(!composer.getITSPublicationExport().has_value()); + BOOST_CHECK(!composer.getMFTPublicationExport().has_value()); +} + +BOOST_AUTO_TEST_CASE(CombinedTrackingResourceFailureUsesSharedPolicyAndResetsWorkspace) +{ + ensureTrivialMagneticFieldIsSet(); + const auto itsSurfaces = ordered(0, ITSNLayers); + const auto mftSurfaces = ordered(ITSNLayers, MFTNLayers); + const auto itsClusters = std::vector{cylinderCluster(3.f, 0.2f, 0.1f, 0), cylinderCluster(4.f, 0.2f, 0.1f, 1)}; + const auto mftClusters = std::vector{diskCluster(1.f, 0.5f, detail::mftLayerZ(0), 0), diskCluster(1.f, 0.5f, detail::mftLayerZ(1), 1)}; + + PrescribedDecoder itsDecoder{o2::detectors::DetID::ITS, SurfaceKind::Cylinder, itsClusters}; + PrescribedDecoder mftDecoder{o2::detectors::DetID::MFT, SurfaceKind::Disk, mftClusters}; + std::vector itsCompact, mftCompact; + std::vector itsPatterns, mftPatterns; + std::vector itsRofs, mftRofs; + const auto itsSource = makeSource(ClusterSourceId{0}, o2::detectors::DetID::ITS, itsSurfaces, itsDecoder, itsCompact, itsPatterns, itsRofs, itsClusters); + const auto mftSource = makeSource(ClusterSourceId{1}, o2::detectors::DetID::MFT, mftSurfaces, mftDecoder, mftCompact, mftPatterns, mftRofs, mftClusters); + + // One run has one resource budget and one failure policy. The combined + // scalar baseline is ITS, so exhausting that budget drops and resets the + // one frame-owned workspace atomically. + auto itsParams = makeItsParams(); + itsParams.MaxMemory = 1; + itsParams.DropTFUponFailure = true; + + auto composer = makeComposer(itsParams, makeMftParams()); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + const auto result = composer.process(itsSource, mftSource, o2::InteractionRecord{50, 5}); + BOOST_CHECK(result.outcome == TrackingOutcome::RecoverableDropped); + BOOST_CHECK_EQUAL(composer.frame->getTotalClusters(), 0); + BOOST_CHECK_EQUAL(&composer.getITSScratch(), &composer.getMFTScratch()); + BOOST_CHECK(frame.getGenericTracks().empty()); + BOOST_CHECK(!composer.getITSPublicationExport().has_value()); + BOOST_CHECK(!composer.getMFTPublicationExport().has_value()); +} + +namespace +{ + +/// A minimal, always-valid ITS+MFT source pair sharing the two-cluster +/// fixture already used by CombinedLoadingBackfillsIndependentCompactScratches. +struct MinimalFixture { + std::vector itsSurfaces = ordered(0, ITSNLayers); + std::vector mftSurfaces = ordered(ITSNLayers, MFTNLayers); + std::vector itsClusters{cylinderCluster(3.f, 0.2f, 0.1f, 0), cylinderCluster(4.f, 0.2f, 0.1f, 1)}; + std::vector mftClusters{diskCluster(1.f, 0.5f, detail::mftLayerZ(0), 0), diskCluster(1.f, 0.5f, detail::mftLayerZ(1), 1)}; + PrescribedDecoder itsDecoder{o2::detectors::DetID::ITS, SurfaceKind::Cylinder, itsClusters}; + PrescribedDecoder mftDecoder{o2::detectors::DetID::MFT, SurfaceKind::Disk, mftClusters}; + std::vector itsCompact, mftCompact; + std::vector itsPatterns, mftPatterns; + std::vector itsRofs, mftRofs; + ClusterSourceInput itsSource; + ClusterSourceInput mftSource; + + MinimalFixture() + { + itsSource = makeSource(ClusterSourceId{0}, o2::detectors::DetID::ITS, itsSurfaces, itsDecoder, itsCompact, itsPatterns, itsRofs, itsClusters); + mftSource = makeSource(ClusterSourceId{1}, o2::detectors::DetID::MFT, mftSurfaces, mftDecoder, mftCompact, mftPatterns, mftRofs, mftClusters); + } +}; + +/// A malformed (gap-before-second-cluster) ROF partition for one detector's +/// source, reproducing MultiSourceLoadError::InvalidROFRange -- a +/// *recoverable* per-TF data error under isRecoverableLoadError() +/// (TimeFrameLoadFailure.cxx) -- without touching the other detector's +/// (still valid) source. +void makeRofGap(std::vector& rofs) +{ + rofs = {ROFRecord{{100, 5}, 0, 0, 1}, ROFRecord{{140, 5}, 0, 2, 1}}; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(RecoverableITSLoadFailureIsDroppedOnlyWhenITSDropTFAllows) +{ + ensureTrivialMagneticFieldIsSet(); + + for (const bool itsDropTF : {true, false}) { + MinimalFixture fixture; + makeRofGap(fixture.itsRofs); + fixture.itsSource.rofs = fixture.itsRofs; + + auto itsParams = makeItsParams(); + itsParams.DropTFUponFailure = itsDropTF; + auto composer = makeComposer(itsParams, makeMftParams()); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + const auto result = composer.process(fixture.itsSource, fixture.mftSource, o2::InteractionRecord{50, 5}); + const auto expected = itsDropTF ? TrackingOutcome::RecoverableDropped : TrackingOutcome::Structural; + BOOST_CHECK_MESSAGE(result.outcome == expected, "ITS DropTFUponFailure=" << itsDropTF); + // Every non-success path still performs exactly one whole reset: + // both scratches, the shared TimeFrame's GenericTracks, and both + // publication exports are empty/invalid regardless of classification. + BOOST_CHECK_EQUAL(composer.frame->getTotalClusters(), 0); + BOOST_CHECK_EQUAL(composer.frame->getTotalClusters(), 0); + BOOST_CHECK(frame.getGenericTracks().empty()); + BOOST_CHECK(!composer.getITSPublicationExport().has_value()); + BOOST_CHECK(!composer.getMFTPublicationExport().has_value()); + } +} + +BOOST_AUTO_TEST_CASE(RecoverableMFTLoadFailureUsesSharedCombinedDropPolicy) +{ + ensureTrivialMagneticFieldIsSet(); + + for (const bool combinedDropTF : {true, false}) { + MinimalFixture fixture; + makeRofGap(fixture.mftRofs); + fixture.mftSource.rofs = fixture.mftRofs; + + auto itsParams = makeItsParams(); + itsParams.DropTFUponFailure = combinedDropTF; + auto mftParams = makeMftParams(); + mftParams.DropTFUponFailure = !combinedDropTF; + auto composer = makeComposer(itsParams, mftParams); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + const auto result = composer.process(fixture.itsSource, fixture.mftSource, o2::InteractionRecord{50, 5}); + const auto expected = combinedDropTF ? TrackingOutcome::RecoverableDropped : TrackingOutcome::Structural; + BOOST_CHECK_MESSAGE(result.outcome == expected, "combined DropTFUponFailure=" << combinedDropTF); + BOOST_CHECK_EQUAL(composer.frame->getTotalClusters(), 0); + BOOST_CHECK_EQUAL(composer.frame->getTotalClusters(), 0); + BOOST_CHECK(frame.getGenericTracks().empty()); + BOOST_CHECK(!composer.getITSPublicationExport().has_value()); + BOOST_CHECK(!composer.getMFTPublicationExport().has_value()); + } +} + +BOOST_AUTO_TEST_CASE(StructuralLoadErrorIsAlwaysStructuralRegardlessOfDropTF) +{ + ensureTrivialMagneticFieldIsSet(); + + // A missing dictionary is MultiSourceLoadError::MissingDictionary, never + // recoverable under isRecoverableLoadError() -- DropTFUponFailure=true + // must not turn this into a dropped TF. + MinimalFixture fixture; + fixture.itsSource.dictionary = nullptr; + + auto itsParams = makeItsParams(); + itsParams.DropTFUponFailure = true; + auto composer = makeComposer(itsParams, makeMftParams()); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + const auto result = composer.process(fixture.itsSource, fixture.mftSource, o2::InteractionRecord{50, 5}); + BOOST_CHECK(result.outcome == TrackingOutcome::Structural); + BOOST_CHECK(frame.getGenericTracks().empty()); + BOOST_CHECK(!composer.getITSPublicationExport().has_value()); +} + +BOOST_AUTO_TEST_CASE(UnrecognizedLoadSourceIsAlwaysStructural) +{ + ensureTrivialMagneticFieldIsSet(); + + // validateSources() rejects any id other than its own fixed ITS=0/MFT=1 + // contract as MultiSourceLoadError::UnsupportedDetector before ever + // calling loadSources() -- LoadSourcesResult::source then carries the + // caller's own (unrecognized) id verbatim. Even if a future loader + // variant ever reported a recoverable error against such an id, this + // boundary must still classify Structural: an unidentifiable source is + // never eligible for recoverable/DropTFUponFailure treatment. + MinimalFixture fixture; + fixture.itsSource.id = ClusterSourceId{5}; + + auto composer = makeComposer(makeItsParams(), makeMftParams()); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + const auto result = composer.process(fixture.itsSource, fixture.mftSource, o2::InteractionRecord{50, 5}); + BOOST_CHECK(result.outcome == TrackingOutcome::Structural); + BOOST_CHECK(frame.getGenericTracks().empty()); + BOOST_CHECK(!composer.getITSPublicationExport().has_value()); + BOOST_CHECK(!composer.getMFTPublicationExport().has_value()); +} + +BOOST_AUTO_TEST_CASE(StructuralTrackingExceptionIsClassifiedStructuralAfterWholeReset) +{ + ensureTrivialMagneticFieldIsSet(); + + // MaxMemory=1 with the shared DropTFUponFailure left false makes the one + // tracker propagate the resource exception to the composition boundary. + MinimalFixture fixture; + auto itsParams = makeItsParams(); + itsParams.MaxMemory = 1; + + auto composer = makeComposer(itsParams, makeMftParams()); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + const auto result = composer.process(fixture.itsSource, fixture.mftSource, o2::InteractionRecord{50, 5}); + BOOST_CHECK(result.outcome == TrackingOutcome::Structural); + BOOST_CHECK_EQUAL(composer.frame->getTotalClusters(), 0); + BOOST_CHECK_EQUAL(composer.frame->getTotalClusters(), 0); + BOOST_CHECK(frame.getGenericTracks().empty()); + BOOST_CHECK(!composer.getITSPublicationExport().has_value()); + BOOST_CHECK(!composer.getMFTPublicationExport().has_value()); +} + +BOOST_AUTO_TEST_CASE(SequentialSuccessfulTFsReplaceStateWithoutStaleAccumulation) +{ + ensureTrivialMagneticFieldIsSet(); + const auto itsSurfaces = ordered(0, ITSNLayers); + const auto mftSurfaces = ordered(ITSNLayers, MFTNLayers); + const auto itsParams = makeItsParams(); + const auto mftParams = makeMftParams(); + // A genuine nonzero-track fixture (same construction as + // ITSAndMFTAcceptedResultsReproduceStandaloneCountsInOneCombinedPass): if + // GenericTrack/TrackClusterIndices storage ever accumulated across TFs + // instead of being replaced, the second TF's count below would silently + // double rather than reproduce the same per-TF value. + const auto itsClusters = buildItsHelixChainClusters(itsParams.LayerRadii, Bz, 1.f, 0.4f, 0.3f); + BOOST_REQUIRE_EQUAL(itsClusters.size(), static_cast(ITSNLayers)); + const auto mftClusters = buildMftChainClusters(mftParams, Bz, MFTNLayers - 1); + BOOST_REQUIRE_EQUAL(mftClusters.size(), static_cast(MFTNLayers)); + + PrescribedDecoder itsDecoder{o2::detectors::DetID::ITS, SurfaceKind::Cylinder, itsClusters}; + PrescribedDecoder mftDecoder{o2::detectors::DetID::MFT, SurfaceKind::Disk, mftClusters}; + std::vector itsCompact, mftCompact; + std::vector itsPatterns, mftPatterns; + std::vector itsRofs, mftRofs; + const auto itsSource = makeSource(ClusterSourceId{0}, o2::detectors::DetID::ITS, itsSurfaces, itsDecoder, itsCompact, itsPatterns, itsRofs, itsClusters); + const auto mftSource = makeSource(ClusterSourceId{1}, o2::detectors::DetID::MFT, mftSurfaces, mftDecoder, mftCompact, mftPatterns, mftRofs, mftClusters); + + auto composer = makeComposer(itsParams, mftParams); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + const auto firstResult = composer.process(itsSource, mftSource, o2::InteractionRecord{50, 5}); + BOOST_REQUIRE(firstResult.outcome == TrackingOutcome::Success); + BOOST_REQUIRE_GT(firstResult.nITSTracks + firstResult.nMFTTracks, 0u); + const auto firstGenericTrackCount = frame.getGenericTracks().size(); + BOOST_REQUIRE_EQUAL(firstGenericTrackCount, firstResult.nITSTracks + firstResult.nMFTTracks); + + // No explicit reset between successful TFs: loadTimeFrameSources() + // load()'s frame commit atomically replaces the + // normalized frame and clears mGenericTracks/mTrackClusterIndices in the + // same commit (TimeFrame.h), so the second process() call alone -- on the + // identical fixture again -- must reproduce the same per-TF count, not + // the first TF's count plus the second's. + const auto secondResult = composer.process(itsSource, mftSource, o2::InteractionRecord{60, 6}); + BOOST_REQUIRE(secondResult.outcome == TrackingOutcome::Success); + + BOOST_CHECK_EQUAL(secondResult.nITSTracks, firstResult.nITSTracks); + BOOST_CHECK_EQUAL(secondResult.nMFTTracks, firstResult.nMFTTracks); + BOOST_CHECK_EQUAL(frame.getGenericTracks().size(), firstGenericTrackCount); + BOOST_CHECK_EQUAL(composer.frame->getTotalClusters(), + static_cast(itsClusters.size() + mftClusters.size())); + BOOST_CHECK_EQUAL(&composer.getITSScratch(), &composer.getMFTScratch()); +} + +BOOST_AUTO_TEST_CASE(OrderedSurfaceGettersAreAlwaysValidUnlikePublicationExports) +{ + auto composer = makeComposer(makeItsParams(), makeMftParams()); + TimeFrame frame; + composer.adoptFrame(frame); + + // Configuration is installed before ordered-surface access; publication + // exports remain unavailable until an event is processed. + const auto itsSurfacesBefore = composer.getITSLayerMapping(); + const auto mftSurfacesBefore = composer.getMFTLayerMapping(); + BOOST_REQUIRE_EQUAL(itsSurfacesBefore.size(), static_cast(ITSNLayers)); + BOOST_REQUIRE_EQUAL(mftSurfacesBefore.size(), static_cast(MFTNLayers)); + BOOST_CHECK(itsSurfacesBefore[0] == LayerId{0}); + BOOST_CHECK(mftSurfacesBefore[0] == LayerId{ITSNLayers}); + BOOST_CHECK(!composer.getITSPublicationExport().has_value()); + BOOST_CHECK(!composer.getMFTPublicationExport().has_value()); + + // Still identical after a failure (which invalidates the publication + // exports but must never move the fixed catalog-offset spans). + ensureTrivialMagneticFieldIsSet(); + MinimalFixture fixture; + makeRofGap(fixture.mftRofs); + fixture.mftSource.rofs = fixture.mftRofs; + composer.setBz(Bz); + composer.setNThreads(1); + const auto failed = composer.process(fixture.itsSource, fixture.mftSource, o2::InteractionRecord{50, 5}); + BOOST_REQUIRE(failed.outcome != TrackingOutcome::Success); + BOOST_CHECK(composer.getITSLayerMapping().data() == itsSurfacesBefore.data()); + BOOST_CHECK(composer.getMFTLayerMapping().data() == mftSurfacesBefore.data()); +} + +BOOST_AUTO_TEST_CASE(CompatibilitySidecarGettersReflectSealAndReset) +{ + ensureTrivialMagneticFieldIsSet(); + MinimalFixture fixture; + auto composer = makeComposer(makeItsParams(), makeMftParams()); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + // Not yet sealed before any process() call. + BOOST_CHECK(!composer.getITSSharedClusterCompatibility().isSealed()); + + const auto result = composer.process(fixture.itsSource, fixture.mftSource, o2::InteractionRecord{50, 5}); + BOOST_REQUIRE(result.outcome == TrackingOutcome::Success); + // A successful run always seals the ITS sidecar (Tracker:: + // clustersToTracks() -> markTracks() -> sealFromMarkedTracks()), which is + // exactly what stageITSGenericTrackOutput() requires + // (GenericTrackOutputAdapter.h). + BOOST_CHECK(composer.getITSSharedClusterCompatibility().isSealed()); + + // A whole reset clears both sidecars back to their pre-process() state. + makeRofGap(fixture.mftRofs); + fixture.mftSource.rofs = fixture.mftRofs; + const auto failed = composer.process(fixture.itsSource, fixture.mftSource, o2::InteractionRecord{60, 6}); + BOOST_REQUIRE(failed.outcome != TrackingOutcome::Success); + BOOST_CHECK(!composer.getITSSharedClusterCompatibility().isSealed()); + BOOST_CHECK(composer.getITSSharedClusterCompatibility().entries().empty()); +} + +BOOST_AUTO_TEST_CASE(ExplicitScheduleDrivesITSThenMFTThroughTheDelegatedEngine) +{ + // Same construction as + // ITSAndMFTAcceptedResultsReproduceStandaloneCountsInOneCombinedPass, + // narrowed to the one claim this test adds: process()'s ITS-then-MFT + // GenericTrack ordering and per-detector publication exports are produced + // by the explicit [ITS, MFT] Tracker invocation order + // (the workflow-owned explicit schedule), not a hand-unrolled pair of + // clustersToTracks() calls. + ensureTrivialMagneticFieldIsSet(); + const auto itsSurfaces = ordered(0, ITSNLayers); + const auto mftSurfaces = ordered(ITSNLayers, MFTNLayers); + const auto itsParams = makeItsParams(); + const auto mftParams = makeMftParams(); + const auto itsClusters = buildItsHelixChainClusters(itsParams.LayerRadii, Bz, 1.f, 0.4f, 0.3f); + BOOST_REQUIRE_EQUAL(itsClusters.size(), static_cast(ITSNLayers)); + const auto mftClusters = buildMftChainClusters(mftParams, Bz, MFTNLayers - 1); + BOOST_REQUIRE_EQUAL(mftClusters.size(), static_cast(MFTNLayers)); + + PrescribedDecoder itsDecoder{o2::detectors::DetID::ITS, SurfaceKind::Cylinder, itsClusters}; + PrescribedDecoder mftDecoder{o2::detectors::DetID::MFT, SurfaceKind::Disk, mftClusters}; + std::vector itsCompact, mftCompact; + std::vector itsPatterns, mftPatterns; + std::vector itsRofs, mftRofs; + const auto itsSource = makeSource(ClusterSourceId{0}, o2::detectors::DetID::ITS, itsSurfaces, itsDecoder, itsCompact, itsPatterns, itsRofs, itsClusters); + const auto mftSource = makeSource(ClusterSourceId{1}, o2::detectors::DetID::MFT, mftSurfaces, mftDecoder, mftCompact, mftPatterns, mftRofs, mftClusters); + + auto composer = makeComposer(itsParams, mftParams); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + const auto result = composer.process(itsSource, mftSource, o2::InteractionRecord{50, 5}); + BOOST_REQUIRE(result.outcome == TrackingOutcome::Success); + BOOST_REQUIRE_GT(result.nITSTracks, 0u); + BOOST_REQUIRE_GT(result.nMFTTracks, 0u); + + // GenericTrack ordering: every ITS-range entry precedes every MFT-range + // entry -- the observable footprint of the engine having run track() in + // schedule order [ITS, MFT], not some other order. + const auto itsMask = LayerMask{uint32_t{(1u << ITSNLayers) - 1u}}; + const auto mftMask = LayerMask{static_cast(((1u << MFTNLayers) - 1u) << ITSNLayers)}; + const auto& commonTracks = frame.getGenericTracks(); + BOOST_REQUIRE_EQUAL(commonTracks.size(), result.nITSTracks + result.nMFTTracks); + bool seenMft = false; + for (const auto& track : commonTracks) { + const bool isMft = track.hitLayers.isSubsetOf(mftMask) && !track.hitLayers.empty(); + if (isMft) { + seenMft = true; + } else { + BOOST_CHECK(track.hitLayers.isSubsetOf(itsMask)); + BOOST_CHECK_MESSAGE(!seenMft, "an ITS GenericTrack appeared after an MFT one: schedule order was not ITS-then-MFT"); + } + } + BOOST_CHECK(seenMft); + + // Per-detector publication exports still resolve correctly through the + // participant-owned scratch/plan the composition reads from. + const auto itsExport = composer.getITSPublicationExport(); + const auto mftExport = composer.getMFTPublicationExport(); + BOOST_REQUIRE(itsExport.has_value()); + BOOST_REQUIRE(mftExport.has_value()); + BOOST_CHECK(itsExport->detector == o2::detectors::DetID::ITS); + BOOST_CHECK(itsExport->source == ClusterSourceId{0}); + BOOST_CHECK_EQUAL(itsExport->layerMapping.size(), static_cast(ITSNLayers)); + BOOST_CHECK(mftExport->detector == o2::detectors::DetID::MFT); + BOOST_CHECK(mftExport->source == ClusterSourceId{1}); + BOOST_CHECK_EQUAL(mftExport->layerMapping.size(), static_cast(MFTNLayers)); +} + +BOOST_AUTO_TEST_CASE(AtomicLoadFailureInvokesEngineResetOnlyAndLeavesNoParticipantOrSidecarState) +{ + // A load failure must reach the single frame reset directly -- + // Tracker::run() (and therefore either leg's kernel sequence) must never run on a + // partially/never-loaded event. Externally this means: zero tracks + // reported, both legs' scratches and both detector compatibility + // sidecars back to their pre-process() empty/unsealed state (never + // populated, since track() never ran), and both publication exports + // invalidated. + ensureTrivialMagneticFieldIsSet(); + MinimalFixture fixture; + makeRofGap(fixture.itsRofs); + fixture.itsSource.rofs = fixture.itsRofs; + + auto composer = makeComposer(makeItsParams(), makeMftParams()); + TimeFrame frame; + composer.adoptFrame(frame); + composer.setBz(Bz); + composer.setNThreads(1); + + const auto result = composer.process(fixture.itsSource, fixture.mftSource, o2::InteractionRecord{50, 5}); + BOOST_REQUIRE(result.outcome != TrackingOutcome::Success); + BOOST_CHECK_EQUAL(result.nITSTracks, 0u); + BOOST_CHECK_EQUAL(result.nMFTTracks, 0u); + + BOOST_CHECK_EQUAL(composer.frame->getTotalClusters(), 0); + BOOST_CHECK_EQUAL(composer.frame->getTotalClusters(), 0); + BOOST_CHECK(frame.getGenericTracks().empty()); + BOOST_CHECK(frame.getTrackClusterIndices().empty()); + // Neither sidecar was ever sealed/populated by this process() call -- + // proof that track() (and therefore the engine's executeEvent()) was + // never reached on this partially loaded event. + BOOST_CHECK(!composer.getITSSharedClusterCompatibility().isSealed()); + BOOST_CHECK(composer.getITSSharedClusterCompatibility().entries().empty()); + BOOST_CHECK(!composer.getITSPublicationExport().has_value()); + BOOST_CHECK(!composer.getMFTPublicationExport().has_value()); +} + +BOOST_AUTO_TEST_CASE(DetectorConfigurationIsSharedAcrossPassesAndOwnsCatalogMaterial) +{ + auto init = test::makeCombinedConfiguration(makeItsParams(), makeMftParams()); + std::vector catalog(init.catalog.surfaces, init.catalog.surfaces + init.catalog.nSurfaces); + catalog[0].material = {0.123f, 0.456f}; + init.catalog = {catalog.data(), static_cast(catalog.size())}; + init.plan.detector.LayerRadii[0] = 2.7f; // Deliberate lookup approximation, distinct from the surface. + init.plan.execution = {123456789, true}; + init.plan.iterations.resize(3, init.plan.iterations.front()); + init.plan.iterations[1].TrackletMinPt = 0.2f; + init.plan.iterations[2].TrackletMinPt = 0.1f; + TimeFrame frame; + Tracker tracker; + BOOST_REQUIRE(tracker.initialize(frame, init).ok()); + init.plan.detector.LayerRadii[0] = 99.f; + catalog[0].material = {}; + const auto ownedCatalog = frame.getLayout().getSurfaceCatalog(); + BOOST_CHECK_EQUAL(ownedCatalog.surfaces[0].material.xOverX0, 0.123f); + BOOST_CHECK_EQUAL(ownedCatalog.surfaces[0].material.arealDensityGPerCm2, 0.456f); + BOOST_CHECK_EQUAL(tracker.getDetectorConfiguration().layerRadii[0], 2.7f); + BOOST_CHECK(ownedCatalog.surfaces[0].referenceCoordinate != tracker.getDetectorConfiguration().layerRadii[0]); + BOOST_CHECK_EQUAL(tracker.getExecutionPolicy().MaxMemory, 123456789u); + BOOST_CHECK(tracker.getExecutionPolicy().DropTFUponFailure); + BOOST_REQUIRE_EQUAL(tracker.getIterationConfigurations().size(), 3u); + BOOST_CHECK_EQUAL(tracker.getIterationConfigurations()[1].parameters.TrackletMinPt, 0.2f); + BOOST_CHECK_EQUAL(tracker.getIterationConfigurations()[2].parameters.TrackletMinPt, 0.1f); + + const auto& cache = tracker.getDetectorConfiguration().indexTableConfigs; + BOOST_REQUIRE_EQUAL(cache.configurationCount(), 2u); + BOOST_CHECK_EQUAL(&cache[0], &cache[ITSNLayers - 1]); + BOOST_CHECK_EQUAL(&cache[ITSNLayers], &cache[ITSNLayers + MFTNLayers - 1]); + BOOST_CHECK(&cache[0] != &cache[ITSNLayers]); + BOOST_CHECK(cache[0].getCoordType() == IndexTableCoordType::PhiZ); + BOOST_CHECK(cache[ITSNLayers].getCoordType() == IndexTableCoordType::PhiR); + auto copy = cache; + BOOST_CHECK(©[0] != &cache[0]); + BOOST_CHECK_EQUAL(©[0], ©[1]); + BOOST_CHECK_EQUAL(copy[ITSNLayers].getNcolBins(), cache[ITSNLayers].getNcolBins()); +} + +BOOST_AUTO_TEST_CASE(SingleKindIndexCacheUsesOneConfigurationAndRejectsInvalidCatalogs) +{ + for (const auto catalog : {SurfaceCatalogView{kITSStaticSurfaceCatalog.data(), ITSNLayers}, + SurfaceCatalogView{kMFTStaticSurfaceCatalog.data(), MFTNLayers}}) { + IndexTableConfigurationSet cache; + BOOST_REQUIRE(cache.reset(catalog)); + BOOST_CHECK_EQUAL(cache.size(), catalog.nSurfaces); + BOOST_CHECK_EQUAL(cache.configurationCount(), 1u); + BOOST_CHECK_EQUAL(&cache[0], &cache[catalog.nSurfaces - 1]); + BOOST_CHECK(!cache.reset({nullptr, 1})); + BOOST_CHECK_EQUAL(cache.size(), 0u); + BOOST_CHECK_EQUAL(cache.configurationCount(), 0u); + } + auto invalid = kITSStaticSurfaceCatalog[0]; + invalid.kind = static_cast(255); + IndexTableConfigurationSet cache; + BOOST_CHECK(!cache.reset({&invalid, 1})); + BOOST_CHECK_EQUAL(cache.size(), 0u); + BOOST_CHECK(!cache.reset({&invalid, MaxLayoutSurfaces + 1})); +} + +BOOST_AUTO_TEST_CASE(DenseTraversalIdsKeepTheirTypesAndRejectOutOfRangeSlots) +{ + auto init = test::makeCombinedConfiguration(makeItsParams(), makeMftParams()); + TimeFrame frame; + Tracker tracker; + BOOST_REQUIRE(tracker.initialize(frame, init).ok()); + const auto& configuration = tracker.getIterationConfigurations().front(); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + for (const auto id : configuration.edgeIds()) { + BOOST_REQUIRE(configuration.getEdgeSlot(id)); + BOOST_CHECK_EQUAL(*configuration.getEdgeSlot(id), id.value()); + } + for (const auto id : configuration.cellIds()) { + BOOST_REQUIRE(configuration.getCellSlot(id)); + BOOST_CHECK_EQUAL(*configuration.getCellSlot(id), id.value()); + } + BOOST_CHECK(!configuration.getEdgeSlot(EdgeId{})); + BOOST_CHECK(!configuration.getCellSlot(CellPathId{})); + BOOST_CHECK(!configuration.getEdgeSlot(EdgeId{static_cast(configuration.topology.edges.size())})); + BOOST_CHECK(!configuration.getCellSlot(CellPathId{static_cast(configuration.topology.paths.size())})); + const IterationConfiguration empty; + BOOST_CHECK(empty.edgeIds().empty()); + BOOST_CHECK(empty.cellIds().empty()); + BOOST_CHECK(!empty.getEdgeSlot(EdgeId{0})); + BOOST_CHECK(!empty.getCellSlot(CellPathId{0})); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testComputeLayerCellsOrchestration.cxx b/Detectors/ITSMFT/common/tracking/test/testComputeLayerCellsOrchestration.cxx new file mode 100644 index 0000000000000..106ca35f8a94a --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testComputeLayerCellsOrchestration.cxx @@ -0,0 +1,1301 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// Orchestration coverage for TrackerTraits::computeLayerCells after +// its detector-family branch was replaced by a one-shot outer dispatch to +// cell-seed leaves (Architecture.md Sec 10/10.1). cell-seed leaves's +// numerical parity with the legacy inline formulas is already proven by +// testTrackletFinding.cxx; this file does not re-derive or +// duplicate that formula. It proves instead that the real public +// computeLayerCells() entry point: +// - resolves the three clusters for a candidate in strict +// {inner, middle, outer} order and stores the corresponding linearized +// triplet factor without prematurely constructing a track state; +// - exercises cylinder and disk cells through the same public orchestration +// entry point, with coordinate differences confined to cell-seed leaves; +// - leaves cellIndex indexing, the LUT, MC-label construction, and +// one-pass/two-pass ordering untouched; +// - fails closed (TraversalException::InvalidTraversalSchedule) through the +// existing public API alone, with no test-only seam into private +// traversal-cache state. + +#define BOOST_TEST_MODULE ITSMFT ComputeLayerCells orchestration +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "CommonConstants/MathConstants.h" +#include "CommonDataFormat/InteractionRecord.h" +#include "DataFormatsITSMFT/CompCluster.h" +#include "DataFormatsITSMFT/ROFRecord.h" +#include "DataFormatsITSMFT/TopologyDictionary.h" +#include "DetectorsCommonDataFormats/DetID.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/ClusterDecoding.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/detail/TimeFrameScratch.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/Tracker.h" +#include "ITSMFTTracking/TrackerTraits.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "ITSMFTTracking/TripletFitting.h" +#include "ITSMFTTracking/Constants.h" +#include "MFTTracking/Constants.h" + +#include "TraversalTestSupport.h" + +#include "TrackingParameterTestSupport.h" + +using o2::itsmft::tracking::test::ReferenceTrackingParameters; +using namespace o2::itsmft; +using namespace o2::itsmft::tracking; + +namespace +{ + +constexpr float Bz = 0.5f; + +// Preflight-only fixtures (Rig::establishLayout()) load zero real clusters -- +// this decoder's decode() is never actually invoked there. It exists only to +// satisfy loadNormalizedSource()'s interface, mirroring +// testTrackerFailureContract.cxx's LegacyLikeDecoder. +class NeverDecodedDecoder final : public ClusterDecoder +{ + public: + explicit NeverDecodedDecoder(o2::detectors::DetID::ID detector) : mDetector(detector) {} + + o2::itsmft::tracking::ClusterDecodeResult decode( + const CompClusterExt&, BoundedPatternCursor&, const TopologyDictionary*, + uint32_t, bool) const override + { + return {}; + } + + private: + o2::detectors::DetID::ID mDetector; +}; + +// Stage-B normalized-CA-measurements slice: computeLayerCells() now reads +// the TimeFrame's source-indexed SurfaceMeasurements. Candidate fixtures +// therefore load their three clusters through the real loadNormalizedSource() +// path -- backfilling both the normalized frame and every legacy +// compatibility structure (unsorted clusters, TrackingFrameInfo, external +// indices, ROF boundaries) together, in lockstep -- rather than poking legacy +// structures directly. This decoder returns exactly the caller-supplied +// SurfaceMeasurement for a given detector-local layer (encoded as the +// synthetic CompClusterExt's chipID/sensorID) as decoded geometry facts. +class FixedMeasurementDecoder final : public ClusterDecoder +{ + public: + struct MeasurementPair { + DecodedCluster decoded{}; + }; + + FixedMeasurementDecoder(o2::detectors::DetID::ID detector, SurfaceKind kind) : mDetector(detector), mKind(kind) {} + + void setMeasurement(int layer, const MeasurementPair& measurement) { mByLayer[layer] = measurement; } + + o2::itsmft::tracking::ClusterDecodeResult decode( + const CompClusterExt& cluster, + BoundedPatternCursor&, + const TopologyDictionary*, + uint32_t, + bool) const override + { + o2::itsmft::tracking::ClusterDecodeResult result; + const int layer = cluster.getSensorID(); + const auto it = mByLayer.find(layer); + BOOST_REQUIRE(it != mByLayer.end()); + result.decoded = it->second.decoded; + result.decoded.layer = layer; + return result; + } + + private: + o2::detectors::DetID::ID mDetector; + SurfaceKind mKind; + std::map mByLayer; +}; + +const TopologyDictionary& dict() +{ + static const TopologyDictionary d; + return d; +} + +std::vector identitySurfaces(uint16_t nLayers) +{ + std::vector mapping; + mapping.reserve(nLayers); + for (uint16_t i = 0; i < nLayers; ++i) { + mapping.push_back(LayerId{i}); + } + return mapping; +} + +// The test-only reference material values become the authoritative catalog +// material before running computeLayerCells(). Production has no shadow vector. +std::vector makeCatalog(uint16_t nLayers, o2::detectors::DetID::ID det, + gsl::span kinds, gsl::span layerxX0) +{ + std::vector surfaces; + surfaces.reserve(nLayers); + for (uint16_t i = 0; i < nLayers; ++i) { + const auto kind = kinds[i]; + surfaces.push_back(SurfaceDescriptor{i, static_cast(det), kind}); + surfaces.back().chartRange = kind == SurfaceKind::Disk ? SurfaceChartRange{0.1f, 20.f} : SurfaceChartRange{-20.f, 20.f}; + surfaces.back().referenceCoordinate = kind == SurfaceKind::Cylinder + ? 3.f + static_cast(i) + : -0.4f - 0.2f * static_cast(i); + const float xOverX0 = layerxX0[i]; + surfaces.back().material.xOverX0 = xOverX0; + surfaces.back().material.arealDensityGPerCm2 = xOverX0 * o2::its::constants::Radl * o2::its::constants::Rho; + } + return surfaces; +} + +// Same construction as testTrackletFinding.cxx's helpers -- plain +// input-struct builders, not a reimplementation of any fit formula. +GlobalMeasurement makeGlobalCluster(float x, float y, float z, int id = 0) +{ + GlobalMeasurement measurement{}; + measurement.position = {x, y, z}; + measurement.radius = std::hypot(x, y); + measurement.phi = std::atan2(y, x); + measurement.clusterId = static_cast(id); + return measurement; +} + +struct TestLocalMeasurement { + float xTrackingFrame{0.f}; + float alphaTrackingFrame{0.f}; + std::array positionTrackingFrame{}; + std::array covarianceTrackingFrame{}; +}; + +TestLocalMeasurement makeBarrelHit(float xTF, float alpha, float y, float z, float sigma2Y = 1.e-4f, float sigma2Z = 1.e-4f) +{ + return {xTF, alpha, {y, z}, {sigma2Y, 0.f, sigma2Z}}; +} + +TestLocalMeasurement makeDiskHit(float z, float x, float y, float sigma2X = 1.e-2f, float sigma2Y = 1.e-2f) +{ + return {z, 0.f, {x, y}, {sigma2X, 0.f, sigma2Y}}; +} + +// Test-local field-mapping helpers (not a production API), matching the same +// Cylinder/Disk field mapping used by the production migration and by +// testCellFinding.cxx: the single SurfaceMeasurement now +// standing in for the retired {Cluster, TrackingFrameInfo} pair at each +// candidate position. +FixedMeasurementDecoder::MeasurementPair barrelMeasurementFor(const GlobalMeasurement& cluster, const TestLocalMeasurement& hit) +{ + FixedMeasurementDecoder::MeasurementPair measurement{}; + measurement.decoded.global = {cluster.x, cluster.y, cluster.z}; + measurement.decoded.cylinderFrame = {hit.xTrackingFrame, hit.positionTrackingFrame[0], + hit.positionTrackingFrame[1], hit.alphaTrackingFrame}; + measurement.decoded.rowColumnCovariance = {hit.covarianceTrackingFrame[0], + hit.covarianceTrackingFrame[1], + hit.covarianceTrackingFrame[2]}; + return measurement; +} + +FixedMeasurementDecoder::MeasurementPair diskMeasurementFor(const GlobalMeasurement& cluster, const TestLocalMeasurement& hit) +{ + FixedMeasurementDecoder::MeasurementPair measurement{}; + measurement.decoded.global = {cluster.x, cluster.y, cluster.z}; + measurement.decoded.rowColumnCovariance = {hit.covarianceTrackingFrame[0], 0.f, + hit.covarianceTrackingFrame[2]}; + return measurement; +} + +void checkTripletFitFactorEqual(const TripletFitFactor& lhs, const TripletFitFactor& rhs) +{ + BOOST_CHECK_EQUAL(lhs.psi.theta, rhs.psi.theta); + BOOST_CHECK_EQUAL(lhs.psi.phi, rhs.psi.phi); + BOOST_CHECK_EQUAL(lhs.rho.theta, rhs.rho.theta); + BOOST_CHECK_EQUAL(lhs.rho.phi, rhs.rho.phi); + for (int hit = 0; hit < 3; ++hit) { + for (int coordinate = 0; coordinate < 3; ++coordinate) { + BOOST_CHECK_EQUAL(lhs.h[hit].theta[coordinate], rhs.h[hit].theta[coordinate]); + BOOST_CHECK_EQUAL(lhs.h[hit].phi[coordinate], rhs.h[hit].phi[coordinate]); + } + } +} + +void checkTrackSeedContents(const TrackSeed& trackSeed, const CellSeed& cell, + SurfaceKind expectedKind) +{ + BOOST_CHECK_EQUAL(trackSeed.getHitLayerMask().value(), cell.getHitLayerMask().value()); + for (int slot = 0; slot < 3; ++slot) { + const auto reference = cell.getClusterReference(slot); + BOOST_CHECK_EQUAL(trackSeed.getCluster(reference.surfacePosition), reference.clusterIndex); + } + BOOST_CHECK_EQUAL(trackSeed.getLevel(), cell.getLevel()); + BOOST_CHECK_EQUAL(trackSeed.getFirstTrackletIndex(), cell.getFirstTrackletIndex()); + BOOST_CHECK_EQUAL(trackSeed.getSecondTrackletIndex(), cell.getSecondTrackletIndex()); + BOOST_CHECK_EQUAL(trackSeed.getTimeStamp().getTimeStamp(), cell.getTimeStamp().getTimeStamp()); + BOOST_CHECK_EQUAL(trackSeed.getTimeStamp().getTimeStampError(), cell.getTimeStamp().getTimeStampError()); + BOOST_CHECK(trackSeed.state().kind == expectedKind); + BOOST_CHECK(std::isfinite(trackSeed.getChi2())); + for (const float parameter : trackSeed.state().parameters) { + BOOST_CHECK(std::isfinite(parameter)); + } + for (const float covariance : trackSeed.state().covariance) { + BOOST_CHECK(std::isfinite(covariance)); + } +} + +void checkTrackSeedsEqual(const TrackSeed& lhs, const TrackSeed& rhs) +{ + BOOST_CHECK_EQUAL(lhs.getHitLayerMask().value(), rhs.getHitLayerMask().value()); + BOOST_CHECK_EQUAL(lhs.getChi2(), rhs.getChi2()); + BOOST_CHECK_EQUAL(lhs.getLevel(), rhs.getLevel()); + BOOST_CHECK_EQUAL(lhs.getFirstTrackletIndex(), rhs.getFirstTrackletIndex()); + BOOST_CHECK_EQUAL(lhs.getSecondTrackletIndex(), rhs.getSecondTrackletIndex()); + BOOST_CHECK_EQUAL(lhs.getTimeStamp().getTimeStamp(), rhs.getTimeStamp().getTimeStamp()); + BOOST_CHECK_EQUAL(lhs.getTimeStamp().getTimeStampError(), rhs.getTimeStamp().getTimeStampError()); + for (int position = 0; position < TrackSeed::MaxSurfaces; ++position) { + BOOST_CHECK_EQUAL(lhs.getCluster(position), rhs.getCluster(position)); + } + for (int parameter = 0; parameter < 5; ++parameter) { + BOOST_CHECK_EQUAL(lhs.state().parameters[parameter], rhs.state().parameters[parameter]); + } + for (int covariance = 0; covariance < 15; ++covariance) { + BOOST_CHECK_EQUAL(lhs.state().covariance[covariance], rhs.state().covariance[covariance]); + } + BOOST_CHECK_EQUAL(lhs.state().referenceCoordinate, rhs.state().referenceCoordinate); + BOOST_CHECK_EQUAL(lhs.state().alpha, rhs.state().alpha); + BOOST_CHECK(lhs.state().kind == rhs.state().kind); + BOOST_CHECK_EQUAL(lhs.state().flags, rhs.state().flags); + BOOST_CHECK_EQUAL(lhs.state().absCharge, rhs.state().absCharge); + BOOST_CHECK(lhs.state().pid == rhs.state().pid); +} + +void checkTrackSeedMaterialization(TrackerTraits& traits, IterationContext& view, + int cellPathId, const CellSeed& cell, + SurfaceKind expectedKind) +{ + TrackSeed trackSeed{}; + OperationFailureReason reason{}; + BOOST_REQUIRE(TrackerTestAccess::buildTrackSeed( + traits, view, cellPathId, cell, trackSeed, reason)); + checkTrackSeedContents(trackSeed, cell, expectedKind); +} + +// Minimal wiring TrackerTraits::computeLayerCells() needs: a real +// layout/topology (so initialiseTimeFrame() genuinely binds +// the edge/cell schedule and tracking parameters +// -- computeLayerCells()'s own private caches, never poked directly), and a +// validly-sized-but-empty normalized load (proven pattern from +// testTrackerFailureContract.cxx: TimeFrame::initialise() unconditionally +// reads mROFramesClusters sizes, which only loadNormalizedSource() sets up +// safely, even for zero clusters). +struct RigFrameStorage { + RigFrameStorage() : pool(std::make_shared()) { frame.setMemoryPool(pool); } + + std::shared_ptr pool; + TimeFrame frame; +}; + +template +struct Rig : RigFrameStorage { + + Rig(o2::detectors::DetID::ID det, SurfaceKind kind, int nThreads = 1) + : params(1), + mDet(det), + mKinds(NLayers, kind) + { + resetDetectorDefaults(params[0], det); + // This file bypasses computeLayerTracklets()'s phi/z/index-table cuts + // entirely (candidates are injected directly, see + // injectCandidateTracklets() below): clearing RebuildClusterLUT keeps + // TimeFrame::initialise() from also exercising prepareClusters()'s + // index-table row/col binning and ROF-mask lookup on the synthetic + // candidate positions/ROF this file uses -- that out-of-scope subsystem + // is not configured for this file's candidates (in particular, no + // multiplicity/UPC ROF mask is ever loaded, so its default view is + // never a valid one to index once real clusters are present, unlike + // when this file loaded zero real clusters). + params[0].PassFlags.reset(IterationStep::RebuildClusterLUT); + traits.setNThreads(nThreads, arena); + frame.setBz(Bz); + } + + // Establishes the catalog/layout and loads a (zero-cluster) normalized + // source. Deliberately not run by the constructor: it builds the catalog's + // nominal material from the *current* params[0].LayerxX0, so callers must + // finish any test reference material override before establishing the layout. + void establishLayout() + { + catalog = makeCatalog(static_cast(NLayers), mDet, gsl::span{mKinds}, gsl::span(params[0].LayerxX0)); + const auto orderedSurfaces = identitySurfaces(static_cast(NLayers)); + const SurfaceCatalogView catalogView{catalog.data(), static_cast(catalog.size())}; + TrackerInitialization configuration; + configuration.catalog = catalogView; + configuration.memoryPool = pool; + configuration.layout = makeDetectorLayout(holeLayers); + configuration.plan = o2::itsmft::tracking::test::makeTrackingPlan(params[0]); + BOOST_REQUIRE(tracker.initialize(frame, configuration).ok()); + tf = &frame.getScratch(); + const auto& layout = frame.getLayout(); + + NeverDecodedDecoder decoder{mDet}; + const o2::InteractionRecord origin{50, 5}; + const ROFTimingConfig timing{40, 0, 0, 0}; + const std::vector noClusters; + const std::vector noPatterns; + const std::vector noRofs; + const auto loadResult = loadTimeFrameSource(frame, decoder, origin, timing, noClusters, noPatterns, noRofs, &dict(), nullptr, mDet, + gsl::span{orderedSurfaces}, layout.getSurfaceCatalog()); + BOOST_REQUIRE(loadResult.ok()); + } + + o2::detectors::DetID::ID detector() const noexcept { return mDet; } + SurfaceKind kind() const noexcept { return mKinds.front(); } + SurfaceKind kind(int layer) const noexcept { return mKinds[layer]; } + void setSurfaceKind(int layer, SurfaceKind kind) { mKinds[layer] = kind; } + + std::vector params; + LayerMask holeLayers{}; + // Gate 4 B3.1: `frame` declared before `tf` so it is constructed first and + // destroyed last (see TimeFrameScratch's own lifetime-contract doc). + TimeFrameScratch* tf{nullptr}; + Tracker tracker; + std::array, MaxLayoutSurfaces> measurementSpans; + TrackerTraits traits; + std::shared_ptr arena; + // The catalog must outlive the immutable layout and all event-local views. + std::vector catalog; + + private: + o2::detectors::DetID::ID mDet; + std::vector mKinds; +}; + +template +IterationContext prepare(Rig& rig) +{ + return TrackerTestAccess::prepare(rig.tracker, rig.frame, 0, rig.measurementSpans); +} + +template +TraversalTopologyView topologyView(const Rig& rig) +{ + return rig.tracker.getIterationConfigurations()[0].getTopologyView(rig.frame.getLayout().getSurfaceCatalog()); +} + +// Loads exactly the three supplied {cluster, hit} candidates at legacy +// layers {0, 1, 2} (every test in this file locates its candidate cell via +// findCellIndex(topology, 0, 1, 2), so the layer mapping is always this +// identity triple) through the real loadNormalizedSource() path, via +// FixedMeasurementDecoder -- so the normalized frame and every legacy +// compatibility structure are populated together, in lockstep, exactly as +// TrackerTraits::initialiseTimeFrame()'s one-time normalized-measurement +// binding requires. Must be called after Rig::establishLayout() (which needs +// the catalog/topology first) and before TrackerTraits::initialiseTimeFrame() +// (which validates the normalized frame against the legacy structures this +// call also populates). +template +void loadCandidateClusters(Rig& rig, + const std::array& clusters, + const std::array& hits) +{ + FixedMeasurementDecoder decoder{rig.detector(), rig.kind()}; + std::vector compClusters; + compClusters.reserve(3); + for (int layer = 0; layer < 3; ++layer) { + compClusters.emplace_back(0, 0, CompCluster::InvalidPatternID, static_cast(layer)); + const auto measurement = rig.kind(layer) == SurfaceKind::Disk + ? diskMeasurementFor(clusters[layer], hits[layer]) + : barrelMeasurementFor(clusters[layer], hits[layer]); + decoder.setMeasurement(layer, measurement); + } + const std::vector noPatterns; + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 3}}; + const o2::InteractionRecord origin{50, 5}; + const ROFTimingConfig timing{40, 0, 0, 0}; + const auto layerMapping = identitySurfaces(static_cast(NLayers)); + const auto result = loadTimeFrameSource(rig.frame, decoder, origin, timing, compClusters, noPatterns, rofs, &dict(), nullptr, rig.detector(), + gsl::span{layerMapping}, rig.frame.getLayout().getSurfaceCatalog()); + BOOST_REQUIRE(result.ok()); +} + +// Finds the cellIndex whose two edges span exactly +// inner->middle->outer, without assuming any particular enumeration order +// out of the sparse topology's builder enumeration. +template +int findCellIndex(const TopologyView& topology, int inner, int middle, int outer) +{ + for (int i = 0; i < topology.nPaths; ++i) { + const auto& cell = topology.getPath(CellPathId{static_cast(i)}); + const auto& first = topology.getEdge(cell.first); + const auto& second = topology.getEdge(cell.second); + if (first.from.value() == inner && first.to.value() == middle && second.from.value() == middle && second.to.value() == outer) { + return i; + } + } + return -1; +} + +Tracklet candidateTracklet(const GlobalMeasurement& first, const GlobalMeasurement& second, + const o2::its::TimeEstBC& timestamp) +{ + const float deltaR = first.radius - second.radius; + const float deltaZ = first.z - second.z; + const float tanLambda = deltaR * deltaR > o2::constants::math::Almost0 + ? deltaZ / deltaR + : std::copysign(o2::constants::math::VeryBig, deltaZ); + const float phi = std::atan2(first.y - second.y, first.x - second.x); + return {0, 0, tanLambda, phi, timestamp}; +} + +// Bypasses the real (untouched, out-of-scope-for-this-change) +// computeLayerTracklets() phi/z/index-table cuts entirely. The real loader +// has already installed the authoritative compact globals and fitting +// measurements; this helper only injects one tracklet per edge of cellIndex, +// wired so +// computeLayerCellsForKind's tracklet-pairing loop finds exactly one +// candidate pair. +template +void injectCandidateTracklets(Rig& rig, int cellIndex, const std::array& clusters) +{ + const auto topology = topologyView(rig); + const auto& cell = topology.getPath(CellPathId{static_cast(cellIndex)}); + const auto& first = topology.getEdge(cell.first); + const auto& second = topology.getEdge(cell.second); + const int layers[3] = {first.from.value(), first.to.value(), second.to.value()}; + + for (int i = 0; i < 3; ++i) { + BOOST_REQUIRE_EQUAL(rig.frame.getClusters()[layers[i]].size(), 1u); + BOOST_CHECK_EQUAL(rig.frame.getClusters()[layers[i]][0].x, clusters[i].x); + BOOST_CHECK_EQUAL(rig.frame.getClusters()[layers[i]][0].y, clusters[i].y); + BOOST_CHECK_EQUAL(rig.frame.getClusters()[layers[i]][0].z, clusters[i].z); + } + + const o2::its::TimeEstBC ts{static_cast(0), static_cast(1)}; + rig.tf->getTracklets()[cell.first.value()].push_back(candidateTracklet(clusters[0], clusters[1], ts)); + rig.tf->getTracklets()[cell.second.value()].push_back(candidateTracklet(clusters[1], clusters[2], ts)); + + auto& secondLUT = rig.tf->getTrackletsLookupTable()[cell.second.value()]; + secondLUT.resize(2); + secondLUT[0] = 0; + secondLUT[1] = 1; +} + +// Gate 4 Slice 0b additions below: multi-cell parity coverage for the +// migrated computeLayerCells()/computeLayerCellsForKind(), extending this +// file's existing single-cell (always layers {0,1,2}) machinery to an +// arbitrary ordered set of N>=3 layers so several simultaneously-populated +// cells (sharing edges between adjacent triples) can be checked in one +// run. + +// Same technique as loadCandidateClusters() (real loadNormalizedSource() +// path via FixedMeasurementDecoder), generalized to N candidate layers +// instead of the fixed {0,1,2} triple. +template +void loadCandidateClustersAtLayers(Rig& rig, + const std::array& layers, + const std::array& clusters, + const std::array& hits) +{ + FixedMeasurementDecoder decoder{rig.detector(), rig.kind()}; + std::vector compClusters; + compClusters.reserve(N); + for (size_t i = 0; i < N; ++i) { + compClusters.emplace_back(0, 0, CompCluster::InvalidPatternID, static_cast(layers[i])); + const auto measurement = rig.kind(layers[i]) == SurfaceKind::Disk + ? diskMeasurementFor(clusters[i], hits[i]) + : barrelMeasurementFor(clusters[i], hits[i]); + decoder.setMeasurement(layers[i], measurement); + } + const std::vector noPatterns; + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, static_cast(N)}}; + const o2::InteractionRecord origin{50, 5}; + const ROFTimingConfig timing{40, 0, 0, 0}; + const auto layerMapping = identitySurfaces(static_cast(NLayers)); + const auto result = loadTimeFrameSource(rig.frame, decoder, origin, timing, compClusters, noPatterns, rofs, &dict(), nullptr, rig.detector(), + gsl::span{layerMapping}, rig.frame.getLayout().getSurfaceCatalog()); + BOOST_REQUIRE(result.ok()); +} + +// Finds the edgeId spanning exactly from->to, mirroring +// findCellIndex()'s linear-search style over the legacy view. +template +int findEdgeId(const TopologyView& topology, int from, int to) +{ + for (int i = 0; i < topology.nEdges; ++i) { + const auto& t = topology.getEdge(EdgeId{static_cast(i)}); + if (t.from.value() == from && t.to.value() == to) { + return i; + } + } + return -1; +} + +// Generalizes injectCandidateTracklets() to an ordered chain of N>=3 layers: +// writes each physical layer's single cluster exactly once, then touches +// each of the N-1 adjacent-pair edges exactly once (one synthetic +// tracklet + one LUT {0,1}), regardless of how many downstream cells in the +// chain share that edge. Naively calling the single-cell +// injectCandidateTracklets() once per overlapping cell would instead +// double-write any shared edge (extra duplicate tracklet, and a LUT +// left however the last call set it) and silently clobber a shared physical +// layer's cluster across calls -- this helper touches every physical layer +// and every edge exactly once, by construction. +template +void injectChainCandidateTracklets(Rig& rig, const std::array& layers, const std::array& clusters) +{ + static_assert(N >= 3, "a chain needs at least 3 layers to form one cell"); + const auto topology = topologyView(rig); + for (size_t i = 0; i < N; ++i) { + BOOST_REQUIRE_EQUAL(rig.frame.getClusters()[layers[i]].size(), 1u); + BOOST_CHECK_EQUAL(rig.frame.getClusters()[layers[i]][0].x, clusters[i].x); + BOOST_CHECK_EQUAL(rig.frame.getClusters()[layers[i]][0].y, clusters[i].y); + BOOST_CHECK_EQUAL(rig.frame.getClusters()[layers[i]][0].z, clusters[i].z); + } + + const o2::its::TimeEstBC ts{static_cast(0), static_cast(1)}; + for (size_t i = 0; i + 1 < N; ++i) { + const int edgeId = findEdgeId(topology, layers[i], layers[i + 1]); + BOOST_REQUIRE_GE(edgeId, 0); + rig.tf->getTracklets()[edgeId].push_back(candidateTracklet(clusters[i], clusters[i + 1], ts)); + auto& lut = rig.tf->getTrackletsLookupTable()[edgeId]; + lut.resize(2); + lut[0] = 0; + lut[1] = 1; + } +} + +std::array makeLocalMeasurements( + const std::array& kinds, + const std::array& clusters) +{ + std::array hits{}; + for (int layer = 0; layer < 3; ++layer) { + const auto& position = clusters[layer].position; + hits[layer] = kinds[layer] == SurfaceKind::Disk + ? makeDiskHit(position.z, position.x, position.y) + : makeBarrelHit(position.x, 0.f, position.y, position.z); + } + return hits; +} + +template +void checkDirectTrackSeedConstruction(const std::array& kinds, + const std::array& clusters) +{ + Rig rig{o2::detectors::DetID::ITS, kinds[0]}; + rig.params[0].MaxChi2ClusterAttachment = 1.e6f; + for (int layer = 0; layer < 3; ++layer) { + rig.setSurfaceKind(layer, kinds[layer]); + rig.params[0].LayerxX0[layer] = 0.f; + } + rig.establishLayout(); + loadCandidateClusters(rig, clusters, makeLocalMeasurements(kinds, clusters)); + + auto view = prepare(rig); + const int cellPathId = findCellIndex(topologyView(rig), 0, 1, 2); + BOOST_REQUIRE_GE(cellPathId, 0); + CellSeed cell{0, 0, 0, 0, 17, 23, o2::its::TimeEstBC{111, 9}}; + cell.setLevel(6); + + TrackSeed first{}; + TrackSeed second{}; + OperationFailureReason firstReason{}; + OperationFailureReason secondReason{}; + BOOST_REQUIRE(TrackerTestAccess::buildTrackSeed( + rig.traits, view, cellPathId, cell, first, firstReason)); + BOOST_REQUIRE(TrackerTestAccess::buildTrackSeed( + rig.traits, view, cellPathId, cell, second, secondReason)); + + checkTrackSeedContents(first, cell, kinds[0]); + checkTrackSeedsEqual(first, second); +} + +constexpr std::array CylinderCylinderCylinder{ + SurfaceKind::Cylinder, SurfaceKind::Cylinder, SurfaceKind::Cylinder}; +constexpr std::array DiskDiskDisk{ + SurfaceKind::Disk, SurfaceKind::Disk, SurfaceKind::Disk}; +constexpr std::array CylinderDiskCylinder{ + SurfaceKind::Cylinder, SurfaceKind::Disk, SurfaceKind::Cylinder}; +constexpr std::array DiskCylinderDisk{ + SurfaceKind::Disk, SurfaceKind::Cylinder, SurfaceKind::Disk}; + +const std::array NominalTrackSeedClusters{ + makeGlobalCluster(3.0f, 0.100f, 0.90f, 0), + makeGlobalCluster(4.0f, 0.150f, 1.05f, 0), + makeGlobalCluster(5.0f, 0.201f, 1.25f, 0)}; + +} // namespace + +BOOST_AUTO_TEST_CASE(BuildTrackSeedCylinderCylinderCylinderIsDeterministic) +{ + checkDirectTrackSeedConstruction(CylinderCylinderCylinder, NominalTrackSeedClusters); +} + +BOOST_AUTO_TEST_CASE(BuildTrackSeedDiskDiskDiskIsDeterministic) +{ + checkDirectTrackSeedConstruction(DiskDiskDisk, NominalTrackSeedClusters); +} + +BOOST_AUTO_TEST_CASE(BuildTrackSeedCylinderDiskCylinderConvertsBackToCylinder) +{ + checkDirectTrackSeedConstruction(CylinderDiskCylinder, NominalTrackSeedClusters); +} + +BOOST_AUTO_TEST_CASE(BuildTrackSeedDiskCylinderDiskConvertsBackToDisk) +{ + checkDirectTrackSeedConstruction(DiskCylinderDisk, NominalTrackSeedClusters); +} + +BOOST_AUTO_TEST_CASE(BuildTrackSeedDegenerateMixedTripletPreservesDestination) +{ + Rig rig{o2::detectors::DetID::ITS, SurfaceKind::Cylinder}; + rig.params[0].MaxChi2ClusterAttachment = 1.e6f; + for (int layer = 0; layer < 3; ++layer) { + rig.setSurfaceKind(layer, CylinderDiskCylinder[layer]); + rig.params[0].LayerxX0[layer] = 0.f; + } + rig.establishLayout(); + + const std::array degenerateClusters{ + makeGlobalCluster(3.f, 0.1f, 0.9f, 0), + makeGlobalCluster(3.f, 0.1f, 1.0f, 0), + makeGlobalCluster(3.f, 0.1f, 1.1f, 0)}; + loadCandidateClusters(rig, degenerateClusters, + makeLocalMeasurements(CylinderDiskCylinder, degenerateClusters)); + auto view = prepare(rig); + const int cellPathId = findCellIndex(topologyView(rig), 0, 1, 2); + BOOST_REQUIRE_GE(cellPathId, 0); + CellSeed cell{0, 0, 0, 0, 17, 23, o2::its::TimeEstBC{111, 9}}; + cell.setLevel(6); + + SurfaceTrackState sentinelState{}; + sentinelState.kind = SurfaceKind::Disk; + sentinelState.referenceCoordinate = -42.f; + sentinelState.parameters[0] = 13.f; + TrackSeed destination{cell, sentinelState, 71.f}; + const TrackSeed before = destination; + OperationFailureReason reason{}; + BOOST_CHECK(!TrackerTestAccess::buildTrackSeed( + rig.traits, view, cellPathId, cell, destination, reason)); + BOOST_CHECK(reason == OperationFailureReason::SurfaceKindConversionFailure); + checkTrackSeedsEqual(destination, before); +} + +// --- Barrel: real orchestration matches the cell-seed leaves oracle ----- + +BOOST_AUTO_TEST_CASE(CylinderComputeLayerCellsMatchesBuildCellSeedOracle) +{ + Rig rig{o2::detectors::DetID::ITS, SurfaceKind::Cylinder}; + rig.params[0].MaxChi2ClusterAttachment = 1.e6f; + rig.params[0].LayerxX0[0] = 0.005f; // inner + rig.params[0].LayerxX0[1] = 0.005f; // middle + rig.params[0].LayerxX0[2] = 0.f; // outer: contractually unused by Cylinder + rig.establishLayout(); + + const std::array clusters{makeGlobalCluster(3.0f, 0.100f, 0.9f, 0), + makeGlobalCluster(4.0f, 0.150f, 1.05f, 0), + makeGlobalCluster(5.0f, 0.201f, 1.20f, 0)}; + loadCandidateClusters(rig, clusters, + {makeBarrelHit(3.f, 0.f, 0.100f, 0.9f), + makeBarrelHit(4.f, 0.f, 0.150f, 1.05f), + makeBarrelHit(5.f, 0.f, 0.201f, 1.20f)}); + + auto view = TrackerTestAccess::prepare(rig.tracker, rig.frame, 0, rig.measurementSpans); + + const auto topology = topologyView(rig); + const int cellIndex = findCellIndex(topology, 0, 1, 2); + BOOST_REQUIRE_GE(cellIndex, 0); + + injectCandidateTracklets(rig, cellIndex, clusters); + + // Any other cellIndex keeps its empty-edge early-continue + // path: cleared once up front, never touched again. + int othercellIndex = -1; + for (int i = 0; i < topology.nPaths; ++i) { + if (i != cellIndex) { + othercellIndex = i; + break; + } + } + BOOST_REQUIRE_GE(othercellIndex, 0); + + TrackerTestAccess::computeCells(rig.traits, view); + + BOOST_CHECK(rig.tf->getCells()[othercellIndex].empty()); + BOOST_CHECK(rig.tf->getCellsLookupTable()[othercellIndex].empty()); + BOOST_CHECK(rig.tf->getCellsLabel(othercellIndex).empty()); + + BOOST_REQUIRE_EQUAL(rig.tf->getCells()[cellIndex].size(), 1u); + const auto& producedCell = rig.tf->getCells()[cellIndex][0]; + BOOST_CHECK(producedCell.tripletFactor().isValid()); + for (int slot = 0; slot < 3; ++slot) { + const auto reference = producedCell.getClusterReference(slot); + BOOST_CHECK_EQUAL(reference.surfacePosition, slot); + BOOST_CHECK_EQUAL(reference.clusterIndex, producedCell.getClusters()[slot]); + } + + BOOST_REQUIRE_EQUAL(rig.tf->getCellsLookupTable()[cellIndex].size(), 2u); + BOOST_CHECK_EQUAL(rig.tf->getCellsLookupTable()[cellIndex][0], 0); + BOOST_CHECK_EQUAL(rig.tf->getCellsLookupTable()[cellIndex][1], 1); + + // hasMCinformation() is false (no labels were loaded), so label + // construction is skipped, exactly as before this change. + BOOST_CHECK(rig.tf->getCellsLabel(cellIndex).empty()); + + // Oracle: independently reconstruct the geometry-only factor from the + // ordered global measurements. Track-state construction belongs to + // TrackerTraits::buildTrackSeed(), after the CA has selected a cell. + const auto layerGlobalMeasurements = gsl::span>{view.layerGlobalMeasurements}; + const auto& oracleGlobalInner = layerGlobalMeasurements[0][producedCell.getFirstClusterIndex()]; + const auto& oracleGlobalMiddle = layerGlobalMeasurements[1][producedCell.getSecondClusterIndex()]; + const auto& oracleGlobalOuter = layerGlobalMeasurements[2][producedCell.getThirdClusterIndex()]; + const std::array measurements{ + oracleGlobalInner, oracleGlobalMiddle, oracleGlobalOuter}; + TripletFitFactor oracleFactor{}; + BOOST_REQUIRE(makeTripletFitFactor(measurements, oracleFactor)); + checkTripletFitFactorEqual(producedCell.tripletFactor(), oracleFactor); + checkTrackSeedMaterialization(rig.traits, view, cellIndex, producedCell, + SurfaceKind::Cylinder); +} + +BOOST_AUTO_TEST_CASE(CylinderCellCombinationUsesTrackletMinPtScattering) +{ + auto acceptedCells = [](float trackletMinPt) { + Rig rig{o2::detectors::DetID::ITS, SurfaceKind::Cylinder}; + rig.params[0].TrackletMinPt = trackletMinPt; + rig.params[0].MaxChi2ClusterAttachment = 1.e6f; + rig.params[0].LayerxX0[0] = 0.005f; + rig.params[0].LayerxX0[1] = 0.01f; + rig.params[0].LayerxX0[2] = 0.f; + rig.establishLayout(); + + const std::array clusters{ + makeGlobalCluster(3.f, 0.100f, 0.9f), + makeGlobalCluster(4.f, 0.150f, 1.05f), + makeGlobalCluster(5.f, 0.201f, 1.22f)}; + loadCandidateClusters(rig, clusters, + {makeBarrelHit(3.f, 0.f, 0.100f, 0.9f, 1.e-6f, 1.e-6f), + makeBarrelHit(4.f, 0.f, 0.150f, 1.05f, 1.e-6f, 1.e-6f), + makeBarrelHit(5.f, 0.f, 0.201f, 1.22f, 1.e-6f, 1.e-6f)}); + + auto view = prepare(rig); + const auto topology = topologyView(rig); + const int cellIndex = findCellIndex(topology, 0, 1, 2); + BOOST_REQUIRE_GE(cellIndex, 0); + injectCandidateTracklets(rig, cellIndex, clusters); + TrackerTestAccess::computeCells(rig.traits, view); + return rig.tf->getCells()[cellIndex].size(); + }; + + BOOST_CHECK_EQUAL(acceptedCells(0.3f), 1u); + BOOST_CHECK_EQUAL(acceptedCells(1.f), 0u); +} + +BOOST_AUTO_TEST_CASE(ForwardCellProjectsScatteringIntoAzimuth) +{ + Rig rig{o2::detectors::DetID::MFT, SurfaceKind::Disk}; + rig.params[0].TrackletMinPt = 0.3f; + rig.params[0].MaxChi2ClusterAttachment = 1.e6f; + rig.params[0].LayerxX0[0] = 0.015f; + rig.params[0].LayerxX0[1] = 0.017f; + rig.params[0].LayerxX0[2] = 0.02f; + rig.establishLayout(); + + const std::array clusters{ + makeGlobalCluster(1.f, 0.f, -0.4f), + makeGlobalCluster(1.01f, 0.f, -0.6f), + makeGlobalCluster(1.01995f, 0.000998f, -0.9f)}; + loadCandidateClusters(rig, clusters, + {makeDiskHit(-0.4f, 1.f, 0.f), + makeDiskHit(-0.6f, 1.01f, 0.f), + makeDiskHit(-0.9f, 1.01995f, 0.000998f)}); + + auto view = prepare(rig); + const auto topology = topologyView(rig); + const int cellIndex = findCellIndex(topology, 0, 1, 2); + BOOST_REQUIRE_GE(cellIndex, 0); + injectCandidateTracklets(rig, cellIndex, clusters); + TrackerTestAccess::computeCells(rig.traits, view); + + BOOST_CHECK_EQUAL(rig.tf->getCells()[cellIndex].size(), 1u); +} + +// --- Disk: real orchestration matches the generic cell-seed oracle ------- + +BOOST_AUTO_TEST_CASE(DiskComputeLayerCellsMatchesBuildCellSeedOracle) +{ + Rig rig{o2::detectors::DetID::MFT, SurfaceKind::Disk}; + rig.params[0].MaxChi2ClusterAttachment = 1.e6f; + rig.params[0].TrackletMinPt = 0.3f; + rig.params[0].LayerxX0[0] = 0.015f; // inner + rig.params[0].LayerxX0[1] = 0.017f; // middle + rig.params[0].LayerxX0[2] = 0.02f; // outer + rig.establishLayout(); + + const std::array clusters{makeGlobalCluster(1.0f, 0.5f, -0.4f, 0), + makeGlobalCluster(1.3f, 0.62f, -0.6f, 0), + makeGlobalCluster(1.7f, 0.78f, -0.9f, 0)}; + loadCandidateClusters(rig, clusters, + {makeDiskHit(-0.4f, 1.0f, 0.5f), + makeDiskHit(-0.6f, 1.3f, 0.62f), + makeDiskHit(-0.9f, 1.7f, 0.78f)}); + + auto view = prepare(rig); + + const auto topology = topologyView(rig); + const int cellIndex = findCellIndex(topology, 0, 1, 2); + BOOST_REQUIRE_GE(cellIndex, 0); + + injectCandidateTracklets(rig, cellIndex, clusters); + + TrackerTestAccess::computeCells(rig.traits, view); + + BOOST_REQUIRE_EQUAL(rig.tf->getCells()[cellIndex].size(), 1u); + const auto& producedCell = rig.tf->getCells()[cellIndex][0]; + BOOST_CHECK(producedCell.tripletFactor().isValid()); + for (int slot = 0; slot < 3; ++slot) { + const auto reference = producedCell.getClusterReference(slot); + BOOST_CHECK_EQUAL(reference.surfacePosition, slot); + BOOST_CHECK_EQUAL(reference.clusterIndex, producedCell.getClusters()[slot]); + } + + const auto layerGlobalMeasurements = gsl::span>{view.layerGlobalMeasurements}; + const auto& oracleGlobalInner = layerGlobalMeasurements[0][producedCell.getFirstClusterIndex()]; + const auto& oracleGlobalMiddle = layerGlobalMeasurements[1][producedCell.getSecondClusterIndex()]; + const auto& oracleGlobalOuter = layerGlobalMeasurements[2][producedCell.getThirdClusterIndex()]; + const std::array measurements{ + oracleGlobalInner, oracleGlobalMiddle, oracleGlobalOuter}; + TripletFitFactor oracleFactor{}; + BOOST_REQUIRE(makeTripletFitFactor(measurements, oracleFactor)); + checkTripletFitFactorEqual(producedCell.tripletFactor(), oracleFactor); + checkTrackSeedMaterialization(rig.traits, view, cellIndex, producedCell, + SurfaceKind::Disk); +} + +// --- One-pass vs two-pass: identical result regardless of thread count ---- + +BOOST_AUTO_TEST_CASE(CylinderComputeLayerCellsOnePassAndTwoPassAgree) +{ + struct Result { + int cellIndex{-1}; + std::vector lut; + TripletFitFactor factor{}; + int cl0{-1}, cl1{-1}, cl2{-1}; + }; + + auto run = [](int nThreads) { + Rig rig{o2::detectors::DetID::ITS, SurfaceKind::Cylinder, nThreads}; + rig.params[0].MaxChi2ClusterAttachment = 1.e6f; + rig.params[0].LayerxX0[0] = 0.005f; + rig.params[0].LayerxX0[1] = 0.005f; + rig.establishLayout(); + + const std::array clusters{makeGlobalCluster(3.0f, 0.100f, 0.9f, 0), + makeGlobalCluster(4.0f, 0.150f, 1.05f, 0), + makeGlobalCluster(5.0f, 0.201f, 1.20f, 0)}; + loadCandidateClusters(rig, clusters, + {makeBarrelHit(3.f, 0.f, 0.100f, 0.9f), + makeBarrelHit(4.f, 0.f, 0.150f, 1.05f), + makeBarrelHit(5.f, 0.f, 0.201f, 1.20f)}); + + auto view = prepare(rig); + + const auto topology = topologyView(rig); + const int cellIndex = findCellIndex(topology, 0, 1, 2); + BOOST_REQUIRE_GE(cellIndex, 0); + + injectCandidateTracklets(rig, cellIndex, clusters); + + TrackerTestAccess::computeCells(rig.traits, view); + + Result r; + r.cellIndex = cellIndex; + const auto& lut = rig.tf->getCellsLookupTable()[cellIndex]; + r.lut.assign(lut.begin(), lut.end()); + BOOST_REQUIRE_EQUAL(rig.tf->getCells()[cellIndex].size(), 1u); + const auto& cell = rig.tf->getCells()[cellIndex][0]; + r.factor = cell.tripletFactor(); + r.cl0 = cell.getFirstClusterIndex(); + r.cl1 = cell.getSecondClusterIndex(); + r.cl2 = cell.getThirdClusterIndex(); + return r; + }; + + const auto onePass = run(1); + const auto twoPass = run(4); + + BOOST_CHECK_EQUAL(onePass.cellIndex, twoPass.cellIndex); + BOOST_CHECK_EQUAL_COLLECTIONS(onePass.lut.begin(), onePass.lut.end(), twoPass.lut.begin(), twoPass.lut.end()); + checkTripletFitFactorEqual(onePass.factor, twoPass.factor); + BOOST_CHECK_EQUAL(onePass.cl0, twoPass.cl0); + BOOST_CHECK_EQUAL(onePass.cl1, twoPass.cl1); + BOOST_CHECK_EQUAL(onePass.cl2, twoPass.cl2); +} + +BOOST_AUTO_TEST_CASE(DiskCellRejectsKinkBeyondNominalScatteringTolerance) +{ + Rig rig{o2::detectors::DetID::MFT, SurfaceKind::Disk}; + rig.params[0].TrackletMinPt = 0.3f; + rig.establishLayout(); + + // This kinked triplet used to be the threading/repeated-call fixture. + // Its dip-angle change exceeds the tolerance with nominal MFT material. + const std::array clusters{makeGlobalCluster(1.0f, 0.5f, -0.4f, 0), + makeGlobalCluster(1.3f, 0.62f, -0.6f, 0), + makeGlobalCluster(1.7f, 0.78f, -0.9f, 0)}; + loadCandidateClusters(rig, clusters, + {makeDiskHit(-0.4f, 1.0f, 0.5f), + makeDiskHit(-0.6f, 1.3f, 0.62f), + makeDiskHit(-0.9f, 1.7f, 0.78f)}); + auto view = prepare(rig); + const auto topology = topologyView(rig); + const int cellIndex = findCellIndex(topology, 0, 1, 2); + BOOST_REQUIRE_GE(cellIndex, 0); + injectCandidateTracklets(rig, cellIndex, clusters); + + const auto& path = topology.getPath(CellPathId{static_cast(cellIndex)}); + const auto& first = rig.tf->getTracklets()[path.first.value()][0]; + const auto& second = rig.tf->getTracklets()[path.second.value()][0]; + const float deltaLambda = std::abs(std::atan(first.tanLambda) - std::atan(second.tanLambda)); + const float angularTolerance = view.configuration.kernelParameters.nSigmaCut * rig.tf->getEdgeMSAngle(path.second.value()); + BOOST_REQUIRE_GT(deltaLambda, angularTolerance); + // Exclude a failed triplet fit as the reason for rejecting this candidate. + const std::array measurements{view.layerGlobalMeasurements[0][0], + view.layerGlobalMeasurements[1][0], + view.layerGlobalMeasurements[2][0]}; + TripletFitFactor factor{}; + BOOST_REQUIRE(makeTripletFitFactor(measurements, factor)); + BOOST_REQUIRE(factor.isValid()); + + TrackerTestAccess::computeCells(rig.traits, view); + BOOST_CHECK(rig.tf->getCells()[cellIndex].empty()); +} + +BOOST_AUTO_TEST_CASE(DiskComputeLayerCellsOnePassAndTwoPassAgree) +{ + struct Result { + int cellIndex{-1}; + std::vector lut; + TripletFitFactor factor{}; + int cl0{-1}, cl1{-1}, cl2{-1}; + }; + + auto run = [](int nThreads) { + Rig rig{o2::detectors::DetID::MFT, SurfaceKind::Disk, nThreads}; + rig.params[0].MaxChi2ClusterAttachment = 1.e6f; + rig.params[0].TrackletMinPt = 0.3f; + rig.establishLayout(); + + // Straight triplet on the synthetic disk planes, comfortably inside the + // nominal-material angular cut: this test checks threading, not rejection. + const std::array clusters{makeGlobalCluster(1.0f, 0.5f, -0.4f, 0), + makeGlobalCluster(1.3f, 0.62f, -0.6f, 0), + makeGlobalCluster(1.6f, 0.74f, -0.8f, 0)}; + loadCandidateClusters(rig, clusters, + {makeDiskHit(-0.4f, 1.0f, 0.5f), + makeDiskHit(-0.6f, 1.3f, 0.62f), + makeDiskHit(-0.8f, 1.6f, 0.74f)}); + + auto view = prepare(rig); + + const auto topology = topologyView(rig); + const int cellIndex = findCellIndex(topology, 0, 1, 2); + BOOST_REQUIRE_GE(cellIndex, 0); + + injectCandidateTracklets(rig, cellIndex, clusters); + + TrackerTestAccess::computeCells(rig.traits, view); + + Result r; + r.cellIndex = cellIndex; + const auto& lut = rig.tf->getCellsLookupTable()[cellIndex]; + r.lut.assign(lut.begin(), lut.end()); + BOOST_REQUIRE_EQUAL(rig.tf->getCells()[cellIndex].size(), 1u); + const auto& cell = rig.tf->getCells()[cellIndex][0]; + r.factor = cell.tripletFactor(); + r.cl0 = cell.getFirstClusterIndex(); + r.cl1 = cell.getSecondClusterIndex(); + r.cl2 = cell.getThirdClusterIndex(); + return r; + }; + + const auto onePass = run(1); + const auto twoPass = run(4); + + BOOST_CHECK_EQUAL(onePass.cellIndex, twoPass.cellIndex); + BOOST_CHECK_EQUAL_COLLECTIONS(onePass.lut.begin(), onePass.lut.end(), twoPass.lut.begin(), twoPass.lut.end()); + checkTripletFitFactorEqual(onePass.factor, twoPass.factor); + BOOST_CHECK_EQUAL(onePass.cl0, twoPass.cl0); + BOOST_CHECK_EQUAL(onePass.cl1, twoPass.cl1); + BOOST_CHECK_EQUAL(onePass.cl2, twoPass.cl2); +} + +BOOST_AUTO_TEST_CASE(RepeatedComputeLayerCellsCallsDoNotRebindOrIncreaseCounts) +{ + Rig rig{o2::detectors::DetID::MFT, SurfaceKind::Disk}; + rig.params[0].MaxChi2ClusterAttachment = 1.e6f; + rig.params[0].TrackletMinPt = 0.3f; + rig.establishLayout(); + + // Use the same accepted straight triplet as the threading test above. + const std::array clusters{makeGlobalCluster(1.0f, 0.5f, -0.4f, 0), + makeGlobalCluster(1.3f, 0.62f, -0.6f, 0), + makeGlobalCluster(1.6f, 0.74f, -0.8f, 0)}; + loadCandidateClusters(rig, clusters, + {makeDiskHit(-0.4f, 1.0f, 0.5f), + makeDiskHit(-0.6f, 1.3f, 0.62f), + makeDiskHit(-0.8f, 1.6f, 0.74f)}); + + auto view = prepare(rig); + + const auto topology = topologyView(rig); + const int cellIndex = findCellIndex(topology, 0, 1, 2); + BOOST_REQUIRE_GE(cellIndex, 0); + + injectCandidateTracklets(rig, cellIndex, clusters); + + TrackerTestAccess::computeCells(rig.traits, view); + BOOST_REQUIRE_EQUAL(rig.tf->getCells()[cellIndex].size(), 1u); + const auto firstFactor = rig.tf->getCells()[cellIndex][0].tripletFactor(); + + TrackerTestAccess::computeCells(rig.traits, view); + TrackerTestAccess::computeCells(rig.traits, view); + + // Re-inject tracklets and recompute (the underlying candidate clusters/ + // measurements loaded above are untouched -- reloading them here would + // invalidate the frame-owned source measurement lookup without a fresh + // initialiseTimeFrame() call to re-resolve it, which is not what this test + // checks): a fresh call after the tracklets were consumed must still + // reproduce the identical triplet factor through the same cache. + injectCandidateTracklets(rig, cellIndex, clusters); + TrackerTestAccess::computeCells(rig.traits, view); + + BOOST_REQUIRE_EQUAL(rig.tf->getCells()[cellIndex].size(), 1u); + checkTripletFitFactorEqual(rig.tf->getCells()[cellIndex][0].tripletFactor(), firstFactor); +} + +// Material-correction preflight has its own focused test target; this file +// covers only direct cell-stage orchestration. + +BOOST_AUTO_TEST_CASE(CylinderComputeLayerCellsMultiCellChainProducesCorrectCellsAndOrder) +{ + // 5-layer chain at global X = 3..7 (small Y, alpha=0.f, matching the + // single-cell oracle tests' convention above): proves edge-level/ + // cell-level parity across three simultaneously-populated cells (0,1,2), + // (1,2,3), (2,3,4) -- each resolved through the migrated + // computeLayerCellsForKind() via a fresh mSurfaceToLegacyLayer lookup + // per derived path -- not just the single path the tests above check, + // while every non-participating cellIndex stays empty. + Rig rig{o2::detectors::DetID::ITS, SurfaceKind::Cylinder}; + rig.params[0].MaxChi2ClusterAttachment = 1.e6f; + for (int layer = 0; layer < ITSNLayers; ++layer) { + rig.params[0].LayerxX0[layer] = 0.005f; + } + rig.establishLayout(); + + // Y values lie exactly on one real circle (center (0,5000), radius 5000, + // through the origin) rather than an ad hoc linear Y(X): a single + // physically consistent curvature across all 5 points avoids the + // rotation-boundary edge cases (BarrelSurfaceStateOperations.cxx's + // csp*ca+snp*sa<0 checks) an inconsistent, near-degenerate linear Y(X) + // can trip for some sub-triples but not others. + constexpr std::array layers{0, 1, 2, 3, 4}; + constexpr std::array xs{3.f, 4.f, 5.f, 6.f, 7.f}; + constexpr std::array ys{0.0009f, 0.0016f, 0.0025f, 0.0036f, 0.0049f}; + constexpr std::array zs{0.90f, 1.05f, 1.20f, 1.35f, 1.50f}; + std::array clusters; + std::array hits; + for (size_t i = 0; i < 5; ++i) { + clusters[i] = makeGlobalCluster(xs[i], ys[i], zs[i], 0); + hits[i] = makeBarrelHit(xs[i], 0.f, ys[i], zs[i]); + } + loadCandidateClustersAtLayers(rig, layers, clusters, hits); + + auto view = prepare(rig); + + const auto topology = topologyView(rig); + injectChainCandidateTracklets(rig, layers, clusters); + + TrackerTestAccess::computeCells(rig.traits, view); + + const std::array, 3> triples{{{0, 1, 2}, {1, 2, 3}, {2, 3, 4}}}; + std::array topologyIds{}; + std::vector participating(topology.nPaths, false); + for (size_t i = 0; i < triples.size(); ++i) { + const auto& triple = triples[i]; + const int cellIndex = findCellIndex(topology, triple[0], triple[1], triple[2]); + BOOST_REQUIRE_GE(cellIndex, 0); + topologyIds[i] = cellIndex; + participating[cellIndex] = true; + + BOOST_REQUIRE_EQUAL(rig.tf->getCells()[cellIndex].size(), 1u); + const auto& producedCell = rig.tf->getCells()[cellIndex][0]; + BOOST_CHECK_EQUAL(producedCell.getFirstClusterIndex(), 0); + BOOST_CHECK_EQUAL(producedCell.getSecondClusterIndex(), 0); + BOOST_CHECK_EQUAL(producedCell.getThirdClusterIndex(), 0); + BOOST_CHECK_EQUAL(producedCell.getHitLayerMask().value(), LayerMask(triple[0], triple[1], triple[2]).value()); + + BOOST_REQUIRE_EQUAL(rig.tf->getCellsLookupTable()[cellIndex].size(), 2u); + BOOST_CHECK_EQUAL(rig.tf->getCellsLookupTable()[cellIndex][0], 0); + BOOST_CHECK_EQUAL(rig.tf->getCellsLookupTable()[cellIndex][1], 1); + } + + for (int i = 0; i < topology.nPaths; ++i) { + if (!participating[i]) { + BOOST_CHECK(rig.tf->getCells()[i].empty()); + } + } + + TrackerTestAccess::findNeighbours(rig.traits, view); + for (size_t i = 0; i < topologyIds.size(); ++i) { + BOOST_CHECK_EQUAL(rig.tf->getCells()[topologyIds[i]][0].getLevel(), static_cast(i + 1)); + if (i == 0) { + BOOST_CHECK(rig.tf->getCellsNeighbours()[topologyIds[i]].empty()); + continue; + } + BOOST_REQUIRE_EQUAL(rig.tf->getCellsNeighbours()[topologyIds[i]].size(), 1u); + BOOST_CHECK_EQUAL(rig.tf->getCellsNeighbours()[topologyIds[i]][0], 0); + BOOST_CHECK_EQUAL(rig.tf->getCellsNeighboursTopology()[topologyIds[i]][0], topologyIds[i - 1]); + } +} + +BOOST_AUTO_TEST_CASE(DiskComputeLayerCellsMultiCellChainProducesCorrectCellsAndOrder) +{ + // Same multi-cell parity property for the Disk/forward family: + // cell-seed leaves genuinely branches per family (Cylinder + // reads [1] then [0]; Disk reads [2],[1],[0] -- see the comment on + // that call in computeLayerCellsForKind()), so multi-edge + // cell-chaining for Disk is real, otherwise-unproven coverage. + Rig rig{o2::detectors::DetID::MFT, SurfaceKind::Disk}; + rig.params[0].MaxChi2ClusterAttachment = 1.e6f; + rig.params[0].TrackletMinPt = 0.3f; + rig.establishLayout(); + + constexpr std::array layers{0, 1, 2, 3, 4}; + constexpr std::array xs{1.0f, 1.3f, 1.6f, 1.9f, 2.2f}; + constexpr std::array ys{0.50f, 0.62f, 0.74f, 0.86f, 0.98f}; + constexpr std::array zs{-0.40f, -0.60f, -0.80f, -1.00f, -1.20f}; + std::array clusters; + std::array hits; + for (size_t i = 0; i < 5; ++i) { + clusters[i] = makeGlobalCluster(xs[i], ys[i], zs[i], 0); + hits[i] = makeDiskHit(zs[i], xs[i], ys[i]); + } + loadCandidateClustersAtLayers(rig, layers, clusters, hits); + + auto view = prepare(rig); + + const auto topology = topologyView(rig); + injectChainCandidateTracklets(rig, layers, clusters); + + TrackerTestAccess::computeCells(rig.traits, view); + + const std::array, 3> triples{{{0, 1, 2}, {1, 2, 3}, {2, 3, 4}}}; + std::array topologyIds{}; + std::vector participating(topology.nPaths, false); + for (size_t i = 0; i < triples.size(); ++i) { + const auto& triple = triples[i]; + const int cellIndex = findCellIndex(topology, triple[0], triple[1], triple[2]); + BOOST_REQUIRE_GE(cellIndex, 0); + topologyIds[i] = cellIndex; + participating[cellIndex] = true; + + BOOST_REQUIRE_EQUAL(rig.tf->getCells()[cellIndex].size(), 1u); + const auto& producedCell = rig.tf->getCells()[cellIndex][0]; + BOOST_CHECK_EQUAL(producedCell.getHitLayerMask().value(), LayerMask(triple[0], triple[1], triple[2]).value()); + + BOOST_REQUIRE_EQUAL(rig.tf->getCellsLookupTable()[cellIndex].size(), 2u); + BOOST_CHECK_EQUAL(rig.tf->getCellsLookupTable()[cellIndex][0], 0); + BOOST_CHECK_EQUAL(rig.tf->getCellsLookupTable()[cellIndex][1], 1); + } + + for (int i = 0; i < topology.nPaths; ++i) { + if (!participating[i]) { + BOOST_CHECK(rig.tf->getCells()[i].empty()); + } + } + + TrackerTestAccess::findNeighbours(rig.traits, view); + for (size_t i = 0; i < topologyIds.size(); ++i) { + BOOST_CHECK_EQUAL(rig.tf->getCells()[topologyIds[i]][0].getLevel(), static_cast(i + 1)); + if (i == 0) { + BOOST_CHECK(rig.tf->getCellsNeighbours()[topologyIds[i]].empty()); + continue; + } + BOOST_REQUIRE_EQUAL(rig.tf->getCellsNeighbours()[topologyIds[i]].size(), 1u); + BOOST_CHECK_EQUAL(rig.tf->getCellsNeighbours()[topologyIds[i]][0], 0); + BOOST_CHECK_EQUAL(rig.tf->getCellsNeighboursTopology()[topologyIds[i]][0], topologyIds[i - 1]); + } +} + +BOOST_AUTO_TEST_CASE(CylinderComputeLayerCellsHoleCellReconstructsCorrectLayerMask) +{ + // MaxHoles=1 with layer 1 an allowed hole introduces a (0,2)-skip-1 + // edge; combined with the adjacent (2,3) edge this forms cell + // (0,2,3) -- a direct, non-adjacent exercise of resolveCellHitLayers() + // (mSurfaceToLegacyLayer) resolving a cell's endpoints correctly, and of + // hole/skipped-surface behaviour staying identical to the pre-migration + // code (which read the same fromLayer/toLayer straight off the legacy + // view). No cluster is placed on layer 1 at all. + Rig rig{o2::detectors::DetID::ITS, SurfaceKind::Cylinder}; + rig.params[0].MaxChi2ClusterAttachment = 1.e6f; + rig.params[0].MaxHoles = 1; + rig.holeLayers = LayerMask{static_cast(1u << 1)}; + rig.establishLayout(); + + constexpr std::array layers{0, 2, 3}; + const std::array clusters{ + makeGlobalCluster(3.f, 0.10f, 0.90f, 0), + makeGlobalCluster(5.f, 0.20f, 1.20f, 0), + makeGlobalCluster(6.f, 0.25f, 1.35f, 0)}; + const std::array hits{ + makeBarrelHit(3.f, 0.f, 0.10f, 0.90f), + makeBarrelHit(5.f, 0.f, 0.20f, 1.20f), + makeBarrelHit(6.f, 0.f, 0.25f, 1.35f)}; + loadCandidateClustersAtLayers(rig, layers, clusters, hits); + + auto view = prepare(rig); + + const auto topology = topologyView(rig); + injectChainCandidateTracklets(rig, layers, clusters); + + TrackerTestAccess::computeCells(rig.traits, view); + + const int cellIndex = findCellIndex(topology, 0, 2, 3); + BOOST_REQUIRE_GE(cellIndex, 0); + BOOST_REQUIRE_EQUAL(rig.tf->getCells()[cellIndex].size(), 1u); + const auto& producedCell = rig.tf->getCells()[cellIndex][0]; + BOOST_CHECK_EQUAL(producedCell.getHitLayerMask().value(), LayerMask(0, 2, 3).value()); + + BOOST_REQUIRE_EQUAL(rig.tf->getCellsLookupTable()[cellIndex].size(), 2u); + BOOST_CHECK_EQUAL(rig.tf->getCellsLookupTable()[cellIndex][0], 0); + BOOST_CHECK_EQUAL(rig.tf->getCellsLookupTable()[cellIndex][1], 1); + + for (int i = 0; i < topology.nPaths; ++i) { + if (i != cellIndex) { + BOOST_CHECK(rig.tf->getCells()[i].empty()); + } + } +} diff --git a/Detectors/ITSMFT/common/tracking/test/testComputeLayerTrackletsOrchestration.cxx b/Detectors/ITSMFT/common/tracking/test/testComputeLayerTrackletsOrchestration.cxx new file mode 100644 index 0000000000000..702e9982a8446 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testComputeLayerTrackletsOrchestration.cxx @@ -0,0 +1,782 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE ITSMFT ComputeLayerTracklets orchestration +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "CommonDataFormat/InteractionRecord.h" +#include "DataFormatsITSMFT/CompCluster.h" +#include "DataFormatsITSMFT/ROFRecord.h" +#include "DataFormatsITSMFT/TopologyDictionary.h" +#include "DetectorsCommonDataFormats/DetID.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/detail/MFTFwdTrackHelpers.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/ClusterDecoding.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/detail/TimeFrameScratch.h" +#include "ITSMFTTracking/detail/TrackerTraversalPreparation.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/TrackerTraits.h" +#include "TraversalTestSupport.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "ITSMFTTracking/Constants.h" +#include "ITSMFTTracking/MathUtils.h" +#include "ITSMFTTracking/ROFLookupTables.h" +#include "MFTTracking/Constants.h" +#include "CommonConstants/MathConstants.h" + +#include "TrackingParameterTestSupport.h" + +using o2::itsmft::tracking::test::ReferenceTrackingParameters; +using namespace o2::itsmft; +using namespace o2::itsmft::tracking; + +namespace +{ + +constexpr float Bz = 0.5f; +constexpr std::array OnePixelPattern{1, 1, 0x80}; + +const TopologyDictionary& dict() +{ + static const TopologyDictionary d; + return d; +} + +std::vector identitySurfaces(uint16_t nLayers) +{ + std::vector mapping; + mapping.reserve(nLayers); + for (uint16_t i = 0; i < nLayers; ++i) { + mapping.push_back(LayerId{i}); + } + return mapping; +} + +std::vector makeCatalog(uint16_t nLayers, o2::detectors::DetID::ID detector, SurfaceKind kind) +{ + std::vector surfaces; + surfaces.reserve(nLayers); + for (uint16_t i = 0; i < nLayers; ++i) { + surfaces.push_back(SurfaceDescriptor{i, static_cast(detector), kind}); + surfaces.back().chartRange = kind == SurfaceKind::Disk ? SurfaceChartRange{0.1f, 20.f} : SurfaceChartRange{-20.f, 20.f}; + surfaces.back().referenceCoordinate = kind == SurfaceKind::Disk + ? o2::mft::constants::mft::LayerZCoordinate()[i % MFTNLayers] + : 3.f + static_cast(i); + // Matches o2::itsmft::resetDetectorDefaults()'s per-detector LayerxX0 + // default, so TrackerTraits::initialiseTimeFrame()'s LegacyMaterialMismatch + // compatibility check passes for these unperturbed fixtures. + const float xOverX0 = detector == o2::detectors::DetID::MFT ? kNominalMFTLayerX0[i % MFTNLayers] : kNominalITSLayerX0[i % ITSNLayers]; + surfaces.back().material.xOverX0 = xOverX0; + surfaces.back().material.arealDensityGPerCm2 = xOverX0 * o2::its::constants::Radl * o2::its::constants::Rho; + } + return surfaces; +} + +class PrescribedDecoder final : public ClusterDecoder +{ + public: + PrescribedDecoder(o2::detectors::DetID::ID detector, SurfaceKind kind, std::vector clusters) + : mDetector{detector}, mKind{kind}, mClusters{std::move(clusters)} + { + } + + o2::itsmft::tracking::ClusterDecodeResult decode( + const CompClusterExt& cluster, + BoundedPatternCursor& patterns, + const TopologyDictionary* dictionary, + uint32_t externalIndex, + bool) const final + { + const auto clusterData = o2::itsmft::ioutils::extractClusterDataBounded(cluster, patterns, dictionary); + if (!clusterData.ok()) { + o2::itsmft::tracking::ClusterDecodeResult result; + result.error = clusterData.error; + return result; + } + + o2::itsmft::tracking::ClusterDecodeResult result; + if (externalIndex >= mClusters.size()) { + return result; + } + auto decoded = mClusters[externalIndex]; + decoded.shape = clusterData.shape; + result.decoded = decoded; + return result; + } + + private: + o2::detectors::DetID::ID mDetector; + SurfaceKind mKind; + std::vector mClusters; +}; + +struct TrackletSnapshot { + int edgeId{-1}; + std::vector tracklets; + std::vector lookup; + o2::its::TimeEstBC expectedTimestamp; + bool nonparticipatingEdgesEmpty{false}; + // Gate 4 Slice 0a additions: full per-(legacy-edgeId) tracklet/LUT + // content and (fromLayer,toLayer) identity, for multi-edge + // candidate-set/order/LUT parity checks that go beyond the single + // `edgeId` above. Indices across these three vectors correspond + // 1:1, in ascending legacy edgeId order. + std::vector allEdgeFromLayer; + std::vector allEdgeToLayer; + std::vector> allTracklets; + std::vector> allLookups; +}; + +/// Independent acceptance oracle for the Gate 3 edge-preparation slice +/// (layerMultipleScatteringAngle, clampEdgeCurvature, +/// prepareEdgeScatteringAndBending, relocated into +/// TrackerTraits::initialiseTimeFrame()). Re-derives the frozen legacy +/// per-layer/per-edge formula directly -- from math_utils::MSangle +/// (barrel) or detail::mftLayerMSAngle (disk, which itself still calls the +/// legacy mftLayerZ()/LayerZCoordinate() constants internally, exactly as +/// production did before this migration) and the exact former +/// TimeFrame::initialise() edge loop -- and deliberately never calls +/// layerMultipleScatteringAngle, clampEdgeCurvature, or +/// prepareEdgeScatteringAndBending, so this is a genuine external +/// oracle for those operations rather than a caller of them. Preserves the +/// half-open [fromLayer, toLayer) MS accumulation range, threads oneOverR in +/// increasing legacy edgeId order exactly as the production loop +/// does, and uses the literal matching each family (`isDisk`selects `0.5f` +/// float for Disk vs `0.5` double-promoted for Cylinder, per the +/// integration review finding preserved -- not canonicalized -- in part 1/4 +/// of this slice). +template +void computeLegacyEdgeMSAndPhiCut(const ReferenceTrackingParameters& trkParam, float bz, bool isDisk, + const TraversalTopologyView& topology, + gsl::span positionResolution, + std::vector& msAnglesOut, std::vector& phiCutsOut) +{ + std::array msAngles{}; + for (unsigned int iLayer{0}; iLayer < NLayers; ++iLayer) { + msAngles[iLayer] = isDisk ? detail::mftLayerMSAngle(iLayer, trkParam) + : o2::its::math_utils::MSangle(0.14f, trkParam.TrackletMinPt, trkParam.LayerxX0[iLayer]); + } + + msAnglesOut.assign(topology.nEdges, 0.f); + phiCutsOut.assign(topology.nEdges, 0.f); + float oneOverR{0.001f * 0.3f * std::abs(bz) / trkParam.TrackletMinPt}; + for (int edgeId{0}; edgeId < static_cast(topology.nEdges); ++edgeId) { + const auto& edge = topology.getEdge(EdgeId{static_cast(edgeId)}); + const int from = edge.from.value(); + const int to = edge.to.value(); + float ms2 = 0.f; + for (int layer = from; layer < to; ++layer) { + ms2 += o2::its::math_utils::Sq(msAngles[layer]); + } + const float msAngle = o2::gpu::CAMath::Sqrt(ms2); + const float r1 = trkParam.LayerRadii[from]; + const float r2 = trkParam.LayerRadii[to]; + if (isDisk) { + oneOverR = (0.5f * oneOverR >= 1.f / r2) ? (2.f / r2) - o2::constants::math::Almost0 : oneOverR; + } else { + oneOverR = (0.5 * oneOverR >= 1.f / r2) ? (2.f / r2) - o2::constants::math::Almost0 : oneOverR; + } + const float res1 = o2::gpu::CAMath::Hypot(trkParam.PVres, positionResolution[from]); + const float res2 = o2::gpu::CAMath::Hypot(trkParam.PVres, positionResolution[to]); + const float cosTheta1half = o2::gpu::CAMath::Sqrt(1.f - o2::its::math_utils::Sq(0.5f * r1 * oneOverR)); + const float cosTheta2half = o2::gpu::CAMath::Sqrt(1.f - o2::its::math_utils::Sq(0.5f * r2 * oneOverR)); + const float x = (r2 * cosTheta1half) - (r1 * cosTheta2half); + const float delta = o2::gpu::CAMath::Sqrt(1.f / (1.f - 0.25f * o2::its::math_utils::Sq(x * oneOverR)) * + (o2::its::math_utils::Sq((0.25f * r1 * r2 * o2::its::math_utils::Sq(oneOverR) / cosTheta2half) + cosTheta1half) * o2::its::math_utils::Sq(res1) + + o2::its::math_utils::Sq((0.25f * r1 * r2 * o2::its::math_utils::Sq(oneOverR) / cosTheta1half) + cosTheta2half) * o2::its::math_utils::Sq(res2))); + msAnglesOut[edgeId] = msAngle; + phiCutsOut[edgeId] = o2::gpu::CAMath::Min(o2::gpu::CAMath::ASin(0.5f * x * oneOverR) + 2.f * msAngle + delta, o2::constants::math::PI * 0.5f); + } +} + +template +TrackletSnapshot runFixture(o2::detectors::DetID::ID detector, + SurfaceKind kind, + SurfaceKind tag, + std::vector decoded, + int nThreads, + std::function customizeParams = {}, + LayerMask holeLayers = {}) +{ + auto pool = std::make_shared(); + TimeFrame frame; + Tracker tracker; + TrackerTraits traits; + std::shared_ptr arena; + std::vector params(1); + resetDetectorDefaults(params[0], detector); + params[0].UseDiamond = true; + params[0].CreateArtefactLabels = false; + params[0].PassFlags.reset(); + params[0].PassFlags.set(IterationStep::FirstPass, IterationStep::RebuildClusterLUT); + if (customizeParams) { + customizeParams(params[0]); + } + + traits.setNThreads(nThreads, arena); + frame.setBz(Bz); + + const auto orderedSurfaces = identitySurfaces(static_cast(NLayers)); + const auto catalog = makeCatalog(static_cast(NLayers), detector, kind); + const SurfaceCatalogView catalogView{catalog.data(), static_cast(catalog.size())}; + TrackerInitialization configuration; + configuration.catalog = catalogView; + configuration.memoryPool = pool; + configuration.layout = makeDetectorLayout(holeLayers); + configuration.plan = o2::itsmft::tracking::test::makeTrackingPlan(params[0]); + BOOST_REQUIRE(tracker.initialize(frame, configuration).ok()); + auto& tf = frame.getScratch(); + const auto& layout = frame.getLayout(); + + std::vector compactClusters; + std::vector patterns; + compactClusters.reserve(decoded.size()); + patterns.reserve(decoded.size() * OnePixelPattern.size()); + for (const auto& cluster : decoded) { + compactClusters.emplace_back(0, 0, CompCluster::InvalidPatternID, cluster.layer); + patterns.insert(patterns.end(), OnePixelPattern.begin(), OnePixelPattern.end()); + } + const std::vector rofs{ROFRecord{{100, 5}, 0, 0, static_cast(compactClusters.size())}}; + PrescribedDecoder decoder{detector, kind, std::move(decoded)}; + const auto load = loadTimeFrameSource(frame, decoder, o2::InteractionRecord{50, 5}, ROFTimingConfig{40, 0, 0, 0}, + compactClusters, patterns, rofs, &dict(), nullptr, detector, + gsl::span{orderedSurfaces}, layout.getSurfaceCatalog()); + BOOST_REQUIRE(load.ok()); + + o2::its::LayerTiming layerTiming{}; + layerTiming.mNROFsTF = 1; + layerTiming.mROFLength = 40; + o2::its::ROFOverlapTable rofTable; + for (int layer = 0; layer < NLayers; ++layer) { + rofTable.defineLayer(layer, layerTiming); + } + rofTable.init(); + // Real production workflow timing construction + // always builds and sets this alongside the ROFOverlapTable above, from + // the same per-layer LayerTiming, regardless of UseDiamond -- the diamond + // vertex derived per-ROF for tracklet finding (TrackerTraits.cxx) is + // checked through the genuine isVertexCompatible() on this table, not a + // useDiamond-skipped shortcut, so this fixture needs it populated too. + o2::its::ROFVertexLookupTable vtxTable; + for (int layer = 0; layer < NLayers; ++layer) { + vtxTable.defineLayer(layer, layerTiming); + } + vtxTable.init(); + o2::its::ROFMaskTable mask{rofTable}; + mask.resetMask(); + for (int layer = 0; layer < NLayers; ++layer) { + mask.setROFsEnabled(layer, 0, 1, 1); + } + frame.setROFViews(RuntimeROFViews{rofTable.getView(), vtxTable.getView(), mask.getView(), {}}); + + std::array, MaxLayoutSurfaces> measurementSpans; + auto view = TrackerTestAccess::prepare(tracker, frame, 0, measurementSpans); + BOOST_CHECK(view.layerGlobalMeasurements.data() == measurementSpans.data()); + const auto layoutView = view.topology; + + // Gate 3 edge-preparation slice: successful initialisation must fill + // every edge entry (relocated from TimeFrame::initialise() into + // TrackerTraits::initialiseTimeFrame(), see TrackletFinding.h). + // Exercised here for both Cylinder and Disk through the + // existing fixture rather than a separate harness. Beyond finiteness, each + // entry is checked bit-for-bit against computeLegacyEdgeMSAndPhiCut's + // independent oracle -- the only replay-grade acceptance evidence for the + // common Cylinder path, since no real-geometry common-CA ITS + // replay exists yet. + { + const auto preparedTopology = layoutView; + const auto& msAngles = tf.getEdgeMSAngles(); + const auto& phiCuts = tf.getEdgePhiCuts(); + BOOST_REQUIRE_EQUAL(msAngles.size(), static_cast(preparedTopology.nEdges)); + BOOST_REQUIRE_EQUAL(phiCuts.size(), static_cast(preparedTopology.nEdges)); + for (int id = 0; id < preparedTopology.nEdges; ++id) { + BOOST_CHECK(std::isfinite(msAngles[id])); + BOOST_CHECK(std::isfinite(phiCuts[id])); + } + + const auto& positionResolution = view.detectorConfiguration.positionResolutions; + std::vector expectedMSAngles; + std::vector expectedPhiCuts; + computeLegacyEdgeMSAndPhiCut(params[0], Bz, kind == SurfaceKind::Disk, preparedTopology, + gsl::span{positionResolution}, + expectedMSAngles, expectedPhiCuts); + BOOST_REQUIRE_EQUAL(expectedMSAngles.size(), msAngles.size()); + BOOST_REQUIRE_EQUAL(expectedPhiCuts.size(), phiCuts.size()); + for (int id = 0; id < preparedTopology.nEdges; ++id) { + BOOST_CHECK_EQUAL(msAngles[id], expectedMSAngles[id]); + BOOST_CHECK_EQUAL(phiCuts[id], expectedPhiCuts[id]); + } + } + + const auto topology = layoutView; + int edgeId = -1; + for (int id = 0; id < topology.nEdges; ++id) { + const auto& edge = topology.getEdge(EdgeId{static_cast(id)}); + if (edge.from.value() == 0 && edge.to.value() == 1) { + edgeId = id; + break; + } + } + BOOST_REQUIRE_GE(edgeId, 0); + + TrackerTestAccess::computeTracklets(traits, view, 0); + + TrackletSnapshot result; + result.edgeId = edgeId; + result.expectedTimestamp = frame.getROFOverlapView().getTimeStamp(0, 0, 1, 0); + const auto& tracklets = tf.getTracklets()[edgeId]; + result.tracklets.assign(tracklets.begin(), tracklets.end()); + const auto& lookup = tf.getTrackletsLookupTable()[edgeId]; + result.lookup.assign(lookup.begin(), lookup.end()); + result.nonparticipatingEdgesEmpty = true; + for (int id = 0; id < topology.nEdges; ++id) { + if (id != edgeId && !tf.getTracklets()[id].empty()) { + result.nonparticipatingEdgesEmpty = false; + break; + } + } + + // Gate 4 Slice 0a: full per-edge snapshot, ascending legacy + // edgeId order, for multi-edge candidate-set/order/LUT parity + // checks (see e.g. ItsIdentityLayoutTrackletsSpanMultipleAdjacentEdgesInOrder). + for (int id = 0; id < topology.nEdges; ++id) { + const auto& edge = topology.getEdge(EdgeId{static_cast(id)}); + result.allEdgeFromLayer.push_back(edge.from.value()); + result.allEdgeToLayer.push_back(edge.to.value()); + const auto& idTracklets = tf.getTracklets()[id]; + result.allTracklets.emplace_back(idTracklets.begin(), idTracklets.end()); + const auto& idLookup = tf.getTrackletsLookupTable()[id]; + result.allLookups.emplace_back(idLookup.begin(), idLookup.end()); + } + return result; +} + +void checkSame(const TrackletSnapshot& serial, const TrackletSnapshot& parallel) +{ + BOOST_CHECK_EQUAL(serial.edgeId, parallel.edgeId); + BOOST_REQUIRE_EQUAL(serial.tracklets.size(), parallel.tracklets.size()); + BOOST_CHECK_EQUAL_COLLECTIONS(serial.lookup.begin(), serial.lookup.end(), parallel.lookup.begin(), parallel.lookup.end()); + for (size_t i = 0; i < serial.tracklets.size(); ++i) { + BOOST_CHECK(serial.tracklets[i] == parallel.tracklets[i]); + BOOST_CHECK_EQUAL(serial.tracklets[i].tanLambda, parallel.tracklets[i].tanLambda); + BOOST_CHECK_EQUAL(serial.tracklets[i].phi, parallel.tracklets[i].phi); + BOOST_CHECK_EQUAL(serial.tracklets[i].getTimeStamp().getTimeStamp(), parallel.tracklets[i].getTimeStamp().getTimeStamp()); + BOOST_CHECK_EQUAL(serial.tracklets[i].getTimeStamp().getTimeStampError(), parallel.tracklets[i].getTimeStamp().getTimeStampError()); + } +} + +void checkExactTracklet(const TrackletSnapshot& snapshot, float expectedTanLambda, float expectedPhi) +{ + BOOST_REQUIRE_EQUAL(snapshot.tracklets.size(), 1u); + const auto& tracklet = snapshot.tracklets.front(); + BOOST_CHECK_EQUAL(tracklet.firstClusterIndex, 0); + BOOST_CHECK_EQUAL(tracklet.secondClusterIndex, 0); + BOOST_CHECK_EQUAL(tracklet.tanLambda, expectedTanLambda); + BOOST_CHECK_EQUAL(tracklet.phi, expectedPhi); + BOOST_CHECK_EQUAL(tracklet.getTimeStamp().getTimeStamp(), snapshot.expectedTimestamp.getTimeStamp()); + BOOST_CHECK_EQUAL(tracklet.getTimeStamp().getTimeStampError(), snapshot.expectedTimestamp.getTimeStampError()); + const std::vector expectedLookup{0, 1}; + BOOST_CHECK_EQUAL_COLLECTIONS(snapshot.lookup.begin(), snapshot.lookup.end(), expectedLookup.begin(), expectedLookup.end()); + BOOST_CHECK(snapshot.nonparticipatingEdgesEmpty); +} + +DecodedCluster cylinderCluster(float radius, float z, int layer) +{ + DecodedCluster cluster{}; + cluster.global = {radius, 0.f, z}; + cluster.cylinderFrame = {radius, 0.f, z, 0.f}; + cluster.rowColumnCovariance = {1.e-4f, 0.f, 1.e-4f}; + cluster.layer = layer; + return cluster; +} + +DecodedCluster diskCluster(float x, float y, float z, int layer) +{ + DecodedCluster cluster{}; + cluster.global = {x, y, z}; + cluster.rowColumnCovariance = {1.e-2f, 0.f, 1.e-2f}; + cluster.layer = layer; + return cluster; +} + +/// A chain of `nHops + 1` disk clusters (layers 0..nHops) consistent with a +/// single forward trajectory: each hop's target position is computed by +/// projecting from the previous hop's own cluster position via +/// detail::mftTrackletProject -- the same primitive +/// projectDiskSearchWindow itself uses internally -- so every adjacent +/// pair in the chain is a genuine geometric match, not just the first one. +std::vector buildMftChainClusters(const ReferenceTrackingParameters& params, float bz, int nHops) +{ + std::vector clusters; + float x = 1.f, y = 0.5f; + float z = detail::mftLayerZ(0); + clusters.push_back(diskCluster(x, y, z, 0)); + for (int hop = 0; hop < nHops; ++hop) { + const float nextZ = detail::mftLayerZ(hop + 1); + float targetX = 0.f, targetY = 0.f; + detail::mftTrackletProject(x, y, z, params.Diamond[0], params.Diamond[1], params.Diamond[2], + hop, hop + 1, bz, params.TrackletMinPt, targetX, targetY); + clusters.push_back(diskCluster(targetX, targetY, nextZ, hop + 1)); + x = targetX; + y = targetY; + z = nextZ; + } + return clusters; +} + +/// Disconnected catalog spanning [0, nCylinders) as Cylinder/ITS surfaces and +/// [nCylinders, nCylinders + nDisks) as Disk/MFT surfaces, in one shared +/// layout-local LayerId space. +} // namespace + +BOOST_AUTO_TEST_CASE(CylinderOnePassAndTwoPassProduceIdenticalTracklets) +{ + const std::vector clusters{ + cylinderCluster(3.f, 0.3f, 0), + cylinderCluster(4.f, 0.4f, 1)}; + const auto serial = runFixture(o2::detectors::DetID::ITS, SurfaceKind::Cylinder, + SurfaceKind::Cylinder, clusters, 1); + const auto parallel = runFixture(o2::detectors::DetID::ITS, SurfaceKind::Cylinder, + SurfaceKind::Cylinder, clusters, 4); + checkExactTracklet(serial, (0.3f - 0.4f) / (3.f - 4.f), o2::gpu::CAMath::ATan2(0.f, -1.f)); + checkExactTracklet(parallel, (0.3f - 0.4f) / (3.f - 4.f), o2::gpu::CAMath::ATan2(0.f, -1.f)); + checkSame(serial, parallel); +} + +BOOST_AUTO_TEST_CASE(DiskOnePassAndTwoPassProduceIdenticalTracklets) +{ + ReferenceTrackingParameters params; + resetDetectorDefaults(params, o2::detectors::DetID::MFT); + const float fromZ = detail::mftLayerZ(0); + const float toZ = detail::mftLayerZ(1); + float targetX = 0.f; + float targetY = 0.f; + detail::mftTrackletProject(1.f, 0.5f, fromZ, + params.Diamond[0], params.Diamond[1], params.Diamond[2], + 0, 1, Bz, params.TrackletMinPt, targetX, targetY); + const std::vector clusters{ + diskCluster(1.f, 0.5f, fromZ, 0), + diskCluster(targetX, targetY, toZ, 1)}; + const auto serial = runFixture(o2::detectors::DetID::MFT, SurfaceKind::Disk, + SurfaceKind::Disk, clusters, 1); + const auto parallel = runFixture(o2::detectors::DetID::MFT, SurfaceKind::Disk, + SurfaceKind::Disk, clusters, 4); + const float sourceRadius = o2::gpu::CAMath::Hypot(1.f, 0.5f); + const float targetRadius = o2::gpu::CAMath::Hypot(targetX, targetY); + const float expectedTanLambda = (fromZ - toZ) / (sourceRadius - targetRadius); + const float expectedPhi = o2::gpu::CAMath::ATan2(0.5f - targetY, 1.f - targetX); + checkExactTracklet(serial, expectedTanLambda, expectedPhi); + checkExactTracklet(parallel, expectedTanLambda, expectedPhi); + checkSame(serial, parallel); +} + +BOOST_AUTO_TEST_CASE(DiskSameRadiusClustersProduceInfiniteSlopeTracklet) +{ + const float fromZ = detail::mftLayerZ(0); + const float toZ = detail::mftLayerZ(1); + const std::vector clusters{ + diskCluster(1.f, 0.5f, fromZ, 0), + diskCluster(1.f, 0.5f, toZ, 1)}; + const auto widenSearch = [](ReferenceTrackingParameters& params) { params.NSigmaCut = 1.e6f; }; + const auto serial = runFixture(o2::detectors::DetID::MFT, SurfaceKind::Disk, + SurfaceKind::Disk, clusters, 1, widenSearch); + const auto parallel = runFixture(o2::detectors::DetID::MFT, SurfaceKind::Disk, + SurfaceKind::Disk, clusters, 4, widenSearch); + const float expectedTanLambda = std::copysign(o2::constants::math::VeryBig, fromZ - toZ); + const float expectedPhi = o2::gpu::CAMath::ATan2(0.f, 0.f); + checkExactTracklet(serial, expectedTanLambda, expectedPhi); + checkExactTracklet(parallel, expectedTanLambda, expectedPhi); + checkSame(serial, parallel); +} + +BOOST_AUTO_TEST_CASE(PerTimeFrameValidationFailureLeavesEdgeArraysZeroFilledNotPartial) +{ + // Edge arrays are cleared before validating normalized measurements. + // Duplicate cluster IDs below must fail before any edge values are computed, + // leaving correctly sized, zero-filled arrays rather than partial results. + auto pool = std::make_shared(); + TimeFrame frame; + Tracker tracker; + TrackerTraits traits; + std::shared_ptr arena; + std::vector params(1); + resetDetectorDefaults(params[0], o2::detectors::DetID::ITS); + params[0].PassFlags.reset(); + params[0].PassFlags.set(IterationStep::FirstPass, IterationStep::RebuildClusterLUT); + + traits.setNThreads(1, arena); + frame.setBz(Bz); + + const auto orderedSurfaces = identitySurfaces(static_cast(ITSNLayers)); + const auto catalog = makeCatalog(static_cast(ITSNLayers), o2::detectors::DetID::ITS, SurfaceKind::Cylinder); + const SurfaceCatalogView catalogView{catalog.data(), static_cast(catalog.size())}; + TrackerInitialization configuration; + configuration.catalog = catalogView; + configuration.memoryPool = pool; + configuration.layout = makeDetectorLayout(); + configuration.plan = o2::itsmft::tracking::test::makeTrackingPlan(params[0]); + BOOST_REQUIRE(tracker.initialize(frame, configuration).ok()); + auto& tf = frame.getScratch(); + const auto& layout = frame.getLayout(); + const auto topologyBuild = deriveTraversalTopology(layout, params[0]); + BOOST_REQUIRE(topologyBuild.ok()); + const auto layoutView = topologyBuild.topology->getView(layout.getSurfaceCatalog()); + + // Same minimal cluster/ROF/mask setup as runFixture(): TimeFrame::initialise() + // (called unconditionally, before any of this test's induced failure) needs + // it to size mIndexTables/mClusters correctly, regardless of what this test + // is actually probing. + const std::vector decoded{cylinderCluster(3.f, 0.3f, 0), cylinderCluster(3.1f, 0.31f, 0), + cylinderCluster(4.f, 0.4f, 1)}; + std::vector compactClusters; + std::vector patterns; + compactClusters.reserve(decoded.size()); + patterns.reserve(decoded.size() * OnePixelPattern.size()); + for (const auto& cluster : decoded) { + compactClusters.emplace_back(0, 0, CompCluster::InvalidPatternID, cluster.layer); + patterns.insert(patterns.end(), OnePixelPattern.begin(), OnePixelPattern.end()); + } + const std::vector rofs{ROFRecord{{100, 5}, 0, 0, static_cast(compactClusters.size())}}; + PrescribedDecoder decoder{o2::detectors::DetID::ITS, SurfaceKind::Cylinder, decoded}; + const auto load = loadTimeFrameSource(frame, decoder, o2::InteractionRecord{50, 5}, ROFTimingConfig{40, 0, 0, 0}, + compactClusters, patterns, rofs, &dict(), nullptr, o2::detectors::DetID::ITS, + gsl::span{orderedSurfaces}, layout.getSurfaceCatalog()); + BOOST_REQUIRE(load.ok()); + auto layer0 = frame.getGlobalMeasurements(LayerId{0}); + BOOST_REQUIRE_EQUAL(layer0.size(), 2u); + layer0[1].clusterId = layer0[0].clusterId; + + o2::its::LayerTiming layerTiming{}; + layerTiming.mNROFsTF = 1; + layerTiming.mROFLength = 40; + o2::its::ROFOverlapTable rofTable; + for (int layer = 0; layer < ITSNLayers; ++layer) { + rofTable.defineLayer(layer, layerTiming); + } + rofTable.init(); + o2::its::ROFVertexLookupTable vtxTable; + for (int layer = 0; layer < ITSNLayers; ++layer) { + vtxTable.defineLayer(layer, layerTiming); + } + vtxTable.init(); + o2::its::ROFMaskTable mask{rofTable}; + mask.resetMask(); + for (int layer = 0; layer < ITSNLayers; ++layer) { + mask.setROFsEnabled(layer, 0, 1, 1); + } + frame.setROFViews(RuntimeROFViews{rofTable.getView(), vtxTable.getView(), mask.getView(), {}}); + + std::array, MaxLayoutSurfaces> measurementSpans; + BOOST_CHECK_EXCEPTION(TrackerTestAccess::prepare(tracker, frame, 0, measurementSpans), TraversalException, [](const TraversalException& error) { + return error.getReason() == TraversalFailureReason::NormalizedMeasurementMismatch; + }); + + const auto topology = layoutView; + const auto& msAngles = tf.getEdgeMSAngles(); + const auto& phiCuts = tf.getEdgePhiCuts(); + BOOST_REQUIRE_EQUAL(msAngles.size(), static_cast(topology.nEdges)); + BOOST_REQUIRE_EQUAL(phiCuts.size(), static_cast(topology.nEdges)); + for (int id = 0; id < topology.nEdges; ++id) { + BOOST_CHECK_EQUAL(msAngles[id], 0.f); + BOOST_CHECK_EQUAL(phiCuts[id], 0.f); + } +} + +// --------------------------------------------------------------------------- +// Gate 4 Slice 0a (sparse-topology tracklet migration) additions below. +// --------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(ItsIdentityLayoutTrackletsSpanMultipleAdjacentEdgesInOrder) +{ + // Collinear track across 4 barrel layers (z = 0.1 * r for every cluster). + // Under ITS's default MaxHoles=0 only strictly-adjacent edges exist + // at all, so this directly proves edge-level tracklet/LUT/order + // parity across three distinct edges simultaneously -- each + // resolved through the migrated computeLayerTrackletsForKind() via a + // fresh mSurfaceToLegacyLayer lookup -- not just the single edge the + // tests above check, while every non-participating edge (touching + // layers 4/5/6) stays empty. + const std::vector clusters{ + cylinderCluster(3.f, 0.3f, 0), + cylinderCluster(4.f, 0.4f, 1), + cylinderCluster(5.f, 0.5f, 2), + cylinderCluster(6.f, 0.6f, 3)}; + const auto snapshot = runFixture(o2::detectors::DetID::ITS, SurfaceKind::Cylinder, + SurfaceKind::Cylinder, clusters, 1); + // Each edge's expected tanLambda is computed from its own specific + // (radius, z) pair rather than one shared constant: although every pair + // shares the same nominal slope (z = 0.1 * r), float subtraction/division + // of different operand pairs does not generally round to the identical + // bit pattern even when the mathematical result is the same value. + constexpr std::array radii{3.f, 4.f, 5.f, 6.f}; + constexpr std::array zs{0.3f, 0.4f, 0.5f, 0.6f}; + const float expectedPhi = o2::gpu::CAMath::ATan2(0.f, -1.f); + const std::vector expectedLookup{0, 1}; + + BOOST_REQUIRE_EQUAL(snapshot.allEdgeFromLayer.size(), snapshot.allTracklets.size()); + BOOST_REQUIRE_EQUAL(snapshot.allEdgeFromLayer.size(), snapshot.allLookups.size()); + bool sawEdge01 = false, sawEdge12 = false, sawEdge23 = false; + for (size_t id = 0; id < snapshot.allEdgeFromLayer.size(); ++id) { + const int from = snapshot.allEdgeFromLayer[id]; + const int to = snapshot.allEdgeToLayer[id]; + const bool participates = (from == 0 && to == 1) || (from == 1 && to == 2) || (from == 2 && to == 3); + if (participates) { + BOOST_REQUIRE_EQUAL(snapshot.allTracklets[id].size(), 1u); + const auto& tracklet = snapshot.allTracklets[id].front(); + BOOST_CHECK_EQUAL(tracklet.firstClusterIndex, 0); + BOOST_CHECK_EQUAL(tracklet.secondClusterIndex, 0); + const float expectedTanLambda = (zs[from] - zs[to]) / (radii[from] - radii[to]); + BOOST_CHECK_EQUAL(tracklet.tanLambda, expectedTanLambda); + BOOST_CHECK_EQUAL(tracklet.phi, expectedPhi); + BOOST_CHECK_EQUAL_COLLECTIONS(snapshot.allLookups[id].begin(), snapshot.allLookups[id].end(), expectedLookup.begin(), expectedLookup.end()); + sawEdge01 |= (from == 0 && to == 1); + sawEdge12 |= (from == 1 && to == 2); + sawEdge23 |= (from == 2 && to == 3); + } else { + BOOST_CHECK(snapshot.allTracklets[id].empty()); + } + } + BOOST_CHECK(sawEdge01); + BOOST_CHECK(sawEdge12); + BOOST_CHECK(sawEdge23); +} + +BOOST_AUTO_TEST_CASE(MftIdentityLayoutTrackletsSpanMultipleAdjacentEdgesInOrder) +{ + // Same multi-edge parity property for the Disk/forward family: + // a 4-disk chain built hop-by-hop with detail::mftTrackletProject (the + // same primitive projectDiskSearchWindow uses internally), proving + // edges (0,1),(1,2),(2,3) each get exactly one correctly-ordered + // tracklet and every other edge stays empty. + ReferenceTrackingParameters params; + resetDetectorDefaults(params, o2::detectors::DetID::MFT); + const auto clusters = buildMftChainClusters(params, Bz, 3); + BOOST_REQUIRE_EQUAL(clusters.size(), 4u); + const auto snapshot = runFixture(o2::detectors::DetID::MFT, SurfaceKind::Disk, + SurfaceKind::Disk, clusters, 1); + const std::vector expectedLookup{0, 1}; + + BOOST_REQUIRE_EQUAL(snapshot.allEdgeFromLayer.size(), snapshot.allTracklets.size()); + BOOST_REQUIRE_EQUAL(snapshot.allEdgeFromLayer.size(), snapshot.allLookups.size()); + bool sawEdge01 = false, sawEdge12 = false, sawEdge23 = false; + for (size_t id = 0; id < snapshot.allEdgeFromLayer.size(); ++id) { + const int from = snapshot.allEdgeFromLayer[id]; + const int to = snapshot.allEdgeToLayer[id]; + const bool participates = (from == 0 && to == 1) || (from == 1 && to == 2) || (from == 2 && to == 3); + if (participates) { + BOOST_REQUIRE_EQUAL(snapshot.allTracklets[id].size(), 1u); + const auto& tracklet = snapshot.allTracklets[id].front(); + BOOST_CHECK_EQUAL(tracklet.firstClusterIndex, 0); + BOOST_CHECK_EQUAL(tracklet.secondClusterIndex, 0); + const auto& source = clusters[from].global; + const auto& target = clusters[to].global; + const float sourceRadius = o2::gpu::CAMath::Hypot(source.x, source.y); + const float targetRadius = o2::gpu::CAMath::Hypot(target.x, target.y); + const float expectedTanLambda = (source.z - target.z) / (sourceRadius - targetRadius); + BOOST_CHECK_EQUAL(tracklet.tanLambda, expectedTanLambda); + BOOST_CHECK_EQUAL_COLLECTIONS(snapshot.allLookups[id].begin(), snapshot.allLookups[id].end(), expectedLookup.begin(), expectedLookup.end()); + sawEdge01 |= (from == 0 && to == 1); + sawEdge12 |= (from == 1 && to == 2); + sawEdge23 |= (from == 2 && to == 3); + } else { + BOOST_CHECK(snapshot.allTracklets[id].empty()); + } + } + BOOST_CHECK(sawEdge01); + BOOST_CHECK(sawEdge12); + BOOST_CHECK(sawEdge23); +} + +BOOST_AUTO_TEST_CASE(ItsHoleEdgeTrackletResolvesCorrectLegacyLayerEndpoints) +{ + // MaxHoles=1 with layer 1 an allowed hole introduces a (0,2)-skip-1 + // edge whose sparse Edge endpoints are LayerId{0}/ + // LayerId{2} -- a direct, non-adjacent exercise of mSurfaceToLegacyLayer + // resolving a edge's endpoints correctly, and of hole/skipped-surface + // behaviour staying identical to the pre-migration code (which read the + // same fromLayer/toLayer straight off the legacy view). No cluster is + // placed on layer 1 at all, so only the hole edge can produce a + // tracklet. + const std::vector clusters{ + cylinderCluster(3.f, 0.3f, 0), + cylinderCluster(5.f, 0.5f, 2)}; + const auto snapshot = runFixture( + o2::detectors::DetID::ITS, SurfaceKind::Cylinder, SurfaceKind::Cylinder, clusters, 1, + [](ReferenceTrackingParameters& p) { + p.MaxHoles = 1; + }, + LayerMask{static_cast(1u << 1)}); + + const float expectedTanLambda = (0.3f - 0.5f) / (3.f - 5.f); + const float expectedPhi = o2::gpu::CAMath::ATan2(0.f, -1.f); + bool sawHoleEdge = false; + BOOST_REQUIRE_EQUAL(snapshot.allEdgeFromLayer.size(), snapshot.allTracklets.size()); + for (size_t id = 0; id < snapshot.allEdgeFromLayer.size(); ++id) { + const int from = snapshot.allEdgeFromLayer[id]; + const int to = snapshot.allEdgeToLayer[id]; + if (from == 0 && to == 2) { + sawHoleEdge = true; + BOOST_REQUIRE_EQUAL(snapshot.allTracklets[id].size(), 1u); + const auto& tracklet = snapshot.allTracklets[id].front(); + BOOST_CHECK_EQUAL(tracklet.tanLambda, expectedTanLambda); + BOOST_CHECK_EQUAL(tracklet.phi, expectedPhi); + const std::vector expectedLookup{0, 1}; + BOOST_CHECK_EQUAL_COLLECTIONS(snapshot.allLookups[id].begin(), snapshot.allLookups[id].end(), expectedLookup.begin(), expectedLookup.end()); + } else { + BOOST_CHECK(snapshot.allTracklets[id].empty()); + } + } + BOOST_CHECK(sawHoleEdge); +} + +BOOST_AUTO_TEST_CASE(DenseLayerIdentityIsDerivedFromDescriptorPosition) +{ + const auto surfaces = makeCatalog(static_cast(ITSNLayers), o2::detectors::DetID::ITS, SurfaceKind::Cylinder); + const auto layout = DetectorLayout{surfaces}; + BOOST_REQUIRE(layout.valid()); + BOOST_REQUIRE_EQUAL(layout.size(), static_cast(ITSNLayers)); + for (uint16_t position = 0; position < ITSNLayers; ++position) { + BOOST_CHECK(&layout[LayerId{position}] == &layout.getLayers()[position]); + } +} + +BOOST_AUTO_TEST_CASE(CombinedCylinderAndDiskLayoutBindsAsOneDisconnectedPlan) +{ + const auto nCylinders = static_cast(ITSNLayers); + const auto nDisks = static_cast(MFTNLayers); + auto surfaces = makeCatalog(nCylinders, o2::detectors::DetID::ITS, SurfaceKind::Cylinder); + auto disks = makeCatalog(nDisks, o2::detectors::DetID::MFT, SurfaceKind::Disk); + surfaces.insert(surfaces.end(), disks.begin(), disks.end()); + DetectorLayoutDefinition definition; + definition.componentOffsets = {0, nCylinders}; + const auto layout = DetectorLayout{surfaces, std::move(definition)}; + ReferenceTrackingParameters parameters; + parameters.NLayers = static_cast(layout.size()); + const auto result = deriveTraversalTopology(layout, parameters); + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(result.topology->edges.size(), static_cast(nCylinders + nDisks - 2)); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testCovarianceSanitization.cxx b/Detectors/ITSMFT/common/tracking/test/testCovarianceSanitization.cxx new file mode 100644 index 0000000000000..4c98a5bfa7d08 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testCovarianceSanitization.cxx @@ -0,0 +1,666 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// M5d covariance-validity correction (doc/decisions/0008-native-refit-activation.md, +// covariance-fault-localization investigation): focused, deterministic +// regression coverage for sanitizeCovariance() (SurfaceTrackState.h) and +// its eight call sites (barrel rotate/propagate x2 overloads/update, forward +// propagation x2 overloads/update). Several fixtures below reproduce a +// real captured production failure verbatim (exact state/covariance/ +// measurement values from a checksummed replay of the +// pp-20ev-run303000-seed20260716-daily20260717 fixture, candidate keys +// "13,6,6,5,4,9,5" (ITS) and "68,71,73,67,72,73,62,76,80,-1" (MFT)) rather +// than a synthetic approximation, per the covariance-fault-localization +// investigation's minimal-reproducer design. + +#define BOOST_TEST_MODULE ITSMFTCovarianceSanitization +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include +#include +#include +#include + +#include "ITSMFTTracking/Propagator.h" + +#include "ITSMFTTracking/detail/SurfaceStateOperations.h" +#include "ITSMFTTracking/SurfaceTrackState.h" +#include "ReconstructionDataFormats/PID.h" +#include "ReconstructionDataFormats/TrackParametrization.h" + +namespace +{ +using namespace o2::itsmft::tracking; + +bool allDiagonalsNonNegative(const SurfaceTrackState& state) +{ + for (uint8_t i = 0; i < 5; ++i) { + if (state.covariance[packedCovarianceIndex(i, i)] < 0.f) { + return false; + } + } + return true; +} + +// Returns the magnitude of the worst pairwise-correlation violation found +// (0 if none), i.e. max(0, |c_ij|/sqrt(c_ii*c_jj) - 1) over every off-diagonal +// pair. Callers with a non-negative diagonal already established can compare +// this against a small float tolerance. +float maxCorrelationViolation(const SurfaceTrackState& state) +{ + float worst = 0.f; + for (uint8_t i = 0; i < 5; ++i) { + for (uint8_t j = 0; j < i; ++j) { + const float dii = state.covariance[packedCovarianceIndex(i, i)]; + const float djj = state.covariance[packedCovarianceIndex(j, j)]; + if (dii <= 0.f || djj <= 0.f) { + continue; + } + const float rho = state.covariance[packedCovarianceIndex(i, j)] / std::sqrt(dii * djj); + worst = std::max(worst, std::abs(rho) - 1.f); + } + } + return worst; +} + +// The DECLARED invariant sanitizeCovariance() (SurfaceTrackState.h) +// establishes -- non-negative diagonals and no individual pairwise +// correlation exceeding unity -- and nothing more. This is deliberately NOT +// a full positive-semi-definite check (that would additionally require, +// e.g., every leading principal minor non-negative / every eigenvalue +// non-negative): the doc comment on sanitizeCovariance() proves with a real +// captured counter-example that pairwise-valid does not imply full PSD, and +// this codebase does not claim otherwise. A test asserting full PSD here +// would be testing an invariant the production code does not establish. +bool covarianceSatisfiesDeclaredInvariant(const SurfaceTrackState& state, float tolerance = 1.e-3f) +{ + return allDiagonalsNonNegative(state) && maxCorrelationViolation(state) <= tolerance; +} + +bool closeTo(float a, float b, float absTol = 5.e-4f, float relTol = 2.e-3f) +{ + const float diff = std::fabs(a - b); + return diff <= absTol || diff <= relTol * std::fabs(b); +} + +template +bool bitEqual(const T& lhs, const T& rhs) +{ + return std::memcmp(&lhs, &rhs, sizeof(T)) == 0; +} +} // namespace + +// --- 1. sanitizeCovariance() itself: the core rule, in isolation. ---------- + +BOOST_AUTO_TEST_CASE(SanitizeCovarianceAbsNegativeDiagonal) +{ + SurfaceTrackState state{}; + state.covariance[packedCovarianceIndex(0, 0)] = -0.25f; + state.covariance[packedCovarianceIndex(1, 1)] = 0.5f; + state.covariance[packedCovarianceIndex(2, 2)] = 0.5f; + state.covariance[packedCovarianceIndex(3, 3)] = 0.5f; + state.covariance[packedCovarianceIndex(4, 4)] = 0.5f; + const float maxDiagonal[5] = {1.f, 1.f, 1.f, 1.f, 1.f}; + sanitizeCovariance(state, maxDiagonal); + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(0, 0)], 0.25f, 1e-4f); + BOOST_CHECK(allDiagonalsNonNegative(state)); +} + +BOOST_AUTO_TEST_CASE(SanitizeCovarianceClampsOverRangeAndRescalesOffDiagonal) +{ + // Pass 2 (pairwise correlation clamp) runs after pass 1 and would itself + // touch an off-diagonal whose implied correlation, computed from the + // POST-pass-1 (already range-clamped) diagonals, still exceeds 1 -- so + // this fixture is deliberately chosen so pass 1's own rescale already + // brings every off-diagonal within pass 2's bound too, isolating pass 1 + // in observable behavior (SanitizeCovarianceClampsOverRangeToCauchySchwarzBound + // below exercises pass 2 specifically, including its interaction with an + // already-pass-1-clamped diagonal). + SurfaceTrackState state{}; + state.covariance[packedCovarianceIndex(0, 0)] = 4.f; // 4x the max below. + state.covariance[packedCovarianceIndex(1, 0)] = 1.f; // Shares row/column 0. + state.covariance[packedCovarianceIndex(2, 0)] = 0.4f; + state.covariance[packedCovarianceIndex(1, 1)] = 0.3f; + state.covariance[packedCovarianceIndex(2, 2)] = 0.3f; + state.covariance[packedCovarianceIndex(3, 3)] = 0.3f; + state.covariance[packedCovarianceIndex(4, 4)] = 0.3f; + const float maxDiagonal[5] = {1.f, 1.f, 1.f, 1.f, 1.f}; + sanitizeCovariance(state, maxDiagonal); + // scale = sqrt(max/old) = sqrt(1/4) = 0.5. + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(0, 0)], 1.f, 1e-4f); + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(1, 0)], 0.5f, 1e-4f); + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(2, 0)], 0.2f, 1e-4f); + // Untouched entries not sharing the clamped row/column. + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(1, 1)], 0.3f, 1e-4f); + // Confirms pass 2 really was a no-op for this fixture, not merely unlucky + // arithmetic: every pairwise correlation is within bound. + BOOST_CHECK_LE(maxCorrelationViolation(state), 1.e-4f); +} + +BOOST_AUTO_TEST_CASE(SanitizeCovarianceClampsOverRangeToCauchySchwarzBound) +{ + // Pass 2 in isolation (diagonals already within maxDiagonal, so pass 1 is + // a no-op here): an off-diagonal whose magnitude implies |correlation|>1 + // is clamped to exactly sqrt(c_ii*c_jj), sign preserved; a pair already + // within bound is untouched. + SurfaceTrackState state{}; + state.covariance[packedCovarianceIndex(0, 0)] = 4.f; + state.covariance[packedCovarianceIndex(1, 1)] = 9.f; + state.covariance[packedCovarianceIndex(1, 0)] = -100.f; // |rho| = 100/sqrt(4*9) = 16.67, deliberately over 1. + state.covariance[packedCovarianceIndex(2, 2)] = 4.f; + state.covariance[packedCovarianceIndex(2, 0)] = 3.f; // |rho| = 3/sqrt(4*4) = 0.75, already within bound. + state.covariance[packedCovarianceIndex(3, 3)] = 1.f; + state.covariance[packedCovarianceIndex(4, 4)] = 1.f; + const float maxDiagonal[5] = {1.e30f, 1.e30f, 1.e30f, 1.e30f, 1.e30f}; // Effectively unreachable: isolates pass 2. + sanitizeCovariance(state, maxDiagonal); + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(1, 0)], -6.f, 1e-4f); // -sqrt(4*9) = -6. + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(2, 0)], 3.f, 1e-4f); // Untouched: already within bound. + BOOST_CHECK_LE(maxCorrelationViolation(state), 1.e-4f); +} + +BOOST_AUTO_TEST_CASE(SanitizeCovariancePreservesSymmetryByConstruction) +{ + // Packed lower-triangular storage: only one entry exists per (row,column) + // pair, so "symmetry" is a representation invariant, not a check -- + // packedCovarianceIndex(i,j) == packedCovarianceIndex(j,i) is exercised + // directly by every read/write sanitizeCovariance performs. Diagonals are + // set generously large (relative to the off-diagonal under test) so pass + // 2's correlation clamp is a no-op here and does not confound the + // symmetry check with a legitimate clamp. + SurfaceTrackState state{}; + state.covariance[packedCovarianceIndex(1, 1)] = 100.f; + state.covariance[packedCovarianceIndex(3, 3)] = 100.f; + state.covariance[packedCovarianceIndex(3, 1)] = 5.f; + const float maxDiagonal[5] = {1.e30f, 1.e30f, 1.e30f, 1.e30f, 1.e30f}; + sanitizeCovariance(state, maxDiagonal); + BOOST_CHECK_EQUAL(packedCovarianceIndex(1, 3), packedCovarianceIndex(3, 1)); + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(1, 3)], 5.f, 1e-4f); +} + +// --- 2. ITS legB reproducer: detail::barrel::update() on the exact captured real -- +// prior state/covariance and measurement (candidate "13,6,6,5,4,9,5", hit 5) +// that produced OperationFailureReason::MaterialFailure / +// MaterialFailureReason::InvalidCovariance before this correction (posterior +// Q2Pt-Q2Pt diagonal = -0.032802999, real production value, captured +// verbatim from the checksummed 20-event replay). + +BOOST_AUTO_TEST_CASE(ITSLegBReproducerNowSanitizesToValidCovariance) +{ + SurfaceTrackState state{}; + state.kind = SurfaceKind::Cylinder; + state.referenceCoordinate = 3.76323366f; + state.alpha = -0.12901926f; + state.parameters[0] = 0.642236829f; + state.parameters[1] = -6.11814785f; + state.parameters[2] = 0.167980343f; + state.parameters[3] = -1.58871007f; + state.parameters[4] = 1.2842629f; + state.absCharge = 1; + state.pid = o2::track::PID::Pion; + const float cov[15] = { + 0.0615117364f, -0.0162716303f, 0.00781002454f, -0.00648284703f, 0.00164899346f, 0.000680086901f, + 0.000152464694f, -0.000262976653f, -1.178005e-05f, 1.45164713e-05f, + -0.22814776f, 0.0546577908f, 0.0237723477f, -0.000194984852f, 0.822642863f}; + for (int i = 0; i < 15; ++i) { + state.covariance[i] = cov[i]; + } + + SurfaceMeasurement meas{}; + meas.frame.u = 0.633100867f; + meas.frame.v = -6.10807085f; + meas.covariance.uu = 1.18710993e-07f; + meas.covariance.uv = 0.f; + meas.covariance.vv = 3.60069805e-07f; + + float chi2 = 0.f; + OperationFailureReason reason{}; + const bool ok = detail::barrel::update(state, meas, chi2, reason); + + BOOST_REQUIRE(ok); + BOOST_CHECK(allDiagonalsNonNegative(state)); + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(4, 4)], 0.0328048468f, 5.f); // sign-flipped, matches production magnitude within float tolerance. +} + +// --- 3. MFT reproducer: detail::forward::update() on the exact captured real ------ +// prior state/covariance and measurement (candidate +// "68,71,73,67,72,73,62,76,80,-1", legB, hit 3) that produced a +// Q2Pt-Q2Pt diagonal of -52.064167 (real production value) before this +// correction. + +BOOST_AUTO_TEST_CASE(MFTReproducerNowSanitizesToValidCovariance) +{ + SurfaceTrackState state{}; + state.kind = SurfaceKind::Disk; + state.referenceCoordinate = -67.6889038f; + state.alpha = 0.f; + state.parameters[0] = -3.40663648f; + state.parameters[1] = -3.04799104f; + state.parameters[2] = -2.40926218f; + state.parameters[3] = -15.2632132f; + state.parameters[4] = -0.0805783421f; + state.absCharge = 1; + state.pid = o2::track::PID::Pion; + const float cov[15] = { + 5.45968469e-05f, 7.92145147e-05f, 2.34508334e-05f, 0.000361069426f, 9.60996113e-05f, 0.00121101411f, + 0.00140110496f, 0.0018511567f, 0.00489055132f, 0.0514357649f, + 0.117691882f, 0.0217776336f, 0.452787817f, 1.21933973f, 168.588654f}; + for (int i = 0; i < 15; ++i) { + state.covariance[i] = cov[i]; + } + + SurfaceMeasurement meas{}; + meas.frame.u = -3.4059999f; + meas.frame.v = -3.04678011f; + meas.covariance.uu = 4.4239976e-05f; + meas.covariance.uv = 0.f; + meas.covariance.vv = 0.000105412393f; + + float chi2 = 0.f; + OperationFailureReason reason{}; + const bool ok = detail::forward::update(state, meas, chi2, reason); + + BOOST_REQUIRE(ok); + BOOST_CHECK(allDiagonalsNonNegative(state)); +} + +// --- 4. Large-step propagation invariant: detail::barrel::propagate(state, linRef, -- +// ...) on the exact captured real inputs that fed the ITS legB reproducer +// above (the immediately preceding hit) must itself leave the covariance +// invariant satisfied before the next update() ever runs. The raw off- +// diagonal transport for this large (~-15.5cm) step makes THREE pairwise +// correlations simultaneously exceed 1 in magnitude -- (Y,Snp), (Y,Q2Pt), +// (Snp,Q2Pt) -- confirmed against the real captured (pre-correction) +// production values: c(Y,Y)=0.0615117364, c(Y,Q2Pt)=-0.22814776, +// c(Q2Pt,Q2Pt)=0.822642863 give rho(Y,Q2Pt) = -0.22814776 / +// sqrt(0.0615117364*0.822642863) = -1.0142..., i.e. |rho|>1 while every +// diagonal individually stays positive and unremarkable -- exactly the +// precondition the covariance-fault-localization investigation traced. +// sanitizeCovariance()'s pass 2 must repair all three before this function +// returns, and the immediately following measurement update (same real +// captured measurement) must then observe the DECLARED invariant on its +// own committed output too -- not merely "not obviously wrong": pass 2 +// alone measurably shrinks (from -0.0328 to a much smaller magnitude) but +// does not eliminate the negative diagonal the update's own naive Kalman +// subtraction still produces from an otherwise-repaired input (see +// sanitizeCovariance()'s own doc comment for the full empirical accounting +// of this), so pass 1 (diagonal abs) remains load-bearing for the +// observable, committed result even with pass 2 active. +BOOST_AUTO_TEST_CASE(LargeStepPropagationRepairsCorrelationBeforeUpdate) +{ + SurfaceTrackState state{}; + state.kind = SurfaceKind::Cylinder; + state.referenceCoordinate = 19.2192478f; + state.alpha = -0.12901926f; + state.parameters[0] = 3.03678966f; + state.parameters[1] = -30.9622726f; + state.parameters[2] = 0.138186395f; + state.parameters[3] = -1.58871007f; + state.parameters[4] = 1.2842629f; + state.absCharge = 1; + state.pid = o2::track::PID::Pion; + const float cov[15] = { + 1.98605832e-07f, -7.50241043e-08f, 2.4906555e-07f, -9.13163856e-09f, -3.06560999e-08f, 1.98362322e-05f, + 3.80933152e-09f, -3.28565477e-08f, -7.25654581e-06f, 1.45164713e-05f, + -1.76052566e-07f, -8.04973183e-07f, 0.00468764221f, -0.000194984852f, 0.822642863f}; + for (int i = 0; i < 15; ++i) { + state.covariance[i] = cov[i]; + } + + SurfaceTrackParameters linRef{}; + linRef.kind = SurfaceKind::Cylinder; + linRef.referenceCoordinate = 19.2192478f; + linRef.alpha = -0.12901926f; + linRef.parameters[0] = 3.03678894f; + linRef.parameters[1] = -30.962265f; + linRef.parameters[2] = 0.137899101f; + linRef.parameters[3] = -1.58717895f; + linRef.parameters[4] = 1.21498108f; + + const float targetX = 3.76323366f; + const float bz = 5.00675011f; + OperationFailureReason reason{}; + const bool ok = detail::barrel::propagate(state, linRef, targetX, bz, reason); + + BOOST_REQUIRE(ok); + BOOST_CHECK(covarianceSatisfiesDeclaredInvariant(state)); + // Diagonals themselves are untouched by pass 2 (only off-diagonals move): + // still match the real captured production values exactly. + BOOST_CHECK(closeTo(state.covariance[packedCovarianceIndex(0, 0)], 0.0615117364f)); + BOOST_CHECK(closeTo(state.covariance[packedCovarianceIndex(4, 4)], 0.822642863f)); + // The (Y,Q2Pt) pair is now repaired to exactly touch (not exceed) the + // Cauchy-Schwarz bound, rather than the real pre-correction production + // value of -0.22814776 (|rho|=1.0142). + const float expectedC40 = -std::sqrt(state.covariance[packedCovarianceIndex(0, 0)] * state.covariance[packedCovarianceIndex(4, 4)]); + BOOST_CHECK(closeTo(state.covariance[packedCovarianceIndex(4, 0)], expectedC40)); + BOOST_CHECK_LE(maxCorrelationViolation(state), 1.e-3f); + + // The following update (same real captured measurement) must observe the + // declared invariant on its own committed output. + SurfaceMeasurement meas{}; + meas.frame.u = 0.633100867f; + meas.frame.v = -6.10807085f; + meas.covariance.uu = 1.18710993e-07f; + meas.covariance.uv = 0.f; + meas.covariance.vv = 3.60069805e-07f; + float chi2 = 0.f; + OperationFailureReason updateReason{}; + BOOST_REQUIRE(detail::barrel::update(state, meas, chi2, updateReason)); + BOOST_CHECK(covarianceSatisfiesDeclaredInvariant(state)); +} + +// --- 5. Every rotate/propagate/update independently sanitizes, both ------- +// families. Each case below uses a deliberate zero-step (rotate: delta==0; +// propagate: dx/dz==0) or an otherwise-trivial transport so the operation's +// own transform is a documented no-op/identity on the covariance, isolating +// the sanitization call itself as the only thing that can explain a clamped +// result -- rather than depending on a from-scratch derivation of each +// operation's own Jacobian to predict a non-trivial expected output. + +SurfaceTrackState makeOverRangeBarrelState() +{ + SurfaceTrackState state{}; + state.kind = SurfaceKind::Cylinder; + state.referenceCoordinate = 4.f; + state.alpha = 0.3f; + state.parameters[0] = 1.25f; + state.parameters[1] = -0.75f; + state.parameters[2] = 0.2f; + state.parameters[3] = -0.35f; + state.parameters[4] = 0.05f; // Small |Q2Pt| so Q2Pt-Q2Pt max isn't reached trivially by other tests. + state.absCharge = 1; + state.pid = o2::track::PID::Pion; + state.covariance[packedCovarianceIndex(0, 0)] = 50.f * o2::track::kCY2max; // Deliberately over range. + state.covariance[packedCovarianceIndex(1, 1)] = 0.01f; + state.covariance[packedCovarianceIndex(2, 2)] = 0.01f; + state.covariance[packedCovarianceIndex(3, 3)] = 0.01f; + state.covariance[packedCovarianceIndex(4, 4)] = 0.01f; + return state; +} + +BOOST_AUTO_TEST_CASE(BarrelRotateSanitizesOnZeroDeltaTrivialStep) +{ + SurfaceTrackState state = makeOverRangeBarrelState(); + OperationFailureReason reason{}; + const bool ok = detail::barrel::rotate(state, state.alpha, reason); // delta == 0: ratio == 1, transform is identity. + BOOST_REQUIRE(ok); + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(0, 0)], o2::track::kCY2max, 1e-3f); +} + +BOOST_AUTO_TEST_CASE(BarrelPropagateSanitizesOnZeroDxTrivialStep) +{ + SurfaceTrackState state = makeOverRangeBarrelState(); + OperationFailureReason reason{}; + const bool ok = detail::barrel::propagate(state, state.referenceCoordinate, 0.5f, reason); // dx == 0: early-return path. + BOOST_REQUIRE(ok); + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(0, 0)], o2::track::kCY2max, 1e-3f); +} + +BOOST_AUTO_TEST_CASE(BarrelUpdateSanitizesReproducer) +{ + // Same fixture and assertion as ITSLegBReproducerNowSanitizesToValidCovariance + // above; kept as a separate, minimally-named case so "update sanitizes" is + // independently visible in the test list without relying on the reproducer + // test's name to convey it. + SurfaceTrackState state{}; + state.kind = SurfaceKind::Cylinder; + state.referenceCoordinate = 3.76323366f; + state.alpha = -0.12901926f; + state.parameters[0] = 0.642236829f; + state.parameters[1] = -6.11814785f; + state.parameters[2] = 0.167980343f; + state.parameters[3] = -1.58871007f; + state.parameters[4] = 1.2842629f; + state.absCharge = 1; + state.pid = o2::track::PID::Pion; + const float cov[15] = { + 0.0615117364f, -0.0162716303f, 0.00781002454f, -0.00648284703f, 0.00164899346f, 0.000680086901f, + 0.000152464694f, -0.000262976653f, -1.178005e-05f, 1.45164713e-05f, + -0.22814776f, 0.0546577908f, 0.0237723477f, -0.000194984852f, 0.822642863f}; + for (int i = 0; i < 15; ++i) { + state.covariance[i] = cov[i]; + } + SurfaceMeasurement meas{}; + meas.frame.u = 0.633100867f; + meas.frame.v = -6.10807085f; + meas.covariance.uu = 1.18710993e-07f; + meas.covariance.vv = 3.60069805e-07f; + float chi2 = 0.f; + OperationFailureReason reason{}; + BOOST_REQUIRE(detail::barrel::update(state, meas, chi2, reason)); + BOOST_CHECK(allDiagonalsNonNegative(state)); +} + +BOOST_AUTO_TEST_CASE(BarrelLinRefRotateSanitizesOnZeroDeltaTrivialStep) +{ + SurfaceTrackState state = makeOverRangeBarrelState(); + SurfaceTrackParameters linRef{}; + linRef.kind = SurfaceKind::Cylinder; + linRef.referenceCoordinate = state.referenceCoordinate; + linRef.alpha = state.alpha; + for (int i = 0; i < 5; ++i) { + linRef.parameters[i] = state.parameters[i]; + } + OperationFailureReason reason{}; + const bool ok = detail::barrel::rotate(state, linRef, state.alpha, 0.5f, reason); + BOOST_REQUIRE(ok); + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(0, 0)], o2::track::kCY2max, 1e-3f); +} + +BOOST_AUTO_TEST_CASE(BarrelLinRefPropagateSanitizesLargeStep) +{ + // Same fixture and assertion as LargeStepPropagationPreservesInvariantBeforeUpdate + // above; kept as a separate, minimally-named case for the same reason as + // BarrelUpdateSanitizesReproducer. + SurfaceTrackState state{}; + state.kind = SurfaceKind::Cylinder; + state.referenceCoordinate = 19.2192478f; + state.alpha = -0.12901926f; + state.parameters[0] = 3.03678966f; + state.parameters[1] = -30.9622726f; + state.parameters[2] = 0.138186395f; + state.parameters[3] = -1.58871007f; + state.parameters[4] = 1.2842629f; + state.absCharge = 1; + state.pid = o2::track::PID::Pion; + const float cov[15] = { + 1.98605832e-07f, -7.50241043e-08f, 2.4906555e-07f, -9.13163856e-09f, -3.06560999e-08f, 1.98362322e-05f, + 3.80933152e-09f, -3.28565477e-08f, -7.25654581e-06f, 1.45164713e-05f, + -1.76052566e-07f, -8.04973183e-07f, 0.00468764221f, -0.000194984852f, 0.822642863f}; + for (int i = 0; i < 15; ++i) { + state.covariance[i] = cov[i]; + } + SurfaceTrackParameters linRef{}; + linRef.kind = SurfaceKind::Cylinder; + linRef.referenceCoordinate = 19.2192478f; + linRef.alpha = -0.12901926f; + linRef.parameters[0] = 3.03678894f; + linRef.parameters[1] = -30.962265f; + linRef.parameters[2] = 0.137899101f; + linRef.parameters[3] = -1.58717895f; + linRef.parameters[4] = 1.21498108f; + OperationFailureReason reason{}; + BOOST_REQUIRE(detail::barrel::propagate(state, linRef, 3.76323366f, 5.00675011f, reason)); + BOOST_CHECK(allDiagonalsNonNegative(state)); +} + +// Forward has no established diagonal-range validity bound (see +// kForwardMaxDiagonal's own doc comment, ForwardSurfaceStateOperations.cxx: +// legacy MFT's fitting engine has no covariance-sanitization mechanism at +// all, so forward's range-clamp sub-pass is deliberately disabled pending a +// separate design decision), so an over-range diagonal is no longer a valid +// forward wiring probe. A deliberately over-correlated off-diagonal pair is: +// the pairwise correlation bound is mathematically universal (Cauchy- +// Schwarz), not a detector-specific bound, and is fully active for forward. +SurfaceTrackState makeOverCorrelatedForwardState() +{ + SurfaceTrackState state{}; + state.kind = SurfaceKind::Disk; + state.referenceCoordinate = -40.f; + state.alpha = 0.f; + state.parameters[0] = 1.f; + state.parameters[1] = -1.f; + state.parameters[2] = 0.1f; + state.parameters[3] = -2.f; + state.parameters[4] = 0.05f; + state.absCharge = 1; + state.pid = o2::track::PID::Pion; + state.covariance[packedCovarianceIndex(0, 0)] = 4.f; + state.covariance[packedCovarianceIndex(1, 0)] = 100.f; // |rho(X,Y)| = 100/sqrt(4*1) = 50, deliberately over 1. + state.covariance[packedCovarianceIndex(1, 1)] = 1.f; + state.covariance[packedCovarianceIndex(2, 2)] = 0.01f; + state.covariance[packedCovarianceIndex(3, 3)] = 0.01f; + state.covariance[packedCovarianceIndex(4, 4)] = 0.01f; + return state; +} + +BOOST_AUTO_TEST_CASE(ForwardPropagateSanitizesOnZeroDzTrivialStep) +{ + SurfaceTrackState state = makeOverCorrelatedForwardState(); + OperationFailureReason reason{}; + const bool ok = Propagator::propagateToReference(state, state.referenceCoordinate, 0.5f, reason); + BOOST_REQUIRE(ok); + BOOST_CHECK(covarianceSatisfiesDeclaredInvariant(state)); + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(1, 0)], 2.f, 1e-3f); // sqrt(4*1) = 2, sign-preserved. +} + +BOOST_AUTO_TEST_CASE(ForwardLinRefPropagateSanitizesOnZeroDzTrivialStep) +{ + SurfaceTrackState state = makeOverCorrelatedForwardState(); + SurfaceTrackParameters linRef{}; + linRef.kind = SurfaceKind::Disk; + linRef.referenceCoordinate = state.referenceCoordinate; + for (int i = 0; i < 5; ++i) { + linRef.parameters[i] = state.parameters[i]; + } + OperationFailureReason reason{}; + const bool ok = Propagator::propagateToReference(state, linRef, state.referenceCoordinate, 0.5f, reason); + BOOST_REQUIRE(ok); + BOOST_CHECK(covarianceSatisfiesDeclaredInvariant(state)); + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(1, 0)], 2.f, 1e-3f); // sqrt(4*1) = 2, sign-preserved. +} + +BOOST_AUTO_TEST_CASE(ForwardUpdateSanitizesReproducer) +{ + SurfaceTrackState state{}; + state.kind = SurfaceKind::Disk; + state.referenceCoordinate = -67.6889038f; + state.alpha = 0.f; + state.parameters[0] = -3.40663648f; + state.parameters[1] = -3.04799104f; + state.parameters[2] = -2.40926218f; + state.parameters[3] = -15.2632132f; + state.parameters[4] = -0.0805783421f; + state.absCharge = 1; + state.pid = o2::track::PID::Pion; + const float cov[15] = { + 5.45968469e-05f, 7.92145147e-05f, 2.34508334e-05f, 0.000361069426f, 9.60996113e-05f, 0.00121101411f, + 0.00140110496f, 0.0018511567f, 0.00489055132f, 0.0514357649f, + 0.117691882f, 0.0217776336f, 0.452787817f, 1.21933973f, 168.588654f}; + for (int i = 0; i < 15; ++i) { + state.covariance[i] = cov[i]; + } + SurfaceMeasurement meas{}; + meas.frame.u = -3.4059999f; + meas.frame.v = -3.04678011f; + meas.covariance.uu = 4.4239976e-05f; + meas.covariance.vv = 0.000105412393f; + float chi2 = 0.f; + OperationFailureReason reason{}; + BOOST_REQUIRE(detail::forward::update(state, meas, chi2, reason)); + BOOST_CHECK(allDiagonalsNonNegative(state)); +} + +// --- 6. preflightValidate remains strict: a deliberately malformed -------- +// *externally supplied* state (never touched by propagate/rotate/update) is +// still rejected by correctForMaterial's preflight, proving the fix does not +// weaken or bypass that check -- it only ensures the Propagator's own +// internal callers never hand it an invalid state in normal operation. + +BOOST_AUTO_TEST_CASE(MalformedExternalBarrelCovarianceStillRejectedByPreflight) +{ + SurfaceTrackState state{}; + state.kind = SurfaceKind::Cylinder; + state.referenceCoordinate = 4.f; + state.alpha = 0.3f; + state.parameters[0] = 1.25f; + state.parameters[1] = -0.75f; + state.parameters[2] = 0.2f; + state.parameters[3] = -0.35f; + state.parameters[4] = 0.8f; + state.absCharge = 1; + state.pid = o2::track::PID::Pion; + for (uint8_t i = 0; i < 5; ++i) { + state.covariance[packedCovarianceIndex(i, i)] = 0.01f; + } + state.covariance[packedCovarianceIndex(4, 4)] = -0.01f; // Deliberately invalid, constructed directly. + + const material::IntegratedMaterialBudget budget{0.01f, 0.05f}; + const auto result = detail::barrel::correctForMaterial(state, budget, material::MaterialTraversalDirection::AlongMomentum); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.failure == material::MaterialFailureReason::InvalidCovariance); +} + +BOOST_AUTO_TEST_CASE(MalformedExternalForwardCovarianceStillRejectedByPreflight) +{ + SurfaceTrackState state{}; + state.kind = SurfaceKind::Disk; + state.referenceCoordinate = -40.f; + state.alpha = 0.f; + state.parameters[0] = 1.f; + state.parameters[1] = -1.f; + state.parameters[2] = 0.1f; + state.parameters[3] = -2.f; + state.parameters[4] = 0.05f; + state.absCharge = 1; + state.pid = o2::track::PID::Pion; + for (uint8_t i = 0; i < 5; ++i) { + state.covariance[packedCovarianceIndex(i, i)] = 0.01f; + } + state.covariance[packedCovarianceIndex(2, 2)] = -0.01f; // Deliberately invalid, constructed directly. + + const material::IntegratedMaterialBudget budget{0.01f, 0.05f}; + const auto result = detail::forward::correctForMaterial(state, budget, material::MaterialTraversalDirection::AlongMomentum); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.failure == material::MaterialFailureReason::InvalidCovariance); +} + +// --- 7. Operation failure remains transactional: a failing rotate/propagate +// call must leave the input state byte-for-byte unchanged -- the +// new sanitization call must never run (and never partially mutate state) +// on a failure path. + +BOOST_AUTO_TEST_CASE(FailingBarrelRotateLeavesStateUnchanged) +{ + SurfaceTrackState state{}; + state.kind = SurfaceKind::Cylinder; + state.referenceCoordinate = 4.f; + state.alpha = 0.3f; + state.parameters[0] = 1.25f; + state.parameters[1] = -0.75f; + state.parameters[2] = 1.5f; // |Snp| >= 1: rotate must reject before touching anything. + state.parameters[3] = -0.35f; + state.parameters[4] = 0.8f; + state.absCharge = 1; + state.pid = o2::track::PID::Pion; + for (uint8_t i = 0; i < 5; ++i) { + state.covariance[packedCovarianceIndex(i, i)] = 0.01f; + } + const SurfaceTrackState original = state; + + OperationFailureReason reason{}; + const bool ok = detail::barrel::rotate(state, state.alpha + 3.0f, reason); // Large rotation: local direction inversion. + + BOOST_CHECK(!ok); + BOOST_CHECK(bitEqual(state, original)); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testDetectorLayout.cxx b/Detectors/ITSMFT/common/tracking/test/testDetectorLayout.cxx new file mode 100644 index 0000000000000..e4643cb6a0ad7 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testDetectorLayout.cxx @@ -0,0 +1,141 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE ITSMFT DetectorLayout +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include + +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/TraversalTopology.h" + +namespace +{ +using namespace o2::itsmft::tracking; +using o2::itsmft::TrackingParameters; + +std::vector catalog(uint16_t count, SurfaceKind kind = SurfaceKind::Cylinder) +{ + std::vector result; + for (uint16_t id = 0; id < count; ++id) { + result.emplace_back(id, 0, kind); + } + return result; +} + +LayerMask mask(std::initializer_list ids) +{ + LayerMask result; + for (const auto id : ids) { + result.set(id); + } + return result; +} + +TrackingParameters parametersFor(const DetectorLayout& layout) +{ + TrackingParameters parameters; + parameters.NLayers = static_cast(layout.size()); + parameters.StartLayerMask = LayerMask::span(0, parameters.NLayers - 1); + return parameters; +} +} // namespace + +BOOST_AUTO_TEST_CASE(LayerMaskCoversThirtyTwoLayoutPositions) +{ + LayerMask surfaces; + surfaces.set(0); + surfaces.set(16); + surfaces.set(31); + BOOST_CHECK(surfaces.has(0)); + BOOST_CHECK(surfaces.has(16)); + BOOST_CHECK(surfaces.has(31)); + BOOST_CHECK_EQUAL(surfaces.count(), 3); +} + +BOOST_AUTO_TEST_CASE(LayoutValidatesLimitsAndDerivesDenseIds) +{ + const auto surfaces = catalog(33); + const auto layout = DetectorLayout{surfaces, makeDetectorLayout()}; + BOOST_CHECK(layout.getError() == DetectorLayoutError::TooManySurfaces); + + const auto dense = catalog(4); + const auto valid = DetectorLayout{dense}; + BOOST_CHECK(valid.valid()); + BOOST_CHECK_EQUAL(valid.size(), 4u); + for (uint16_t position = 0; position < valid.size(); ++position) { + BOOST_CHECK(&valid[LayerId{position}] == &valid.getLayers()[position]); + } +} + +BOOST_AUTO_TEST_CASE(ComponentBoundariesAndKindIndependentCatalogs) +{ + const auto mixed = std::vector{{0, 0, SurfaceKind::Cylinder}, + {1, 0, SurfaceKind::Cylinder}, + {0, 8, SurfaceKind::Disk}, + {1, 8, SurfaceKind::Disk}}; + DetectorLayoutDefinition definition; + definition.componentOffsets = {0, 2}; + const auto layout = DetectorLayout{mixed, std::move(definition)}; + BOOST_REQUIRE(layout.valid()); + BOOST_CHECK(layout.sameComponent(0, 1)); + BOOST_CHECK(!layout.sameComponent(1, 2)); + + const auto topology = deriveTraversalTopology(layout, parametersFor(layout)); + BOOST_REQUIRE(topology.ok()); + BOOST_CHECK_EQUAL(topology.topology->edges.size(), 2u); + BOOST_CHECK(std::all_of(topology.topology->edges.begin(), topology.topology->edges.end(), [](const Edge& edge) { + return edge.from.value() / 2 == edge.to.value() / 2; + })); +} + +BOOST_AUTO_TEST_CASE(HoleAndSeedPoliciesProduceSparseTopology) +{ + DetectorLayoutDefinition definition; + definition.holeLayers = mask({1}); + const std::vector surfaces = catalog(4); + const auto layout = DetectorLayout{surfaces, std::move(definition)}; + auto parameters = parametersFor(layout); + parameters.MaxHoles = 1; + parameters.StartLayerMask = LayerMask{1u << 3}; + parameters.InactiveLayerMask = LayerMask{1u << 1}; + const auto result = deriveTraversalTopology(layout, parameters); + BOOST_REQUIRE(result.ok()); + const auto& topology = *result.topology; + BOOST_CHECK_EQUAL(topology.activeSurfaceList.size(), 3u); + BOOST_CHECK_EQUAL(topology.nLayers, 4u); + BOOST_CHECK(topology.activeSurfaceList[1] == LayerId{2}); + BOOST_CHECK_EQUAL(topology.edges.size(), 2u); + BOOST_CHECK_EQUAL(topology.paths.size(), 1u); + BOOST_CHECK(topology.edges[0].from == LayerId{0}); + BOOST_CHECK(topology.edges[0].to == LayerId{2}); + BOOST_REQUIRE_EQUAL(topology.roadStartPaths.size(), 1u); + BOOST_CHECK(topology.getView(layout.getSurfaceCatalog()).getPath(topology.roadStartPaths.front()).first == EdgeId{0}); +} + +BOOST_AUTO_TEST_CASE(InvalidLayoutAndLayerCountDerivationIsTransactional) +{ + const auto surfaces = catalog(4); + const auto layout = DetectorLayout{surfaces, makeDetectorLayout()}; + auto wrongLayerCount = parametersFor(layout); + wrongLayerCount.NLayers = 7; + const auto invalidCount = deriveTraversalTopology(layout, wrongLayerCount); + BOOST_CHECK(!invalidCount.ok()); + BOOST_CHECK(!invalidCount.topology.has_value()); + BOOST_CHECK(invalidCount.error == TraversalTopologyError::LayerCountMismatch); + + const auto invalidLayout = deriveTraversalTopology(DetectorLayout{}, TrackingParameters{}); + BOOST_CHECK(!invalidLayout.ok()); + BOOST_CHECK(!invalidLayout.topology.has_value()); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testGenericTrack.cxx b/Detectors/ITSMFT/common/tracking/test/testGenericTrack.cxx new file mode 100644 index 0000000000000..27e75a5a8a2ef --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testGenericTrack.cxx @@ -0,0 +1,1184 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// Gate 4 GenericTrack foundation. Covers: +// - GenericTrack/TrackClusterReference/GenericTrackTimestamp layout and +// device-compatibility traits; +// - isValidTrackRange()'s exact validity condition (empty/default, single-, +// multi- and hole-containing ranges, out-of-range and reversed ranges); +// - sorted global storage and source-indexed fitting-measurement lookup; +// - cross-surface and cross-source TrackClusterReference resolution; +// - that a completed track's hitLayers is the union of the LayerId of +// every measurement its range references, and that each resolved +// measurement's own surface matches the reference it was resolved from; +// - that TimeFrame loading clears GenericTrack/track-label/track-reference +// storage on both success and failure; +// - that TimeFrame::resetTimeFrame() invalidates those result sidecars +// together; +// - that GenericTrack itself has no detector/public-output dependency. +// +// This slice does not populate GenericTrack from CA seeds: every track/range +// below is constructed directly by the test. + +#define BOOST_TEST_MODULE ITSMFT GenericTrack +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "CommonDataFormat/InteractionRecord.h" +#include "DataFormatsITSMFT/CompCluster.h" +#include "DataFormatsITSMFT/ROFRecord.h" +#include "DataFormatsITSMFT/TopologyDictionary.h" +#include "DetectorsCommonDataFormats/DetID.h" +#include "ITSMFTTracking/GenericTrack.h" +#include "ITSMFTTracking/DetectorLayout.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/ClusterDecoding.h" +#include "ITSMFTTracking/detail/TimeFrameScratch.h" +#include "ITSMFTTracking/detail/ITSSharedClusterCompatibility.h" +#include "ITSMFTTracking/GenericTrackOutputAdapter.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "SimulationDataFormat/MCCompLabel.h" +#include "SimulationDataFormat/MCTruthContainer.h" + +using namespace o2::itsmft; +using namespace o2::itsmft::tracking; + +// --------------------------------------------------------------------- +// GenericTrack has no detector/public-output dependency. +// +// This is a structural claim about ITSMFTTracking/GenericTrack.h itself, not +// something a runtime assertion can observe: GenericTrack.h's own include +// list (GPUCommonDef.h, and the ITSMFTTracking/Surface{Id,KinematicState, +// Mask,Timing}.h common primitives) contains no DetID.h, +// TrackITS.h/TrackITSExt.h, typed MFT output header, GeometryTGeo.h, or workflow +// header, and GenericTrack/TrackClusterReference declare no +// DetID/NLayers/publication-type field -- every field is either a plain +// scalar, or one of the shared LayerId/SurfaceTrackState/LayerMask/ +// a dense source cluster ID, or a GenericTrackTimestamp device POD. This test case +// exercises GenericTrack using exactly that narrow surface, so that if a +// future edit to GenericTrack.h ever added such a dependency, the type +// itself (constructible, copyable, comparable-by-field here) would still +// need no wider include to keep working -- the absence is enforced by +// review of GenericTrack.h's own include list, restated here as the +// authoritative claim this test documents. +// --------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE(GenericTrackHasNoDetectorOrPublicationOutputDependency) +{ + GenericTrack track{}; + track.innerState.kind = SurfaceKind::Cylinder; + track.outerState.kind = SurfaceKind::Cylinder; + track.chi2 = 1.5f; + track.timestamp = GenericTrackTimestamp{100, 140}; + track.hitLayers.set(0); + track.firstClusterRef = 0; + track.clusterRefEnd = 1; + BOOST_CHECK(track.hitLayers.has(0)); + BOOST_CHECK_EQUAL(trackClusterRefCount(track), 1u); + + const TrackClusterReference reference{LayerId{0}, 0, 17}; + BOOST_CHECK(reference.layer == LayerId{0}); + BOOST_CHECK_EQUAL(reference.clusterId, 17u); +} + +BOOST_AUTO_TEST_CASE(GenericTrackLayoutAndDeviceCompatibilityTraits) +{ + static_assert(std::is_standard_layout_v); + static_assert(std::is_trivially_copyable_v); + static_assert(sizeof(GenericTrack) == 224); + static_assert(alignof(GenericTrack) == alignof(GenericTrackTimestamp)); + static_assert(std::is_standard_layout_v); + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_standard_layout_v); + static_assert(std::is_trivially_copyable_v); + + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + + // Default-constructed: zeroed range, empty mask, no NLayers/detector + // dependency of any kind. Not constructed as `constexpr` here: + // o2::track::PID's constructor (SurfaceTrackState::pid's default + // member initializer) is not itself constexpr, so a GenericTrack instance + // cannot be a core-constant-expression -- a property of PID, unrelated to + // GenericTrack's trivial-copyability asserted above. + const GenericTrack defaultTrack{}; + BOOST_CHECK_EQUAL(defaultTrack.firstClusterRef, 0u); + BOOST_CHECK_EQUAL(defaultTrack.clusterRefEnd, 0u); + BOOST_CHECK(defaultTrack.hitLayers.empty()); + BOOST_CHECK_EQUAL(defaultTrack.chi2, 0.f); + BOOST_CHECK(!defaultTrack.timestamp.isValid()); // default {0,0}: begin < end is false +} + +// --- isValidTrackRange() ------------------------------------------------- + +BOOST_AUTO_TEST_CASE(EmptyDefaultRangeIsValidForAnyContainerSize) +{ + const GenericTrack track{}; + BOOST_CHECK(isValidTrackRange(track, 0)); + BOOST_CHECK(isValidTrackRange(track, 5)); + BOOST_CHECK_EQUAL(trackClusterRefCount(track), 0u); +} + +BOOST_AUTO_TEST_CASE(ValidSingleMultiAndHoleContainingRanges) +{ + // Single-hit range: [0,1) into a 1-element array. + GenericTrack single{}; + single.firstClusterRef = 0; + single.clusterRefEnd = 1; + BOOST_CHECK(isValidTrackRange(single, 1)); + BOOST_CHECK_EQUAL(trackClusterRefCount(single), 1u); + + // Multi-hit range: [1,4) into a 5-element array (some entries before/after + // the range belong to other tracks sharing the same flat array). + GenericTrack multi{}; + multi.firstClusterRef = 1; + multi.clusterRefEnd = 4; + BOOST_CHECK(isValidTrackRange(multi, 5)); + BOOST_CHECK_EQUAL(trackClusterRefCount(multi), 3u); + + // Hole-containing: the range itself is a dense [first,end) span of + // *present* references (holes are never stored as sentinel entries); a + // hole instead shows up as a gap in hitLayers' LayerId numbering. A + // 2-hit track on surfaces {0,2} (skipping surface 1) is a valid, + // completed, hole-containing track: its range is still contiguous and + // valid, only its mask has a gap. + GenericTrack withHole{}; + withHole.firstClusterRef = 0; + withHole.clusterRefEnd = 2; + withHole.hitLayers.set(0); + withHole.hitLayers.set(2); + BOOST_CHECK(isValidTrackRange(withHole, 2)); + BOOST_CHECK_EQUAL(withHole.hitLayers.count(), 2); + BOOST_CHECK(!withHole.hitLayers.has(1)); // the hole +} + +BOOST_AUTO_TEST_CASE(OutOfRangeAndReversedRangesAreRejected) +{ + GenericTrack pastEnd{}; + pastEnd.firstClusterRef = 0; + pastEnd.clusterRefEnd = 6; + BOOST_CHECK(!isValidTrackRange(pastEnd, 5)); // clusterRefEnd > size + + GenericTrack exactlyAtSize{}; + exactlyAtSize.firstClusterRef = 0; + exactlyAtSize.clusterRefEnd = 5; + BOOST_CHECK(isValidTrackRange(exactlyAtSize, 5)); // clusterRefEnd == size is valid (half-open) + + GenericTrack reversed{}; + reversed.firstClusterRef = 3; + reversed.clusterRefEnd = 1; + BOOST_CHECK(!isValidTrackRange(reversed, 5)); // firstClusterRef > clusterRefEnd +} + +// --- Per-surface measurement storage / TrackClusterReference resolution -- + +namespace +{ + +// Minimal, geometry-free decoder (same construction as +// testMultiSourceLoading.cxx/testTimeFrameLifecycle.cxx): sensorID is used +// directly as the detector-local layer. +class FakeClusterDecoder final : public ClusterDecoder +{ + public: + FakeClusterDecoder(o2::detectors::DetID::ID detector, bool disk) : mDetector(detector), mDisk(disk) {} + + o2::itsmft::tracking::ClusterDecodeResult decode( + const CompClusterExt& cluster, + BoundedPatternCursor& patterns, + const TopologyDictionary* dict, + uint32_t, + bool applySysErrors) const override + { + const auto clusterData = o2::itsmft::ioutils::extractClusterDataBounded(cluster, patterns, dict); + if (!clusterData.ok()) { + o2::itsmft::tracking::ClusterDecodeResult result; + result.error = clusterData.error; + return result; + } + + o2::itsmft::tracking::ClusterDecodeResult result; + const int sensorID = cluster.getSensorID(); + auto& decoded = result.decoded; + decoded.global = {static_cast(sensorID), static_cast(cluster.getRow()), static_cast(cluster.getCol())}; + decoded.cylinderFrame = {10.f + sensorID, 1.f, 2.f, 0.1f}; + decoded.rowColumnCovariance = {clusterData.sig2Row, 0.f, clusterData.sig2Col}; + decoded.shape = clusterData.shape; + decoded.layer = sensorID; + return result; + } + + private: + o2::detectors::DetID::ID mDetector; + bool mDisk; +}; + +struct BuiltLayout { + DetectorLayout layout; + std::vector surfaces; + + SurfaceCatalogView getCatalog() const noexcept + { + return layout.getSurfaceCatalog(); + } +}; + +// 4-surface disconnected ITS(cylinder){0,1,2}+MFT(disk){3} layout, matching +// this file's fixtures below. +BuiltLayout makeCombinedLayout() +{ + std::vector surfaces; + surfaces.push_back(SurfaceDescriptor{0, static_cast(o2::detectors::DetID::ITS), SurfaceKind::Cylinder}); + surfaces.push_back(SurfaceDescriptor{1, static_cast(o2::detectors::DetID::ITS), SurfaceKind::Cylinder}); + surfaces.push_back(SurfaceDescriptor{2, static_cast(o2::detectors::DetID::ITS), SurfaceKind::Cylinder}); + surfaces.push_back(SurfaceDescriptor{0, static_cast(o2::detectors::DetID::MFT), SurfaceKind::Disk}); + DetectorLayoutDefinition definition; + definition.componentOffsets = {0, 3}; + return BuiltLayout{DetectorLayout{surfaces, std::move(definition)}, std::move(surfaces)}; +} + +constexpr std::array onePixelPattern{1, 1, 0x80}; + +std::vector makePatternBytes(size_t nClusters) +{ + std::vector bytes; + bytes.reserve(nClusters * onePixelPattern.size()); + for (size_t i = 0; i < nClusters; ++i) { + bytes.insert(bytes.end(), onePixelPattern.begin(), onePixelPattern.end()); + } + return bytes; +} + +const TopologyDictionary& dict() +{ + static const TopologyDictionary d; + return d; +} + +// Builds a combined ITS(surfaces {0,1,2}, source 0)+MFT(surface {3}, source +// 1) TimeFrame with exactly one measurement on each of surfaces +// {0,1,3} (surface 2 is left empty, a deliberate hole in the catalog's own +// numbering -- not exercised by any track in these tests, only present to +// prove per-surface storage does not require every surface to be non-empty). +void loadThreeMeasurementFrame(TimeFrame& frame, const BuiltLayout& layout, + std::vector>* externalIndicesBySurface = nullptr, + std::vector>* clusterSizesBySurface = nullptr) +{ + if (!frame.isConfigured()) { + DetectorLayoutDefinition definition; + definition.componentOffsets.assign(layout.layout.getComponentOffsets().begin(), layout.layout.getComponentOffsets().end()); + definition.holeLayers = layout.layout.getHoleLayers(); + const auto catalog = layout.getCatalog(); + BOOST_REQUIRE(frame.configure(DetectorLayout{gsl::span{catalog.surfaces, catalog.nSurfaces}, + std::move(definition)}, + 0, 0, std::make_shared())); + } + const std::vector itsClusters{ + {10, 20, CompCluster::InvalidPatternID, 0}, + {11, 21, CompCluster::InvalidPatternID, 1}, + }; + const auto itsPatterns = makePatternBytes(itsClusters.size()); + const std::vector itsRofs{ROFRecord{{0, 0}, 0, 0, 2}}; + const std::array itsLayerToSurface{LayerId{0}, LayerId{1}}; + static const FakeClusterDecoder itsDecoder{o2::detectors::DetID::ITS, false}; + + const std::vector mftClusters{{5, 6, CompCluster::InvalidPatternID, 0}}; + const auto mftPatterns = makePatternBytes(mftClusters.size()); + const std::vector mftRofs{ROFRecord{{0, 0}, 0, 0, 1}}; + const std::array mftLayerToSurface{LayerId{3}}; + static const FakeClusterDecoder mftDecoder{o2::detectors::DetID::MFT, true}; + + std::array sources{}; + sources[0].id = ClusterSourceId{0}; + sources[0].detector = o2::detectors::DetID::ITS; + sources[0].clusters = itsClusters; + sources[0].patterns = itsPatterns; + sources[0].rofs = itsRofs; + sources[0].dictionary = &dict(); + sources[0].layerToSurface = itsLayerToSurface; + sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; + sources[0].decoder = &itsDecoder; + + sources[1].id = ClusterSourceId{1}; + sources[1].detector = o2::detectors::DetID::MFT; + sources[1].clusters = mftClusters; + sources[1].patterns = mftPatterns; + sources[1].rofs = mftRofs; + sources[1].dictionary = &dict(); + sources[1].layerToSurface = mftLayerToSurface; + sources[1].timing = ROFTimingConfig{50, 0, 0, 0}; + sources[1].decoder = &mftDecoder; + + BOOST_REQUIRE(loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}, + externalIndicesBySurface, clusterSizesBySurface) + .ok()); +} + +} // namespace + +BOOST_AUTO_TEST_CASE(SurfaceMeasurementStorageUsesStablePreSortIndices) +{ + const auto layout = makeCombinedLayout(); + TimeFrame frame; + loadThreeMeasurementFrame(frame, layout); + + BOOST_REQUIRE_EQUAL(frame.getGlobalMeasurements(LayerId{0}).size(), 1u); + BOOST_REQUIRE_EQUAL(frame.getGlobalMeasurements(LayerId{1}).size(), 1u); + BOOST_REQUIRE_EQUAL(frame.getGlobalMeasurements(LayerId{2}).size(), 0u); + BOOST_REQUIRE_EQUAL(frame.getGlobalMeasurements(LayerId{3}).size(), 1u); + + // The compact global on each layer carries its stable position in that + // layer's pre-sort measurement arrays. + const auto& onZero = frame.getGlobalMeasurements(LayerId{0})[0]; + const auto& onOne = frame.getGlobalMeasurements(LayerId{1})[0]; + const auto& onThree = frame.getGlobalMeasurements(LayerId{3})[0]; + BOOST_CHECK_EQUAL(onZero.clusterId, 0u); + BOOST_CHECK_EQUAL(onOne.clusterId, 0u); + BOOST_CHECK_EQUAL(onThree.clusterId, 0u); + BOOST_CHECK(frame.getSurfaceMeasurement(LayerId{0}, onZero.clusterId) != nullptr); + BOOST_CHECK(frame.getSurfaceMeasurement(LayerId{1}, onOne.clusterId) != nullptr); + BOOST_CHECK(frame.getSurfaceMeasurement(LayerId{3}, onThree.clusterId) != nullptr); + + // An ID beyond the TimeFrame-owned surface's dense range is unresolved. + BOOST_CHECK(frame.getSurfaceMeasurement(LayerId{0}, 99) == nullptr); + // Surface 2 has zero measurements: even index 0 is out of range. + BOOST_CHECK(frame.getGlobalMeasurements(LayerId{2}).empty()); + // Invalid surface id (out of range for a 4-surface catalog). + BOOST_CHECK(frame.getSurfaceMeasurement(LayerId{4}, 0) == nullptr); +} + +BOOST_AUTO_TEST_CASE(CrossSurfaceAndCrossSourceTrackClusterReferenceResolution) +{ + const auto layout = makeCombinedLayout(); + TimeFrame frame; + loadThreeMeasurementFrame(frame, layout); + + // A single common track crossing the ITS/MFT source boundary, traversal + // order inner to outer: surface 0 (ITS, source 0), surface 1 (ITS, source + // 0), surface 3 (MFT, source 1) -- skipping surface 2 as a hole. Each + // reference pairs the surface with that surface's own (surface-local) + // measurement index, never a raw external cluster index or a global + // position. + const std::vector trackClusterIndices{ + {LayerId{0}, 0, 0}, + {LayerId{1}, 0, 0}, + {LayerId{3}, 0, 0}, + }; + + GenericTrack track{}; + track.firstClusterRef = 0; + track.clusterRefEnd = static_cast(trackClusterIndices.size()); + track.hitLayers.set(0); + track.hitLayers.set(1); + track.hitLayers.set(3); + BOOST_REQUIRE(isValidTrackRange(track, static_cast(trackClusterIndices.size()))); + + bool foundITSZero = false, foundITSOne = false, foundMFT = false; + for (uint32_t i = track.firstClusterRef; i < track.clusterRefEnd; ++i) { + const auto& reference = trackClusterIndices[i]; + const auto* measurement = frame.getSurfaceMeasurement(reference.layer, reference.clusterId); + BOOST_REQUIRE(measurement != nullptr); + if (reference.layer == LayerId{0}) { + foundITSZero = true; + } else if (reference.layer == LayerId{1}) { + foundITSOne = true; + } else if (reference.layer == LayerId{3}) { + foundMFT = true; + } + } + BOOST_CHECK(foundITSZero); + BOOST_CHECK(foundITSOne); + BOOST_CHECK(foundMFT); +} + +BOOST_AUTO_TEST_CASE(HitSurfacesEqualsUnionAndEachMeasurementSurfaceMatchesItsReference) +{ + const auto layout = makeCombinedLayout(); + TimeFrame frame; + loadThreeMeasurementFrame(frame, layout); + + const std::vector trackClusterIndices{ + {LayerId{0}, 0, 0}, + {LayerId{1}, 0, 0}, + {LayerId{3}, 0, 0}, + }; + + GenericTrack track{}; + track.firstClusterRef = 0; + track.clusterRefEnd = static_cast(trackClusterIndices.size()); + track.hitLayers.set(0); + track.hitLayers.set(1); + track.hitLayers.set(3); + + LayerMask observed{}; + BOOST_REQUIRE(isValidTrackRange(track, static_cast(trackClusterIndices.size()))); + for (uint32_t i = track.firstClusterRef; i < track.clusterRefEnd; ++i) { + const auto& reference = trackClusterIndices[i]; + const auto* measurement = frame.getSurfaceMeasurement(reference.layer, reference.clusterId); + BOOST_REQUIRE(measurement != nullptr); + observed.set(reference.layer.value()); + } + BOOST_CHECK(observed == track.hitLayers); + + // A hole-containing sub-track referencing only surfaces 0 and 3 (skipping + // 1): still a valid, completed track, mask still matches exactly the + // (smaller) referenced set. + const std::vector holeIndices{ + {LayerId{0}, 0, 0}, + {LayerId{3}, 0, 0}, + }; + GenericTrack holeTrack{}; + holeTrack.firstClusterRef = 0; + holeTrack.clusterRefEnd = 2; + holeTrack.hitLayers.set(0); + holeTrack.hitLayers.set(3); + + LayerMask observedHole{}; + BOOST_REQUIRE(isValidTrackRange(holeTrack, static_cast(holeIndices.size()))); + for (uint32_t i = holeTrack.firstClusterRef; i < holeTrack.clusterRefEnd; ++i) { + const auto& reference = holeIndices[i]; + const auto* measurement = frame.getSurfaceMeasurement(reference.layer, reference.clusterId); + BOOST_REQUIRE(measurement != nullptr); + observedHole.set(reference.layer.value()); + } + BOOST_CHECK(observedHole == holeTrack.hitLayers); + BOOST_CHECK(!observedHole.has(1)); // the hole +} + +// --- TimeFrame reload/wipe lifecycle -------------------------------------- + +namespace +{ + +// Deterministic, geometry-free stand-in for GeometryClusterDecoder +// (same construction as testTimeFrameLifecycle.cxx): sensorID is used +// directly as the detector-local layer. +class LegacyLikeDecoder final : public ClusterDecoder +{ + public: + explicit LegacyLikeDecoder(o2::detectors::DetID::ID detector) : mDetector(detector) {} + + o2::itsmft::tracking::ClusterDecodeResult decode( + const CompClusterExt& cluster, + BoundedPatternCursor& patterns, + const TopologyDictionary* dict, + uint32_t, + bool applySysErrors) const override + { + const auto clusterData = o2::itsmft::ioutils::extractClusterDataBounded(cluster, patterns, dict); + if (!clusterData.ok()) { + o2::itsmft::tracking::ClusterDecodeResult result; + result.error = clusterData.error; + return result; + } + + o2::itsmft::tracking::ClusterDecodeResult result; + const int sensorID = cluster.getSensorID(); + auto& decoded = result.decoded; + decoded.global = {static_cast(sensorID) * 10.f, static_cast(cluster.getRow()), static_cast(cluster.getCol())}; + decoded.cylinderFrame = {static_cast(sensorID) + 100.f, static_cast(cluster.getRow()) + 1.f, static_cast(cluster.getCol()) + 2.f, 0.01f * sensorID}; + decoded.rowColumnCovariance = {clusterData.sig2Row, 0.f, clusterData.sig2Col}; + decoded.shape = clusterData.shape; + decoded.layer = sensorID; + return result; + } + + private: + o2::detectors::DetID::ID mDetector; +}; + +std::vector makeITSTestCatalog() +{ + std::vector surfaces; + surfaces.reserve(ITSNLayers); + for (uint16_t i = 0; i < ITSNLayers; ++i) { + surfaces.push_back(SurfaceDescriptor{i, static_cast(o2::detectors::DetID::ITS), SurfaceKind::Cylinder}); + } + return surfaces; +} + +std::vector identitySurfaces(uint16_t nLayers) +{ + std::vector mapping; + mapping.reserve(nLayers); + for (uint16_t i = 0; i < nLayers; ++i) { + mapping.push_back(LayerId{i}); + } + return mapping; +} + +struct TimeFrameFixture { + TimeFrame tf; + std::vector> externalIndicesBySurface; + std::vector> clusterSizesBySurface; + std::vector layerMapping{identitySurfaces(ITSNLayers)}; + // Keep the catalog with the layout fixture so initialization inputs have one + // explicit owner. + std::vector catalog{makeITSTestCatalog()}; + LegacyLikeDecoder decoder{o2::detectors::DetID::ITS}; + o2::InteractionRecord origin{50, 5}; + ROFTimingConfig timing{40, 0, 0, 0}; + + TimeFrameFixture() + { + DetectorLayout layout{gsl::span{catalog}, makeDetectorLayout()}; + BOOST_REQUIRE(tf.configure(std::move(layout), 0, 0, + std::make_shared())); + } + + // One cluster on layer 0, one ROF: the minimal input that succeeds. + LoadSourcesResult load() + { + const std::vector clusters{{0, 1, CompCluster::InvalidPatternID, 0}}; + const auto patterns = makePatternBytes(clusters.size()); + const std::vector rofs{ROFRecord{{100, 5}, 0, 0, 1}}; + return loadTimeFrameSource(tf, decoder, origin, timing, clusters, patterns, rofs, &dict(), nullptr, o2::detectors::DetID::ITS, + gsl::span{layerMapping}, tf.getLayout().getSurfaceCatalog(), true, + &externalIndicesBySurface, &clusterSizesBySurface); + } +}; + +o2::its::LayerTiming makeFixtureClockTiming() +{ + // TimeFrameFixture::load() deliberately passes a temporary ROF vector to + // the frame loader. Its overlap-table view is therefore not a retained + // publication input. Build the same immutable clock timing explicitly for + // output-boundary tests rather than dereferencing that non-owning view. + o2::its::LayerTiming timing{}; + timing.mNROFsTF = 1; + timing.mROFLength = 40; + timing.mROFDelay = 100; + return timing; +} + +struct TestGenericTrack { + GenericTrack track; + std::vector references; +}; + +TestGenericTrack makeTestGenericTrack() +{ + TestGenericTrack record; + record.track.innerState.kind = SurfaceKind::Cylinder; + record.track.outerState.kind = SurfaceKind::Cylinder; + record.track.timestamp = {100, 140}; + record.track.hitLayers.set(0); + record.references.push_back({LayerId{0}, 0, 0}); + return record; +} + +uint32_t storeTestGenericTrack(TimeFrame& frame, TestGenericTrack record) +{ + const auto index = static_cast(frame.getGenericTracks().size()); + record.track.firstClusterRef = static_cast(frame.getTrackClusterIndices().size()); + frame.getTrackClusterIndices().insert(frame.getTrackClusterIndices().end(), record.references.begin(), record.references.end()); + record.track.clusterRefEnd = static_cast(frame.getTrackClusterIndices().size()); + frame.getGenericTracks().push_back(record.track); + return index; +} + +// Populates the common result sidecars with arbitrary, self-consistent +// content so a subsequent clear can be observed. +void populateCommonResults(TimeFrame& tf) +{ + tf.getTrackClusterIndices().push_back(TrackClusterReference{LayerId{0}, 0, 0}); + tf.getTrackClusterIndices().push_back(TrackClusterReference{LayerId{1}, 0, 1}); + GenericTrack track{}; + track.firstClusterRef = 0; + track.clusterRefEnd = 2; + track.hitLayers.set(0); + track.hitLayers.set(1); + tf.getGenericTracks().push_back(track); + tf.getTrackLabels().push_back(o2::MCCompLabel{1, 0, 0, false}); +} + +} // namespace + +BOOST_AUTO_TEST_CASE(SuccessfulReloadClearsCommonTrackResults) +{ + TimeFrameFixture fixture; + BOOST_REQUIRE(fixture.load().ok()); + + populateCommonResults(fixture.tf); + BOOST_REQUIRE_EQUAL(fixture.tf.getGenericTracks().size(), 1u); + BOOST_REQUIRE_EQUAL(fixture.tf.getTrackLabels().size(), 1u); + BOOST_REQUIRE_EQUAL(fixture.tf.getTrackClusterIndices().size(), 2u); + + // A second, independently successful load on the same TimeFrame: the + // normalized frame is replaced, and the common track result sidecars built + // against the previous frame must be cleared in the same successful commit. + BOOST_REQUIRE(fixture.load().ok()); + BOOST_CHECK(fixture.tf.getGenericTracks().empty()); + BOOST_CHECK(fixture.tf.getTrackLabels().empty()); + BOOST_CHECK(fixture.tf.getTrackClusterIndices().empty()); +} + +BOOST_AUTO_TEST_CASE(ITSSharedClusterCompatibilityUsesExplicitPreSortAssociations) +{ + struct MarkedTrack { + bool shared = false; + bool hasSharedClusters() const { return shared; } + }; + + TimeFrameFixture fixture; + BOOST_REQUIRE(fixture.load().ok()); + const auto record = makeTestGenericTrack(); + ITSSharedClusterCompatibility sidecar; + + // Deliberately use a non-identity conceptual fclusSort permutation. The + // status is read later from the original accepted slots, not this order. + std::array accepted{{{false}, {true}, {false}}}; + const std::array fclusSort{{2, 0, 1}}; + BOOST_CHECK_NE(fclusSort[0], 0); + for (size_t i = 0; i < accepted.size(); ++i) { + ITSSharedClusterCompatibilityTransaction tx{sidecar}; + const auto index = storeTestGenericTrack(fixture.tf, record); + BOOST_REQUIRE(tx.validate(index)); + tx.reserve(); + tx.append(index); + BOOST_CHECK_EQUAL(index, i); + } + BOOST_CHECK_EQUAL(sidecar.pendingSize(), accepted.size()); + BOOST_CHECK(sidecar.sealFromMarkedTracks(accepted)); + BOOST_CHECK(sidecar.isSealed()); + BOOST_REQUIRE_EQUAL(sidecar.entries().size(), accepted.size()); + BOOST_CHECK_EQUAL(sidecar.entries()[0].genericTrackIndex, 0u); + BOOST_CHECK(!sidecar.entries()[0].hasSharedClusters); + BOOST_CHECK_EQUAL(sidecar.entries()[1].genericTrackIndex, 1u); + BOOST_CHECK(sidecar.entries()[1].hasSharedClusters); + + // A later legacy output sort cannot change the already global-index-keyed + // sealed result. + std::reverse(accepted.begin(), accepted.end()); + BOOST_CHECK_EQUAL(sidecar.entries()[1].genericTrackIndex, 1u); + BOOST_CHECK(sidecar.entries()[1].hasSharedClusters); + + // Scratch-only reset has no authority over TimeFrame-owned GenericTracks + // or the bridge-owned compatibility result they index. + fixture.tf.getScratch().reset(); + BOOST_CHECK_EQUAL(fixture.tf.getGenericTracks().size(), 3u); + BOOST_CHECK_EQUAL(sidecar.entries().size(), 3u); + + ITSSharedClusterCompatibility malformed; + const auto malformedIndex = storeTestGenericTrack(fixture.tf, record); + ITSSharedClusterCompatibilityTransaction tx{malformed}; + BOOST_REQUIRE(tx.validate(malformedIndex)); + tx.reserve(); + tx.append(malformedIndex); + BOOST_CHECK(!malformed.sealFromMarkedTracks(accepted)); // pending/track cardinality mismatch + BOOST_CHECK(!malformed.isSealed()); + BOOST_CHECK(malformed.entries().empty()); + + TimeFrameFixture rollbackFixture; + BOOST_REQUIRE(rollbackFixture.load().ok()); + ITSSharedClusterCompatibility rollback; + const auto rollbackIndex = storeTestGenericTrack(rollbackFixture.tf, record); + ITSSharedClusterCompatibilityTransaction rollbackTx{rollback}; + BOOST_REQUIRE(rollbackTx.validate(rollbackIndex)); + rollbackTx.reserve(); + rollbackTx.append(rollbackIndex); + BOOST_CHECK_EQUAL(rollback.pendingSize(), 1u); + + ITSSharedClusterCompatibility sealingFailure; + ITSSharedClusterCompatibilityTransaction sealingTx{sealingFailure}; + const auto sealingIndex = storeTestGenericTrack(rollbackFixture.tf, record); + BOOST_REQUIRE(sealingTx.validate(sealingIndex)); + sealingTx.reserve(); + sealingTx.append(sealingIndex); + std::array oneTrack{{{true}}}; + BOOST_CHECK(sealingFailure.sealFromMarkedTracks(oneTrack)); + BOOST_CHECK(sealingFailure.isSealed()); + BOOST_REQUIRE_EQUAL(sealingFailure.entries().size(), 1u); + + sidecar.clear(); + fixture.tf.resetTimeFrame(); + BOOST_CHECK(fixture.tf.getGenericTracks().empty()); + BOOST_CHECK_EQUAL(sidecar.pendingSize(), 0u); + BOOST_CHECK(sidecar.entries().empty()); +} + +BOOST_AUTO_TEST_CASE(FailedLoadClearsCommonTrackResults) +{ + TimeFrameFixture fixture; + BOOST_REQUIRE(fixture.load().ok()); + + populateCommonResults(fixture.tf); + BOOST_REQUIRE_EQUAL(fixture.tf.getGenericTracks().size(), 1u); + BOOST_REQUIRE_EQUAL(fixture.tf.getTrackLabels().size(), 1u); + BOOST_REQUIRE_EQUAL(fixture.tf.getTrackClusterIndices().size(), 2u); + BOOST_REQUIRE(fixture.tf.getTotalMeasurements() > 0u); + + // Deliberately fail: the frame loader preflight rejects an + // unsupported detector before touching anything. + const std::vector clusters{{0, 1, CompCluster::InvalidPatternID, 0}}; + const auto patterns = makePatternBytes(clusters.size()); + const std::vector rofs{ROFRecord{{200, 5}, 0, 0, 1}}; + const auto& orderedSurfaces = fixture.layerMapping; + const auto failed = loadTimeFrameSource(fixture.tf, fixture.decoder, fixture.origin, fixture.timing, clusters, patterns, rofs, + &dict(), nullptr, o2::detectors::DetID::TPC, + gsl::span{orderedSurfaces}, fixture.tf.getLayout().getSurfaceCatalog()); + BOOST_REQUIRE(!failed.ok()); + BOOST_CHECK(failed.error == MultiSourceLoadError::UnsupportedDetector); + + BOOST_CHECK_EQUAL(fixture.tf.getTotalMeasurements(), 0u); + BOOST_CHECK(fixture.tf.getGenericTracks().empty()); + BOOST_CHECK(fixture.tf.getTrackLabels().empty()); + BOOST_CHECK(fixture.tf.getTrackClusterIndices().empty()); +} + +BOOST_AUTO_TEST_CASE(TimeFrameWipeInvalidatesCommonTrackResultsTogether) +{ + TimeFrame tf; + populateCommonResults(tf); + + BOOST_REQUIRE_EQUAL(tf.getGenericTracks().size(), 1u); + BOOST_REQUIRE_EQUAL(tf.getTrackLabels().size(), 1u); + BOOST_REQUIRE_EQUAL(tf.getTrackClusterIndices().size(), 2u); + BOOST_REQUIRE(isValidTrackRange(tf.getGenericTracks()[0], static_cast(tf.getTrackClusterIndices().size()))); + + tf.resetTimeFrame(); + + BOOST_CHECK(tf.getGenericTracks().empty()); + BOOST_CHECK(tf.getTrackLabels().empty()); + BOOST_CHECK(tf.getTrackClusterIndices().empty()); + + // Reload after wipe: both containers accept new content independently of + // whatever they held before, confirming they are ordinary per-event state + // rather than something resetTimeFrame() leaves in a half-cleared condition. + tf.getTrackClusterIndices().push_back(TrackClusterReference{LayerId{2}, 0, 0}); + GenericTrack reloaded{}; + reloaded.firstClusterRef = 0; + reloaded.clusterRefEnd = 1; + tf.getGenericTracks().push_back(reloaded); + BOOST_CHECK_EQUAL(tf.getGenericTracks().size(), 1u); + BOOST_CHECK_EQUAL(tf.getTrackClusterIndices().size(), 1u); +} + +BOOST_AUTO_TEST_CASE(GenericTrackOutputAdapterTimestampIsSymmetricAndClamped) +{ + GenericTrackOutputAdapterError error = GenericTrackOutputAdapterError::None; + o2::its::LayerTiming clock{}; + clock.mROFLength = 14; + const ClockTimingPublicationView view{clock}; + const auto timestamp = makeOutputTimestamp({100, 120}, view, error); + BOOST_REQUIRE(timestamp); + BOOST_CHECK_EQUAL(timestamp->getTimeStamp(), 110.f); + BOOST_CHECK_EQUAL(timestamp->getTimeStampError(), 7.f); + BOOST_CHECK(!makeOutputTimestamp({20, 20}, view, error)); + BOOST_CHECK(error == GenericTrackOutputAdapterError::InvalidTimestamp); +} + +BOOST_AUTO_TEST_CASE(GenericTrackOutputAdapterUsesLegacyPublicationOrder) +{ + TimeFrameFixture fixture; + BOOST_REQUIRE(fixture.load().ok()); + + auto later = makeTestGenericTrack(); + later.track.timestamp = {200, 240}; + later.track.chi2 = 1.f; + auto earlier = makeTestGenericTrack(); + earlier.track.timestamp = {100, 140}; + earlier.track.chi2 = 2.f; + BOOST_CHECK_EQUAL(storeTestGenericTrack(fixture.tf, later), 0u); + BOOST_CHECK_EQUAL(storeTestGenericTrack(fixture.tf, earlier), 1u); + + o2::its::LayerTiming clock{}; + clock.mROFLength = 40; + GenericTrackOutputAdapterError error = GenericTrackOutputAdapterError::None; + const GenericTrackOutputAdapterSelection selection{{0u, 1u}}; + const auto ordered = makeLegacyOutputOrder(fixture.tf, selection, ClockTimingPublicationView{clock}, error); + BOOST_REQUIRE(ordered); + BOOST_REQUIRE_EQUAL(ordered->size(), 2u); + BOOST_CHECK_EQUAL((*ordered)[0].globalIndex, 1u); + BOOST_CHECK_EQUAL((*ordered)[1].globalIndex, 0u); +} + +BOOST_AUTO_TEST_CASE(ClockTimingPublicationViewDelegatesLegacyClockSemantics) +{ + for (const uint32_t length : {9u, 10u}) { + o2::its::LayerTiming legacy{}; + legacy.mNROFsTF = 4; + legacy.mROFLength = length; + legacy.mROFDelay = 3; + legacy.mROFBias = 2; + const ClockTimingPublicationView view{legacy}; + const std::array timestamps{{{5, 6}, {5, 5 + length}, {5 + length, 5 + 2 * length}, {5 + 3 * length, 5 + 4 * length}}}; + for (const auto timestamp : timestamps) { + const auto asymmetric = view.makeTimeEstBC(timestamp); + BOOST_REQUIRE(asymmetric); + auto expected = asymmetric->makeSymmetrical(); + if (expected.getTimeStampError() > legacy.mROFLength * .5f) + expected.setTimeStampError(legacy.mROFLength * .5f); + const auto actual = view.makeOutputTimestamp(timestamp); + BOOST_REQUIRE(actual); + BOOST_CHECK_EQUAL(actual->getTimeStamp(), expected.getTimeStamp()); + BOOST_CHECK_EQUAL(actual->getTimeStampError(), expected.getTimeStampError()); + BOOST_CHECK_EQUAL(view.getROF(*actual), legacy.getROF(expected)); + } + } + o2::its::LayerTiming clock{}; + const ClockTimingPublicationView view{clock}; + BOOST_CHECK(!view.makeTimeEstBC({0, 0})); + BOOST_CHECK(!view.makeTimeEstBC({-1, 1})); + BOOST_CHECK(!view.makeTimeEstBC({0, static_cast(std::numeric_limits::max()) + 1})); + BOOST_CHECK(!view.makeTimeEstBC({0, static_cast(std::numeric_limits::max()) + 1})); +} + +BOOST_AUTO_TEST_CASE(ITSGenericPublicationPreservesClusterLayoutAndReordersLabelsWithoutMutatingSources) +{ + TimeFrameFixture fixture; + fixture.externalIndicesBySurface = {{500000}, {0}, {7}, {42}, {0}, {123456}, {0}}; + fixture.clusterSizesBySurface = {{3}, {0}, {11}, {9}, {0}, {15}, {0}}; + auto later = makeTestGenericTrack(); + later.track.timestamp = {120, 130}; + later.track.hitLayers.set(2); + later.track.hitLayers.set(5); + later.references = {{LayerId{0}, 0, 0}, {LayerId{2}, 0, 0}, {LayerId{5}, 0, 0}}; + auto earlier = makeTestGenericTrack(); + earlier.track.timestamp = {100, 110}; + earlier.track.hitLayers = {}; + earlier.track.hitLayers.set(3); + earlier.references = {{LayerId{3}, 0, 0}}; + ITSSharedClusterCompatibility shared; + for (auto record : {later, earlier}) { + const auto index = storeTestGenericTrack(fixture.tf, record); + ITSSharedClusterCompatibilityTransaction transaction{shared}; + BOOST_REQUIRE(transaction.validate(index)); + transaction.reserve(); + transaction.append(index); + } + struct MarkedTrack { + bool shared; + bool hasSharedClusters() const { return shared; } + }; + const std::array marked{{{true}, {false}}}; + BOOST_REQUIRE(shared.sealFromMarkedTracks(marked)); + const std::array labels{{{7, 3, 1, true}, {8, 3, 1, false}}}; + fixture.tf.getTrackLabels().assign(labels.begin(), labels.end()); + const auto snapshotBytes = [](const auto& values) { + const auto* first = reinterpret_cast(values.data()); + return std::vector(first, first + values.size() * sizeof(values[0])); + }; + const auto tracksBefore = snapshotBytes(fixture.tf.getGenericTracks()); + const auto referencesBefore = snapshotBytes(fixture.tf.getTrackClusterIndices()); + const auto indicesBefore = fixture.externalIndicesBySurface; + const auto sizesBefore = fixture.clusterSizesBySurface; + const std::vector rofs{ROFRecord{{100, 5}, 0, 7, 3}}; + const GenericTrackPublicationContext context{o2::detectors::DetID::ITS, ClusterSourceId{0}, rofs, + ClockTimingPublicationView{makeFixtureClockTiming()}, fixture.layerMapping, + &fixture.externalIndicesBySurface, &fixture.clusterSizesBySurface}; + GenericTrackOutputAdapterError error{}; + // Every publication owns a fresh flattened range; repeated staging must not + // change the frame or append onto the preceding publication's indices. + for (int publication = 0; publication < 2; ++publication) { + auto output = stageITSGenericTrackOutput(fixture.tf, context, shared, true, error); + BOOST_REQUIRE(output); + BOOST_REQUIRE_EQUAL(output->tracks.size(), 2u); + const std::vector expected{42, 123456, 7, 500000}; + BOOST_CHECK_EQUAL_COLLECTIONS(output->clusterIndices.begin(), output->clusterIndices.end(), expected.begin(), expected.end()); + BOOST_CHECK_EQUAL(output->tracks[0].getFirstClusterEntry(), 0); + BOOST_CHECK_EQUAL(output->tracks[0].getNumberOfClusters(), 1); + BOOST_CHECK_EQUAL(output->tracks[1].getFirstClusterEntry(), 1); + BOOST_CHECK_EQUAL(output->tracks[1].getNumberOfClusters(), 3); + BOOST_CHECK_EQUAL(output->tracks[0].getClusterSize(3), 9); + for (int layer = 0; layer < ITSNLayers; ++layer) { + const int expectedSize = layer == 0 ? 3 : layer == 2 ? 11 + : layer == 5 ? 15 + : 0; + BOOST_CHECK_EQUAL(output->tracks[1].getClusterSize(layer), expectedSize); + } + BOOST_CHECK(!output->tracks[0].hasSharedClusters()); + BOOST_CHECK(output->tracks[1].hasSharedClusters()); + BOOST_REQUIRE_EQUAL(output->labels.size(), 2u); + BOOST_CHECK_EQUAL(output->labels[0].getRawValue(), labels[1].getRawValue()); + BOOST_CHECK_EQUAL(output->labels[1].getRawValue(), labels[0].getRawValue()); + BOOST_CHECK_EQUAL(output->trackROFs[0].getNEntries(), 2); + BOOST_CHECK_EQUAL(output->trackROFs[0].getFlags(), rofs[0].getFlags()); + BOOST_CHECK(snapshotBytes(fixture.tf.getGenericTracks()) == tracksBefore); + BOOST_CHECK(snapshotBytes(fixture.tf.getTrackClusterIndices()) == referencesBefore); + BOOST_CHECK(fixture.externalIndicesBySurface == indicesBefore); + BOOST_CHECK(fixture.clusterSizesBySurface == sizesBefore); + BOOST_CHECK_EQUAL(fixture.tf.getTrackLabels()[0].getRawValue(), labels[0].getRawValue()); + BOOST_CHECK_EQUAL(fixture.tf.getTrackLabels()[1].getRawValue(), labels[1].getRawValue()); + BOOST_CHECK_EQUAL(rofs[0].getFirstEntry(), 7); + BOOST_CHECK_EQUAL(rofs[0].getNEntries(), 3); + } +} + +BOOST_AUTO_TEST_CASE(ITSGenericPublicationOmitsTracksWithoutClusterReferences) +{ + TimeFrameFixture fixture; + auto record = makeTestGenericTrack(); + record.references.clear(); + record.track.hitLayers = {}; + storeTestGenericTrack(fixture.tf, record); + const std::vector rofs{ROFRecord{{100, 5}, 0, 7, 3}}; + const GenericTrackPublicationContext context{o2::detectors::DetID::ITS, ClusterSourceId{0}, rofs, + ClockTimingPublicationView{makeFixtureClockTiming()}, fixture.layerMapping}; + ITSSharedClusterCompatibility unsealed; + GenericTrackOutputAdapterError error{}; + const auto output = stageITSGenericTrackOutput(fixture.tf, context, unsealed, false, error); + BOOST_REQUIRE(output); + BOOST_CHECK(output->tracks.empty()); + BOOST_CHECK(output->clusterIndices.empty()); + BOOST_CHECK(output->labels.empty()); + BOOST_REQUIRE_EQUAL(output->trackROFs.size(), 1u); + BOOST_CHECK_EQUAL(output->trackROFs[0].getFirstEntry(), 0); + BOOST_CHECK_EQUAL(output->trackROFs[0].getNEntries(), 0); + BOOST_CHECK_EQUAL(fixture.tf.getGenericTracks().size(), 1u); +} + +BOOST_AUTO_TEST_CASE(GenericTrackOutputAdapterStagesITSAndFailsClosed) +{ + TimeFrameFixture fixture; + BOOST_REQUIRE(fixture.load().ok()); + auto record = makeTestGenericTrack(); + record.track.chi2 = 3.f; + ITSSharedClusterCompatibility shared; + const auto genericTrackIndex = storeTestGenericTrack(fixture.tf, record); + const o2::MCCompLabel storedLabel{7, 3, 1, true}; + fixture.tf.getTrackLabels().push_back(storedLabel); + ITSSharedClusterCompatibilityTransaction transaction{shared}; + BOOST_REQUIRE(transaction.validate(genericTrackIndex)); + transaction.reserve(); + transaction.append(genericTrackIndex); + struct MarkedTrack { + bool shared{}; + bool hasSharedClusters() const { return shared; } + }; + const std::array marked{{{true}}}; + BOOST_REQUIRE(shared.sealFromMarkedTracks(marked)); + const auto& measurement = fixture.tf.getGlobalMeasurements(LayerId{0})[0]; + BOOST_REQUIRE_EQUAL(measurement.clusterId, 0u); + BOOST_REQUIRE_EQUAL(fixture.externalIndicesBySurface[0].size(), 1u); + BOOST_REQUIRE_EQUAL(fixture.clusterSizesBySurface[0].size(), 1u); + fixture.externalIndicesBySurface[0][measurement.clusterId] = 42u; + fixture.clusterSizesBySurface[0][measurement.clusterId] = 13u; + const auto source = ClusterSourceId{0}; + const std::vector rofs{ROFRecord{{100, 5}, 0, 7, 3}}; + GenericTrackOutputAdapterError error = GenericTrackOutputAdapterError::None; + const auto clock = makeFixtureClockTiming(); + const GenericTrackOutputTimingContext timing{rofs, ClockTimingPublicationView{clock}}; + auto output = stageITSGenericTrackOutput(fixture.tf, + gsl::span{fixture.layerMapping}, timing, shared, + true, error, &fixture.externalIndicesBySurface, &fixture.clusterSizesBySurface); + BOOST_REQUIRE(output); + BOOST_CHECK_EQUAL(output->tracks.size(), 1u); + BOOST_CHECK_EQUAL(output->clusterIndices.size(), 1u); + BOOST_CHECK_EQUAL(output->clusterIndices[0], 42); + BOOST_CHECK_EQUAL(output->tracks[0].getClusterSize(0), 13); + BOOST_CHECK(output->tracks[0].hasSharedClusters()); + BOOST_CHECK_EQUAL(output->tracks[0].getChi2(), 3.f); + BOOST_CHECK_EQUAL(output->trackROFs[0].getFirstEntry(), 0); + BOOST_CHECK_EQUAL(output->trackROFs[0].getNEntries(), 1); + BOOST_CHECK_EQUAL(output->trackROFs[0].getFlags(), rofs[0].getFlags()); + BOOST_REQUIRE_EQUAL(output->labels.size(), 1u); + BOOST_CHECK_EQUAL(output->labels[0].getRawValue(), storedLabel.getRawValue()); + + // This is the workflow-facing binding: the workflow combines its own ROF + // span with the immutable export returned by the tracking interface. The + // adapter accepts no scratch state and keeps the source/layout binding + // explicit at this boundary. + const GenericTrackPublicationContext publicationContext{ + o2::detectors::DetID::ITS, source, rofs, ClockTimingPublicationView{clock}, + gsl::span{fixture.layerMapping}, + &fixture.externalIndicesBySurface, &fixture.clusterSizesBySurface}; + const auto contextOutput = stageITSGenericTrackOutput(fixture.tf, publicationContext, shared, true, error); + BOOST_REQUIRE(contextOutput); + BOOST_CHECK_EQUAL(contextOutput->tracks.size(), output->tracks.size()); + BOOST_REQUIRE_EQUAL(contextOutput->clusterIndices.size(), output->clusterIndices.size()); + for (size_t i = 0; i < output->clusterIndices.size(); ++i) { + BOOST_CHECK_EQUAL(contextOutput->clusterIndices[i], output->clusterIndices[i]); + } + BOOST_CHECK_EQUAL(contextOutput->trackROFs.size(), output->trackROFs.size()); + + fixture.tf.getTrackLabels().clear(); + BOOST_CHECK(!stageITSGenericTrackOutput(fixture.tf, publicationContext, shared, true, error)); + BOOST_CHECK(error == GenericTrackOutputAdapterError::MissingMCLabels); + fixture.tf.getTrackLabels().push_back(storedLabel); + + auto wrongDetectorContext = publicationContext; + wrongDetectorContext.detector = o2::detectors::DetID::MFT; + BOOST_CHECK(!stageITSGenericTrackOutput(fixture.tf, wrongDetectorContext, shared, false, error)); + BOOST_CHECK(error == GenericTrackOutputAdapterError::MixedDetector); + + auto missingClusterSizesContext = publicationContext; + missingClusterSizesContext.clusterSizesBySurface = nullptr; + BOOST_CHECK(!stageITSGenericTrackOutput(fixture.tf, missingClusterSizesContext, shared, false, error)); + BOOST_CHECK(error == GenericTrackOutputAdapterError::UnresolvedReference); + + // Legacy publication retains a track even when its selected output + // timestamp falls outside the workflow ROF span; it simply does not + // increment a TrackROF entry. The adapter must preserve that behavior. + fixture.tf.getGenericTracks()[0].timestamp = {1000, 1001}; + const auto outOfRangeOutput = stageITSGenericTrackOutput(fixture.tf, publicationContext, shared, false, error); + BOOST_REQUIRE(outOfRangeOutput); + BOOST_REQUIRE_EQUAL(outOfRangeOutput->tracks.size(), 1u); + BOOST_CHECK_EQUAL(outOfRangeOutput->trackROFs[0].getFirstEntry(), 0); + BOOST_CHECK_EQUAL(outOfRangeOutput->trackROFs[0].getNEntries(), 0); + fixture.tf.getGenericTracks()[0].timestamp = record.track.timestamp; + + const auto oldTracks = fixture.tf.getGenericTracks().size(); + const auto oldReferences = fixture.tf.getTrackClusterIndices().size(); + // The original workflow ROF span can have more (or fewer) entries than + // the LayerTiming clock. The GenericTrack adapter preserves that span + // verbatim and groups only in-range clock slots. + const std::vector mismatchedROFs{ROFRecord{{100, 5}, 0, 1, 2}, ROFRecord{{100, 6}, 1, 2, 3}}; + const GenericTrackOutputTimingContext mismatchedROF{mismatchedROFs, ClockTimingPublicationView{clock}}; + const auto mismatchedOutput = stageITSGenericTrackOutput(fixture.tf, + gsl::span{fixture.layerMapping}, mismatchedROF, shared, false, error, + &fixture.externalIndicesBySurface, &fixture.clusterSizesBySurface); + BOOST_REQUIRE(mismatchedOutput); + BOOST_REQUIRE_EQUAL(mismatchedOutput->trackROFs.size(), mismatchedROFs.size()); + BOOST_CHECK_EQUAL(mismatchedOutput->trackROFs[0].getNEntries(), 1); + BOOST_CHECK_EQUAL(mismatchedOutput->trackROFs[1].getNEntries(), 0); + BOOST_CHECK_EQUAL(fixture.tf.getGenericTracks().size(), oldTracks); + BOOST_CHECK_EQUAL(fixture.tf.getTrackClusterIndices().size(), oldReferences); +} + +BOOST_AUTO_TEST_CASE(GenericTrackOutputAdapterStagesMFTCompatibilityWithoutSeedPt) +{ + const auto layout = makeCombinedLayout(); + TimeFrame frame; + std::vector> externalIndicesBySurface; + std::vector> clusterSizesBySurface; + loadThreeMeasurementFrame(frame, layout, &externalIndicesBySurface, &clusterSizesBySurface); + TestGenericTrack record; + record.track.innerState.kind = SurfaceKind::Disk; + record.track.outerState.kind = SurfaceKind::Disk; + record.track.innerState.referenceCoordinate = -77.f; + record.track.outerState.referenceCoordinate = -12.f; + for (uint8_t i = 0; i < 5; ++i) { + record.track.innerState.parameters[i] = 0.5f + i; + record.track.outerState.parameters[i] = 3.5f + i; + } + for (uint8_t i = 0; i < 15; ++i) { + record.track.innerState.covariance[i] = 0.01f * (i + 1); + record.track.outerState.covariance[i] = 0.02f * (i + 1); + } + record.track.chi2 = 8.f; + record.track.timestamp = {100, 124}; + record.track.hitLayers.set(3); + record.references.push_back({LayerId{3}, 0, 0}); + storeTestGenericTrack(frame, record); + const o2::MCCompLabel storedLabel{11, 4, 2, false}; + frame.getTrackLabels().push_back(storedLabel); + const auto& measurement = frame.getGlobalMeasurements(LayerId{3})[0]; + const auto source = ClusterSourceId{1}; + const std::vector rofs{ROFRecord{{7, 9}, 2, 4, 5}}; + GenericTrackOutputAdapterError error = GenericTrackOutputAdapterError::None; + o2::its::LayerTiming clock{}; + clock.mNROFsTF = 1; + clock.mROFLength = 18; + clock.mROFDelay = 100; + const GenericTrackOutputTimingContext timing{rofs, ClockTimingPublicationView{clock}}; + const std::array surfaces{LayerId{3}}; + const auto output = stageMFTGenericTrackOutput(frame, surfaces, timing, true, error, + &externalIndicesBySurface, &clusterSizesBySurface); + BOOST_REQUIRE(output); + BOOST_REQUIRE_EQUAL(output->tracks.size(), 1u); + BOOST_CHECK_EQUAL(output->tracks[0].getZ(), -77.); + BOOST_CHECK_EQUAL(output->tracks[0].getOutParam().getZ(), -12.); + BOOST_CHECK_EQUAL(output->tracks[0].getCovariances()(4, 3), record.track.innerState.covariance[packedCovarianceIndex(4, 3)]); + BOOST_CHECK_EQUAL(output->tracks[0].getOutParam().getCovariances()(4, 3), record.track.outerState.covariance[packedCovarianceIndex(4, 3)]); + BOOST_CHECK_EQUAL(output->tracks[0].getTrackChi2(), 8.); + BOOST_CHECK_EQUAL(output->tracks[0].getInvQPtSeed(), 0.); + BOOST_CHECK_EQUAL(output->tracks[0].getChi2QPtSeed(), 0.); + BOOST_REQUIRE_EQUAL(output->seedPatterns.size(), 1u); + BOOST_CHECK_EQUAL(output->seedPatterns[0], 0x1u); + BOOST_CHECK_EQUAL(output->clusterIndices[0], static_cast(measurement.clusterId)); + BOOST_CHECK_EQUAL(output->trackROFs[0].getFirstEntry(), 0); + BOOST_CHECK_EQUAL(output->trackROFs[0].getNEntries(), 1); + BOOST_CHECK_EQUAL(output->trackROFs[0].getFlags(), rofs[0].getFlags()); + BOOST_REQUIRE_EQUAL(output->labels.size(), 1u); + BOOST_CHECK_EQUAL(output->labels[0].getRawValue(), storedLabel.getRawValue()); + + const GenericTrackPublicationContext publicationContext{ + o2::detectors::DetID::MFT, source, rofs, ClockTimingPublicationView{clock}, surfaces, + &externalIndicesBySurface, &clusterSizesBySurface}; + const auto contextOutput = stageMFTGenericTrackOutput(frame, publicationContext, true, error); + BOOST_REQUIRE(contextOutput); + BOOST_CHECK_EQUAL(contextOutput->tracks.size(), output->tracks.size()); + BOOST_CHECK_EQUAL_COLLECTIONS(contextOutput->seedPatterns.begin(), contextOutput->seedPatterns.end(), + output->seedPatterns.begin(), output->seedPatterns.end()); + + auto wrongDetectorContext = publicationContext; + wrongDetectorContext.detector = o2::detectors::DetID::ITS; + BOOST_CHECK(!stageMFTGenericTrackOutput(frame, wrongDetectorContext, false, error)); + BOOST_CHECK(error == GenericTrackOutputAdapterError::MixedDetector); + BOOST_CHECK_EQUAL(frame.getGenericTracks().size(), 1u); + BOOST_CHECK_EQUAL(frame.getTrackClusterIndices().size(), 1u); +} + +BOOST_AUTO_TEST_CASE(GenericTrackOutputAdapterRejectsMalformedInputsWithoutMutatingOwners) +{ + TimeFrameFixture fixture; + BOOST_REQUIRE(fixture.load().ok()); + const auto record = makeTestGenericTrack(); + storeTestGenericTrack(fixture.tf, record); + const std::vector rofs{ROFRecord{{1, 2}, 0, 0, 1}}; + const auto clock = makeFixtureClockTiming(); + const GenericTrackOutputTimingContext timing{rofs, ClockTimingPublicationView{clock}}; + const auto surfaces = gsl::span{fixture.layerMapping}; + const auto tracks = fixture.tf.getGenericTracks().size(); + const auto refs = fixture.tf.getTrackClusterIndices().size(); + const auto measurements = fixture.tf.getTotalMeasurements(); + GenericTrackOutputAdapterError error = GenericTrackOutputAdapterError::None; + ITSSharedClusterCompatibility unsealed; + BOOST_CHECK(!stageITSGenericTrackOutput(fixture.tf, surfaces, timing, unsealed, false, error)); + BOOST_CHECK(error == GenericTrackOutputAdapterError::MissingCompatibility); + const std::array foreignSurfaces{LayerId{3}}; + const auto foreignSelection = stageITSGenericTrackOutput(fixture.tf, foreignSurfaces, timing, unsealed, false, error); + BOOST_REQUIRE(foreignSelection); + BOOST_CHECK(foreignSelection->tracks.empty()); + BOOST_CHECK_EQUAL(fixture.tf.getGenericTracks().size(), tracks); + BOOST_CHECK_EQUAL(fixture.tf.getTrackClusterIndices().size(), refs); + BOOST_CHECK_EQUAL(fixture.tf.getTotalMeasurements(), measurements); + + fixture.tf.getGenericTracks()[0].clusterRefEnd = refs + 1; + BOOST_CHECK(!selectGenericTracksForSurfaces(fixture.tf, surfaces, error)); + BOOST_CHECK(error == GenericTrackOutputAdapterError::InvalidTrackRange); + fixture.tf.getGenericTracks()[0].clusterRefEnd = refs; + fixture.tf.getTrackClusterIndices()[0].layer = LayerId::invalid(); + BOOST_CHECK(!selectGenericTracksForSurfaces(fixture.tf, surfaces, error)); + BOOST_CHECK(error == GenericTrackOutputAdapterError::UnresolvedReference); + fixture.tf.getTrackClusterIndices()[0].layer = LayerId{0}; + + ITSSharedClusterCompatibility sealed; + ITSSharedClusterCompatibilityTransaction tx{sealed}; + const auto secondTrackIndex = storeTestGenericTrack(fixture.tf, record); + BOOST_REQUIRE(tx.validate(secondTrackIndex)); + tx.reserve(); + tx.append(secondTrackIndex); + struct Marked { + bool hasSharedClusters() const { return false; } + }; + const std::array none{}; + BOOST_CHECK(!sealed.sealFromMarkedTracks(none)); // pending cardinality mismatch fails closed + BOOST_CHECK(!sealed.isSealed()); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testITSCommonCATrackingModeConfiguration.cxx b/Detectors/ITSMFT/common/tracking/test/testITSCommonCATrackingModeConfiguration.cxx new file mode 100644 index 0000000000000..bbb688a505b4f --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testITSCommonCATrackingModeConfiguration.cxx @@ -0,0 +1,231 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// Workflow-onboarding Slice 1: focused tests for the dedicated +// ITSCommonCATrackerParam configuration type (TrackingConfigParam.h) and the +// real ITS Sync and Async branches of TrackingMode::getTrackingParameters() +// (Configuration.cxx). No workflow spec exists yet -- these tests call the +// common-tracking library directly, the same way +// the workflow loading tests already document that the +// ITS branch of getTrackingParameters() used to unconditionally +// LOGP(fatal, ...) regardless of mode; that fatal is now real per-mode +// behaviour instead, exercised here. + +#define BOOST_TEST_MODULE ITSMFT ITSCommonCATrackingModeConfiguration +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include "TrackingParameterTestSupport.h" +#include + +#include +#include +#include + +#include "DetectorsCommonDataFormats/DetID.h" +#include "DetectorsBase/Propagator.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "ITSMFTTracking/ITSTrackingConfigParam.h" +#include "ITStracking/Configuration.h" + +using namespace o2::itsmft; +using namespace o2::itsmft::tracking; + +namespace +{ +struct MagneticFieldFixture { + MagneticFieldFixture() { o2::base::Propagator::initFieldFromGRP(0.f, 0.f, true, false); } +}; +} // namespace + +BOOST_TEST_GLOBAL_FIXTURE(MagneticFieldFixture); + +// --- Dedicated name is distinct from every other registered CA param name -- + +BOOST_AUTO_TEST_CASE(DedicatedNameIsDistinctFromLegacyAndMFTNames) +{ + const auto& itsCommonCA = ITSCommonCATrackerParam::Instance(); + const auto& itsLegacy = o2::its::TrackerParamConfig::Instance(); + const auto& mftCommonCA = TrackerParamConfig::Instance(); + + BOOST_CHECK_EQUAL(itsCommonCA.getName(), "ITSCommonCATrackerParam"); + BOOST_CHECK_EQUAL(itsLegacy.getName(), "ITSCATrackerParam"); + BOOST_CHECK_EQUAL(mftCommonCA.getName(), "MFTCATrackerParam"); + + BOOST_CHECK(itsCommonCA.getName() != itsLegacy.getName()); + BOOST_CHECK(itsCommonCA.getName() != mftCommonCA.getName()); + BOOST_CHECK(itsLegacy.getName() != mftCommonCA.getName()); +} + +// --- ITSCommonCATrackerParam defaults match the documented Sync baseline --- + +BOOST_AUTO_TEST_CASE(DedicatedDefaultsMatchDocumentedSyncBaseline) +{ + const auto& tc = ITSCommonCATrackerParam::Instance(); + BOOST_CHECK_EQUAL(tc.dropTFUponFailure, false); + BOOST_CHECK_EQUAL(tc.printMemory, false); + BOOST_CHECK_EQUAL(tc.maxMemory, std::numeric_limits::max()); + BOOST_CHECK_EQUAL(tc.saveTimeBenchmarks, false); + BOOST_CHECK_EQUAL(tc.useDiamond, false); + BOOST_CHECK_EQUAL(tc.diamondPos[0], 0.f); + BOOST_CHECK_EQUAL(tc.diamondPos[1], 0.f); + BOOST_CHECK_EQUAL(tc.diamondPos[2], 0.f); + BOOST_CHECK_EQUAL(tc.pvRes, -1.f); + BOOST_CHECK_EQUAL(tc.nThreads, 1); +} + +// --- ITS Sync construction is valid, one-iteration, with expected values --- + +BOOST_AUTO_TEST_CASE(ITSSyncTrackingParametersAreValidOneIteration) +{ + const auto trackParams = o2::itsmft::tracking::test::referenceTrackingParameters(o2::detectors::DetID::ITS, TrackingMode::Sync); + + BOOST_REQUIRE_EQUAL(trackParams.size(), 1u); + const auto& p = trackParams[0]; + + BOOST_CHECK_EQUAL(p.NLayers, tracking::ITSNLayers); + BOOST_CHECK_EQUAL(p.MinTrackLength, tracking::kCAMinTrackLength); + BOOST_CHECK_EQUAL(p.MinPt.size(), static_cast(tracking::ITSNLayers - + tracking::kCAMinTrackLength + 1)); + BOOST_CHECK_EQUAL(p.StartLayerMask.count(), tracking::ITSNLayers); // default mask: all 7 barrel layers active + + // Administrative fields wired straight from the dedicated config's defaults. + BOOST_CHECK_EQUAL(p.DropTFUponFailure, false); + BOOST_CHECK_EQUAL(p.MaxMemory, std::numeric_limits::max()); + BOOST_CHECK_EQUAL(p.UseDiamond, false); + + // resetDetectorDefaults(..., DetID::ITS) supplies real barrel geometry + // defaults (TrackingParameters' own struct defaults); confirm they were + // not clobbered. + BOOST_CHECK_EQUAL(p.LayerRadii.size(), static_cast(tracking::ITSNLayers)); + BOOST_CHECK_EQUAL(p.LayerZ.size(), static_cast(tracking::ITSNLayers)); +} + +BOOST_AUTO_TEST_CASE(ITSSyncTrackingParametersAreDeterministic) +{ + const auto a = o2::itsmft::tracking::test::referenceTrackingParameters(o2::detectors::DetID::ITS, TrackingMode::Sync); + const auto b = o2::itsmft::tracking::test::referenceTrackingParameters(o2::detectors::DetID::ITS, TrackingMode::Sync); + BOOST_REQUIRE_EQUAL(a.size(), b.size()); + BOOST_CHECK_EQUAL(a[0].MinTrackLength, b[0].MinTrackLength); + BOOST_CHECK_EQUAL(a[0].NLayers, b[0].NLayers); + BOOST_CHECK(a[0].LayerRadii == b[0].LayerRadii); +} + +BOOST_AUTO_TEST_CASE(ITSAsyncMatchesLegacySelectionParameters) +{ + const auto common = o2::itsmft::tracking::test::referenceTrackingParameters(o2::detectors::DetID::ITS, TrackingMode::Async); + const auto legacy = o2::its::TrackingMode::getTrackingParameters(o2::its::TrackingMode::Async); + + BOOST_REQUIRE_EQUAL(common.size(), 3u); + BOOST_REQUIRE_EQUAL(common.size(), legacy.size()); + for (size_t iteration = 0; iteration < common.size(); ++iteration) { + const auto& commonIteration = common[iteration]; + const auto& legacyIteration = legacy[iteration]; + BOOST_CHECK_EQUAL(commonIteration.ColBins, legacyIteration.ZBins); + BOOST_CHECK_EQUAL(commonIteration.RowBins, legacyIteration.PhiBins); + BOOST_CHECK_EQUAL(commonIteration.MinTrackLength, legacyIteration.MinTrackLength); + BOOST_CHECK_EQUAL(commonIteration.TrackletMinPt, legacyIteration.TrackletMinPt); + BOOST_CHECK_EQUAL(commonIteration.StartLayerMask.value(), legacyIteration.StartLayerMask.value()); + BOOST_REQUIRE_EQUAL(commonIteration.MinPt.size(), legacyIteration.MinPt.size()); + for (size_t length = 0; length < commonIteration.MinPt.size(); ++length) { + BOOST_CHECK_EQUAL(commonIteration.MinPt[length], legacyIteration.MinPt[length]); + } + } + + // These are currently intentional algorithm limitations, not selection + // mismatches: common CA has no CellDeltaTanLambdaSigma analogue and its ITS + // cylindrical surfaces do not yet support the legacy material LUT. + BOOST_CHECK(legacy.front().CorrType == o2::base::PropagatorImpl::MatCorrType::USEMatCorrLUT); + BOOST_CHECK(common.front().CorrType == o2::base::PropagatorImpl::MatCorrType::USEMatCorrNONE); +} + +// --- Every unsupported TrackingMode fails closed, none silently mapped ----- +// +// LOGP(fatal, ...) normally terminates the process (FairLogger default). A +// process-local OnFatal handler converts it into a catchable exception so +// this remains a normal, non-crashing ctest case. Each ITSMFT test source +// file builds its own executable (o2_add_test == one binary per SOURCES +// file), so this handler cannot leak into unrelated test binaries. + +namespace +{ +struct FatalToExceptionFixture { + FatalToExceptionFixture() + { + fair::Logger::OnFatal([]() { throw std::runtime_error("fatal"); }); + } +}; +} // namespace + +BOOST_FIXTURE_TEST_CASE(EveryUnsupportedITSModeFailsClosed, FatalToExceptionFixture) +{ + const std::array unsupported{ + TrackingMode::Off, TrackingMode::Unset, TrackingMode::Cosmics}; + + for (const auto mode : unsupported) { + BOOST_CHECK_THROW(o2::itsmft::tracking::test::referenceTrackingParameters(o2::detectors::DetID::ITS, mode), std::runtime_error); + } +} + +BOOST_FIXTURE_TEST_CASE(SyncStillSucceedsAfterFatalHandlerInstalled, FatalToExceptionFixture) +{ + // The OnFatal fixture above must not turn the supported paths into false + // failures: Sync and Async should still construct normally. + BOOST_CHECK_NO_THROW(o2::itsmft::tracking::test::referenceTrackingParameters(o2::detectors::DetID::ITS, TrackingMode::Sync)); + BOOST_CHECK_NO_THROW(o2::itsmft::tracking::test::referenceTrackingParameters(o2::detectors::DetID::ITS, TrackingMode::Async)); +} + +// Sync/Async/Cosmics require a configured magnetic-field singleton. The +// detector defaults and the early-return Off path can be tested directly. + +BOOST_AUTO_TEST_CASE(MFTDefaultsUseTheCommonFourHitSelection) +{ + TrackingParameters params; + resetDetectorDefaults(params, o2::detectors::DetID::MFT); + + BOOST_CHECK_EQUAL(TrackerParamConfig::MinTrackLength, 4); + BOOST_CHECK_EQUAL(params.MinPt.size(), static_cast(tracking::MFTNLayers - 4 + 1)); + BOOST_CHECK_EQUAL(params.ColBins, 64); + BOOST_CHECK_EQUAL(params.RowBins, 128); +} + +BOOST_FIXTURE_TEST_CASE(MFTOffStillReturnsEmptyNotFatal, FatalToExceptionFixture) +{ + BOOST_CHECK_NO_THROW({ + const auto trackParams = o2::itsmft::tracking::test::referenceTrackingParameters(o2::detectors::DetID::MFT, TrackingMode::Off); + BOOST_CHECK(trackParams.empty()); + }); +} + +// --- workflow-onboarding Slice 2: diamondPos/pvRes are wired through ------- +// +// Mutates the global ITSCommonCATrackerParam singleton via +// ConfigurableParam::setValue -- deliberately placed last in this +// translation unit so no other test observes the mutated state. + +BOOST_AUTO_TEST_CASE(DiamondPosAndPVresAreWiredIntoITSSyncTrackingParameters) +{ + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "diamondPos[0]", 1.5f); + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "diamondPos[1]", -2.5f); + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "diamondPos[2]", 3.5f); + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "pvRes", 0.25f); + o2::conf::ConfigurableParam::setValue("ITSCommonCATrackerParam", "useDiamond", true); + + const auto trackParams = o2::itsmft::tracking::test::referenceTrackingParameters(o2::detectors::DetID::ITS, TrackingMode::Sync); + BOOST_REQUIRE_EQUAL(trackParams.size(), 1u); + const auto& p = trackParams[0]; + + BOOST_CHECK_EQUAL(p.UseDiamond, true); + BOOST_CHECK_EQUAL(p.Diamond[0], 1.5f); + BOOST_CHECK_EQUAL(p.Diamond[1], -2.5f); + BOOST_CHECK_EQUAL(p.Diamond[2], 3.5f); + BOOST_CHECK_EQUAL(p.PVres, 0.25f); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testITSMFTSurfaceSpecProjection.cxx b/Detectors/ITSMFT/common/tracking/test/testITSMFTSurfaceSpecProjection.cxx new file mode 100644 index 0000000000000..b34c34f95be92 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testITSMFTSurfaceSpecProjection.cxx @@ -0,0 +1,192 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE Test ITSMFTTracking ITSMFTSurfaceSpecProjection +#include + +#include +#include +#include +#include +#include +#include + +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "ITSMFTTracking/Constants.h" + +using namespace o2::itsmft::tracking; + +namespace +{ +uint32_t bitsOf(float value) +{ + uint32_t bits{}; + std::memcpy(&bits, &value, sizeof(bits)); + return bits; +} + +float parseToken(const char* token) +{ + char* endptr = nullptr; + const float value = std::strtof(token, &endptr); + BOOST_REQUIRE_MESSAGE(endptr != nullptr && *endptr == '\0', "strtof left unparsed characters in \"" << token << "\""); + return value; +} + +struct ExpectedSurface { + uint16_t index; + const char* referenceCoordinateToken; // verbatim from the C1 lossless JSON provenance + uint8_t detectorId; + SurfaceKind kind; +}; + +// Tokens copied verbatim from +// O2-validation-artifacts/itsmft/gate4-b1-slice1-nominal-geometry-validation/ +// pp-20ev-run303000-seed20260716-daily20260717/acceptance-cleanup-c1-lossless-json/ +// {its,mft}-report.json (geometry SHA-256 +// 2a428746b3a0b57179d5ffe631afc9c4afb4ca41cc9baa948ff670099b9204e4; full +// provenance in doc/decisions/0004-its-mft-static-surface-spec-tables.md). +const std::vector kExpectedITS{ + {0, "2.3259652", 0, SurfaceKind::Cylinder}, + {1, "3.1353536", 0, SurfaceKind::Cylinder}, + {2, "3.9162421", 0, SurfaceKind::Cylinder}, + {3, "19.58824", 0, SurfaceKind::Cylinder}, + {4, "24.527159", 0, SurfaceKind::Cylinder}, + {5, "34.354595", 0, SurfaceKind::Cylinder}, + {6, "39.310642", 0, SurfaceKind::Cylinder}, +}; + +const std::vector kExpectedMFT{ + {0, "-45.2889", 8, SurfaceKind::Disk}, + {1, "-46.7111", 8, SurfaceKind::Disk}, + {2, "-48.5889", 8, SurfaceKind::Disk}, + {3, "-50.0111", 8, SurfaceKind::Disk}, + {4, "-52.3889", 8, SurfaceKind::Disk}, + {5, "-53.8111", 8, SurfaceKind::Disk}, + {6, "-67.6889", 8, SurfaceKind::Disk}, + {7, "-69.1111", 8, SurfaceKind::Disk}, + {8, "-76.0889", 8, SurfaceKind::Disk}, + {9, "-77.5111", 8, SurfaceKind::Disk}, +}; + +template +void checkAuthoredLiteralsMatchProvenanceTokens(const std::vector& expected) +{ + BOOST_REQUIRE_EQUAL(Spec::surfaces.size(), expected.size()); + for (const auto& row : expected) { + const auto& authored = Spec::surfaces[row.index]; + const float fromToken = parseToken(row.referenceCoordinateToken); + BOOST_CHECK_MESSAGE(bitsOf(authored.nominalReferenceCoordinate) == bitsOf(fromToken), + "surface " << row.index << ": authored literal (bits 0x" << std::hex + << bitsOf(authored.nominalReferenceCoordinate) << ") does not bit-match provenance token \"" + << row.referenceCoordinateToken << "\" (bits 0x" << bitsOf(fromToken) << ")" << std::dec); + } +} + +template +void checkIdentityAndKind(const std::vector& expected) +{ + for (const auto& row : expected) { + const auto& authored = Spec::surfaces[row.index]; + BOOST_CHECK_EQUAL(authored.identity.detectorId, row.detectorId); + BOOST_CHECK_EQUAL(authored.identity.detectorSurfaceIndex, row.index); + BOOST_CHECK(authored.kind == row.kind); + } +} + +template +void checkProjectionPreservesEveryFieldBitExactly(const std::vector& expected) +{ + for (const auto& row : expected) { + const auto& authored = Spec::surfaces[row.index]; + const auto projected = toRuntimeSurfaceDescriptor(authored); + BOOST_CHECK_EQUAL(projected.detectorSurfaceIndex, authored.identity.detectorSurfaceIndex); + BOOST_CHECK_EQUAL(projected.detectorId, authored.identity.detectorId); + BOOST_CHECK(projected.kind == authored.kind); + BOOST_CHECK_EQUAL(projected.flags, 0); + BOOST_CHECK_EQUAL(bitsOf(projected.referenceCoordinate), bitsOf(authored.nominalReferenceCoordinate)); + BOOST_CHECK_EQUAL(bitsOf(projected.material.xOverX0), bitsOf(authored.material.xOverX0)); + BOOST_CHECK_EQUAL(bitsOf(projected.material.arealDensityGPerCm2), bitsOf(authored.material.arealDensityGPerCm2)); + } +} +} // namespace + +static_assert(SurfaceSpec); +static_assert(SurfaceSpec); +static_assert(SurfaceCount == ITSNLayers); +static_assert(SurfaceCount == MFTNLayers); +static_assert(SurfaceSpecsCanBeConcatenated); + +BOOST_AUTO_TEST_CASE(ITSAuthoredLiteralsMatchProvenanceTokensBitExactly) +{ + checkAuthoredLiteralsMatchProvenanceTokens(kExpectedITS); +} + +BOOST_AUTO_TEST_CASE(MFTAuthoredLiteralsMatchProvenanceTokensBitExactly) +{ + checkAuthoredLiteralsMatchProvenanceTokens(kExpectedMFT); +} + +BOOST_AUTO_TEST_CASE(ITSIdentityKindAndIndexingFamily) +{ + checkIdentityAndKind(kExpectedITS); +} + +BOOST_AUTO_TEST_CASE(MFTIdentityKindAndIndexingFamily) +{ + checkIdentityAndKind(kExpectedMFT); +} + +BOOST_AUTO_TEST_CASE(ITSMaterialMatchesNominalDefaultsAndRadlRhoFormula) +{ + for (int layer = 0; layer < ITSNLayers; ++layer) { + const auto& surface = ITSSurfaceSpec::surfaces[layer]; + BOOST_CHECK_EQUAL(surface.material.xOverX0, kNominalITSLayerX0[layer]); + BOOST_CHECK_CLOSE(surface.material.arealDensityGPerCm2, + kNominalITSLayerX0[layer] * o2::its::constants::Radl * o2::its::constants::Rho, 1.e-6f); + } +} + +BOOST_AUTO_TEST_CASE(MFTMaterialMatchesNominalDefaultsAndRadlRhoFormula) +{ + for (int layer = 0; layer < MFTNLayers; ++layer) { + const auto& surface = MFTSurfaceSpec::surfaces[layer]; + BOOST_CHECK_EQUAL(surface.material.xOverX0, kNominalMFTLayerX0[layer]); + BOOST_CHECK_CLOSE(surface.material.arealDensityGPerCm2, + kNominalMFTLayerX0[layer] * o2::its::constants::Radl * o2::its::constants::Rho, 1.e-6f); + } +} + +BOOST_AUTO_TEST_CASE(MFTSensorPairsShareThePhysicalDiskBudget) +{ + float totalX0 = 0.f; + float totalArealDensity = 0.f; + for (int disk = 0; disk < MFTDisks; ++disk) { + const auto& front = kMFTStaticSurfaceCatalog[2 * disk].material; + const auto& back = kMFTStaticSurfaceCatalog[2 * disk + 1].material; + BOOST_CHECK_CLOSE(front.xOverX0 + back.xOverX0, kMFTNominalRadLength / MFTDisks, 1.e-4f); + totalX0 += front.xOverX0 + back.xOverX0; + totalArealDensity += front.arealDensityGPerCm2 + back.arealDensityGPerCm2; + } + BOOST_CHECK_CLOSE(totalX0, kMFTNominalRadLength, 1.e-4f); + BOOST_CHECK_CLOSE(totalArealDensity, + kMFTNominalRadLength * o2::its::constants::Radl * o2::its::constants::Rho, 1.e-4f); +} + +BOOST_AUTO_TEST_CASE(ITSProjectionPreservesEveryFieldBitExactly) +{ + checkProjectionPreservesEveryFieldBitExactly(kExpectedITS); +} + +BOOST_AUTO_TEST_CASE(MFTProjectionPreservesEveryFieldBitExactly) +{ + checkProjectionPreservesEveryFieldBitExactly(kExpectedMFT); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testMFTCATrackingConfiguration.cxx b/Detectors/ITSMFT/common/tracking/test/testMFTCATrackingConfiguration.cxx new file mode 100644 index 0000000000000..67a80ce88ccce --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testMFTCATrackingConfiguration.cxx @@ -0,0 +1,193 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE MFT CA Tracking Configuration +#define BOOST_TEST_DYN_LINK +#include "TrackingParameterTestSupport.h" +#include + +#include +#include +#include +#include + +#include "CommonUtils/ConfigurableParam.h" +#include "DetectorsBase/Propagator.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "ITSMFTTracking/TraversalTopology.h" +#include "ITSMFTTracking/IndexTableConfiguration.h" + +using namespace o2::itsmft; +using namespace o2::itsmft::tracking; +using o2::conf::ConfigurableParam; +using MFTParam = TrackerParamConfig; + +namespace +{ +struct FieldFixture { + FieldFixture() { o2::base::Propagator::initFieldFromGRP(0.f, 0.f, true, false); } +}; +struct RestoreConfiguration { + ~RestoreConfiguration() + { + ConfigurableParam::updateFromString("MFTCATrackerParam.nIterations=-1;MFTCATrackerParam.materialModel=nominal;MFTCATrackerParam.useFastMaterial=true;MFTCATrackerParam.useMatCorrTGeo=false;MFTCATrackerParam.startLayerMask[0]=0"); + } +}; +auto resolve(TrackingMode::Type mode) +{ + return o2::itsmft::tracking::test::referenceTrackingParameters(o2::detectors::DetID::MFT, mode); +} +} // namespace + +BOOST_TEST_GLOBAL_FIXTURE(FieldFixture); + +BOOST_AUTO_TEST_CASE(DefaultAsyncUsesAllPresetPasses) +{ + BOOST_CHECK_EQUAL(resolve(TrackingMode::Sync).size(), 1); + BOOST_CHECK_EQUAL(resolve(TrackingMode::Async).size(), 3); + BOOST_CHECK(resolve(TrackingMode::Off).empty()); + const auto async = resolve(TrackingMode::Async); + BOOST_CHECK_LT(async[2].TrackletMinPt, async[0].TrackletMinPt); + BOOST_CHECK_LT(async[2].MinTrackLength, async[0].MinTrackLength); +} + +BOOST_FIXTURE_TEST_CASE(ParserPassLimitsAreExplicitAndChecked, RestoreConfiguration) +{ + for (const int count : {1, 2, 3}) { + ConfigurableParam::updateFromString("MFTCATrackerParam.nIterations=" + std::to_string(count)); + BOOST_CHECK_EQUAL(resolve(TrackingMode::Async).size(), count); + } + for (const int count : {0, -2, 4}) { + ConfigurableParam::updateFromString("MFTCATrackerParam.nIterations=" + std::to_string(count)); + BOOST_CHECK_THROW(resolve(TrackingMode::Async), std::invalid_argument); + } + ConfigurableParam::updateFromString("MFTCATrackerParam.nIterations=2"); + BOOST_CHECK_THROW(resolve(TrackingMode::Sync), std::invalid_argument); + ConfigurableParam::updateFromString("MFTCATrackerParam.nIterations=-1"); + BOOST_CHECK_EQUAL(resolve(TrackingMode::Async).size(), 3); +} + +BOOST_FIXTURE_TEST_CASE(ParserMaterialSelectionNamesOnlyImplementedProviders, RestoreConfiguration) +{ + using MatCorr = o2::base::PropagatorF::MatCorrType; + BOOST_CHECK(resolve(TrackingMode::Sync).front().CorrType == MatCorr::USEMatCorrNONE); + for (const auto model : {"LUT", "TGeo", "none", "unknown"}) { + ConfigurableParam::updateFromString(std::string("MFTCATrackerParam.materialModel=") + model); + BOOST_CHECK_EXCEPTION(resolve(TrackingMode::Sync), std::invalid_argument, + [model](const auto& error) { return std::string(error.what()).find(model) != std::string::npos; }); + } + ConfigurableParam::updateFromString("MFTCATrackerParam.materialModel=nominal;MFTCATrackerParam.useMatCorrTGeo=true"); + BOOST_CHECK_EXCEPTION(resolve(TrackingMode::Sync), std::invalid_argument, + [](const auto& error) { return std::string(error.what()).find("TGeo") != std::string::npos; }); + ConfigurableParam::updateFromString("MFTCATrackerParam.useMatCorrTGeo=false;MFTCATrackerParam.useFastMaterial=false"); + BOOST_CHECK_EXCEPTION(resolve(TrackingMode::Sync), std::invalid_argument, + [](const auto& error) { return std::string(error.what()).find("LUT") != std::string::npos; }); + for (const auto kind : {SurfaceKind::Cylinder, SurfaceKind::Disk}) { + BOOST_CHECK(materialCorrectionModeSupport(kind, MatCorr::USEMatCorrNONE) == MaterialCorrectionModeSupport::Supported); + BOOST_CHECK(materialCorrectionModeSupport(kind, MatCorr::USEMatCorrLUT) == MaterialCorrectionModeSupport::Unsupported); + BOOST_CHECK(materialCorrectionModeSupport(kind, MatCorr::USEMatCorrTGeo) == MaterialCorrectionModeSupport::Unsupported); + } +} + +BOOST_FIXTURE_TEST_CASE(ParserOuterLayerMasksReachTheResolvedRoadStarts, RestoreConfiguration) +{ + const DetectorLayout layout{kMFTStaticSurfaceCatalog}; + for (const auto mode : {TrackingMode::Sync, TrackingMode::Async}) { + for (const uint32_t mask : {uint32_t{1} << 8, uint32_t{1} << 9, (uint32_t{1} << 8) | (uint32_t{1} << 9)}) { + ConfigurableParam::updateFromString("MFTCATrackerParam.startLayerMask[0]=" + std::to_string(mask)); + BOOST_CHECK_EQUAL(MFTParam::Instance().startLayerMask[0], mask); + const auto topology = deriveTraversalTopology(layout, resolve(mode).front()); + BOOST_REQUIRE(topology.ok()); + LayerMask actual; + for (const auto path : topology.topology->roadStartPaths) { + const auto edge = topology.topology->paths[path.value()].second; + actual.set(topology.topology->edges[edge.value()].to.value()); + } + BOOST_CHECK_EQUAL(actual.value(), mask); + } + } + ConfigurableParam::updateFromString("MFTCATrackerParam.startLayerMask[0]=1024"); + BOOST_CHECK_THROW(resolve(TrackingMode::Async), std::invalid_argument); + ConfigurableParam::updateFromString("MFTCATrackerParam.startLayerMask[0]=0"); + BOOST_CHECK_EQUAL(resolve(TrackingMode::Sync).front().StartLayerMask.count(), MFTNLayers); + auto* dictionary = TClass::GetClass(typeid(MFTParam)); + BOOST_REQUIRE(dictionary); + auto* member = dictionary->GetDataMember("startLayerMask"); + BOOST_REQUIRE(member); + BOOST_CHECK_EQUAL(member->GetArrayDim(), 1); + BOOST_CHECK_EQUAL(member->GetMaxIndex(0), MaxIter); + BOOST_CHECK_EQUAL(member->GetUnitSize(), sizeof(uint32_t)); +} + +BOOST_AUTO_TEST_CASE(DormantMFTOverridesFailWithTheirPublicNames) +{ + const std::array, 8> overrides{{{"printMemory=true", "printMemory=false"}, + {"saveTimeBenchmarks=true", "saveTimeBenchmarks=false"}, + {"fataliseUponFailure=false", "fataliseUponFailure=true"}, + {"deltaTanLres=0.01", "deltaTanLres=-1"}, + {"doUPCIteration=true", "doUPCIteration=false"}, + {"overrideBeamEstimation=true", "overrideBeamEstimation=false"}, + {"useDiamond=false", "useDiamond=true"}, + {"perPrimaryVertexProcessing=true", "perPrimaryVertexProcessing=false"}}}; + for (const auto& [unsupported, reset] : overrides) { + const std::string key = std::string{"MFTCATrackerParam."} + unsupported; + ConfigurableParam::updateFromString(key); + const auto namesField = [&key](const std::invalid_argument& error) { + return std::string{error.what()}.find(key.substr(0, key.find('='))) != std::string::npos; + }; + BOOST_CHECK_EXCEPTION(TrackingMode::validateCommonCAOptions(o2::detectors::DetID::MFT), std::invalid_argument, namesField); + BOOST_CHECK_EXCEPTION(resolve(TrackingMode::Sync), std::invalid_argument, namesField); + ConfigurableParam::updateFromString(std::string{"MFTCATrackerParam."} + reset); + } + BOOST_CHECK_NO_THROW(resolve(TrackingMode::Sync)); +} + +BOOST_AUTO_TEST_CASE(DormantITSDiagnosticOverridesFailBeforePresetConstruction) +{ + for (const auto* field : {"printMemory", "saveTimeBenchmarks"}) { + const std::string key = std::string{"ITSCommonCATrackerParam."} + field; + ConfigurableParam::updateFromString(key + "=true"); + BOOST_CHECK_EXCEPTION(TrackingMode::validateCommonCAOptions(o2::detectors::DetID::ITS), std::invalid_argument, + [&key](const auto& error) { return std::string{error.what()}.find(key) != std::string::npos; }); + BOOST_CHECK_THROW(o2::itsmft::tracking::test::referenceTrackingParameters(o2::detectors::DetID::ITS, TrackingMode::Sync), std::invalid_argument); + ConfigurableParam::updateFromString(key + "=false"); + } + BOOST_CHECK_NO_THROW(o2::itsmft::tracking::test::referenceTrackingParameters(o2::detectors::DetID::ITS, TrackingMode::Sync)); +} + +BOOST_AUTO_TEST_CASE(PublicMFTIndexBinsControlRadiusAndPhiLookup) +{ + ConfigurableParam::updateFromString("MFTCATrackerParam.LUTbinsU=32;MFTCATrackerParam.LUTbinsV=24"); + const auto parameters = resolve(TrackingMode::Sync).front(); + std::array ranges; + ranges.fill({0.f, 16.f}); + IndexTableUtilsCore index; + BOOST_REQUIRE(bindIndexTableConfiguration(index, parameters, MFTNLayers, SurfaceKind::Disk, ranges) == IndexTableConfigError::None); + BOOST_CHECK(index.getCoordType() == IndexTableCoordType::PhiR); + BOOST_CHECK_EQUAL(index.getRowBinIndex(o2::constants::math::PI), 12); + BOOST_CHECK_EQUAL(index.getColBinIndex(0, 8.f), 16); + ConfigurableParam::updateFromString("MFTCATrackerParam.LUTbinsU=64;MFTCATrackerParam.LUTbinsV=128"); +} + +BOOST_AUTO_TEST_CASE(AsyncOnlyOverridesAreRejectedInOtherActiveModes) +{ + for (const auto* field : {"minTrackLgtIter[0]", "minPtIterLgt[0]"}) { + const std::string key = std::string{"MFTCATrackerParam."} + field; + ConfigurableParam::updateFromString(key + "=5"); + BOOST_CHECK_NO_THROW(resolve(TrackingMode::Async)); + for (const auto mode : {TrackingMode::Sync, TrackingMode::Cosmics}) { + BOOST_CHECK_EXCEPTION(resolve(mode), std::invalid_argument, + [&key](const auto& error) { return std::string{error.what()}.find(key.substr(0, key.find('['))) != std::string::npos; }); + } + ConfigurableParam::updateFromString(key + "=0"); + } +} diff --git a/Detectors/ITSMFT/common/tracking/test/testMFTNormalizedRefit.cxx b/Detectors/ITSMFT/common/tracking/test/testMFTNormalizedRefit.cxx new file mode 100644 index 0000000000000..e4c35b67c5170 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testMFTNormalizedRefit.cxx @@ -0,0 +1,539 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// Focused normalized-measurement authority and covariance coverage for the +// descriptor-driven seed refit path. + +#define BOOST_TEST_MODULE ITSMFT MFTNormalizedRefit +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ITSMFTTracking/RefitDriver.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/Constants.h" +#include "MFTTracking/Constants.h" + +using namespace o2::itsmft::tracking; + +namespace +{ + +constexpr int NLayers = o2::mft::constants::mft::LayersNumber; +// Field-off exercises the native linear propagation model deterministically. +constexpr float Bz = 0.f; +constexpr float DefaultSigma2 = 2.5e-7f; // (~0.5 micron)^2, MFT-scale resolution + +SurfaceTrackState makeDiskRefitStateFixture( + const SurfaceMeasurement& inner, const SurfaceMeasurement& outer, + float trackletMinPt) +{ + const float dx = outer.frame.u - inner.frame.u; + const float dy = outer.frame.v - inner.frame.v; + const float transverseLength = std::hypot(dx, dy); + const float qOverPt = trackletMinPt > 0.f ? 1.f / trackletMinPt : 0.f; + + SurfaceTrackState state{}; + state.referenceCoordinate = outer.frame.q; + state.parameters[0] = outer.frame.u; + state.parameters[1] = outer.frame.v; + state.parameters[2] = std::atan2(dy, dx); + state.parameters[3] = (outer.frame.q - inner.frame.q) / transverseLength; + state.parameters[4] = qOverPt; + state.covariance[packedCovarianceIndex(0, 0)] = outer.covariance.uu; + state.covariance[packedCovarianceIndex(1, 0)] = outer.covariance.uv; + state.covariance[packedCovarianceIndex(1, 1)] = outer.covariance.vv; + state.covariance[packedCovarianceIndex(2, 2)] = 1.f; + state.covariance[packedCovarianceIndex(3, 3)] = 1.f; + const float qOverPtSigma = std::clamp(std::abs(qOverPt), 1.f, 10.f); + state.covariance[packedCovarianceIndex(4, 4)] = qOverPtSigma * qOverPtSigma; + state.kind = SurfaceKind::Disk; + state.absCharge = 1; + state.pid = o2::track::PID::Pion; + return state; +} + +// A straight track through every MFT disk. +struct StraightTrackGeometry { + std::array x{}; + std::array y{}; + std::array z{}; + float xSlope{}; + + explicit StraightTrackGeometry(float slope) : xSlope(slope) + { + const auto zLayer = o2::mft::constants::mft::LayerZCoordinate(); + const float z0 = zLayer[0]; + for (int layer = 0; layer < NLayers; ++layer) { + z[layer] = zLayer[layer]; + x[layer] = 1.f + xSlope * (z[layer] - z0); + y[layer] = 0.5f - 0.006f * (z[layer] - z0); + } + } +}; + +// Owns one normalized refit fixture. +struct RefitFixture { + std::array, NLayers> storage; + std::array, NLayers> globalStorage; + std::vector> layerGlobals = std::vector>(NLayers); + std::vector catalogSurfaces; + SurfaceCatalogView catalog{}; + TimeFrame frame; + TrackSeed seed; + o2::itsmft::TrackingParameters params; + int nHitLayers{0}; + + explicit RefitFixture(const StraightTrackGeometry& geometry, int hits = NLayers) + : nHitLayers(hits) + { + params.MinTrackLength = 5; + params.MinPt.assign(NLayers + 1, 0.f); + params.MaxChi2NDF = 30.f; + + catalogSurfaces.resize(NLayers); + for (int layer = 0; layer < NLayers; ++layer) { + catalogSurfaces[layer].detectorSurfaceIndex = static_cast(layer); + catalogSurfaces[layer].kind = SurfaceKind::Disk; + catalogSurfaces[layer].material = NominalSurfaceMaterial{0.f, 0.f}; + } + catalog = SurfaceCatalogView{catalogSurfaces.data(), static_cast(catalogSurfaces.size())}; + BOOST_REQUIRE(frame.configure(DetectorLayout{catalogSurfaces, makeDetectorLayout()}, 0, 0, + std::make_shared())); + + uint16_t mask = 0; + for (int layer = 0; layer < hits; ++layer) { + setMeasurement(layer, geometry.x[layer], geometry.y[layer], geometry.z[layer], + DefaultSigma2, DefaultSigma2); + seed.getClusters()[layer] = 0; + mask |= static_cast(uint16_t(1) << layer); + } + seed.setHitLayerMask(LayerMask{mask}); + + // The native driver starts from the CA seed state. + const int innerLayer = 0; + const int outerLayer = hits - 1; + seed.state() = makeDiskRefitStateFixture( + storage[innerLayer][0], storage[outerLayer][0], params.TrackletMinPt); + } + + void setMeasurement(int layer, float x, float y, float z, float uu, float vv, float uv = 0.f) + { + SurfaceMeasurement m{}; + // Disk measurements propagate to frame.q, their global z coordinate. + m.frame = {z, x, y, 0.f}; + m.covariance.uu = uu; + m.covariance.vv = vv; + m.covariance.uv = uv; + GlobalMeasurement global{}; + global.position = {x, y, z}; + global.radius = std::hypot(x, y); + global.covariance = {uu, uv, 0.f, vv, 0.f, 0.f}; + global.clusterId = 0u; + storage[layer].assign(1, m); + globalStorage[layer].assign(1, global); + layerGlobals[layer] = globalStorage[layer]; + } + + void syncFrame() + { + frame.resetTimeFrame(); + for (int layer = 0; layer < NLayers; ++layer) { + for (std::size_t cluster = 0; cluster < globalStorage[layer].size(); ++cluster) { + frame.addMeasurement(LayerId{static_cast(layer)}, globalStorage[layer][cluster], + storage[layer][cluster]); + } + } + } +}; + +bool refit(RefitFixture& fixture, TrackingCandidate& candidate) +{ + fixture.syncFrame(); + SurfaceTrackState innerState{}; + SurfaceTrackState outerState{}; + float chi2 = 0.f; + OperationFailureReason reason{}; + if (!fitTrackSeedLegs(fixture.seed, fixture.frame, fixture.layerGlobals, fixture.catalog, Bz, + fixture.params.ShiftRefToCluster, fixture.params.MaxChi2ClusterAttachment, + fixture.params.MaxChi2NDF, fixture.params.RepeatRefitOut, + gsl::span(fixture.params.MinPt), + innerState, outerState, chi2, reason)) { + return false; + } + candidate.seed = fixture.seed; + candidate.track.innerState = innerState; + candidate.track.outerState = outerState; + candidate.track.chi2 = chi2; + return true; +} + +void checkTrackUnchanged(const TrackingCandidate& before, const TrackingCandidate& after) +{ + BOOST_CHECK_EQUAL(before.seed.getHitLayerMask().value(), after.seed.getHitLayerMask().value()); + for (int position = 0; position < TrackSeed::MaxSurfaces; ++position) { + BOOST_CHECK_EQUAL(before.seed.getCluster(position), after.seed.getCluster(position)); + } + for (int i = 0; i < 5; ++i) { + BOOST_CHECK_EQUAL(before.track.innerState.parameters[i], after.track.innerState.parameters[i]); + BOOST_CHECK_EQUAL(before.track.outerState.parameters[i], after.track.outerState.parameters[i]); + } + for (int i = 0; i < 15; ++i) { + BOOST_CHECK_EQUAL(before.track.innerState.covariance[i], after.track.innerState.covariance[i]); + BOOST_CHECK_EQUAL(before.track.outerState.covariance[i], after.track.outerState.covariance[i]); + } + BOOST_CHECK_EQUAL(before.track.innerState.referenceCoordinate, after.track.innerState.referenceCoordinate); + BOOST_CHECK_EQUAL(before.track.outerState.referenceCoordinate, after.track.outerState.referenceCoordinate); + BOOST_CHECK_EQUAL(static_cast(before.track.innerState.kind), static_cast(after.track.innerState.kind)); + BOOST_CHECK_EQUAL(static_cast(before.track.outerState.kind), static_cast(after.track.outerState.kind)); + BOOST_CHECK_EQUAL(before.track.chi2, after.track.chi2); + BOOST_CHECK_EQUAL(before.phi, after.phi); + BOOST_CHECK_EQUAL(before.eta, after.eta); + BOOST_CHECK_EQUAL(before.charge, after.charge); +} + +} // namespace + +// --- Normalized data drives the output -------------------------------------- + +BOOST_AUTO_TEST_CASE(NormalizedGlobalCoordinateChangeAltersOutput) +{ + const StraightTrackGeometry geometry(0.3f); + + RefitFixture reference(geometry); + TrackingCandidate referenceTrack; + BOOST_REQUIRE(refit(reference, referenceTrack)); + + // Perturb only the normalized global.x of one interior layer -- legacy + // backfill is absent (never populated) in both fixtures, so this isolates + // the normalized measurement as the sole cause of the changed outcome. The + // shift is far larger than DefaultSigma2's resolution, so the previously + // ~0 chi2/ndf now certainly exceeds MaxChi2NDF. + RefitFixture perturbed(geometry); + auto perturbedMeasurement = perturbed.storage[5].front(); + perturbedMeasurement.frame.u += 0.05f; + perturbed.storage[5].assign(1, perturbedMeasurement); + + TrackingCandidate perturbedTrack; + const bool perturbedOk = refit(perturbed, perturbedTrack); + BOOST_CHECK(!perturbedOk); +} + +BOOST_AUTO_TEST_CASE(NormalizedCovarianceChangeAltersOutput) +{ + const StraightTrackGeometry geometry(0.3f); + + RefitFixture reference(geometry); + TrackingCandidate referenceTrack; + BOOST_REQUIRE(refit(reference, referenceTrack)); + + // Scale up every layer's diagonal covariance uniformly (legacy backfill + // again absent in both fixtures): with exact-colinear points the fitted + // position/chi2 are unaffected, but the posterior parameter covariance the + // Kalman filter propagates is not -- a strictly larger measurement variance + // must not shrink the output covariance. This is a generic Kalman-filter + // property, unaffected by which per-hit update formula produces it. + RefitFixture loose(geometry); + for (int layer = 0; layer < NLayers; ++layer) { + auto m = loose.storage[layer].front(); + m.covariance.uu *= 400.f; + m.covariance.vv *= 400.f; + loose.storage[layer].assign(1, m); + } + TrackingCandidate looseTrack; + BOOST_REQUIRE(refit(loose, looseTrack)); + + BOOST_CHECK_GT(looseTrack.track.outerState.covariance[packedCovarianceIndex(0, 0)], + referenceTrack.track.outerState.covariance[packedCovarianceIndex(0, 0)]); + BOOST_CHECK_GT(looseTrack.track.outerState.covariance[packedCovarianceIndex(1, 1)], + referenceTrack.track.outerState.covariance[packedCovarianceIndex(1, 1)]); +} + +// --- C. Invalid normalized input fails cleanly, destination untouched ------- + +BOOST_AUTO_TEST_CASE(NonFiniteSurfaceCoordinateFailsCleanly) +{ + const StraightTrackGeometry geometry(0.3f); + RefitFixture fx(geometry); + auto m = fx.storage[3].front(); + m.frame.u = std::numeric_limits::quiet_NaN(); + fx.storage[3].assign(1, m); + + TrackingCandidate before; + TrackingCandidate track = before; + BOOST_CHECK(!refit(fx, track)); + checkTrackUnchanged(before, track); +} + +BOOST_AUTO_TEST_CASE(InfiniteSurfaceCoordinateFailsCleanly) +{ + const StraightTrackGeometry geometry(0.3f); + RefitFixture fx(geometry); + auto m = fx.storage[3].front(); + m.frame.q = std::numeric_limits::infinity(); + fx.storage[3].assign(1, m); + + TrackingCandidate before; + TrackingCandidate track = before; + BOOST_CHECK(!refit(fx, track)); + checkTrackUnchanged(before, track); +} + +BOOST_AUTO_TEST_CASE(NonFiniteCovarianceFailsCleanly) +{ + const StraightTrackGeometry geometry(0.3f); + RefitFixture fx(geometry); + auto m = fx.storage[3].front(); + m.covariance.uu = std::numeric_limits::quiet_NaN(); + fx.storage[3].assign(1, m); + + TrackingCandidate before; + TrackingCandidate track = before; + BOOST_CHECK(!refit(fx, track)); + checkTrackUnchanged(before, track); +} + +BOOST_AUTO_TEST_CASE(NegativeCovarianceFailsCleanly) +{ + const StraightTrackGeometry geometry(0.3f); + RefitFixture fx(geometry); + auto m = fx.storage[3].front(); + m.covariance.vv = -1.f; + fx.storage[3].assign(1, m); + + TrackingCandidate before; + TrackingCandidate track = before; + BOOST_CHECK(!refit(fx, track)); + checkTrackUnchanged(before, track); +} + +BOOST_AUTO_TEST_CASE(OutOfRangeClusterIndexFailsCleanly) +{ + const StraightTrackGeometry geometry(0.3f); + RefitFixture fx(geometry); + fx.seed.getClusters()[3] = 99; // storage[3] only ever has one element (index 0) + + TrackingCandidate before; + TrackingCandidate track = before; + BOOST_CHECK(!refit(fx, track)); + checkTrackUnchanged(before, track); +} + +BOOST_AUTO_TEST_CASE(InvalidClusterRefFailsCleanly) +{ + const StraightTrackGeometry geometry(0.3f); + RefitFixture fx(geometry); + auto m = fx.globalStorage[3].front(); + m.clusterId = std::numeric_limits::max(); + fx.globalStorage[3].assign(1, m); + fx.layerGlobals[3] = fx.globalStorage[3]; + + TrackingCandidate before; + TrackingCandidate track = before; + BOOST_CHECK(!refit(fx, track)); + checkTrackUnchanged(before, track); +} + +BOOST_AUTO_TEST_CASE(RefitRejectsInvalidSurfaceCountsWithoutChangingOutput) +{ + RefitFixture fixture(StraightTrackGeometry{0.3f}); + fixture.syncFrame(); + for (const std::size_t count : {std::size_t{0}, std::size_t{MaxLayoutSurfaces + 1}}) { + std::vector> layers(count); + TrackingCandidate before; + before.track.innerState = fixture.seed.state(); + before.track.outerState = fixture.seed.state(); + before.track.chi2 = 123.f; + auto after = before; + OperationFailureReason reason{}; + BOOST_CHECK(!fitTrackSeedLegs(fixture.seed, fixture.frame, layers, fixture.catalog, Bz, + fixture.params.ShiftRefToCluster, fixture.params.MaxChi2ClusterAttachment, + fixture.params.MaxChi2NDF, true, fixture.params.MinPt, + after.track.innerState, after.track.outerState, after.track.chi2, reason)); + BOOST_CHECK(reason == OperationFailureReason::InvalidSurfaceCatalogAssociation); + checkTrackUnchanged(before, after); + } +} + +BOOST_AUTO_TEST_CASE(RefitBufferHandlesMaximumLayoutAndRepeatedLegs) +{ + RefitFixture fixture(StraightTrackGeometry{0.3f}); + for (const bool repeat : {false, true}) { + fixture.params.RepeatRefitOut = repeat; + fixture.layerGlobals.resize(NLayers); + TrackingCandidate compact; + BOOST_REQUIRE(refit(fixture, compact)); + // Additional absent surfaces must neither overflow the bounded buffer + // nor retain a measurement from the preceding refit leg. + fixture.layerGlobals.resize(MaxLayoutSurfaces); + TrackingCandidate maximum; + BOOST_REQUIRE(refit(fixture, maximum)); + checkTrackUnchanged(compact, maximum); + } +} + +// --- D. Preservation --------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(PreservesSeedMembershipForGenericRefit) +{ + const StraightTrackGeometry geometry(0.3f); + // Holes at layers 2 and 7: MinTrackLength(5) <= 8 remaining hits. + RefitFixture fx(geometry); + fx.seed.getClusters()[2] = o2::its::constants::UnusedIndex; + fx.seed.getClusters()[7] = o2::its::constants::UnusedIndex; + LayerMask mask = fx.seed.getHitLayerMask(); + mask.reset(2); + mask.reset(7); + fx.seed.setHitLayerMask(mask); + + TrackingCandidate track; + BOOST_REQUIRE(refit(fx, track)); + + BOOST_CHECK_EQUAL(track.getNumberOfClusters(), NLayers - 2); + for (int layer = 0; layer < NLayers; ++layer) { + if (layer == 2 || layer == 7) { + BOOST_CHECK_EQUAL(track.getClusterIndex(layer), o2::its::constants::UnusedIndex); + BOOST_CHECK(!track.seed.hasCluster(layer)); + } else { + BOOST_CHECK_EQUAL(track.getClusterIndex(layer), 0); + BOOST_CHECK(track.seed.hasCluster(layer)); + } + } +} + +// The native update uses the full uu/uv/vv measurement covariance. +BOOST_AUTO_TEST_CASE(OffDiagonalCovarianceIsUsedByNativeUpdate) +{ + const StraightTrackGeometry geometry(0.3f); + + RefitFixture reference(geometry); + TrackingCandidate referenceTrack; + BOOST_REQUIRE(refit(reference, referenceTrack)); + + RefitFixture withUv(geometry); + // A generous per-hit/per-track chi2 gate: this test's goal is only to + // prove a physically valid off-diagonal correlation changes the native + // update's output, not to probe chi2-gate behavior -- a nonzero + // correlation legitimately raises the predicted chi2 against a reference + // fit tuned for the uncorrelated (uv == 0) case. + withUv.params.MaxChi2ClusterAttachment = 1.e4f; + withUv.params.MaxChi2NDF = 1.e4f; + for (int layer = 0; layer < NLayers; ++layer) { + auto m = withUv.storage[layer].front(); + // A modest, physically valid correlation (|coefficient| << 1) suffices + // to prove the point. + m.covariance.uv = 0.05f * std::sqrt(m.covariance.uu * m.covariance.vv); + withUv.storage[layer].assign(1, m); + } + TrackingCandidate withUvTrack; + BOOST_REQUIRE(refit(withUv, withUvTrack)); + + BOOST_CHECK_NE(withUvTrack.track.outerState.covariance[packedCovarianceIndex(0, 0)], + referenceTrack.track.outerState.covariance[packedCovarianceIndex(0, 0)]); +} + +// --- Regression: stable pre-sort seed-cluster identity --------------------- + +BOOST_AUTO_TEST_CASE(GenericRefitUsesStablePreSortClusterIdentity) +{ + // Every hit layer has sorted seed index zero pointing at pre-sort cluster + // ID one. The generic refit must use that stable ID to retrieve the matching + // SurfaceMeasurement, rather than treating the sorted position as the ID. + const StraightTrackGeometry geometry(0.3f); + std::array, NLayers> storage; + std::array, NLayers> globalStorage; + std::vector> layerGlobals = std::vector>(NLayers); + std::vector catalogSurfaces(NLayers); + for (int layer = 0; layer < NLayers; ++layer) { + catalogSurfaces[layer].kind = SurfaceKind::Disk; + catalogSurfaces[layer].material = NominalSurfaceMaterial{0.f, 0.f}; + } + SurfaceCatalogView catalog{catalogSurfaces.data(), static_cast(catalogSurfaces.size())}; + TrackSeed seed; + o2::itsmft::TrackingParameters params; + params.MinTrackLength = 5; + params.MinPt.assign(NLayers + 1, 0.f); + params.MaxChi2NDF = 30.f; + + uint16_t mask = 0; + for (int layer = 0; layer < NLayers; ++layer) { + SurfaceMeasurement m{}; + m.frame = {geometry.z[layer], geometry.x[layer], geometry.y[layer], 0.f}; + m.covariance.uu = DefaultSigma2; + m.covariance.vv = DefaultSigma2; + auto distractor = m; + distractor.covariance.uu = std::numeric_limits::quiet_NaN(); + GlobalMeasurement global{}; + global.position = {geometry.x[layer], geometry.y[layer], geometry.z[layer]}; + global.radius = std::hypot(geometry.x[layer], geometry.y[layer]); + global.covariance = {DefaultSigma2, 0.f, 0.f, DefaultSigma2, 0.f, 0.f}; + global.clusterId = 1u; + auto distractorGlobal = global; + distractorGlobal.position.x += 100.f; + distractorGlobal.radius = std::hypot(distractorGlobal.position.x, distractorGlobal.position.y); + distractorGlobal.clusterId = 0u; + storage[layer] = {distractor, m}; + globalStorage[layer] = {global, distractorGlobal}; + layerGlobals[layer] = globalStorage[layer]; + + seed.getClusters()[layer] = 0; + mask |= static_cast(uint16_t(1) << layer); + } + seed.setHitLayerMask(LayerMask{mask}); + + seed.state() = makeDiskRefitStateFixture( + storage[0][1], storage[NLayers - 1][1], params.TrackletMinPt); + + TrackingCandidate track; + std::vector> globals(NLayers); + std::vector> measurements(NLayers); + for (int layer = 0; layer < NLayers; ++layer) { + globals[layer] = globalStorage[layer]; + measurements[layer] = storage[layer]; + } + TimeFrame frame; + BOOST_REQUIRE(frame.configure(DetectorLayout{catalogSurfaces, makeDetectorLayout()}, 0, 0, + std::make_shared())); + for (int layer = 0; layer < NLayers; ++layer) { + for (std::size_t cluster = 0; cluster < globals[layer].size(); ++cluster) { + frame.addMeasurement(LayerId{static_cast(layer)}, globals[layer][cluster], + measurements[layer][cluster]); + } + } + SurfaceTrackState innerState{}; + SurfaceTrackState outerState{}; + float chi2 = 0.f; + OperationFailureReason reason{}; + BOOST_REQUIRE(fitTrackSeedLegs(seed, frame, layerGlobals, catalog, Bz, + params.ShiftRefToCluster, params.MaxChi2ClusterAttachment, params.MaxChi2NDF, + params.RepeatRefitOut, gsl::span(params.MinPt), + innerState, outerState, chi2, reason)); + track.seed = seed; + track.track.innerState = innerState; + track.track.outerState = outerState; + track.track.chi2 = chi2; + + for (int layer = 0; layer < NLayers; ++layer) { + BOOST_CHECK(track.seed.hasCluster(layer)); + BOOST_CHECK_EQUAL(track.getClusterIndex(layer), 0); + BOOST_CHECK_EQUAL(layerGlobals[layer][0].clusterId, 1u); + } +} diff --git a/Detectors/ITSMFT/common/tracking/test/testMaterialPhysics.cxx b/Detectors/ITSMFT/common/tracking/test/testMaterialPhysics.cxx new file mode 100644 index 0000000000000..e2ce56bb787c8 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testMaterialPhysics.cxx @@ -0,0 +1,626 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE ITSMFTMaterialPhysics +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include +#include +#include +#include +#include + +#include "CommonConstants/MathConstants.h" +#include "ITSMFTTracking/MaterialPhysics.h" +#include "ReconstructionDataFormats/PID.h" +#include "ReconstructionDataFormats/TrackParametrization.h" +#include "ReconstructionDataFormats/TrackUtils.h" + +namespace +{ +using namespace o2::itsmft::tracking::material; +using o2::track::PID; + +constexpr float AbsTol = 1.e-5f; +constexpr float RelTol = 5.e-4f; + +bool closeTo(float a, float b, float absTol = AbsTol, float relTol = RelTol) +{ + const float diff = std::fabs(a - b); + return diff <= absTol || diff <= relTol * std::fabs(b); +} + +// Reference copies of the production-private Highland/straggling constants, +// used only to build the double-precision oracle below. Retained here as +// characterization/reference evidence; not production arithmetic. +constexpr double kHighlandConst2 = 0.0136 * 0.0136; +constexpr double kStragglingConst = 0.0007; +constexpr float kMinMomentumGeV = 0.01f; + +// Higher-precision (double) replica of the accepted capped-substep +// algorithm. This independently re-derives, at double precision, the exact +// sequence of operations the float production kernel performs, and serves +// only as test-side characterization/reference evidence -- it is never +// linked into or used by production code. +struct Oracle { + double momentumAfterGeV{}; + double signedEnergyChangeGeV{}; + double highlandTheta2Rad2{}; + double relativeInverseMomentumVariance{}; + uint8_t substeps{0}; + bool requestedAboveCap{false}; + bool stopped{false}; + bool nonFinite{false}; +}; + +Oracle referenceCharged(double p0, double mass, double absCharge, double xOverX0, double arealDensity, + bool alongMomentum) +{ + Oracle oracle{}; + const double q2 = absCharge * absCharge; + const double e0 = std::sqrt(p0 * p0 + mass * mass); + const double beta2 = (p0 * p0) / (e0 * e0); + + double e = e0; + double p = p0; + + if (arealDensity > 0.) { + const double ekin = e0 - mass; + const double bg0 = p0 / mass; + const double dedx0 = o2::track::BetheBlochSolidOpt(bg0) * q2; + const double fullStepLoss = dedx0 * arealDensity; + const double ratio = std::fabs(fullStepLoss) / ekin * o2::track::ELoss2EKinThreshInv; + if (!std::isfinite(ratio) || ratio >= static_cast(o2::track::MaxELossIter)) { + oracle.substeps = static_cast(o2::track::MaxELossIter); + oracle.requestedAboveCap = true; + } else { + oracle.substeps = static_cast(1 + static_cast(ratio)); + } + const double arealDensityStep = arealDensity / static_cast(oracle.substeps); + for (uint8_t i = 0; i < oracle.substeps; ++i) { + const double bg = p / mass; + const double dedx = o2::track::BetheBlochSolidOpt(bg) * q2; + const double dE = dedx * arealDensityStep; + e = alongMomentum ? (e - dE) : (e + dE); + if (!std::isfinite(e)) { + oracle.nonFinite = true; + break; + } + if (e <= mass) { + oracle.stopped = true; + break; + } + p = std::sqrt(e * e - mass * mass); + if (!std::isfinite(p)) { + oracle.nonFinite = true; + break; + } + } + } + + oracle.momentumAfterGeV = p; + oracle.signedEnergyChangeGeV = e - e0; + oracle.highlandTheta2Rad2 = (xOverX0 > 0.) ? (kHighlandConst2 / (beta2 * p0 * p0) * xOverX0 * q2) : 0.; + oracle.relativeInverseMomentumVariance = (oracle.signedEnergyChangeGeV != 0.) + ? (kStragglingConst * kStragglingConst * std::fabs(oracle.signedEnergyChangeGeV) * e0 * e0 / (p0 * p0 * p0 * p0)) + : 0.; + return oracle; +} + +void expectDeterministicFailure(const MaterialOperationResult& result, MaterialFailureReason reason, float momentumGeV) +{ + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.failure == reason); + if (std::isnan(momentumGeV)) { + BOOST_CHECK(std::isnan(result.momentumBeforeGeV)); + } else { + BOOST_CHECK_EQUAL(result.momentumBeforeGeV, momentumGeV); + } + BOOST_CHECK_EQUAL(result.momentumAfterGeV, 0.f); + BOOST_CHECK_EQUAL(result.signedEnergyChangeGeV, 0.f); + BOOST_CHECK_EQUAL(result.highlandTheta2Rad2, 0.f); + BOOST_CHECK_EQUAL(result.relativeInverseMomentumVariance, 0.f); + BOOST_CHECK_EQUAL(result.energyLossSubsteps, 0); + BOOST_CHECK(result.flags == MaterialOperationFlags::None); + BOOST_CHECK_EQUAL(result.reserved, 0); +} +} // namespace + +BOOST_AUTO_TEST_CASE(RepresentationLayout) +{ + static_assert(std::is_standard_layout_v); + static_assert(std::is_trivially_copyable_v); + static_assert(sizeof(IntegratedMaterialBudget) == 8); + static_assert(alignof(IntegratedMaterialBudget) == 4); + + static_assert(std::is_standard_layout_v); + static_assert(std::is_trivially_copyable_v); + static_assert(sizeof(MaterialOperationResult) == 24); + static_assert(alignof(MaterialOperationResult) == 4); + + static_assert(sizeof(MaterialTraversalDirection) == 1); + static_assert(sizeof(MaterialFailureReason) == 1); + static_assert(sizeof(MaterialOperationFlags) == 1); + + // Lock the exact numeric values already reported as part of the reviewed + // API, even though the enums are not yet a durable serialized/device ABI. + static_assert(static_cast(MaterialTraversalDirection::AlongMomentum) == 0); + static_assert(static_cast(MaterialTraversalDirection::OppositeMomentum) == 1); + + static_assert(static_cast(MaterialFailureReason::None) == 0); + static_assert(static_cast(MaterialFailureReason::SourceSurfaceKindMismatch) == 1); + static_assert(static_cast(MaterialFailureReason::NonFiniteState) == 2); + static_assert(static_cast(MaterialFailureReason::InvalidStateKinematics) == 3); + static_assert(static_cast(MaterialFailureReason::InvalidPID) == 4); + static_assert(static_cast(MaterialFailureReason::ChargedMasslessPID) == 5); + static_assert(static_cast(MaterialFailureReason::InvalidDirection) == 6); + static_assert(static_cast(MaterialFailureReason::InvalidMaterial) == 7); + static_assert(static_cast(MaterialFailureReason::StoppedInMaterial) == 8); + static_assert(static_cast(MaterialFailureReason::MomentumBelowMinimum) == 9); + static_assert(static_cast(MaterialFailureReason::ExcessiveScattering) == 10); + static_assert(static_cast(MaterialFailureReason::InvalidCovariance) == 11); + static_assert(static_cast(MaterialFailureReason::NonFiniteResult) == 12); + + static_assert(static_cast(MaterialOperationFlags::None) == 0); + static_assert(static_cast(MaterialOperationFlags::SubstepCountClamped) == 1); + + BOOST_CHECK(true); +} + +BOOST_AUTO_TEST_CASE(EveryValidPidIdNeutralSucceeds) +{ + IntegratedMaterialBudget material{0.01f, 0.1f}; + for (uint8_t id = 0; id < PID::NIDsTot; ++id) { + PID pid(static_cast(id)); + auto result = calculateMaterialPhysics(1.f, pid, 0, MaterialTraversalDirection::AlongMomentum, material); + BOOST_CHECK_MESSAGE(result.ok(), "PID id " << static_cast(id) << " failed with reason " << static_cast(result.failure)); + BOOST_CHECK_EQUAL(result.momentumAfterGeV, 1.f); + BOOST_CHECK_EQUAL(result.energyLossSubsteps, 0); + } +} + +BOOST_AUTO_TEST_CASE(EveryValidMassivePidIdChargedSucceeds) +{ + IntegratedMaterialBudget material{0.01f, 0.05f}; + for (uint8_t id = 0; id < PID::NIDsTot; ++id) { + PID pid(static_cast(id)); + if (pid.getMass() == 0.f) { + continue; // massless PIDs are covered by ChargedMasslessRejection below + } + auto result = calculateMaterialPhysics(2.f, pid, 1, MaterialTraversalDirection::AlongMomentum, material); + BOOST_CHECK_MESSAGE(result.ok(), "PID id " << static_cast(id) << " failed with reason " << static_cast(result.failure)); + } +} + +BOOST_AUTO_TEST_CASE(InvalidPidIdsRejectedBeforeMassLookup) +{ + IntegratedMaterialBudget material{0.f, 0.f}; + for (uint8_t id : {static_cast(PID::NIDsTot), static_cast(255)}) { + PID pid(static_cast(id)); + auto neutral = calculateMaterialPhysics(1.f, pid, 0, MaterialTraversalDirection::AlongMomentum, material); + expectDeterministicFailure(neutral, MaterialFailureReason::InvalidPID, 1.f); + auto charged = calculateMaterialPhysics(1.f, pid, 1, MaterialTraversalDirection::AlongMomentum, material); + expectDeterministicFailure(charged, MaterialFailureReason::InvalidPID, 1.f); + } +} + +BOOST_AUTO_TEST_CASE(PidAndChargeAreIndependent) +{ + // PID::Electron has a nominal charge of 1 in the PID table, but absCharge + // is supplied independently and must be the only source of q^2 scaling. + IntegratedMaterialBudget material{0.05f, 0.f}; + auto q1 = calculateMaterialPhysics(2.f, PID::Electron, 1, MaterialTraversalDirection::AlongMomentum, material); + auto q2result = calculateMaterialPhysics(2.f, PID::Electron, 2, MaterialTraversalDirection::AlongMomentum, material); + BOOST_REQUIRE(q1.ok()); + BOOST_REQUIRE(q2result.ok()); + // Highland variance scales with absCharge^2, independent of PID::getCharge(). + BOOST_CHECK(closeTo(q2result.highlandTheta2Rad2, 4.f * q1.highlandTheta2Rad2)); +} + +BOOST_AUTO_TEST_CASE(NeutralMassiveAndMasslessAccepted) +{ + IntegratedMaterialBudget material{0.2f, 5.f}; + for (PID pid : {PID(PID::K0), PID(PID::Photon)}) { + auto result = calculateMaterialPhysics(3.f, pid, 0, MaterialTraversalDirection::OppositeMomentum, material); + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(result.momentumAfterGeV, 3.f); + BOOST_CHECK_EQUAL(result.signedEnergyChangeGeV, 0.f); + BOOST_CHECK_EQUAL(result.highlandTheta2Rad2, 0.f); + BOOST_CHECK_EQUAL(result.relativeInverseMomentumVariance, 0.f); + BOOST_CHECK_EQUAL(result.energyLossSubsteps, 0); + } +} + +BOOST_AUTO_TEST_CASE(ChargedMasslessRejected) +{ + IntegratedMaterialBudget material{0.f, 0.f}; + for (uint8_t absCharge : {1, 2, 3, 255}) { + auto result = calculateMaterialPhysics(1.f, PID::Photon, absCharge, MaterialTraversalDirection::AlongMomentum, material); + expectDeterministicFailure(result, MaterialFailureReason::ChargedMasslessPID, 1.f); + } +} + +BOOST_AUTO_TEST_CASE(AbsChargeVariantsScaleHighlandQuadratically) +{ + IntegratedMaterialBudget material{0.03f, 0.f}; // MCS-only: isolates the charge scaling. + auto base = calculateMaterialPhysics(1.5f, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material); + BOOST_REQUIRE(base.ok()); + for (uint8_t absCharge : {2, 3, 200}) { + auto result = calculateMaterialPhysics(1.5f, PID::Pion, absCharge, MaterialTraversalDirection::AlongMomentum, material); + BOOST_REQUIRE(result.ok()); + const float expectedRatio = static_cast(absCharge) * static_cast(absCharge); + BOOST_CHECK(closeTo(result.highlandTheta2Rad2, expectedRatio * base.highlandTheta2Rad2)); + } +} + +BOOST_AUTO_TEST_CASE(DirectionInvalidCastRejected) +{ + IntegratedMaterialBudget material{0.f, 0.f}; + for (uint8_t raw : {2, 255}) { + auto direction = static_cast(raw); + auto result = calculateMaterialPhysics(1.f, PID::Pion, 1, direction, material); + expectDeterministicFailure(result, MaterialFailureReason::InvalidDirection, 1.f); + } +} + +BOOST_AUTO_TEST_CASE(ValidationPrecedenceWithCombinedInvalidInputs) +{ + const auto badDirection = static_cast(255); + const IntegratedMaterialBudget badMaterial{0.1f, -1.f}; + const IntegratedMaterialBudget goodMaterial{0.f, 0.f}; + const float badMomentum = -1.f; + const float goodMomentum = 1.f; + const PID badPid(static_cast(255)); + const PID goodMasslessPid = PID::Photon; + + // 1. invalid direction wins over invalid material/momentum/PID. + auto r1 = calculateMaterialPhysics(badMomentum, badPid, 1, badDirection, badMaterial); + BOOST_CHECK(r1.failure == MaterialFailureReason::InvalidDirection); + + // 2. invalid material wins over invalid momentum/PID. + auto r2 = calculateMaterialPhysics(badMomentum, badPid, 1, MaterialTraversalDirection::AlongMomentum, badMaterial); + BOOST_CHECK(r2.failure == MaterialFailureReason::InvalidMaterial); + + // 3. invalid momentum wins over invalid PID. + auto r3 = calculateMaterialPhysics(badMomentum, badPid, 1, MaterialTraversalDirection::AlongMomentum, goodMaterial); + BOOST_CHECK(r3.failure == MaterialFailureReason::MomentumBelowMinimum); + + // 4. invalid PID wins over charged-massless inspection: an unresolvable + // id must surface InvalidPID, never attempting the mass lookup that + // ChargedMasslessPID depends on. + auto r4 = calculateMaterialPhysics(goodMomentum, badPid, 1, MaterialTraversalDirection::AlongMomentum, goodMaterial); + BOOST_CHECK(r4.failure == MaterialFailureReason::InvalidPID); + + // Control: same absCharge/direction/material/momentum, but a valid + // massless PID -- confirms r4 is really about id validity winning over + // the charged-massless inspection, not some unrelated mismatch. + auto r4Control = calculateMaterialPhysics(goodMomentum, goodMasslessPid, 1, MaterialTraversalDirection::AlongMomentum, goodMaterial); + BOOST_CHECK(r4Control.failure == MaterialFailureReason::ChargedMasslessPID); +} + +BOOST_AUTO_TEST_CASE(MaterialFieldsMustBeNonNegative) +{ + const std::vector invalidMaterials = { + {-1.f, 0.1f}, {0.1f, -1.f}, {-1.f, -1.f}}; + for (auto material : invalidMaterials) { + auto result = calculateMaterialPhysics(1.f, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material); + expectDeterministicFailure(result, MaterialFailureReason::InvalidMaterial, 1.f); + } +} + +BOOST_AUTO_TEST_CASE(MomentumMustBePositive) +{ + IntegratedMaterialBudget material{0.f, 0.f}; + for (float momentum : {0.f, -1.f}) { + auto result = calculateMaterialPhysics(momentum, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material); + expectDeterministicFailure(result, MaterialFailureReason::MomentumBelowMinimum, momentum); + } +} + +BOOST_AUTO_TEST_CASE(ZeroMaterialIsAPassThrough) +{ + IntegratedMaterialBudget material{0.f, 0.f}; + auto result = calculateMaterialPhysics(1.f, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material); + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(result.momentumAfterGeV, 1.f); + BOOST_CHECK_EQUAL(result.signedEnergyChangeGeV, 0.f); + BOOST_CHECK_EQUAL(result.highlandTheta2Rad2, 0.f); + BOOST_CHECK_EQUAL(result.relativeInverseMomentumVariance, 0.f); + BOOST_CHECK_EQUAL(result.energyLossSubsteps, 0); + BOOST_CHECK(result.flags == MaterialOperationFlags::None); +} + +BOOST_AUTO_TEST_CASE(McsOnlyMaterialMatchesAnalyticHighland) +{ + const float p0 = 2.f; + const float mass = PID(PID::Pion).getMass(); + IntegratedMaterialBudget material{0.05f, 0.f}; + auto result = calculateMaterialPhysics(p0, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material); + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(result.momentumAfterGeV, p0); + BOOST_CHECK_EQUAL(result.signedEnergyChangeGeV, 0.f); + BOOST_CHECK_EQUAL(result.energyLossSubsteps, 0); + BOOST_CHECK_EQUAL(result.relativeInverseMomentumVariance, 0.f); + + const double e0 = std::sqrt(static_cast(p0) * p0 + static_cast(mass) * mass); + const double beta2 = (static_cast(p0) * p0) / (e0 * e0); + const double expectedTheta2 = kHighlandConst2 / (beta2 * p0 * p0) * material.xOverX0; + BOOST_CHECK(closeTo(result.highlandTheta2Rad2, static_cast(expectedTheta2))); +} + +BOOST_AUTO_TEST_CASE(EnergyLossOnlyMaterialProducesNoScattering) +{ + IntegratedMaterialBudget material{0.f, 0.02f}; + auto result = calculateMaterialPhysics(2.f, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material); + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(result.highlandTheta2Rad2, 0.f); + BOOST_CHECK_LT(result.momentumAfterGeV, 2.f); + BOOST_CHECK_LT(result.signedEnergyChangeGeV, 0.f); + BOOST_CHECK_GT(result.energyLossSubsteps, 0); + BOOST_CHECK_GT(result.relativeInverseMomentumVariance, 0.f); +} + +BOOST_AUTO_TEST_CASE(CombinedMaterialMatchesOracle) +{ + const float p0 = 1.2f; + const PID pid = PID::Kaon; + const uint8_t absCharge = 1; + IntegratedMaterialBudget material{0.04f, 0.03f}; + auto result = calculateMaterialPhysics(p0, pid, absCharge, MaterialTraversalDirection::AlongMomentum, material); + BOOST_REQUIRE(result.ok()); + + auto oracle = referenceCharged(p0, pid.getMass(), absCharge, material.xOverX0, material.arealDensityGPerCm2, true); + BOOST_CHECK(!oracle.stopped && !oracle.nonFinite); + BOOST_CHECK_EQUAL(result.energyLossSubsteps, oracle.substeps); + BOOST_CHECK(closeTo(result.momentumAfterGeV, static_cast(oracle.momentumAfterGeV))); + BOOST_CHECK(closeTo(result.signedEnergyChangeGeV, static_cast(oracle.signedEnergyChangeGeV))); + BOOST_CHECK(closeTo(result.highlandTheta2Rad2, static_cast(oracle.highlandTheta2Rad2))); + BOOST_CHECK(closeTo(result.relativeInverseMomentumVariance, static_cast(oracle.relativeInverseMomentumVariance))); +} + +BOOST_AUTO_TEST_CASE(LossAndGainHaveOppositeSignedEnergyChange) +{ + const float p0 = 1.5f; + IntegratedMaterialBudget material{0.f, 0.005f}; // small enough to stay single-substep + auto loss = calculateMaterialPhysics(p0, PID::Proton, 1, MaterialTraversalDirection::AlongMomentum, material); + auto gain = calculateMaterialPhysics(p0, PID::Proton, 1, MaterialTraversalDirection::OppositeMomentum, material); + BOOST_REQUIRE(loss.ok()); + BOOST_REQUIRE(gain.ok()); + BOOST_CHECK_EQUAL(loss.energyLossSubsteps, 1); + BOOST_CHECK_EQUAL(gain.energyLossSubsteps, 1); + BOOST_CHECK_LT(loss.signedEnergyChangeGeV, 0.f); + BOOST_CHECK_GT(gain.signedEnergyChangeGeV, 0.f); + BOOST_CHECK(closeTo(loss.signedEnergyChangeGeV, -gain.signedEnergyChangeGeV, AbsTol, 1.e-2f)); + BOOST_CHECK_LT(loss.momentumAfterGeV, p0); + BOOST_CHECK_GT(gain.momentumAfterGeV, p0); +} + +BOOST_AUTO_TEST_CASE(SubstepCountsAcrossRange) +{ + const float p0 = 1.f; + const PID pid = PID::Proton; + const double mass = pid.getMass(); + const double e0 = std::sqrt(static_cast(p0) * p0 + mass * mass); + const double ekin = e0 - mass; + const double bg0 = p0 / mass; + const double dedx0 = o2::track::BetheBlochSolidOpt(bg0); + + auto arealDensityForRatio = [&](double ratio) { + return ratio * ekin / (o2::track::ELoss2EKinThreshInv * dedx0); + }; + + // na = 1 + floor(ratio); choose ratio well inside each unit interval. + const struct { + double ratio; + uint8_t expectedSubsteps; + bool expectedClamped; + } cases[] = { + {0.3, 1, false}, + {5.5, 6, false}, + {48.9, 49, false}, + {49.5, 50, false}, // na = 50, must NOT be reported as clamped + {60.0, 50, true}, + {1.e6, 50, true}, + }; + + // OppositeMomentum (energy gain) is used deliberately: it isolates the + // substep-count bookkeeping from the (physically legitimate) risk that a + // large requested ratio also represents more energy loss than the + // particle's kinetic energy can absorb, which is covered separately by + // the StoppingIsDetected test. + for (const auto& c : cases) { + IntegratedMaterialBudget material{0.f, static_cast(arealDensityForRatio(c.ratio))}; + auto result = calculateMaterialPhysics(p0, pid, 1, MaterialTraversalDirection::OppositeMomentum, material); + BOOST_REQUIRE_MESSAGE(result.ok(), "unexpected failure " << static_cast(result.failure) << " for ratio " << c.ratio); + BOOST_CHECK_EQUAL(result.energyLossSubsteps, c.expectedSubsteps); + if (c.expectedClamped) { + BOOST_CHECK(result.flags == MaterialOperationFlags::SubstepCountClamped); + } else { + BOOST_CHECK(result.flags == MaterialOperationFlags::None); + } + + auto oracle = referenceCharged(p0, mass, 1., 0., material.arealDensityGPerCm2, false); + BOOST_REQUIRE(!oracle.stopped && !oracle.nonFinite); + BOOST_CHECK(closeTo(result.momentumAfterGeV, static_cast(oracle.momentumAfterGeV))); + } +} + +BOOST_AUTO_TEST_CASE(ClampedSubstepsStillProcessCompleteArealDensity) +{ + // Use OppositeMomentum (energy gain) so a very large ratio clamps the + // substep count without stopping the particle, letting us verify the + // full arealDensityGPerCm2 was processed across exactly 50 substeps. + const float p0 = 1.f; + const PID pid = PID::Proton; + IntegratedMaterialBudget material{0.f, 500.f}; + auto result = calculateMaterialPhysics(p0, pid, 1, MaterialTraversalDirection::OppositeMomentum, material); + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(result.energyLossSubsteps, o2::track::MaxELossIter); + BOOST_CHECK(result.flags == MaterialOperationFlags::SubstepCountClamped); + + auto oracle = referenceCharged(p0, pid.getMass(), 1., 0., material.arealDensityGPerCm2, false); + BOOST_REQUIRE(!oracle.stopped && !oracle.nonFinite); + BOOST_CHECK_EQUAL(oracle.substeps, o2::track::MaxELossIter); + BOOST_CHECK(closeTo(result.signedEnergyChangeGeV, static_cast(oracle.signedEnergyChangeGeV), AbsTol, 2.e-3f)); +} + +BOOST_AUTO_TEST_CASE(BetheBlochIsRecomputedPerSubstep) +{ + // A naive fixed-dedx-at-entry integration must differ measurably from the + // recompute-per-substep result once the momentum changes appreciably + // across the traversal. + const float p0 = 0.3f; + const PID pid = PID::Proton; + const double mass = pid.getMass(); + IntegratedMaterialBudget material{0.f, 1.f}; + auto result = calculateMaterialPhysics(p0, pid, 1, MaterialTraversalDirection::AlongMomentum, material); + BOOST_REQUIRE(result.ok()); + BOOST_REQUIRE_GT(result.energyLossSubsteps, 1); + + const double e0 = std::sqrt(static_cast(p0) * p0 + mass * mass); + const double bg0 = p0 / mass; + const double dedx0 = o2::track::BetheBlochSolidOpt(bg0); + const double naiveEnergyAfter = e0 - dedx0 * material.arealDensityGPerCm2; + + auto oracle = referenceCharged(p0, mass, 1., 0., material.arealDensityGPerCm2, true); + BOOST_REQUIRE(!oracle.stopped && !oracle.nonFinite); + const double recomputedEnergyAfter = e0 + oracle.signedEnergyChangeGeV; + + BOOST_CHECK(closeTo(result.signedEnergyChangeGeV, static_cast(oracle.signedEnergyChangeGeV))); + BOOST_CHECK_GT(std::fabs(recomputedEnergyAfter - naiveEnergyAfter), 1.e-4); +} + +BOOST_AUTO_TEST_CASE(StoppingIsDetected) +{ + IntegratedMaterialBudget material{0.f, 50.f}; // grossly exceeds a 0.5 GeV/c proton's kinetic energy + auto result = calculateMaterialPhysics(0.5f, PID::Proton, 1, MaterialTraversalDirection::AlongMomentum, material); + expectDeterministicFailure(result, MaterialFailureReason::StoppedInMaterial, 0.5f); +} + +BOOST_AUTO_TEST_CASE(FinalMomentumBoundary) +{ + IntegratedMaterialBudget material{0.f, 0.f}; // zero material: momentumAfter == momentumBefore exactly + auto atThreshold = calculateMaterialPhysics(kMinMomentumGeV, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material); + BOOST_REQUIRE(atThreshold.ok()); + BOOST_CHECK_EQUAL(atThreshold.momentumAfterGeV, kMinMomentumGeV); + + auto belowThreshold = calculateMaterialPhysics(std::nextafter(kMinMomentumGeV, 0.f), PID::Pion, 1, + MaterialTraversalDirection::AlongMomentum, material); + expectDeterministicFailure(belowThreshold, MaterialFailureReason::MomentumBelowMinimum, std::nextafter(kMinMomentumGeV, 0.f)); +} + +BOOST_AUTO_TEST_CASE(ExcessiveScatteringIsRejected) +{ + IntegratedMaterialBudget material{500.f, 0.f}; // absurdly thick, drives theta^2 past pi^2 + auto result = calculateMaterialPhysics(0.1f, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material); + expectDeterministicFailure(result, MaterialFailureReason::ExcessiveScattering, 0.1f); +} + +BOOST_AUTO_TEST_CASE(HugeFiniteArealDensityDeterministicallyStops) +{ + // 1e30 g/cm^2 is many orders of magnitude beyond what a 1 GeV/c proton's + // kinetic energy can absorb: even after the substep count clamps to 50 + // (since the requested count vastly exceeds it), the very first substep's + // energy loss drives the particle's energy far below its rest mass. This + // must terminate deterministically without any float-to-int UB in the + // substep-count calculation. + const float p0 = 1.f; + IntegratedMaterialBudget material{0.f, 1.e30f}; + auto result = calculateMaterialPhysics(p0, PID::Proton, 1, MaterialTraversalDirection::AlongMomentum, material); + expectDeterministicFailure(result, MaterialFailureReason::StoppedInMaterial, p0); + + auto repeat = calculateMaterialPhysics(p0, PID::Proton, 1, MaterialTraversalDirection::AlongMomentum, material); + BOOST_CHECK_EQUAL(std::memcmp(&result, &repeat, sizeof(MaterialOperationResult)), 0); +} + +BOOST_AUTO_TEST_CASE(DirectBetheBlochReferenceValue) +{ + const float p0 = 1.f; + const PID pid = PID::Proton; + const double mass = pid.getMass(); + IntegratedMaterialBudget material{0.f, 0.001f}; // small enough to guarantee a single substep + auto result = calculateMaterialPhysics(p0, pid, 1, MaterialTraversalDirection::AlongMomentum, material); + BOOST_REQUIRE(result.ok()); + BOOST_REQUIRE_EQUAL(result.energyLossSubsteps, 1); + + const double e0 = std::sqrt(static_cast(p0) * p0 + mass * mass); + const double bg0 = p0 / mass; + const double dedx = o2::track::BetheBlochSolidOpt(bg0); + const double expectedEnergyAfter = e0 - dedx * material.arealDensityGPerCm2; + const double expectedSignedChange = expectedEnergyAfter - e0; + BOOST_CHECK(closeTo(result.signedEnergyChangeGeV, static_cast(expectedSignedChange))); +} + +BOOST_AUTO_TEST_CASE(ChargeSquaredScalesSingleSubstepEnergyLoss) +{ + // Material thin enough that absCharge up to 3 (q^2 up to 9) still resolves + // to a single substep for every case below. PID::Electron's nominal + // PID::getCharge() is fixed at 1 regardless of absCharge, so any observed + // scaling with absCharge (not with PID::getCharge()) demonstrates that + // getCharge() is never consulted. + const float p0 = 1.f; + const PID pid = PID::Electron; + const double mass = pid.getMass(); + const IntegratedMaterialBudget material{0.f, 0.0001f}; + + const double e0 = std::sqrt(static_cast(p0) * p0 + mass * mass); + const double bg0 = p0 / mass; + const double dedxUnit = o2::track::BetheBlochSolidOpt(bg0); // reference dE/dx at q^2 = 1 + + float baseSignedChange = 0.f; + float baseVariance = 0.f; + for (uint8_t absCharge : {1, 2, 3}) { + auto result = calculateMaterialPhysics(p0, pid, absCharge, MaterialTraversalDirection::AlongMomentum, material); + BOOST_REQUIRE(result.ok()); + BOOST_REQUIRE_EQUAL(result.energyLossSubsteps, 1); + + const double q2 = static_cast(absCharge) * absCharge; + const double expectedDE = dedxUnit * q2 * material.arealDensityGPerCm2; + const double expectedEnergyAfter = e0 - expectedDE; + const double expectedSignedChange = expectedEnergyAfter - e0; + const double expectedMomentumAfter = std::sqrt(expectedEnergyAfter * expectedEnergyAfter - mass * mass); + const double expectedVariance = kStragglingConst * kStragglingConst * std::fabs(expectedSignedChange) * e0 * e0 / + (static_cast(p0) * p0 * p0 * p0); + + BOOST_CHECK(closeTo(result.signedEnergyChangeGeV, static_cast(expectedSignedChange))); + BOOST_CHECK(closeTo(result.momentumAfterGeV, static_cast(expectedMomentumAfter))); + BOOST_CHECK(closeTo(result.relativeInverseMomentumVariance, static_cast(expectedVariance))); + + if (absCharge == 1) { + baseSignedChange = result.signedEnergyChangeGeV; + baseVariance = result.relativeInverseMomentumVariance; + } else { + const float q2f = static_cast(absCharge) * static_cast(absCharge); + BOOST_CHECK(closeTo(result.signedEnergyChangeGeV, q2f * baseSignedChange)); + BOOST_CHECK(closeTo(result.relativeInverseMomentumVariance, q2f * baseVariance)); + } + } +} + +BOOST_AUTO_TEST_CASE(RepeatedCallsAreByteIdentical) +{ + IntegratedMaterialBudget material{0.03f, 0.02f}; + auto a = calculateMaterialPhysics(1.3f, PID::Kaon, 1, MaterialTraversalDirection::AlongMomentum, material); + auto b = calculateMaterialPhysics(1.3f, PID::Kaon, 1, MaterialTraversalDirection::AlongMomentum, material); + BOOST_CHECK_EQUAL(std::memcmp(&a, &b, sizeof(MaterialOperationResult)), 0); +} + +BOOST_AUTO_TEST_CASE(ReservedIsAlwaysZero) +{ + IntegratedMaterialBudget material{0.02f, 0.01f}; + auto success = calculateMaterialPhysics(1.f, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material); + BOOST_CHECK_EQUAL(success.reserved, 0); + auto failure = calculateMaterialPhysics(-1.f, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material); + BOOST_CHECK_EQUAL(failure.reserved, 0); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testMultiSourceLoading.cxx b/Detectors/ITSMFT/common/tracking/test/testMultiSourceLoading.cxx new file mode 100644 index 0000000000000..01dbcb5614c63 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testMultiSourceLoading.cxx @@ -0,0 +1,1316 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE ITSMFT MultiSourceLoading +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include +#include + +#include + +#include "CommonDataFormat/InteractionRecord.h" +#include "DataFormatsITSMFT/CompCluster.h" +#include "DataFormatsITSMFT/ROFRecord.h" +#include "DataFormatsITSMFT/TopologyDictionary.h" +#include "DetectorsCommonDataFormats/DetID.h" +#include "ITSMFTTracking/DetectorLayout.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/ClusterDecoding.h" +#include "SimulationDataFormat/MCCompLabel.h" +#include "SimulationDataFormat/MCTruthContainer.h" + +using namespace o2::itsmft; +using namespace o2::itsmft::tracking; + +namespace +{ + +// Host-only test decoder (no geometry singletons): maps a chip ID to a +// detector-local layer via an explicit table, and reuses the same pattern +// consumption path (extractClusterData) that the production decoder uses, +// so pattern-cursor bookkeeping is exercised identically. +enum class Corruption { + None, + NegativeLayer, + LayerOutOfRange +}; + +class FakeClusterDecoder final : public ClusterDecoder +{ + public: + FakeClusterDecoder(o2::detectors::DetID::ID detector, std::vector sensorToLayer, bool disk, Corruption corruption = Corruption::None) + : mDetector(detector), mSensorToLayer(std::move(sensorToLayer)), mDisk(disk), mCorruption(corruption) + { + } + + o2::itsmft::tracking::ClusterDecodeResult decode( + const CompClusterExt& cluster, + BoundedPatternCursor& patterns, + const TopologyDictionary* dict, + uint32_t, + bool) const override + { + if (mCorruption == Corruption::NegativeLayer) { + o2::itsmft::tracking::ClusterDecodeResult result; + result.decoded.layer = -1; + return result; + } + if (mCorruption == Corruption::LayerOutOfRange) { + o2::itsmft::tracking::ClusterDecodeResult result; + result.decoded.layer = std::numeric_limits::max(); + return result; + } + + const auto clusterData = o2::itsmft::ioutils::extractClusterDataBounded(cluster, patterns, dict); + if (!clusterData.ok()) { + o2::itsmft::tracking::ClusterDecodeResult result; + result.error = clusterData.error; + return result; + } + + o2::itsmft::tracking::ClusterDecodeResult result; + const auto sensorID = cluster.getSensorID(); + const int layer = (sensorID >= 0 && static_cast(sensorID) < mSensorToLayer.size()) ? mSensorToLayer[sensorID] : -1; + auto& decoded = result.decoded; + decoded.global = {static_cast(sensorID), static_cast(cluster.getRow()), static_cast(cluster.getCol())}; + decoded.cylinderFrame = {10.f + sensorID, 1.f, 2.f, 0.1f}; + decoded.rowColumnCovariance = {clusterData.sig2Row, 0.f, clusterData.sig2Col}; + decoded.shape = clusterData.shape; + decoded.layer = layer; + return result; + } + + private: + o2::detectors::DetID::ID mDetector; + std::vector mSensorToLayer; + bool mDisk; + Corruption mCorruption; +}; + +// Geometry-free decoder used only to exercise the normalized loader's +// dictionary/common/group/explicit pattern contract. Pattern ID 0 represents +// a common dictionary entry (no explicit bytes), pattern ID 1 represents a +// grouped dictionary entry (explicit bytes required), and InvalidPatternID +// represents an ordinary explicit pattern. +class PatternContractDecoder final : public ClusterDecoder +{ + public: + o2::itsmft::tracking::ClusterDecodeResult decode( + const CompClusterExt& cluster, + BoundedPatternCursor& patterns, + const TopologyDictionary* dictionary, + uint32_t, + bool) const override + { + o2::itsmft::tracking::ClusterDecodeResult result; + if (dictionary == nullptr) { + result.error = ClusterDecodeError::MissingDictionary; + return result; + } + ClusterShape shape{1, 1, 1}; + if (cluster.getPatternID() != 0) { + ClusterPattern pattern; + result.error = patterns.acquirePattern(pattern); + if (!result.ok()) { + return result; + } + shape = {static_cast(pattern.getNPixels()), + static_cast(pattern.getRowSpan()), + static_cast(pattern.getColumnSpan())}; + } + + auto& decoded = result.decoded; + decoded.global = {1.f, 2.f, 3.f}; + decoded.cylinderFrame = {4.f, 5.f, 6.f, 0.f}; + decoded.rowColumnCovariance = {0.1f, 0.f, 0.2f}; + decoded.shape = shape; + decoded.layer = 0; + return result; + } +}; + +struct BuiltLayout { + DetectorLayout layout; + std::vector surfaces; + + bool valid() const noexcept { return layout.valid(); } + SurfaceCatalogView getCatalog() const noexcept + { + return layout.getSurfaceCatalog(); + } +}; + +// 4-surface disconnected ITS(cylinder)+MFT(disk) layout: surfaces {0,1} are +// ITS layers 0/1, surfaces {2,3} are MFT layers 0/1. No edges are +// needed to exercise loading. +BuiltLayout makeCombinedLayout() +{ + std::vector surfaces; + surfaces.push_back(SurfaceDescriptor{0, static_cast(o2::detectors::DetID::ITS), SurfaceKind::Cylinder}); + surfaces.push_back(SurfaceDescriptor{1, static_cast(o2::detectors::DetID::ITS), SurfaceKind::Cylinder}); + surfaces.push_back(SurfaceDescriptor{0, static_cast(o2::detectors::DetID::MFT), SurfaceKind::Disk}); + surfaces.push_back(SurfaceDescriptor{1, static_cast(o2::detectors::DetID::MFT), SurfaceKind::Disk}); + DetectorLayoutDefinition definition; + definition.componentOffsets = {0, 2}; + return BuiltLayout{DetectorLayout{surfaces, std::move(definition)}, std::move(surfaces)}; +} + +void configureFrame(TimeFrame& frame, const BuiltLayout& built) +{ + DetectorLayoutDefinition definition; + const auto& layout = built.layout; + definition.componentOffsets.assign(layout.getComponentOffsets().begin(), layout.getComponentOffsets().end()); + definition.holeLayers = layout.getHoleLayers(); + const auto catalog = layout.getSurfaceCatalog(); + BOOST_REQUIRE(frame.configure(DetectorLayout{gsl::span{catalog.surfaces, catalog.nSurfaces}, + std::move(definition)}, + 0, 0, std::make_shared())); +} + +// One explicit (non-grouped) 1-pixel pattern: rowSpan=1, colSpan=1, one +// bitmap byte. Three bytes are consumed per cluster. +constexpr std::array onePixelPattern{1, 1, 0x80}; + +std::vector makePatternBytes(size_t nClusters) +{ + std::vector bytes; + bytes.reserve(nClusters * onePixelPattern.size()); + for (size_t i = 0; i < nClusters; ++i) { + bytes.insert(bytes.end(), onePixelPattern.begin(), onePixelPattern.end()); + } + return bytes; +} + +const TopologyDictionary& dict() +{ + static const TopologyDictionary d; + return d; +} + +const std::array itsLayerToSurface{LayerId{0}, LayerId{1}}; +const std::array mftLayerToSurface{LayerId{2}, LayerId{3}}; +const std::array firstITSSurface{LayerId{0}}; +const std::array secondITSSurface{LayerId{1}}; +const std::array firstMFTSurface{LayerId{2}}; + +} // namespace + +BOOST_AUTO_TEST_CASE(SingleITSSourceLoadsIntoExpectedSurfaces) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + const std::vector clusters{ + {10, 20, CompCluster::InvalidPatternID, 0}, // sensor 0 -> layer 0 + {11, 21, CompCluster::InvalidPatternID, 1}, // sensor 1 -> layer 1 + }; + const auto patterns = makePatternBytes(clusters.size()); + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 2}}; + + FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0, 1}, false}; + ClusterSourceInput src; + src.id = ClusterSourceId{0}; + src.detector = o2::detectors::DetID::ITS; + src.clusters = clusters; + src.patterns = patterns; + src.rofs = rofs; + src.dictionary = &dict(); + src.layerToSurface = itsLayerToSurface; + src.timing = ROFTimingConfig{40, 0, 0, 0}; + src.decoder = &decoder; + + TimeFrame frame; + configureFrame(frame, layout); + std::vector> externalIndicesBySurface; + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}, + &externalIndicesBySurface); + BOOST_REQUIRE(result.ok()); + // A success result must retain the timingDetail default: it is only ever + // meaningful when error == TimingError. + BOOST_CHECK(result.timingDetail == TimingBuildError::None); + + BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{0}).size(), 1u); + BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{1}).size(), 1u); + BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{2}).size(), 0u); + BOOST_CHECK_EQUAL(externalIndicesBySurface[0][0], 0u); +} + +BOOST_AUTO_TEST_CASE(InvalidTimingConfigurationIsReportedWithBuildErrorDetail) +{ + // computeROFIntervalBC()'s own exhaustive TimingBuildError coverage lives + // in testSurfaceTiming.cxx (InvalidROFLengthIsRejected, OverflowIsDetected + // AndChecked, InvalidSourceROFIsRejected); this test only proves that + // loadSources() actually plumbs that detail into LoadSourcesResult rather + // than discarding it. InvalidROFLength (rofLength <= 0) is the only one of + // the three practically reachable through loadSources() itself: + // InvalidSourceROF would require a source ROF count exceeding UINT32_MAX, + // and Overflow requires contrived BC values already covered directly at + // the computeROFIntervalBC() level. + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + const std::vector clusters{{1, 1, CompCluster::InvalidPatternID, 0}}; + const auto patterns = makePatternBytes(clusters.size()); + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; + + FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; + ClusterSourceInput src; + src.id = ClusterSourceId{0}; + src.detector = o2::detectors::DetID::ITS; + src.clusters = clusters; + src.patterns = patterns; + src.rofs = rofs; + src.dictionary = &dict(); + src.layerToSurface = itsLayerToSurface; + src.timing = ROFTimingConfig{0, 0, 0, 0}; // rofLength <= 0 + src.decoder = &decoder; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_CHECK(result.error == MultiSourceLoadError::TimingError); + BOOST_CHECK(result.timingDetail == TimingBuildError::InvalidROFLength); + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); +} + +BOOST_AUTO_TEST_CASE(SingleMFTSourceLoadsIntoExpectedSurfaces) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + const std::vector clusters{ + {5, 6, CompCluster::InvalidPatternID, 0}, + {7, 8, CompCluster::InvalidPatternID, 1}, + }; + const auto patterns = makePatternBytes(clusters.size()); + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 2}}; + + FakeClusterDecoder decoder{o2::detectors::DetID::MFT, {0, 1}, true}; + ClusterSourceInput src; + src.id = ClusterSourceId{0}; + src.detector = o2::detectors::DetID::MFT; + src.clusters = clusters; + src.patterns = patterns; + src.rofs = rofs; + src.dictionary = &dict(); + src.layerToSurface = mftLayerToSurface; + src.timing = ROFTimingConfig{40, 0, 0, 0}; + src.decoder = &decoder; + + TimeFrame frame; + configureFrame(frame, layout); + std::vector> externalIndicesBySurface; + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}, + &externalIndicesBySurface); + BOOST_REQUIRE(result.ok()); + + BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{2}).size(), 1u); + BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{3}).size(), 1u); + BOOST_CHECK_EQUAL(externalIndicesBySurface[2][0], 0u); +} + +BOOST_AUTO_TEST_CASE(CombinedITSAndMFTSourcesLoadTogether) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + const std::vector itsClusters{{1, 1, CompCluster::InvalidPatternID, 0}}; + const auto itsPatterns = makePatternBytes(itsClusters.size()); + const std::vector itsRofs{ROFRecord{{0, 0}, 0, 0, 1}}; + FakeClusterDecoder itsDecoder{o2::detectors::DetID::ITS, {0}, false}; + + const std::vector mftClusters{{2, 2, CompCluster::InvalidPatternID, 0}}; + const auto mftPatterns = makePatternBytes(mftClusters.size()); + const std::vector mftRofs{ROFRecord{{0, 0}, 0, 0, 1}}; + FakeClusterDecoder mftDecoder{o2::detectors::DetID::MFT, {1}, true}; // sensor 0 -> layer 1 -> surface 3 + + std::array sources{}; + sources[0].id = ClusterSourceId{0}; + sources[0].detector = o2::detectors::DetID::ITS; + sources[0].clusters = itsClusters; + sources[0].patterns = itsPatterns; + sources[0].rofs = itsRofs; + sources[0].dictionary = &dict(); + sources[0].layerToSurface = itsLayerToSurface; + sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; + sources[0].decoder = &itsDecoder; + + sources[1].id = ClusterSourceId{1}; + sources[1].detector = o2::detectors::DetID::MFT; + sources[1].clusters = mftClusters; + sources[1].patterns = mftPatterns; + sources[1].rofs = mftRofs; + sources[1].dictionary = &dict(); + sources[1].layerToSurface = mftLayerToSurface; + sources[1].timing = ROFTimingConfig{50, 0, 0, 0}; + sources[1].decoder = &mftDecoder; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); + BOOST_REQUIRE(result.ok()); + + BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{0}).size(), 1u); + BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{3}).size(), 1u); +} + +BOOST_AUTO_TEST_CASE(TwoSourcesCannotOwnTheSameSurface) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + const std::vector clustersA{{1, 1, CompCluster::InvalidPatternID, 0}}; + const std::vector clustersB{{2, 2, CompCluster::InvalidPatternID, 0}}; + const auto patternsA = makePatternBytes(clustersA.size()); + const auto patternsB = makePatternBytes(clustersB.size()); + const std::vector rofsA{ROFRecord{{0, 0}, 0, 0, 1}}; + const std::vector rofsB{ROFRecord{{0, 0}, 0, 0, 1}}; + + FakeClusterDecoder decoderA{o2::detectors::DetID::ITS, {0}, false}; + FakeClusterDecoder decoderB{o2::detectors::DetID::ITS, {0}, false}; + + std::array sources{}; + sources[0].id = ClusterSourceId{0}; + sources[0].detector = o2::detectors::DetID::ITS; + sources[0].clusters = clustersA; + sources[0].patterns = patternsA; + sources[0].rofs = rofsA; + sources[0].dictionary = &dict(); + sources[0].layerToSurface = itsLayerToSurface; + sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; + sources[0].decoder = &decoderA; + + sources[1].id = ClusterSourceId{1}; + sources[1].detector = o2::detectors::DetID::ITS; + sources[1].clusters = clustersB; + sources[1].patterns = patternsB; + sources[1].rofs = rofsB; + sources[1].dictionary = &dict(); + sources[1].layerToSurface = itsLayerToSurface; + sources[1].timing = ROFTimingConfig{40, 0, 0, 0}; + sources[1].decoder = &decoderB; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); + BOOST_CHECK(result.error == MultiSourceLoadError::InvalidLayerMapping); + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); +} + +BOOST_AUTO_TEST_CASE(IdenticalExternalIndicesInDifferentSourcesDoNotCollide) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + const std::vector clustersA{{1, 1, CompCluster::InvalidPatternID, 0}}; // external index 0 + const std::vector clustersB{{2, 2, CompCluster::InvalidPatternID, 0}}; // external index 0 too + const auto patternsA = makePatternBytes(clustersA.size()); + const auto patternsB = makePatternBytes(clustersB.size()); + const std::vector rofsA{ROFRecord{{0, 0}, 0, 0, 1}}; + const std::vector rofsB{ROFRecord{{0, 0}, 0, 0, 1}}; + + o2::dataformats::MCTruthContainer labelsA; + labelsA.addElement(0, o2::MCCompLabel{1, 0, 0}); + o2::dataformats::MCTruthContainer labelsB; + labelsB.addElement(0, o2::MCCompLabel{2, 0, 0}); + + FakeClusterDecoder decoderA{o2::detectors::DetID::ITS, {0}, false}; + FakeClusterDecoder decoderB{o2::detectors::DetID::ITS, {0}, false}; + + std::array sources{}; + sources[0].id = ClusterSourceId{0}; + sources[0].detector = o2::detectors::DetID::ITS; + sources[0].clusters = clustersA; + sources[0].patterns = patternsA; + sources[0].rofs = rofsA; + sources[0].dictionary = &dict(); + sources[0].labels = &labelsA; + sources[0].layerToSurface = firstITSSurface; + sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; + sources[0].decoder = &decoderA; + + sources[1].id = ClusterSourceId{1}; + sources[1].detector = o2::detectors::DetID::ITS; + sources[1].clusters = clustersB; + sources[1].patterns = patternsB; + sources[1].rofs = rofsB; + sources[1].dictionary = &dict(); + sources[1].labels = &labelsB; + sources[1].layerToSurface = secondITSSurface; + sources[1].timing = ROFTimingConfig{40, 0, 0, 0}; + sources[1].decoder = &decoderB; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); + BOOST_REQUIRE(result.ok()); + + const auto onSurfaceZero = frame.getGlobalMeasurements(LayerId{0}); + BOOST_REQUIRE_EQUAL(onSurfaceZero.size(), 1u); + BOOST_REQUIRE_EQUAL(frame.getGlobalMeasurements(LayerId{1}).size(), 1u); + BOOST_CHECK_EQUAL(onSurfaceZero[0].clusterId, 0u); + BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{1})[0].clusterId, 0u); + + const auto labelSpanA = frame.getLabels(LayerId{0}, 0); + const auto labelSpanB = frame.getLabels(LayerId{1}, 0); + BOOST_REQUIRE_EQUAL(labelSpanA.size(), 1u); + BOOST_REQUIRE_EQUAL(labelSpanB.size(), 1u); + BOOST_CHECK(labelSpanA[0] != labelSpanB[0]); +} + +BOOST_AUTO_TEST_CASE(OriginalClusterIdResolvesLabelsAndCompactGlobal) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + const std::vector clusters{{1, 1, CompCluster::InvalidPatternID, 0}}; + const auto patterns = makePatternBytes(clusters.size()); + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; + + o2::dataformats::MCTruthContainer labels; + labels.addElement(0, o2::MCCompLabel{1, 0, 0}); + + FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; + ClusterSourceInput src; + src.id = ClusterSourceId{0}; + src.detector = o2::detectors::DetID::ITS; + src.clusters = clusters; + src.patterns = patterns; + src.rofs = rofs; + src.dictionary = &dict(); + src.labels = &labels; + src.layerToSurface = itsLayerToSurface; + src.timing = ROFTimingConfig{40, 0, 0, 0}; + src.decoder = &decoder; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_REQUIRE(result.ok()); + + constexpr uint32_t clusterId = 0; + const auto labelPlain = frame.getLabels(LayerId{0}, clusterId); + BOOST_REQUIRE_EQUAL(labelPlain.size(), 1u); + + // The sorted global value carries only the stable source-local ID. + const auto measurement = frame.getGlobalMeasurements(LayerId{0})[0]; + BOOST_CHECK_EQUAL(measurement.clusterId, clusterId); +} + +BOOST_AUTO_TEST_CASE(IndependentROFCountsAcrossSourcesAreAllowed) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + // Source A: 3 ROFs of 1 cluster each. Source B: 1 ROF of 1 cluster. + const std::vector clustersA{ + {1, 1, CompCluster::InvalidPatternID, 0}, + {2, 2, CompCluster::InvalidPatternID, 0}, + {3, 3, CompCluster::InvalidPatternID, 0}}; + const auto patternsA = makePatternBytes(clustersA.size()); + const std::vector rofsA{ + ROFRecord{{0, 0}, 0, 0, 1}, + ROFRecord{{40, 0}, 1, 1, 1}, + ROFRecord{{80, 0}, 2, 2, 1}}; + + const std::vector clustersB{{4, 4, CompCluster::InvalidPatternID, 0}}; + const auto patternsB = makePatternBytes(clustersB.size()); + const std::vector rofsB{ROFRecord{{0, 0}, 0, 0, 1}}; + + FakeClusterDecoder decoderA{o2::detectors::DetID::ITS, {0}, false}; + FakeClusterDecoder decoderB{o2::detectors::DetID::ITS, {0}, false}; + + std::array sources{}; + sources[0].id = ClusterSourceId{0}; + sources[0].detector = o2::detectors::DetID::ITS; + sources[0].clusters = clustersA; + sources[0].patterns = patternsA; + sources[0].rofs = rofsA; + sources[0].dictionary = &dict(); + sources[0].layerToSurface = firstITSSurface; + sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; + sources[0].decoder = &decoderA; + + sources[1].id = ClusterSourceId{1}; + sources[1].detector = o2::detectors::DetID::ITS; + sources[1].clusters = clustersB; + sources[1].patterns = patternsB; + sources[1].rofs = rofsB; + sources[1].dictionary = &dict(); + sources[1].layerToSurface = secondITSSurface; + sources[1].timing = ROFTimingConfig{100, 0, 0, 0}; + sources[1].decoder = &decoderB; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); + BOOST_REQUIRE(result.ok()); + + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 4u); +} + +BOOST_AUTO_TEST_CASE(OverlappingAndNonOverlappingSourceTimingIntervals) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + const std::vector clustersA{{1, 1, CompCluster::InvalidPatternID, 0}}; + const std::vector clustersB{{2, 2, CompCluster::InvalidPatternID, 0}}; + const auto patternsA = makePatternBytes(clustersA.size()); + const auto patternsB = makePatternBytes(clustersB.size()); + // Source A ROF at BC 0..40 (TF-relative); source B ROF at real BC 30 -> its + // own interval overlaps A's despite a different, unrelated ROF ordinal. + const std::vector rofsA{ROFRecord{{0, 0}, 0, 0, 1}}; + const std::vector rofsB{ROFRecord{{30, 0}, 0, 0, 1}}; + // Source C ROF at real BC 1000: far away, must not overlap A. + const std::vector clustersC{{3, 3, CompCluster::InvalidPatternID, 0}}; + const auto patternsC = makePatternBytes(clustersC.size()); + const std::vector rofsC{ROFRecord{{1000, 0}, 0, 0, 1}}; + + FakeClusterDecoder decoderA{o2::detectors::DetID::ITS, {0}, false}; + FakeClusterDecoder decoderB{o2::detectors::DetID::ITS, {0}, false}; + FakeClusterDecoder decoderC{o2::detectors::DetID::MFT, {0}, true}; + + std::array sources{}; + sources[0].id = ClusterSourceId{0}; + sources[0].detector = o2::detectors::DetID::ITS; + sources[0].clusters = clustersA; + sources[0].patterns = patternsA; + sources[0].rofs = rofsA; + sources[0].dictionary = &dict(); + sources[0].layerToSurface = firstITSSurface; + sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; + sources[0].decoder = &decoderA; + + sources[1].id = ClusterSourceId{1}; + sources[1].detector = o2::detectors::DetID::ITS; + sources[1].clusters = clustersB; + sources[1].patterns = patternsB; + sources[1].rofs = rofsB; + sources[1].dictionary = &dict(); + sources[1].layerToSurface = secondITSSurface; + sources[1].timing = ROFTimingConfig{40, 0, 0, 0}; + sources[1].decoder = &decoderB; + + sources[2].id = ClusterSourceId{2}; + sources[2].detector = o2::detectors::DetID::MFT; + sources[2].clusters = clustersC; + sources[2].patterns = patternsC; + sources[2].rofs = rofsC; + sources[2].dictionary = &dict(); + sources[2].layerToSurface = firstMFTSurface; + sources[2].timing = ROFTimingConfig{40, 0, 0, 0}; + sources[2].decoder = &decoderC; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); + BOOST_REQUIRE(result.ok()); + + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 3u); +} + +BOOST_AUTO_TEST_CASE(TriggeredAndContinuousReadoutAreBothSupportedTogether) +{ + // Continuous source: ROFs sit at a fixed cadence equal to the readout + // length, so consecutive interval begins are regularly spaced by + // rofLength (mirrors a periodic strobe). Triggered source: ROFs sit at + // sparse, irregular real interaction records (individual triggers) with a + // short trigger-specific window, so consecutive interval begins follow the + // trigger BCs exactly rather than any ordinal*rofLength formula. Both must + // load into the same frame and their intervals must remain independently + // and correctly comparable via intersection. + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + const std::vector continuousClusters{ + {1, 1, CompCluster::InvalidPatternID, 0}, + {2, 2, CompCluster::InvalidPatternID, 0}, + {3, 3, CompCluster::InvalidPatternID, 0}}; + const auto continuousPatterns = makePatternBytes(continuousClusters.size()); + const std::vector continuousRofs{ + ROFRecord{{0, 0}, 0, 0, 1}, + ROFRecord{{40, 0}, 1, 1, 1}, + ROFRecord{{80, 0}, 2, 2, 1}}; + constexpr TFBC continuousRofLength = 40; + + const std::vector triggeredClusters{ + {4, 4, CompCluster::InvalidPatternID, 0}, + {5, 5, CompCluster::InvalidPatternID, 0}, + {6, 6, CompCluster::InvalidPatternID, 0}}; + const auto triggeredPatterns = makePatternBytes(triggeredClusters.size()); + // Sparse, irregular trigger BCs; a short single-BC-scale trigger window. + const std::vector triggeredRofs{ + ROFRecord{{5, 0}, 0, 0, 1}, + ROFRecord{{137, 0}, 1, 1, 1}, + ROFRecord{{812, 0}, 2, 2, 1}}; + constexpr TFBC triggeredRofLength = 4; + + FakeClusterDecoder continuousDecoder{o2::detectors::DetID::ITS, {0}, false}; + FakeClusterDecoder triggeredDecoder{o2::detectors::DetID::ITS, {0}, false}; + + std::array sources{}; + sources[0].id = ClusterSourceId{0}; + sources[0].detector = o2::detectors::DetID::ITS; + sources[0].clusters = continuousClusters; + sources[0].patterns = continuousPatterns; + sources[0].rofs = continuousRofs; + sources[0].dictionary = &dict(); + sources[0].layerToSurface = firstITSSurface; + sources[0].timing = ROFTimingConfig{continuousRofLength, 0, 0, 0}; + sources[0].decoder = &continuousDecoder; + + sources[1].id = ClusterSourceId{1}; + sources[1].detector = o2::detectors::DetID::ITS; + sources[1].clusters = triggeredClusters; + sources[1].patterns = triggeredPatterns; + sources[1].rofs = triggeredRofs; + sources[1].dictionary = &dict(); + sources[1].layerToSurface = secondITSSurface; + sources[1].timing = ROFTimingConfig{triggeredRofLength, 0, 0, 0}; + sources[1].decoder = &triggeredDecoder; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); + BOOST_REQUIRE(result.ok()); + + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 6u); +} + +BOOST_AUTO_TEST_CASE(SourceSpecificPatternCursorsAreIndependent) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + const std::vector clustersA{ + {1, 1, CompCluster::InvalidPatternID, 0}, + {2, 2, CompCluster::InvalidPatternID, 0}}; + const std::vector clustersB{ + {3, 3, CompCluster::InvalidPatternID, 0}, + {4, 4, CompCluster::InvalidPatternID, 0}}; + const auto patternsA = makePatternBytes(clustersA.size()); + const auto patternsB = makePatternBytes(clustersB.size()); + const std::vector rofsA{ROFRecord{{0, 0}, 0, 0, 2}}; + const std::vector rofsB{ROFRecord{{0, 0}, 0, 0, 2}}; + + FakeClusterDecoder decoderA{o2::detectors::DetID::ITS, {0}, false}; + FakeClusterDecoder decoderB{o2::detectors::DetID::ITS, {0}, false}; + + std::array sources{}; + sources[0].id = ClusterSourceId{0}; + sources[0].detector = o2::detectors::DetID::ITS; + sources[0].clusters = clustersA; + sources[0].patterns = patternsA; + sources[0].rofs = rofsA; + sources[0].dictionary = &dict(); + sources[0].layerToSurface = firstITSSurface; + sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; + sources[0].decoder = &decoderA; + + sources[1].id = ClusterSourceId{1}; + sources[1].detector = o2::detectors::DetID::ITS; + sources[1].clusters = clustersB; + sources[1].patterns = patternsB; + sources[1].rofs = rofsB; + sources[1].dictionary = &dict(); + sources[1].layerToSurface = secondITSSurface; + sources[1].timing = ROFTimingConfig{40, 0, 0, 0}; + sources[1].decoder = &decoderB; + + TimeFrame frame; + configureFrame(frame, layout); + std::vector> clusterSizesBySurface; + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}, + nullptr, &clusterSizesBySurface); + BOOST_REQUIRE(result.ok()); + + // Every cluster consumed exactly one 1-pixel pattern regardless of source. + for (const auto layer : {LayerId{0}, LayerId{1}}) { + for (const auto& m : frame.getGlobalMeasurements(layer)) { + BOOST_CHECK_EQUAL(clusterSizesBySurface[layer.value()][m.clusterId], 1u); + } + } +} + +BOOST_AUTO_TEST_CASE(CommonDictionaryPatternDoesNotConsumeExplicitBytes) +{ + const auto layout = makeCombinedLayout(); + const std::vector clusters{ + {1, 1, 0, 0}, // common dictionary pattern + {2, 2, CompCluster::InvalidPatternID, 0}}; // explicit pattern + const std::vector patterns{onePixelPattern.begin(), onePixelPattern.end()}; + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 2}}; + PatternContractDecoder decoder; + + ClusterSourceInput src; + src.id = ClusterSourceId{0}; + src.detector = o2::detectors::DetID::ITS; + src.clusters = clusters; + src.patterns = patterns; + src.rofs = rofs; + src.dictionary = &dict(); + src.layerToSurface = itsLayerToSurface; + src.timing = ROFTimingConfig{40, 0, 0, 0}; + src.decoder = &decoder; + + TimeFrame frame; + configureFrame(frame, layout); + std::vector> clusterSizesBySurface; + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}, + nullptr, &clusterSizesBySurface); + BOOST_REQUIRE(result.ok()); + BOOST_REQUIRE_EQUAL(frame.getGlobalMeasurements(LayerId{0}).size(), 2u); + BOOST_CHECK_EQUAL(clusterSizesBySurface[0][frame.getGlobalMeasurements(LayerId{0})[0].clusterId], 1u); + BOOST_CHECK_EQUAL(clusterSizesBySurface[0][frame.getGlobalMeasurements(LayerId{0})[1].clusterId], 1u); +} + +BOOST_AUTO_TEST_CASE(ExplicitAndGroupedPatternTruncationIsTypedAndContextual) +{ + const auto layout = makeCombinedLayout(); + PatternContractDecoder decoder; + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; + constexpr std::array encoded{3, 3, 0x80, 0x80}; + + for (const auto patternID : {CompCluster::InvalidPatternID, static_cast(1)}) { + const std::vector clusters{{1, 1, patternID, 0}}; + for (size_t available = 0; available < encoded.size(); ++available) { + ClusterSourceInput src; + src.id = ClusterSourceId{0}; + src.detector = o2::detectors::DetID::ITS; + src.clusters = clusters; + src.patterns = gsl::span{encoded.data(), available}; + src.rofs = rofs; + src.dictionary = &dict(); + src.layerToSurface = itsLayerToSurface; + src.timing = ROFTimingConfig{40, 0, 0, 0}; + src.decoder = &decoder; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_CHECK(result.error == MultiSourceLoadError::TruncatedExplicitPattern); + BOOST_CHECK(result.source == ClusterSourceId{0}); + BOOST_CHECK_EQUAL(result.rof, 0u); + BOOST_CHECK_EQUAL(result.clusterIndex, 0u); + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); + } + } + + const std::vector malformedClusters{{1, 1, CompCluster::InvalidPatternID, 0}}; + const std::array malformedPattern{0, 1}; + ClusterSourceInput malformedSource; + malformedSource.id = ClusterSourceId{0}; + malformedSource.detector = o2::detectors::DetID::ITS; + malformedSource.clusters = malformedClusters; + malformedSource.patterns = malformedPattern; + malformedSource.rofs = rofs; + malformedSource.dictionary = &dict(); + malformedSource.layerToSurface = itsLayerToSurface; + malformedSource.timing = ROFTimingConfig{40, 0, 0, 0}; + malformedSource.decoder = &decoder; + TimeFrame frame; + configureFrame(frame, layout); + const auto malformed = loadSources( + frame, layout.getCatalog(), + gsl::span(&malformedSource, 1), {0, 0}); + BOOST_CHECK(malformed.error == MultiSourceLoadError::MalformedExplicitPattern); + BOOST_CHECK_EQUAL(malformed.rof, 0u); + BOOST_CHECK_EQUAL(malformed.clusterIndex, 0u); +} + +BOOST_AUTO_TEST_CASE(ExactPatternConsumptionSucceedsAndTrailingBytesAreRejected) +{ + const auto layout = makeCombinedLayout(); + PatternContractDecoder decoder; + const std::vector clusters{{1, 1, CompCluster::InvalidPatternID, 0}}; + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; + + auto makeSource = [&](gsl::span patterns) { + ClusterSourceInput src; + src.id = ClusterSourceId{0}; + src.detector = o2::detectors::DetID::ITS; + src.clusters = clusters; + src.patterns = patterns; + src.rofs = rofs; + src.dictionary = &dict(); + src.layerToSurface = itsLayerToSurface; + src.timing = ROFTimingConfig{40, 0, 0, 0}; + src.decoder = &decoder; + return src; + }; + + const std::vector exact{onePixelPattern.begin(), onePixelPattern.end()}; + auto exactSource = makeSource(exact); + TimeFrame frame; + configureFrame(frame, layout); + BOOST_REQUIRE(loadSources(frame, layout.getCatalog(), gsl::span(&exactSource, 1), {0, 0}).ok()); + + const std::vector trailing{1, 1, 0x80, 0xff}; + auto trailingSource = makeSource(trailing); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&trailingSource, 1), {0, 0}); + BOOST_CHECK(result.error == MultiSourceLoadError::TrailingPatternData); + BOOST_CHECK_EQUAL(result.rof, 1u); + BOOST_CHECK_EQUAL(result.clusterIndex, 1u); + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); + + auto missingDictionarySource = makeSource(exact); + missingDictionarySource.dictionary = nullptr; + const auto missingDictionary = loadSources( + frame, layout.getCatalog(), + gsl::span(&missingDictionarySource, 1), {0, 0}); + BOOST_CHECK(missingDictionary.error == MultiSourceLoadError::MissingDictionary); + BOOST_CHECK(missingDictionary.source == ClusterSourceId{0}); + BOOST_CHECK_EQUAL(missingDictionary.rof, 0u); + BOOST_CHECK_EQUAL(missingDictionary.clusterIndex, 0u); + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); +} + +BOOST_AUTO_TEST_CASE(MissingDictionaryIsTypedBeforeProductionGeometryDecode) +{ + ITSGeometryClusterDecoder decoder; + const CompClusterExt cluster{1, 1, CompCluster::InvalidPatternID, 0}; + BoundedPatternCursor patterns{onePixelPattern}; + const auto decoded = decoder.decode(cluster, patterns, nullptr, 0, false); + BOOST_CHECK(decoded.error == ClusterDecodeError::MissingDictionary); + BOOST_CHECK_EQUAL(patterns.consumed(), 0u); +} + +BOOST_AUTO_TEST_CASE(AbsentLabelsAreLegal) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + const std::vector clusters{{1, 1, CompCluster::InvalidPatternID, 0}}; + const auto patterns = makePatternBytes(clusters.size()); + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; + + FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; + ClusterSourceInput src; + src.id = ClusterSourceId{0}; + src.detector = o2::detectors::DetID::ITS; + src.clusters = clusters; + src.patterns = patterns; + src.rofs = rofs; + src.dictionary = &dict(); + src.labels = nullptr; // no MC labels for this source + src.layerToSurface = itsLayerToSurface; + src.timing = ROFTimingConfig{40, 0, 0, 0}; + src.decoder = &decoder; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_REQUIRE(result.ok()); + + BOOST_CHECK(frame.getLabels(LayerId{0}, 0).empty()); + BOOST_CHECK(frame.getLabels(LayerId{}, 0).empty()); +} + +BOOST_AUTO_TEST_CASE(NonDenseAndDuplicateAndInvalidSourceIdsAreRejected) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + const std::vector clusters{{1, 1, CompCluster::InvalidPatternID, 0}}; + const auto patterns = makePatternBytes(clusters.size()); + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; + FakeClusterDecoder decoderA{o2::detectors::DetID::ITS, {0}, false}; + FakeClusterDecoder decoderB{o2::detectors::DetID::ITS, {0}, false}; + + auto makeSource = [&](ClusterSourceId id, FakeClusterDecoder& decoder) { + ClusterSourceInput src; + src.id = id; + src.detector = o2::detectors::DetID::ITS; + src.clusters = clusters; + src.patterns = patterns; + src.rofs = rofs; + src.dictionary = &dict(); + src.layerToSurface = itsLayerToSurface; + src.timing = ROFTimingConfig{40, 0, 0, 0}; + src.decoder = &decoder; + return src; + }; + + { + // Non-dense: ids {0, 2} for two sources. + std::array sources{makeSource(ClusterSourceId{0}, decoderA), makeSource(ClusterSourceId{2}, decoderB)}; + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.error == MultiSourceLoadError::NonDenseSourceIds); + } + { + // Duplicate ids {0, 0}. + std::array sources{makeSource(ClusterSourceId{0}, decoderA), makeSource(ClusterSourceId{0}, decoderB)}; + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.error == MultiSourceLoadError::DuplicateSourceId); + } + { + // Explicitly invalid id. + std::array sources{makeSource(ClusterSourceId::invalid(), decoderA)}; + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.error == MultiSourceLoadError::NonDenseSourceIds); + } +} + +BOOST_AUTO_TEST_CASE(InvalidROFClusterRangesAreRejected) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + const std::vector clusters{ + {1, 1, CompCluster::InvalidPatternID, 0}, + {2, 2, CompCluster::InvalidPatternID, 0}}; + const auto patterns = makePatternBytes(clusters.size()); + FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; + + auto makeSrc = [&](const std::vector& rofs) { + ClusterSourceInput src; + src.id = ClusterSourceId{0}; + src.detector = o2::detectors::DetID::ITS; + src.clusters = clusters; + src.patterns = patterns; + src.rofs = rofs; + src.dictionary = &dict(); + src.layerToSurface = itsLayerToSurface; + src.timing = ROFTimingConfig{40, 0, 0, 0}; + src.decoder = &decoder; + return src; + }; + + { + // Out of bounds: firstEntry+nEntries exceeds the cluster span. + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 5}}; + auto src = makeSrc(rofs); + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.error == MultiSourceLoadError::InvalidROFRange); + } + { + // Overlapping ranges. + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 2}, ROFRecord{{40, 0}, 1, 1, 1}}; + auto src = makeSrc(rofs); + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.error == MultiSourceLoadError::InvalidROFRange); + } + { + // Leading gap: first ROF does not begin at cluster index 0. + const std::vector rofs{ROFRecord{{0, 0}, 0, 1, 1}}; + auto src = makeSrc(rofs); + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.error == MultiSourceLoadError::InvalidROFRange); + } + { + // Internal gap: rof0 covers [0,1), rof1 covers [2,2) i.e. starts at 2 + // while only cluster index 1 is unreferenced in between (2 clusters + // total, so this leaves cluster 1 outside any ROF). + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}, ROFRecord{{40, 0}, 1, 2, 0}}; + auto src = makeSrc(rofs); + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.error == MultiSourceLoadError::InvalidROFRange); + } + { + // Trailing cluster: the ROFs cover only the first cluster, leaving the + // second cluster unreferenced by any ROF. + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; + auto src = makeSrc(rofs); + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.error == MultiSourceLoadError::InvalidROFRange); + } + { + // Clusters without ROFs: zero ROFs is only valid when clusters is also + // empty, but this source has two clusters. + const std::vector rofs{}; + auto src = makeSrc(rofs); + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.error == MultiSourceLoadError::InvalidROFRange); + } +} + +BOOST_AUTO_TEST_CASE(ZeroROFsIsValidWithZeroClusters) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + const std::vector clusters{}; + const std::vector patterns{}; + const std::vector rofs{}; + FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; + + ClusterSourceInput src; + src.id = ClusterSourceId{0}; + src.detector = o2::detectors::DetID::ITS; + src.clusters = clusters; + src.patterns = patterns; + src.rofs = rofs; + src.dictionary = &dict(); + src.layerToSurface = itsLayerToSurface; + src.timing = ROFTimingConfig{40, 0, 0, 0}; + src.decoder = &decoder; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_CHECK(result.ok()); + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); +} + +BOOST_AUTO_TEST_CASE(InvalidLayerToSurfaceMappingIsRejected) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + const std::vector clusters{{1, 1, CompCluster::InvalidPatternID, 1}}; // sensor 1 -> layer 1 + const auto patterns = makePatternBytes(clusters.size()); + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; + FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {-1, 1}, false}; + + ClusterSourceInput src; + src.id = ClusterSourceId{0}; + src.detector = o2::detectors::DetID::ITS; + src.clusters = clusters; + src.patterns = patterns; + src.rofs = rofs; + src.dictionary = &dict(); + src.layerToSurface = gsl::span(itsLayerToSurface.data(), 1); // too short: only covers layer 0 + src.timing = ROFTimingConfig{40, 0, 0, 0}; + src.decoder = &decoder; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.error == MultiSourceLoadError::InvalidLayerMapping); +} + +BOOST_AUTO_TEST_CASE(DetectorSurfaceMismatchIsRejected) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + const std::vector clusters{{1, 1, CompCluster::InvalidPatternID, 0}}; + const auto patterns = makePatternBytes(clusters.size()); + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; + FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; + + ClusterSourceInput src; + src.id = ClusterSourceId{0}; + src.detector = o2::detectors::DetID::ITS; + src.clusters = clusters; + src.patterns = patterns; + src.rofs = rofs; + src.dictionary = &dict(); + // Deliberately mapped to an MFT surface: ITS source, MFT surface. + const std::array wrongMapping{LayerId{2}}; + src.layerToSurface = wrongMapping; + src.timing = ROFTimingConfig{40, 0, 0, 0}; + src.decoder = &decoder; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.error == MultiSourceLoadError::DetectorSurfaceMismatch); +} + +BOOST_AUTO_TEST_CASE(UnsafeDecodedLayerIsRejected) +{ + // The loader validates the decoded detector-local layer before using it to + // index the authoritative layer-to-surface mapping. + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + const std::vector clusters{{1, 1, CompCluster::InvalidPatternID, 0}}; + const auto patterns = makePatternBytes(clusters.size()); + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; + + const std::array corruptions{ + Corruption::NegativeLayer, Corruption::LayerOutOfRange}; + for (const auto corruption : corruptions) { + FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false, corruption}; + ClusterSourceInput src; + src.id = ClusterSourceId{0}; + src.detector = o2::detectors::DetID::ITS; + src.clusters = clusters; + src.patterns = patterns; + src.rofs = rofs; + src.dictionary = &dict(); + src.layerToSurface = itsLayerToSurface; + src.timing = ROFTimingConfig{40, 0, 0, 0}; + src.decoder = &decoder; + + TimeFrame frame; + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.error == MultiSourceLoadError::InvalidLayerMapping); + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); + } +} + +BOOST_AUTO_TEST_CASE(FailedLoadLeavesNoPartialState) +{ + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + const std::vector clusters{{1, 1, CompCluster::InvalidPatternID, 0}}; + const auto patterns = makePatternBytes(clusters.size()); + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; + FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; + + ClusterSourceInput goodSrc; + goodSrc.id = ClusterSourceId{0}; + goodSrc.detector = o2::detectors::DetID::ITS; + goodSrc.clusters = clusters; + goodSrc.patterns = patterns; + goodSrc.rofs = rofs; + goodSrc.dictionary = &dict(); + goodSrc.layerToSurface = itsLayerToSurface; + goodSrc.timing = ROFTimingConfig{40, 0, 0, 0}; + goodSrc.decoder = &decoder; + + TimeFrame frame; + configureFrame(frame, layout); + BOOST_REQUIRE(loadSources(frame, layout.getCatalog(), gsl::span(&goodSrc, 1), {0, 0}).ok()); + BOOST_REQUIRE_EQUAL(frame.getTotalMeasurements(), 1u); + + // Now attempt an invalid load (duplicate ids) on the SAME frame. + std::array badSources{goodSrc, goodSrc}; // both id==0 + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(badSources), {0, 0}); + BOOST_REQUIRE(!result.ok()); + + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); +} + +BOOST_AUTO_TEST_CASE(FailedLoadAfterFirstSourceDecodedLeavesNoPartialState) +{ + // Unlike FailedLoadLeavesNoPartialState (which fails during up-front + // source-id validation, before any source is decoded), this exercises + // failure during decode/validation of the SECOND source, after the first + // source has already been written to the TimeFrame. + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + + const std::vector clusters{{1, 1, CompCluster::InvalidPatternID, 0}}; + const auto patterns = makePatternBytes(clusters.size()); + const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; + o2::dataformats::MCTruthContainer labels; + labels.addElement(0, o2::MCCompLabel{1, 0, 0}); + FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; + + ClusterSourceInput goodSrc; + goodSrc.id = ClusterSourceId{0}; + goodSrc.detector = o2::detectors::DetID::ITS; + goodSrc.clusters = clusters; + goodSrc.patterns = patterns; + goodSrc.rofs = rofs; + goodSrc.dictionary = &dict(); + goodSrc.labels = &labels; + goodSrc.layerToSurface = itsLayerToSurface; + goodSrc.timing = ROFTimingConfig{40, 0, 0, 0}; + goodSrc.decoder = &decoder; + + TimeFrame frame; + configureFrame(frame, layout); + BOOST_REQUIRE(loadSources(frame, layout.getCatalog(), gsl::span(&goodSrc, 1), {0, 0}).ok()); + + BOOST_REQUIRE_EQUAL(frame.getGlobalMeasurements(LayerId{0}).size(), 1u); + BOOST_REQUIRE_EQUAL(frame.getLabels(LayerId{0}, 0).size(), 1u); + + // Second source: dense/unique id (so id-level validation passes and the + // decoder actually runs for source 0), but fails once ITS is asked to map + // onto an MFT surface -- i.e. only after source 0 has already been decoded. + FakeClusterDecoder decoderA{o2::detectors::DetID::ITS, {0}, false}; + FakeClusterDecoder decoderB{o2::detectors::DetID::ITS, {0}, false}; + + ClusterSourceInput srcA = goodSrc; + srcA.decoder = &decoderA; + + ClusterSourceInput srcB; + srcB.id = ClusterSourceId{1}; + srcB.detector = o2::detectors::DetID::ITS; + srcB.clusters = clusters; + srcB.patterns = patterns; + srcB.rofs = rofs; + srcB.dictionary = &dict(); + const std::array wrongMapping{LayerId{2}}; // MFT surface for an ITS source + srcB.layerToSurface = wrongMapping; + srcB.timing = ROFTimingConfig{40, 0, 0, 0}; + srcB.decoder = &decoderB; + + std::array sources{srcA, srcB}; + const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); + BOOST_REQUIRE(!result.ok()); + BOOST_CHECK(result.error == MultiSourceLoadError::DetectorSurfaceMismatch); + BOOST_CHECK(result.source == ClusterSourceId{1}); + + BOOST_CHECK(frame.getGlobalMeasurements(LayerId{0}).empty()); + BOOST_CHECK(frame.getLabels(LayerId{0}, 0).empty()); + BOOST_CHECK(frame.getLabels(LayerId{2}, 0).empty()); +} + +BOOST_AUTO_TEST_CASE(EmptyFrameAccessorsAvoidNullPointerArithmetic) +{ + TimeFrame frame; + + BOOST_CHECK(frame.getSurfaceMeasurement(LayerId{0}, 0) == nullptr); + BOOST_CHECK(frame.getLabels(LayerId{0}, 0).empty()); + + // Loading zero sources into a layout with surfaces is legal and must + // leave every per-surface bucket empty. + const auto layout = makeCombinedLayout(); + BOOST_REQUIRE(layout.valid()); + configureFrame(frame, layout); + const auto result = loadSources(frame, layout.getCatalog(), gsl::span{}, {0, 0}); + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); + + BOOST_CHECK(frame.getSurfaceMeasurement(LayerId{0}, 0) == nullptr); + BOOST_CHECK(frame.getGlobalMeasurements(LayerId{0}).empty()); +} + +BOOST_AUTO_TEST_CASE(UnconfiguredFrameRejectsEvenAnEmptyLoad) +{ + // A layout with no surfaces at all, combined with zero sources, is the + // most degenerate legal input: nothing to validate, nothing to decode, + // nothing to commit. + const SurfaceCatalogView emptyCatalog{}; + + TimeFrame frame; + const auto result = loadSources(frame, emptyCatalog, gsl::span{}, {0, 0}); + BOOST_CHECK(result.error == MultiSourceLoadError::FrameNotConfigured); + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); + BOOST_CHECK_EQUAL(frame.getNMeasurementSurfaces(), 0u); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testPropagator.cxx b/Detectors/ITSMFT/common/tracking/test/testPropagator.cxx new file mode 100644 index 0000000000000..8c4de92364ae4 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testPropagator.cxx @@ -0,0 +1,965 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE ITSMFTPropagator +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include +#include +#include +#include +#include + +#include "CommonConstants/MathConstants.h" +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "ITSMFTTracking/RefitDriver.h" +#include "ITSMFTTracking/detail/SurfaceStateOperations.h" +#include "ITSMFTTracking/Propagator.h" + +#if __has_include("ITSMFTTracking/BarrelSurfaceStateOperations.h") || __has_include("ITSMFTTracking/ForwardSurfaceStateOperations.h") +#error "coordinate-family state operations must remain private to Propagator" +#endif + +using namespace o2::itsmft::tracking; + +namespace +{ + +template +bool bitEqual(const T& lhs, const T& rhs) +{ + return std::memcmp(&lhs, &rhs, sizeof(T)) == 0; +} + +// --- Barrel fixtures (same convention as testRefitHit.cxx's barrelState()) -- + +SurfaceTrackState barrelState(uint8_t absCharge = 1, o2::track::PID pid = o2::track::PID::Pion) +{ + SurfaceTrackState state{}; + state.parameters[0] = 1.25f; + state.parameters[1] = -0.75f; + state.parameters[2] = 0.2f; + state.parameters[3] = -0.35f; + state.parameters[4] = 0.8f; + state.referenceCoordinate = 4.f; + state.alpha = 0.3f; + state.kind = SurfaceKind::Cylinder; + state.absCharge = absCharge; + state.pid = pid; + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column <= row; ++column) { + state.covariance[packedCovarianceIndex(row, column)] = row == column ? 0.01f * (row + 1) : 0.0002f * (row + column + 1); + } + } + return state; +} + +SurfaceTrackParameters barrelLinRef(const SurfaceTrackState& state) +{ + return SurfaceTrackParameters{state}; +} + +SurfaceMeasurement barrelMeasurement() +{ + SurfaceMeasurement measurement{}; + measurement.frame.q = 2.5f; + measurement.frame.frameAngle = 0.3f; // same alpha as barrelState(): no rotation needed + measurement.frame.u = 0.8f; + measurement.frame.v = -0.45f; + measurement.covariance = {0.04f, 0.012f, 0.09f}; + return measurement; +} + +constexpr float BarrelBz = 5.f; + +SurfaceDescriptor cylinderDescriptor(NominalSurfaceMaterial material) +{ + SurfaceDescriptor descriptor{}; + descriptor.kind = SurfaceKind::Cylinder; + descriptor.referenceCoordinate = 2.5f; + descriptor.material = material; + return descriptor; +} + +// --- Disk fixtures (same convention as testRefitHit.cxx's diskState()) ----- + +SurfaceTrackState diskState(uint8_t absCharge = 1, o2::track::PID pid = o2::track::PID::Pion) +{ + SurfaceTrackState state{}; + state.parameters[0] = 1.25f; + state.parameters[1] = -0.75f; + state.parameters[2] = 0.35f; + state.parameters[3] = -2.5f; + state.parameters[4] = 0.8f; + state.referenceCoordinate = -45.f; + state.kind = SurfaceKind::Disk; + state.absCharge = absCharge; + state.pid = pid; + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column <= row; ++column) { + state.covariance[packedCovarianceIndex(row, column)] = row == column ? 0.01f * (row + 1) : 0.0002f * (row + column + 1); + } + } + return state; +} + +SurfaceTrackParameters diskLinRef(const SurfaceTrackState& state) +{ + return SurfaceTrackParameters{state}; +} + +SurfaceMeasurement diskMeasurement() +{ + SurfaceMeasurement measurement{}; + measurement.frame = {-50.f, 0.8f, -0.45f, 0.f}; + measurement.frame.q = -50.f; + measurement.frame.u = 0.8f; + measurement.frame.v = -0.45f; + measurement.covariance = {0.04f, 0.f, 0.09f}; + return measurement; +} + +constexpr float DiskBz = 5.f; + +SurfaceDescriptor diskDescriptor(NominalSurfaceMaterial material) +{ + SurfaceDescriptor descriptor{}; + descriptor.kind = SurfaceKind::Disk; + descriptor.referenceCoordinate = -50.f; + descriptor.material = material; + return descriptor; +} + +// Independent double-precision helix intersections for numerical derivatives. +// The target reference plane is fixed for every perturbed source state. +std::array intersectConversionPlane(const SurfaceTrackState& source, + const std::array& p, + const SurfaceTrackState& target, double bz) +{ + double x = p[0], y = p[1], z = source.referenceCoordinate, phi = p[2]; + if (source.kind == SurfaceKind::Cylinder) { + x = source.referenceCoordinate * std::cos(double(source.alpha)) - p[0] * std::sin(double(source.alpha)); + y = source.referenceCoordinate * std::sin(double(source.alpha)) + p[0] * std::cos(double(source.alpha)); + z = p[1]; + phi = source.alpha + std::asin(p[2]); + } + const double curvature = source.absCharge == 0 ? 0. : p[4] * bz * o2::constants::math::B2C; + auto pointAt = [&](double path) { + const double halfAngle = curvature * path / 2.; + const double sinc = halfAngle == 0. ? 1. : std::sin(halfAngle) / halfAngle; + return std::array{x + path * sinc * std::cos(phi + halfAngle), + y + path * sinc * std::sin(phi + halfAngle), z + path * p[3]}; + }; + double path = 0.; + if (target.kind == SurfaceKind::Disk) { + path = (target.referenceCoordinate - z) / p[3]; + const auto position = pointAt(path); + return {position[0], position[1], phi + curvature * path, p[3], p[4]}; + } + const double csA = std::cos(double(target.alpha)), snA = std::sin(double(target.alpha)); + // Newton iteration finds the local intersection continuously connected to + // the nominal point; it does not reuse the production Jacobian. + for (int iteration = 0; iteration < 6; ++iteration) { + const auto position = pointAt(path); + path -= (position[0] * csA + position[1] * snA - target.referenceCoordinate) / + std::cos(phi + curvature * path - target.alpha); + } + const auto position = pointAt(path); + return {-position[0] * snA + position[1] * csA, position[2], + std::sin(phi + curvature * path - target.alpha), p[3], p[4]}; +} + +void checkConversionCovariance(const SurfaceTrackState& source, float bz) +{ + auto target = source; + OperationFailureReason reason{}; + const auto targetKind = source.kind == SurfaceKind::Cylinder ? SurfaceKind::Disk : SurfaceKind::Cylinder; + BOOST_REQUIRE(Propagator::convertKind(target, targetKind, bz, reason)); + double jacobian[5][5]{}; + std::array nominal{}; + std::copy(std::begin(source.parameters), std::end(source.parameters), nominal.begin()); + constexpr double step = 1.e-5; + for (int column = 0; column < 5; ++column) { + auto plus = nominal, minus = nominal; + plus[column] += step; + minus[column] -= step; + const auto high = intersectConversionPlane(source, plus, target, bz); + const auto low = intersectConversionPlane(source, minus, target, bz); + for (int row = 0; row < 5; ++row) { + jacobian[row][column] = (high[row] - low[row]) / (2. * step); + } + } + for (int row = 0; row < 5; ++row) { + for (int column = 0; column <= row; ++column) { + double expected = 0.; + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 5; ++j) { + expected += jacobian[row][i] * source.covariance[packedCovarianceIndex(i, j)] * jacobian[column][j]; + } + } + const float actual = target.covariance[packedCovarianceIndex(row, column)]; + BOOST_CHECK_SMALL(double(actual) - expected, 1.e-7 + 2.e-5 * std::abs(expected)); + } + } +} + +} // namespace + +// --- 1/2: same-family propagate-to-measurement succeeds --------------------- + +BOOST_AUTO_TEST_CASE(CylinderToCylinderPropagateAndUpdateSucceeds) +{ + auto state = barrelState(); + auto linRef = barrelLinRef(state); + const auto measurement = barrelMeasurement(); + const auto descriptor = cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f}); + float chi2 = 0.f; + OperationFailureReason reason{}; + + BOOST_REQUIRE(Propagator::propagateToMeasurement(state, linRef, descriptor, measurement, BarrelBz, + material::MaterialTraversalDirection::AlongMomentum, + false, 0.f, chi2, false, reason)); + BOOST_CHECK_EQUAL(static_cast(state.kind), static_cast(SurfaceKind::Cylinder)); + BOOST_CHECK_EQUAL(state.referenceCoordinate, measurement.frame.q); + BOOST_CHECK(std::isfinite(chi2)); + BOOST_CHECK_GE(chi2, 0.f); +} + +BOOST_AUTO_TEST_CASE(DiskToDiskPropagateAndUpdateSucceeds) +{ + auto state = diskState(); + auto linRef = diskLinRef(state); + const auto measurement = diskMeasurement(); + const auto descriptor = diskDescriptor(NominalSurfaceMaterial{0.f, 0.f}); + float chi2 = 0.f; + OperationFailureReason reason{}; + + BOOST_REQUIRE(Propagator::propagateToMeasurement(state, linRef, descriptor, measurement, DiskBz, + material::MaterialTraversalDirection::AlongMomentum, + false, 0.f, chi2, false, reason)); + BOOST_CHECK_EQUAL(static_cast(state.kind), static_cast(SurfaceKind::Disk)); + BOOST_CHECK_EQUAL(state.referenceCoordinate, measurement.frame.q); + BOOST_CHECK(std::isfinite(chi2)); + BOOST_CHECK_GE(chi2, 0.f); +} + +BOOST_AUTO_TEST_CASE(AcceptedForwardPropagationSelectsFieldAndLowFieldPaths) +{ + auto fieldOn = diskState(); + auto lowPositive = diskState(); + auto lowNegative = diskState(); + OperationFailureReason reason{}; + + BOOST_REQUIRE(Propagator::propagateToReference(fieldOn, -50.f, 5.f, reason)); + BOOST_REQUIRE(Propagator::propagateToReference(lowPositive, -50.f, 0.01f, reason)); + BOOST_REQUIRE(Propagator::propagateToReference(lowNegative, -50.f, -0.01f, reason)); + BOOST_CHECK(bitEqual(lowPositive, lowNegative)); + BOOST_CHECK(!bitEqual(fieldOn, lowPositive)); +} + +BOOST_AUTO_TEST_CASE(PropagatorSelectsCompatibilityFromStateKind) +{ + auto cylinderReference = barrelState(); + auto cylinderCandidate = cylinderReference; + auto diskReference = diskState(); + auto diskCandidate = diskReference; + float chi2 = -1.f; + OperationFailureReason reason{}; + + BOOST_REQUIRE(Propagator::stateChi2(cylinderReference, cylinderCandidate, chi2, reason)); + BOOST_CHECK_EQUAL(chi2, 0.f); + BOOST_REQUIRE(Propagator::stateChi2(diskReference, diskCandidate, chi2, reason)); + BOOST_CHECK_EQUAL(chi2, 0.f); + BOOST_CHECK(!Propagator::stateChi2(cylinderReference, diskCandidate, chi2, reason)); + BOOST_CHECK(reason == OperationFailureReason::SourceSurfaceKindMismatch); +} + +// --- 3: compatible family never converts -- exact agreement with a direct +// detail::barrel::rotate/propagate/correctForMaterial/predictedChi2/update replay --- + +BOOST_AUTO_TEST_CASE(CompatibleFamilyMatchesDirectBarrelPrimitiveReplay) +{ + auto viaPropagator = barrelState(); + auto viaPropagatorRef = barrelLinRef(viaPropagator); + auto viaDirect = viaPropagator; + auto viaDirectRef = viaPropagatorRef; + const auto measurement = barrelMeasurement(); + const auto material = NominalSurfaceMaterial{0.01f, 0.001f}; + const auto descriptor = cylinderDescriptor(material); + float chi2Propagator = 0.f; + float chi2Direct = 0.f; + OperationFailureReason reason{}; + + BOOST_REQUIRE(Propagator::propagateToMeasurement(viaPropagator, viaPropagatorRef, descriptor, measurement, BarrelBz, + material::MaterialTraversalDirection::OppositeMomentum, + false, 0.f, chi2Propagator, true, reason)); + + BOOST_REQUIRE(detail::barrel::rotate(viaDirect, viaDirectRef, measurement.frame.frameAngle, BarrelBz, reason)); + BOOST_REQUIRE(detail::barrel::propagate(viaDirect, viaDirectRef, measurement.frame.q, BarrelBz, reason)); + const auto materialResult = detail::barrel::correctForMaterial( + viaDirect, viaDirectRef, material::IntegratedMaterialBudget{material.xOverX0, material.arealDensityGPerCm2}, + material::MaterialTraversalDirection::OppositeMomentum); + BOOST_REQUIRE(materialResult.ok()); + float predChi2 = 0.f; + BOOST_REQUIRE(detail::barrel::predictedChi2(viaDirect, measurement, predChi2, reason)); + float updateChi2 = 0.f; + BOOST_REQUIRE(detail::barrel::update(viaDirect, measurement, updateChi2, reason)); + chi2Direct = updateChi2; + BOOST_REQUIRE(detail::barrel::shiftReferenceToMeasurement(viaDirectRef, measurement, reason)); + + BOOST_CHECK(bitEqual(viaPropagator, viaDirect)); + BOOST_CHECK(bitEqual(viaPropagatorRef, viaDirectRef)); + BOOST_CHECK_EQUAL(chi2Propagator, chi2Direct); +} + +BOOST_AUTO_TEST_CASE(BarrelMaterialUsesLegacyIncidencePathLength) +{ + auto state = barrelState(); + state.parameters[2] = 0.6f; + state.parameters[3] = 1.2f; + const auto original = state; + const material::IntegratedMaterialBudget nominalMaterial{0.01f, 0.001f}; + + const float snp = original.parameters[2]; + const float tgl = original.parameters[3]; + const float incidenceScale = std::sqrt((1.f + tgl * tgl) / ((1.f - snp) * (1.f + snp))); + const material::IntegratedMaterialBudget legacyMaterial{ + nominalMaterial.xOverX0 * incidenceScale, + nominalMaterial.arealDensityGPerCm2 * incidenceScale}; + const float transverseMomentum = static_cast(original.absCharge) / std::abs(original.parameters[4]); + const float momentum = transverseMomentum * std::sqrt(1.f + tgl * tgl); + + const auto expected = material::calculateMaterialPhysics(momentum, original.pid, original.absCharge, + material::MaterialTraversalDirection::AlongMomentum, + legacyMaterial); + const auto uncorrected = material::calculateMaterialPhysics(momentum, original.pid, original.absCharge, + material::MaterialTraversalDirection::AlongMomentum, + nominalMaterial); + const auto result = detail::barrel::correctForMaterial(state, nominalMaterial, + material::MaterialTraversalDirection::AlongMomentum); + + BOOST_REQUIRE(expected.ok()); + BOOST_REQUIRE(uncorrected.ok()); + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(result.momentumBeforeGeV, expected.momentumBeforeGeV); + BOOST_CHECK_EQUAL(result.momentumAfterGeV, expected.momentumAfterGeV); + BOOST_CHECK_EQUAL(result.signedEnergyChangeGeV, expected.signedEnergyChangeGeV); + BOOST_CHECK_EQUAL(result.highlandTheta2Rad2, expected.highlandTheta2Rad2); + BOOST_CHECK_EQUAL(result.relativeInverseMomentumVariance, expected.relativeInverseMomentumVariance); + BOOST_CHECK_EQUAL(result.energyLossSubsteps, expected.energyLossSubsteps); + BOOST_CHECK_GT(result.highlandTheta2Rad2, uncorrected.highlandTheta2Rad2); + BOOST_CHECK_LT(result.momentumAfterGeV, uncorrected.momentumAfterGeV); +} + +BOOST_AUTO_TEST_CASE(LinearizedBarrelMaterialUsesLegacyReferenceIncidence) +{ + auto state = barrelState(); + state.parameters[2] = 0.1f; + state.parameters[3] = 0.2f; + auto linRef = barrelLinRef(state); + linRef.parameters[2] = 0.6f; + linRef.parameters[3] = 1.2f; + const float stateQ2PtBefore = state.parameters[4]; + const float referenceQ2PtBefore = linRef.parameters[4]; + const material::IntegratedMaterialBudget nominalMaterial{0.01f, 0.001f}; + + const float snp = linRef.parameters[2]; + const float tgl = linRef.parameters[3]; + const float incidenceScale = std::sqrt((1.f + tgl * tgl) / ((1.f - snp) * (1.f + snp))); + const material::IntegratedMaterialBudget legacyMaterial{ + nominalMaterial.xOverX0 * incidenceScale, + nominalMaterial.arealDensityGPerCm2 * incidenceScale}; + const float stateTgl = state.parameters[3]; + const float transverseMomentum = static_cast(state.absCharge) / std::abs(state.parameters[4]); + const float momentum = transverseMomentum * std::sqrt(1.f + stateTgl * stateTgl); + + const auto expected = material::calculateMaterialPhysics(momentum, state.pid, state.absCharge, + material::MaterialTraversalDirection::AlongMomentum, + legacyMaterial); + const auto result = detail::barrel::correctForMaterial(state, linRef, nominalMaterial, + material::MaterialTraversalDirection::AlongMomentum); + + BOOST_REQUIRE(expected.ok()); + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(result.momentumAfterGeV, expected.momentumAfterGeV); + BOOST_CHECK_EQUAL(result.highlandTheta2Rad2, expected.highlandTheta2Rad2); + const float expectedStateQ2Pt = (stateQ2PtBefore * result.momentumBeforeGeV) / result.momentumAfterGeV; + const float expectedReferenceQ2Pt = (referenceQ2PtBefore * result.momentumBeforeGeV) / result.momentumAfterGeV; + BOOST_CHECK_EQUAL(state.parameters[4], expectedStateQ2Pt); + BOOST_CHECK_EQUAL(linRef.parameters[4], expectedReferenceQ2Pt); +} + +BOOST_AUTO_TEST_CASE(LinearizedBarrelMaterialKeepsReferenceQ2PtForMCSOnly) +{ + auto state = barrelState(); + auto linRef = barrelLinRef(state); + const auto referenceBefore = linRef; + + const auto result = detail::barrel::correctForMaterial( + state, linRef, material::IntegratedMaterialBudget{0.01f, 0.f}, + material::MaterialTraversalDirection::AlongMomentum); + + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(result.momentumBeforeGeV, result.momentumAfterGeV); + BOOST_CHECK(bitEqual(linRef, referenceBefore)); +} + +BOOST_AUTO_TEST_CASE(FailingLinearizedBarrelMaterialLeavesStateAndReferenceUnchanged) +{ + auto state = barrelState(); + auto linRef = barrelLinRef(state); + const auto stateBefore = state; + const auto referenceBefore = linRef; + + const auto result = detail::barrel::correctForMaterial( + state, linRef, material::IntegratedMaterialBudget{1.e8f, 0.f}, + material::MaterialTraversalDirection::AlongMomentum); + + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.failure == material::MaterialFailureReason::ExcessiveScattering); + BOOST_CHECK(bitEqual(state, stateBefore)); + BOOST_CHECK(bitEqual(linRef, referenceBefore)); +} + +BOOST_AUTO_TEST_CASE(CompatibleFamilyMatchesDirectForwardPrimitiveReplay) +{ + auto viaPropagator = diskState(); + auto viaPropagatorRef = diskLinRef(viaPropagator); + auto viaDirect = viaPropagator; + auto viaDirectRef = viaPropagatorRef; + const auto measurement = diskMeasurement(); + const auto material = NominalSurfaceMaterial{0.01f, 0.001f}; + const auto descriptor = diskDescriptor(material); + float chi2Propagator = 0.f; + float chi2Direct = 0.f; + OperationFailureReason reason{}; + + BOOST_REQUIRE(Propagator::propagateToMeasurement(viaPropagator, viaPropagatorRef, descriptor, measurement, DiskBz, + material::MaterialTraversalDirection::OppositeMomentum, + false, 0.f, chi2Propagator, true, reason)); + + BOOST_REQUIRE(detail::forward::propagate(viaDirect, viaDirectRef, measurement.frame.q, DiskBz, reason)); + const auto materialResult = detail::forward::correctForMaterial( + viaDirect, viaDirectRef, material::IntegratedMaterialBudget{material.xOverX0, material.arealDensityGPerCm2}, + material::MaterialTraversalDirection::OppositeMomentum); + BOOST_REQUIRE(materialResult.ok()); + float predChi2 = 0.f; + BOOST_REQUIRE(detail::forward::predictedChi2(viaDirect, measurement, predChi2, reason)); + float updateChi2 = 0.f; + BOOST_REQUIRE(detail::forward::update(viaDirect, measurement, updateChi2, reason)); + chi2Direct = updateChi2; + BOOST_REQUIRE(detail::forward::shiftReferenceToMeasurement(viaDirectRef, measurement, reason)); + + BOOST_CHECK(bitEqual(viaPropagator, viaDirect)); + BOOST_CHECK(bitEqual(viaPropagatorRef, viaDirectRef)); + BOOST_CHECK_EQUAL(chi2Propagator, chi2Direct); +} + +BOOST_AUTO_TEST_CASE(ForwardMaterialUsesLegacyIncidencePathLength) +{ + auto state = diskState(); + state.parameters[3] = -0.5f; + const auto original = state; + const material::IntegratedMaterialBudget nominalMaterial{0.01f, 0.001f}; + + const float tgl = original.parameters[3]; + const float incidenceScale = std::sqrt(1.f + tgl * tgl) / std::abs(tgl); + const material::IntegratedMaterialBudget legacyMaterial{ + nominalMaterial.xOverX0 * incidenceScale, + nominalMaterial.arealDensityGPerCm2 * incidenceScale}; + const float transverseMomentum = static_cast(original.absCharge) / std::abs(original.parameters[4]); + const float momentum = transverseMomentum * std::sqrt(1.f + tgl * tgl); + + const auto expected = material::calculateMaterialPhysics(momentum, original.pid, original.absCharge, + material::MaterialTraversalDirection::AlongMomentum, + legacyMaterial); + const auto uncorrected = material::calculateMaterialPhysics(momentum, original.pid, original.absCharge, + material::MaterialTraversalDirection::AlongMomentum, + nominalMaterial); + const auto result = detail::forward::correctForMaterial(state, nominalMaterial, + material::MaterialTraversalDirection::AlongMomentum); + + BOOST_REQUIRE(expected.ok()); + BOOST_REQUIRE(uncorrected.ok()); + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(result.momentumBeforeGeV, expected.momentumBeforeGeV); + BOOST_CHECK_EQUAL(result.momentumAfterGeV, expected.momentumAfterGeV); + BOOST_CHECK_EQUAL(result.signedEnergyChangeGeV, expected.signedEnergyChangeGeV); + BOOST_CHECK_EQUAL(result.highlandTheta2Rad2, expected.highlandTheta2Rad2); + BOOST_CHECK_EQUAL(result.relativeInverseMomentumVariance, expected.relativeInverseMomentumVariance); + BOOST_CHECK_EQUAL(result.energyLossSubsteps, expected.energyLossSubsteps); + BOOST_CHECK_GT(result.highlandTheta2Rad2, uncorrected.highlandTheta2Rad2); + BOOST_CHECK_LT(result.momentumAfterGeV, uncorrected.momentumAfterGeV); +} + +BOOST_AUTO_TEST_CASE(LinearizedForwardMaterialUsesReferenceIncidence) +{ + auto state = diskState(); + auto linRef = diskLinRef(state); + linRef.parameters[3] = -0.5f; + const float stateQ2PtBefore = state.parameters[4]; + const float referenceQ2PtBefore = linRef.parameters[4]; + const material::IntegratedMaterialBudget nominalMaterial{0.01f, 0.001f}; + + const float referenceTgl = linRef.parameters[3]; + const float incidenceScale = std::sqrt(1.f + referenceTgl * referenceTgl) / std::abs(referenceTgl); + const material::IntegratedMaterialBudget scaledMaterial{ + nominalMaterial.xOverX0 * incidenceScale, + nominalMaterial.arealDensityGPerCm2 * incidenceScale}; + const float stateTgl = state.parameters[3]; + const float transverseMomentum = static_cast(state.absCharge) / std::abs(state.parameters[4]); + const float momentum = transverseMomentum * std::sqrt(1.f + stateTgl * stateTgl); + + const auto expected = material::calculateMaterialPhysics(momentum, state.pid, state.absCharge, + material::MaterialTraversalDirection::AlongMomentum, + scaledMaterial); + const auto result = detail::forward::correctForMaterial(state, linRef, nominalMaterial, + material::MaterialTraversalDirection::AlongMomentum); + + BOOST_REQUIRE(expected.ok()); + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(result.momentumAfterGeV, expected.momentumAfterGeV); + BOOST_CHECK_EQUAL(result.highlandTheta2Rad2, expected.highlandTheta2Rad2); + const float expectedStateQ2Pt = (stateQ2PtBefore * result.momentumBeforeGeV) / result.momentumAfterGeV; + const float expectedReferenceQ2Pt = (referenceQ2PtBefore * result.momentumBeforeGeV) / result.momentumAfterGeV; + BOOST_CHECK_EQUAL(state.parameters[4], expectedStateQ2Pt); + BOOST_CHECK_EQUAL(linRef.parameters[4], expectedReferenceQ2Pt); +} + +BOOST_AUTO_TEST_CASE(LinearizedForwardMaterialKeepsReferenceQ2PtForMCSOnly) +{ + auto state = diskState(); + auto linRef = diskLinRef(state); + const auto referenceBefore = linRef; + + const auto result = detail::forward::correctForMaterial( + state, linRef, material::IntegratedMaterialBudget{0.01f, 0.f}, + material::MaterialTraversalDirection::AlongMomentum); + + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(result.momentumBeforeGeV, result.momentumAfterGeV); + BOOST_CHECK(bitEqual(linRef, referenceBefore)); +} + +BOOST_AUTO_TEST_CASE(FailingLinearizedForwardMaterialLeavesStateAndReferenceUnchanged) +{ + auto state = diskState(); + auto linRef = diskLinRef(state); + const auto stateBefore = state; + const auto referenceBefore = linRef; + + const auto result = detail::forward::correctForMaterial( + state, linRef, material::IntegratedMaterialBudget{1.e8f, 0.f}, + material::MaterialTraversalDirection::AlongMomentum); + + BOOST_CHECK(!result.ok()); + BOOST_CHECK(result.failure == material::MaterialFailureReason::ExcessiveScattering); + BOOST_CHECK(bitEqual(state, stateBefore)); + BOOST_CHECK(bitEqual(linRef, referenceBefore)); +} + +// --- 4: incompatible family converts, then propagates ----------------------- + +BOOST_AUTO_TEST_CASE(BarrelStateConvertsToForwardThenPropagatesToDiskMeasurement) +{ + auto state = barrelState(); + auto linRef = barrelLinRef(state); + const auto poisonState = state; + + // A disk far enough along z that the converted (Forward) state can reach it. + SurfaceMeasurement measurement{}; + measurement.frame.q = -10.f; + measurement.frame.u = 5.f; + measurement.frame.v = -5.f; + measurement.covariance = {10.f, 0.f, 10.f}; // loose: the point is not expected to land exactly here + const auto descriptor = diskDescriptor(NominalSurfaceMaterial{0.f, 0.f}); + float chi2 = 0.f; + OperationFailureReason reason{}; + + const bool ok = Propagator::propagateToMeasurement(state, linRef, descriptor, measurement, BarrelBz, + material::MaterialTraversalDirection::AlongMomentum, + false, 0.f, chi2, false, reason); + BOOST_REQUIRE(ok); + BOOST_CHECK_EQUAL(static_cast(state.kind), static_cast(SurfaceKind::Disk)); + BOOST_CHECK_EQUAL(state.referenceCoordinate, measurement.frame.q); + BOOST_CHECK_EQUAL(state.absCharge, poisonState.absCharge); + BOOST_CHECK(state.pid == poisonState.pid); + for (float value : state.parameters) { + BOOST_CHECK(std::isfinite(value)); + } + for (float value : state.covariance) { + BOOST_CHECK(std::isfinite(value)); + } +} + +BOOST_AUTO_TEST_CASE(KindConversionRelinearizesAtConvertedState) +{ + auto nominalState = barrelState(); + auto nominalRef = barrelLinRef(nominalState); + auto perturbedState = nominalState; + auto perturbedRef = nominalRef; + perturbedRef.parameters[0] += 0.1f; + perturbedRef.parameters[1] -= 0.2f; + perturbedRef.parameters[2] += 0.01f; + perturbedRef.parameters[3] -= 0.02f; + perturbedRef.parameters[4] += 0.001f; + + SurfaceMeasurement measurement{}; + measurement.frame.q = -10.f; + measurement.frame.u = 5.f; + measurement.frame.v = -5.f; + measurement.covariance = {10.f, 0.f, 10.f}; + const auto descriptor = diskDescriptor(NominalSurfaceMaterial{0.f, 0.f}); + float nominalChi2 = 0.f; + float perturbedChi2 = 0.f; + OperationFailureReason reason{}; + + BOOST_REQUIRE(Propagator::propagateToMeasurement(nominalState, nominalRef, descriptor, measurement, BarrelBz, + material::MaterialTraversalDirection::AlongMomentum, + false, 0.f, nominalChi2, false, reason)); + BOOST_REQUIRE(Propagator::propagateToMeasurement(perturbedState, perturbedRef, descriptor, measurement, BarrelBz, + material::MaterialTraversalDirection::AlongMomentum, + false, 0.f, perturbedChi2, false, reason)); + + BOOST_CHECK(bitEqual(perturbedState, nominalState)); + BOOST_CHECK(bitEqual(perturbedRef, nominalRef)); + BOOST_CHECK_EQUAL(perturbedChi2, nominalChi2); +} + +BOOST_AUTO_TEST_CASE(ReverseKindConversionRelinearizesAtConvertedState) +{ + auto nominalState = diskState(); + auto nominalRef = diskLinRef(nominalState); + auto perturbedState = nominalState; + auto perturbedRef = nominalRef; + perturbedRef.parameters[0] += 0.1f; + perturbedRef.parameters[1] -= 0.2f; + perturbedRef.parameters[2] += 0.01f; + perturbedRef.parameters[3] -= 0.02f; + perturbedRef.parameters[4] += 0.001f; + + const auto measurement = barrelMeasurement(); + const auto descriptor = cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f}); + float nominalChi2 = 0.f; + float perturbedChi2 = 0.f; + OperationFailureReason reason{}; + + BOOST_REQUIRE(Propagator::propagateToMeasurement(nominalState, nominalRef, descriptor, measurement, DiskBz, + material::MaterialTraversalDirection::AlongMomentum, + false, 0.f, nominalChi2, false, reason)); + BOOST_REQUIRE(Propagator::propagateToMeasurement(perturbedState, perturbedRef, descriptor, measurement, DiskBz, + material::MaterialTraversalDirection::AlongMomentum, + false, 0.f, perturbedChi2, false, reason)); + + BOOST_CHECK(bitEqual(perturbedState, nominalState)); + BOOST_CHECK(bitEqual(perturbedRef, nominalRef)); + BOOST_CHECK_EQUAL(perturbedChi2, nominalChi2); +} + +BOOST_AUTO_TEST_CASE(ConversionCovarianceMatchesFixedPlaneHelixDifferences) +{ + for (const float bz : {-5.f, 0.f, 5.f}) { + for (const float sign : {-1.f, 1.f}) { + auto barrel = barrelState(); + barrel.parameters[3] *= sign; + barrel.parameters[4] *= sign; + checkConversionCovariance(barrel, bz); + auto disk = diskState(); + disk.parameters[3] *= sign; + disk.parameters[4] *= sign; + checkConversionCovariance(disk, bz); + } + } +} + +BOOST_AUTO_TEST_CASE(BarrelZUncertaintySurvivesConversionAndRoundTrip) +{ + auto state = barrelState(); + state.alpha = 0.f; + state.referenceCoordinate = 10.f; + state.parameters[0] = 0.f; + state.parameters[2] = 0.f; + state.parameters[3] = 2.f; + std::fill(std::begin(state.covariance), std::end(state.covariance), 0.f); + state.covariance[packedCovarianceIndex(1, 1)] = 1.f; + const auto before = state; + OperationFailureReason reason{}; + BOOST_REQUIRE(Propagator::convertKind(state, SurfaceKind::Disk, 5.f, reason)); + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(0, 0)], 0.25f, 1.e-4f); + const float curvature = before.parameters[4] * 5.f * o2::constants::math::B2C; + BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(2, 0)], curvature / 4.f, 1.e-4f); + BOOST_REQUIRE(Propagator::convertKind(state, SurfaceKind::Cylinder, 5.f, reason)); + for (int i = 0; i < 15; ++i) { + BOOST_CHECK_SMALL(state.covariance[i] - before.covariance[i], 1.e-6f); + } +} + +BOOST_AUTO_TEST_CASE(ConversionRejectsSingularAndNonFiniteInputsTransactionally) +{ + for (const float tanl : {0.f, std::numeric_limits::quiet_NaN(), std::numeric_limits::infinity()}) { + auto state = barrelState(); + state.parameters[3] = tanl; + const auto before = state; + OperationFailureReason reason{}; + BOOST_CHECK(!Propagator::convertKind(state, SurfaceKind::Disk, 5.f, reason)); + BOOST_CHECK(reason == OperationFailureReason::SurfaceKindConversionFailure); + BOOST_CHECK(bitEqual(state, before)); + } +} + +BOOST_AUTO_TEST_CASE(NonlinearAttachmentUsesTargetKindAndRollsBackAfterConversion) +{ + for (const bool startOnDisk : {false, true}) { + const auto source = startOnDisk ? diskState() : barrelState(); + const auto target = startOnDisk ? cylinderDescriptor({0.f, 0.f}) : diskDescriptor({0.f, 0.f}); + auto converted = source; + OperationFailureReason reason{}; + BOOST_REQUIRE(Propagator::convertKind(converted, target.kind, 0.f, reason)); + SurfaceMeasurement measurement{}; + measurement.frame = {converted.referenceCoordinate, converted.parameters[0], converted.parameters[1], converted.alpha}; + measurement.covariance = {0.04f, 0.f, 0.04f}; + auto state = source; + float chi2 = 0.f; + BOOST_REQUIRE(Propagator::attachMeasurement(state, target, measurement, 0.f, + material::MaterialTraversalDirection::OppositeMomentum, + true, 100.f, chi2, reason)); + BOOST_CHECK(state.kind == target.kind); + for (int i = 0; i < 5; ++i) { + BOOST_CHECK_SMALL(state.parameters[i] - converted.parameters[i], 1.e-5f); + } + BOOST_CHECK_SMALL(chi2, 1.e-5f); + + // Conversion may succeed while the measurement gate fails; neither the + // converted representation nor a partial chi2 may escape to the caller. + measurement.frame.u += 10.f; + state = source; + chi2 = 3.f; + BOOST_CHECK(!Propagator::attachMeasurement(state, target, measurement, 0.f, + material::MaterialTraversalDirection::OppositeMomentum, + true, 1.e-6f, chi2, reason)); + BOOST_CHECK(reason == OperationFailureReason::PredictedChi2Failure); + BOOST_CHECK(bitEqual(state, source)); + BOOST_CHECK_EQUAL(chi2, 3.f); + } +} + +BOOST_AUTO_TEST_CASE(ConvertFamilyPreservesChargeAndPID) +{ + auto state = barrelState(2, o2::track::PID::Kaon); + OperationFailureReason reason{}; + BOOST_REQUIRE(Propagator::convertKind(state, SurfaceKind::Disk, BarrelBz, reason)); + BOOST_CHECK_EQUAL(static_cast(state.kind), static_cast(SurfaceKind::Disk)); + BOOST_CHECK_EQUAL(state.absCharge, uint8_t{2}); + BOOST_CHECK(state.pid == o2::track::PID::Kaon); +} + +BOOST_AUTO_TEST_CASE(ConvertFamilySameFamilyIsNoOpSuccess) +{ + auto state = barrelState(); + const auto before = state; + OperationFailureReason reason{}; + BOOST_REQUIRE(Propagator::convertKind(state, SurfaceKind::Cylinder, DiskBz, reason)); + BOOST_CHECK(bitEqual(state, before)); +} + +// --- 5: degenerate conversion fails, transactionally ------------------------ + +BOOST_AUTO_TEST_CASE(ForwardToBarrelConversionFailsAtOriginTransactionally) +{ + auto state = diskState(); + state.parameters[0] = 0.f; // X + state.parameters[1] = 0.f; // Y: R == 0, alpha undefined + const auto poison = state; + OperationFailureReason reason{}; + + BOOST_CHECK(!Propagator::convertKind(state, SurfaceKind::Cylinder, DiskBz, reason)); + BOOST_CHECK_EQUAL(static_cast(reason), static_cast(OperationFailureReason::SurfaceKindConversionFailure)); + BOOST_CHECK(bitEqual(state, poison)); +} + +BOOST_AUTO_TEST_CASE(ForwardToBarrelRejectsUnrepresentableDirectionsTransactionally) +{ + for (const float phi : {o2::constants::math::PI, -2.f, 2.f, o2::constants::math::PIHalf}) { + auto state = diskState(); + state.parameters[0] = 10.f; + state.parameters[1] = 0.f; + state.parameters[2] = phi; + const auto before = state; + OperationFailureReason reason{}; + BOOST_CHECK(!Propagator::convertKind(state, SurfaceKind::Cylinder, DiskBz, reason)); + BOOST_CHECK(reason == OperationFailureReason::SurfaceKindConversionFailure); + BOOST_CHECK(bitEqual(state, before)); + } +} + +// --- Zero-material and nonzero-material (MatLUT/nominal-material) paths ----- + +BOOST_AUTO_TEST_CASE(ZeroMaterialPathSucceeds) +{ + auto state = barrelState(); + auto linRef = barrelLinRef(state); + const auto measurement = barrelMeasurement(); + const auto descriptor = cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f}); + float chi2 = 0.f; + OperationFailureReason reason{}; + BOOST_REQUIRE(Propagator::propagateToMeasurement(state, linRef, descriptor, measurement, BarrelBz, + material::MaterialTraversalDirection::AlongMomentum, + false, 0.f, chi2, false, reason)); +} + +BOOST_AUTO_TEST_CASE(NonzeroNominalMaterialChangesResultRelativeToZeroMaterial) +{ + auto zeroState = barrelState(); + auto zeroRef = barrelLinRef(zeroState); + auto materialState = barrelState(); + auto materialRef = barrelLinRef(materialState); + const auto measurement = barrelMeasurement(); + const auto zeroDescriptor = cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f}); + const auto materialDescriptor = cylinderDescriptor(NominalSurfaceMaterial{0.05f, 0.01f}); + float zeroChi2 = 0.f; + float materialChi2 = 0.f; + OperationFailureReason reason{}; + + BOOST_REQUIRE(Propagator::propagateToMeasurement(zeroState, zeroRef, zeroDescriptor, measurement, BarrelBz, + material::MaterialTraversalDirection::OppositeMomentum, + false, 0.f, zeroChi2, false, reason)); + BOOST_REQUIRE(Propagator::propagateToMeasurement(materialState, materialRef, materialDescriptor, measurement, BarrelBz, + material::MaterialTraversalDirection::OppositeMomentum, + false, 0.f, materialChi2, false, reason)); + + // The material budget is read from the target SurfaceDescriptor (the + // "MatLUT" mechanism, task requirement 6) -- not equal, not a parallel + // model producing a byte-identical result either. + BOOST_CHECK(!bitEqual(zeroState, materialState)); +} + +// --- Holes are skipped by the native refit driver ---------------------------- + +BOOST_AUTO_TEST_CASE(RefitDriverSkipsHoleSlots) +{ + auto state = barrelState(); + auto linRef = barrelLinRef(state); + const auto measurement = barrelMeasurement(); + + std::array surfaces{cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f})}; + SurfaceCatalogView catalog{surfaces.data(), static_cast(surfaces.size())}; + + const detail::RefitMeasurementSlot present{measurement, LayerId{0}, true}; + const detail::RefitMeasurementSlot hole{}; + + std::array slots{hole, present, hole}; + float chi2 = 0.f; + uint32_t acceptedHitCount = 999; + OperationFailureReason reason{}; + + BOOST_REQUIRE(detail::driveRefitLeg(state, linRef, chi2, acceptedHitCount, slots, catalog, BarrelBz, + material::MaterialTraversalDirection::AlongMomentum, false, 100.f, reason)); + BOOST_CHECK_EQUAL(acceptedHitCount, 1u); +} + +BOOST_AUTO_TEST_CASE(FullMFTRefitLegUsesOneDetectorMaterialBudget) +{ + const SurfaceCatalogView catalog{kMFTStaticSurfaceCatalog.data(), MFTNLayers}; + for (const auto direction : {material::MaterialTraversalDirection::AlongMomentum, + material::MaterialTraversalDirection::OppositeMomentum}) { + const bool alongMomentum = direction == material::MaterialTraversalDirection::AlongMomentum; + auto state = diskState(); + state.referenceCoordinate = kMFTStaticSurfaceCatalog[alongMomentum ? 0 : MFTNLayers - 1].referenceCoordinate; + // Field-off and exact measurements isolate the accumulated energy loss. + for (uint8_t row = 0; row < 5; ++row) { + for (uint8_t column = 0; column < row; ++column) { + state.covariance[packedCovarianceIndex(row, column)] = 0.f; + } + } + auto linRef = diskLinRef(state); + const float tanl = state.parameters[3]; + const float momentumScale = std::sqrt(1.f + tanl * tanl); + float expectedMomentum = momentumScale / std::abs(state.parameters[4]); + const float initialMomentum = expectedMomentum; + const float pathX0 = kMFTNominalRadLength / MFTNLayers * momentumScale / std::abs(tanl); + const material::IntegratedMaterialBudget expectedMaterial{ + pathX0, pathX0 * o2::its::constants::Radl * o2::its::constants::Rho}; + std::array slots{}; + for (int hit = 0; hit < MFTNLayers; ++hit) { + const auto layer = static_cast(alongMomentum ? hit : MFTNLayers - 1 - hit); + auto& slot = slots[hit]; + slot.surface = LayerId{layer}; + slot.present = true; + const float z = kMFTStaticSurfaceCatalog[layer].referenceCoordinate; + const float transverseDistance = (z - state.referenceCoordinate) / tanl; + slot.measurement.frame = {z, + state.parameters[0] + transverseDistance * std::cos(state.parameters[2]), + state.parameters[1] + transverseDistance * std::sin(state.parameters[2]), 0.f}; + slot.measurement.covariance = {0.04f, 0.f, 0.04f}; + const auto result = material::calculateMaterialPhysics(expectedMomentum, state.pid, state.absCharge, + direction, expectedMaterial); + BOOST_REQUIRE(result.ok()); + expectedMomentum = result.momentumAfterGeV; + } + float chi2 = 0.f; + uint32_t acceptedHitCount = 0; + OperationFailureReason reason{}; + BOOST_REQUIRE(detail::driveRefitLeg(state, linRef, chi2, acceptedHitCount, slots, catalog, 0.f, + direction, false, 100.f, reason)); + BOOST_CHECK_EQUAL(acceptedHitCount, MFTNLayers); + BOOST_CHECK_CLOSE(momentumScale / std::abs(state.parameters[4]), expectedMomentum, 1.e-4f); + BOOST_CHECK(alongMomentum ? expectedMomentum < initialMomentum : expectedMomentum > initialMomentum); + } +} + +// --- 10/11: chi2-gate failure and atomicity ---------------------------------- + +BOOST_AUTO_TEST_CASE(Chi2GateRejectsOversizedPredictedChi2Transactionally) +{ + auto state = barrelState(); + auto linRef = barrelLinRef(state); + const auto poisonState = state; + const auto poisonRef = linRef; + auto measurement = barrelMeasurement(); + measurement.frame.u += 5.f; // far outlier vs the state's predicted local Y + const auto descriptor = cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f}); + float chi2 = 0.f; + const float poisonChi2 = chi2; + OperationFailureReason reason{}; + + const bool ok = Propagator::propagateToMeasurement(state, linRef, descriptor, measurement, BarrelBz, + material::MaterialTraversalDirection::AlongMomentum, + true, 1.e-6f, chi2, false, reason); + BOOST_CHECK(!ok); + BOOST_CHECK_EQUAL(static_cast(reason), static_cast(OperationFailureReason::PredictedChi2Failure)); + BOOST_CHECK(bitEqual(state, poisonState)); + BOOST_CHECK(bitEqual(linRef, poisonRef)); + BOOST_CHECK_EQUAL(chi2, poisonChi2); +} + +BOOST_AUTO_TEST_CASE(UnrecognizedTargetSurfaceKindFails) +{ + auto state = barrelState(); + auto linRef = barrelLinRef(state); + const auto poisonState = state; + const auto measurement = barrelMeasurement(); + SurfaceDescriptor descriptor = cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f}); + // SurfaceKind currently only has Cylinder/Disk (both recognized); this + // proves the routing guard itself, not a reachable production input. + descriptor.kind = static_cast(0xFFu); + float chi2 = 0.f; + OperationFailureReason reason{}; + + const bool ok = Propagator::propagateToMeasurement(state, linRef, descriptor, measurement, BarrelBz, + material::MaterialTraversalDirection::AlongMomentum, + false, 0.f, chi2, false, reason); + BOOST_CHECK(!ok); + BOOST_CHECK_EQUAL(static_cast(reason), static_cast(OperationFailureReason::SurfaceKindConversionFailure)); + BOOST_CHECK(bitEqual(state, poisonState)); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testROFLookupTables.cxx b/Detectors/ITSMFT/common/tracking/test/testROFLookupTables.cxx index 486af25ee72cb..2bbac03ce5b9b 100644 --- a/Detectors/ITSMFT/common/tracking/test/testROFLookupTables.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testROFLookupTables.cxx @@ -815,3 +815,123 @@ BOOST_AUTO_TEST_CASE(rofvertex_exact_compatibility) BOOST_CHECK(!view.isVertexCompatible(3, 2, vertices[1])); BOOST_CHECK(!view.isVertexCompatible(3, 2, vertices[2])); } + +BOOST_AUTO_TEST_CASE(runtime_overlap_matches_interval_intersections_and_owns_copies) +{ + using o2::itsmft::tracking::ROFOverlapTable; + for (int layers : {1, 2, 7, 10, 17}) { + ROFOverlapTable table{layers}; + for (int layer = 0; layer < layers; ++layer) { + table.defineLayer(layer, 3 + layer % 3, 20 + 3 * layer, 2 * layer, 5, 3); + } + table.init(); + const auto originalSize = table.getFlatTableSize(); + table.init(); + BOOST_CHECK_EQUAL(table.getFlatTableSize(), originalSize); + auto copy = table; + BOOST_CHECK(copy.getView().mLayers != table.getView().mLayers); + BOOST_CHECK(copy.getView().mIndices != table.getView().mIndices); + auto moved = std::move(copy); + // Replacing the original must not invalidate the copied/moved table. + table = ROFOverlapTable{0}; + const auto view = moved.getView(); + BOOST_CHECK_EQUAL(view.mLayerCount, layers); + BOOST_CHECK_EQUAL(moved.getIndicesSize(), layers * layers); + for (int from = 0; from < layers; ++from) { + const auto& source = view.getLayer(from); + for (int to = 0; to < layers; ++to) { + if (from == to) { + continue; + } + const auto& destination = view.getLayer(to); + for (uint32_t rof = 0; rof < source.mNROFsTF; ++rof) { + const int64_t lower = std::max(0, int64_t(source.getROFStartInBC(rof)) - source.mROFAddTimeErr); + const int64_t upper = int64_t(source.getROFEndInBC(rof)) + source.mROFAddTimeErr; + std::vector expected; + for (uint32_t candidate = 0; candidate < destination.mNROFsTF; ++candidate) { + const int64_t otherLower = std::max(0, int64_t(destination.getROFStartInBC(candidate)) - destination.mROFAddTimeErr); + const int64_t otherUpper = int64_t(destination.getROFEndInBC(candidate)) + destination.mROFAddTimeErr; + if (lower < otherUpper && otherLower < upper) { + expected.push_back(candidate); + } + } + const auto actual = view.getOverlap(from, to, rof); + BOOST_CHECK_EQUAL(actual.getEntries(), expected.size()); + if (!expected.empty()) { + BOOST_CHECK_EQUAL(actual.getFirstEntry(), expected.front()); + } + } + } + } + // Exercise the same pointer/count interface used by the legacy GPU uploader. + const auto deviceView = moved.getDeviceView(view.mFlatTable, view.mIndices, view.mLayers); + BOOST_CHECK_EQUAL(deviceView.mLayerCount, layers); + BOOST_CHECK(deviceView.mFlatTable == view.mFlatTable); + BOOST_CHECK(deviceView.mIndices == view.mIndices); + BOOST_CHECK(deviceView.mLayers == view.mLayers); + } +} + +BOOST_AUTO_TEST_CASE(runtime_vertex_tables_rebuild_and_reset_after_copy) +{ + using o2::itsmft::tracking::ROFVertexLookupTable; + for (int layers : {1, 7, 10, 17}) { + ROFVertexLookupTable table{layers}; + for (int layer = 0; layer < layers; ++layer) { + table.defineLayer(layer, 3, 50, 0, 0, 0); + } + o2::its::Vertex vertex; + // ITS vertex timestamps store an interval start and width: [45, 55). + vertex.getTimeStamp().setTimeStamp(45); + vertex.getTimeStamp().setTimeStampError(10); + table.init(&vertex, 1); + table.init(&vertex, 1); + BOOST_CHECK_EQUAL(table.getFlatTableSize(), 3 * layers); + auto copy = table; + table.update(nullptr, 0); + auto moved = std::move(copy); + for (int layer = 0; layer < layers; ++layer) { + BOOST_CHECK_EQUAL(moved.getView().getVertices(layer, 0).getEntries(), 1); + BOOST_CHECK_EQUAL(moved.getView().getVertices(layer, 1).getEntries(), 1); + BOOST_CHECK_EQUAL(moved.getView().getVertices(layer, 2).getEntries(), 0); + BOOST_CHECK_EQUAL(table.getView().getVertices(layer, 0).getEntries(), 0); + } + const auto view = moved.getView(); + const auto deviceView = moved.getDeviceView(view.mFlatTable, view.mIndices, view.mLayers); + BOOST_CHECK_EQUAL(deviceView.mLayerCount, layers); + BOOST_CHECK_EQUAL(moved.getIndicesSize(), layers); + } +} + +BOOST_AUTO_TEST_CASE(runtime_masks_swap_timing_together_with_storage) +{ + using namespace o2::itsmft::tracking; + ROFOverlapTable firstTiming{2}, secondTiming{5}; + for (int layer = 0; layer < 2; ++layer) { + firstTiming.defineLayer(layer, 3, 20, 0, 0, 0); + } + for (int layer = 0; layer < 5; ++layer) { + secondTiming.defineLayer(layer, 4, 50, 0, 0, 0); + } + ROFMaskTable first{firstTiming}, second{secondTiming}; + first.setROFEnabled(1, 2); + second.setROFEnabled(4, 3); + first.swap(second); + BOOST_CHECK_EQUAL(first.getEntries(), 5); + BOOST_CHECK_EQUAL(second.getEntries(), 2); + BOOST_CHECK(first.getView().isROFEnabled(4, 3)); + BOOST_CHECK(second.getView().isROFEnabled(1, 2)); + first.resetMask(); + first.selectROF({120, 1}); + BOOST_CHECK(first.getView().isROFEnabled(4, 2)); + BOOST_CHECK(!first.getView().isROFEnabled(4, 3)); + auto copy = first; + first.resetMask(); + BOOST_CHECK(copy.getView().isROFEnabled(4, 2)); + const auto view = copy.getView(); + const auto deviceView = copy.getDeviceView(view.mFlatMask, view.mLayerROFOffsets); + BOOST_CHECK_EQUAL(deviceView.mLayerCount, 5); + BOOST_CHECK(deviceView.isROFEnabled(4, 2)); + BOOST_CHECK_THROW((o2::its::ROFMaskTable<2>{secondTiming}), std::invalid_argument); + BOOST_CHECK_THROW(ROFOverlapTable{-1}, std::invalid_argument); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testSlabBumpAllocator.cxx b/Detectors/ITSMFT/common/tracking/test/testSlabBumpAllocator.cxx index f12e1b3d2c1fd..38bd255a27146 100644 --- a/Detectors/ITSMFT/common/tracking/test/testSlabBumpAllocator.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testSlabBumpAllocator.cxx @@ -29,6 +29,7 @@ #include "ITSMFTTracking/BoundedAllocator.h" #include "ITSMFTTracking/CapacityEstimator.h" +#include "ITSMFTTracking/IdTypes.h" #include "ITSMFTTracking/SlabBumpAllocator.h" using namespace o2::itsmft::tracking; @@ -615,7 +616,7 @@ BOOST_AUTO_TEST_CASE(estimator_reset_forgets_inflated_margins) BOOST_AUTO_TEST_CASE(estimator_updates_immediately_and_commit_retains_updates) { CapacityEstimator est; - const auto key = CapacityEstimator::makeKey(SlabSite::Neighbours, 2, 0, 4); + const auto key = CapacityEstimator::makeKey(SlabSite::Neighbours, 2, 0, CellPathId{4}); est.update(key, 100., 120, 100, 95, 7, true, false); const auto immediate = est.statistics(key); BOOST_TEST(immediate.requested == 120u); @@ -637,7 +638,7 @@ BOOST_AUTO_TEST_CASE(estimator_updates_immediately_and_commit_retains_updates) BOOST_AUTO_TEST_CASE(estimator_rollback_restores_the_first_touch_state_exactly) { CapacityEstimator est; - const auto key = CapacityEstimator::makeKey(SlabSite::Neighbours, 2, 0, 4); + const auto key = CapacityEstimator::makeKey(SlabSite::Neighbours, 2, 0, CellPathId{4}); constexpr double scale = 100.; est.update(key, scale, 120, 100, 95, 7, true, false); const auto before = snapshot(est, key, scale); @@ -657,7 +658,7 @@ BOOST_AUTO_TEST_CASE(estimator_rollback_restores_the_first_touch_state_exactly) BOOST_AUTO_TEST_CASE(estimator_rollback_removes_a_transaction_created_key) { CapacityEstimator est; - const auto key = CapacityEstimator::makeKey(SlabSite::Cells, 3, 0, 5); + const auto key = CapacityEstimator::makeKey(SlabSite::Cells, 3, 0, CellPathId{5}); constexpr double scale = 50.; const auto absent = snapshot(est, key, scale); @@ -673,7 +674,7 @@ BOOST_AUTO_TEST_CASE(estimator_rollback_removes_a_transaction_created_key) BOOST_AUTO_TEST_CASE(estimator_nested_transaction_rejection_preserves_the_active_transaction) { CapacityEstimator est; - const auto key = CapacityEstimator::makeKey(SlabSite::Roads, 1, 0, 2); + const auto key = CapacityEstimator::makeKey(SlabSite::Roads, 1, 0, CellPathId{2}); constexpr double scale = 100.; est.update(key, scale, 50, 50, 40, 0, false, false); const auto before = snapshot(est, key, scale); @@ -693,8 +694,8 @@ BOOST_AUTO_TEST_CASE(estimator_nested_transaction_rejection_preserves_the_active BOOST_AUTO_TEST_CASE(estimator_reset_clears_active_transaction_and_learning) { CapacityEstimator est; - const auto existing = CapacityEstimator::makeKey(SlabSite::Tracklets, 1, 0, 2); - const auto created = CapacityEstimator::makeKey(SlabSite::Tracklets, 1, 0, 3); + const auto existing = CapacityEstimator::makeKey(SlabSite::Tracklets, 1, 0, EdgeId{2}); + const auto created = CapacityEstimator::makeKey(SlabSite::Tracklets, 1, 0, EdgeId{3}); constexpr double scale = 100.; est.update(existing, scale, 200, 180, 170, 3, true, false); est.beginTransaction(); @@ -722,13 +723,13 @@ BOOST_AUTO_TEST_CASE(estimator_keys_separate_the_road_walk_steps) BOOST_TEST(b != c); } -BOOST_AUTO_TEST_CASE(estimator_keys_separate_stage_iteration_and_site) +BOOST_AUTO_TEST_CASE(estimator_keys_separate_stage_iteration_and_typed_site) { - const auto edge0 = CapacityEstimator::makeKey(SlabSite::Tracklets, 0, 0, 0); - const auto edge1 = CapacityEstimator::makeKey(SlabSite::Tracklets, 0, 0, 1); - const auto nextIteration = CapacityEstimator::makeKey(SlabSite::Tracklets, 1, 0, 0); - const auto path0 = CapacityEstimator::makeKey(SlabSite::Cells, 0, 0, 0); - const auto path1 = CapacityEstimator::makeKey(SlabSite::Cells, 0, 0, 1); + const auto edge0 = CapacityEstimator::makeKey(SlabSite::Tracklets, 0, 0, EdgeId{0}); + const auto edge1 = CapacityEstimator::makeKey(SlabSite::Tracklets, 0, 0, EdgeId{1}); + const auto nextIteration = CapacityEstimator::makeKey(SlabSite::Tracklets, 1, 0, EdgeId{0}); + const auto path0 = CapacityEstimator::makeKey(SlabSite::Cells, 0, 0, CellPathId{0}); + const auto path1 = CapacityEstimator::makeKey(SlabSite::Cells, 0, 0, CellPathId{1}); BOOST_TEST(edge0 != edge1); BOOST_TEST(edge0 != nextIteration); BOOST_TEST(edge0 != path0); diff --git a/Detectors/ITSMFT/common/tracking/test/testSurfaceTiming.cxx b/Detectors/ITSMFT/common/tracking/test/testSurfaceTiming.cxx new file mode 100644 index 0000000000000..8a9ab9219d4ba --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testSurfaceTiming.cxx @@ -0,0 +1,261 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE ITSMFT SurfaceTiming +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include + +#include "CommonDataFormat/InteractionRecord.h" +#include "ITSMFTTracking/SurfaceTiming.h" + +using namespace o2::itsmft::tracking; +using o2::its::LayerTiming; + +BOOST_AUTO_TEST_CASE(ROFIntervalBCViewsAreDeviceFriendly) +{ + static_assert(std::is_standard_layout_v); + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_standard_layout_v); + static_assert(std::is_trivially_copyable_v); + BOOST_CHECK(std::is_standard_layout_v); + BOOST_CHECK(std::is_trivially_copyable_v); +} + +BOOST_AUTO_TEST_CASE(BeginEndFollowLayerTimingSignConvention) +{ + // Mirrors o2::its::LayerTiming::getROFStartInBC/getROFEndInBC: start is the + // ROF's own BC plus delay plus bias; end is start plus the readout length. + const o2::InteractionRecord origin{100, 0}; + const o2::InteractionRecord rofIR{150, 0}; // 50 BC after origin + const ROFTimingConfig cfg{/*rofLength*/ 40, /*rofDelay*/ 5, /*rofBias*/ -2, /*rofAddTimeErr*/ 3}; + + const auto built = computeROFIntervalBC(rofIR, origin, cfg, 7); + BOOST_REQUIRE(built.ok()); + BOOST_CHECK_EQUAL(built.interval.begin, 53); // 50 + 5 - 2 + BOOST_CHECK_EQUAL(built.interval.end, 93); // begin + rofLength + BOOST_CHECK_EQUAL(built.interval.sourceROF, 7u); + BOOST_CHECK(built.interval.isValid()); +} + +BOOST_AUTO_TEST_CASE(NegativeTFRelativeBCIsLegal) +{ + const o2::InteractionRecord origin{200, 0}; + const o2::InteractionRecord rofIR{50, 0}; // 150 BC before origin + const ROFTimingConfig cfg{40, 0, 0, 0}; + + const auto built = computeROFIntervalBC(rofIR, origin, cfg, 0); + BOOST_REQUIRE(built.ok()); + BOOST_CHECK_EQUAL(built.interval.begin, -150); + BOOST_CHECK_EQUAL(built.interval.end, -110); + BOOST_CHECK(built.interval.isValid()); +} + +BOOST_AUTO_TEST_CASE(InvalidROFLengthIsRejected) +{ + const o2::InteractionRecord origin{0, 0}; + const ROFTimingConfig cfg{0, 0, 0, 0}; + const auto built = computeROFIntervalBC(origin, origin, cfg, 0); + BOOST_CHECK(!built.ok()); + BOOST_CHECK(built.error == TimingBuildError::InvalidROFLength); +} + +BOOST_AUTO_TEST_CASE(OverflowIsDetectedAndChecked) +{ + const o2::InteractionRecord origin{0, 0}; + const o2::InteractionRecord rofIR{0, 0}; + ROFTimingConfig cfg{1, std::numeric_limits::max(), 1, 0}; + const auto built = computeROFIntervalBC(rofIR, origin, cfg, 0); + BOOST_CHECK(!built.ok()); + BOOST_CHECK(built.error == TimingBuildError::Overflow); +} + +BOOST_AUTO_TEST_CASE(InvalidSourceROFIsRejected) +{ + const o2::InteractionRecord origin{0, 0}; + const ROFTimingConfig cfg{40, 0, 0, 0}; + const auto built = computeROFIntervalBC(origin, origin, cfg, std::numeric_limits::max()); + BOOST_CHECK(!built.ok()); + BOOST_CHECK(built.error == TimingBuildError::InvalidSourceROF); +} + +BOOST_AUTO_TEST_CASE(WidenAppliesSymmetricMarginOnDemandOnly) +{ + const ROFIntervalBC interval{10, 20, 3, 0}; + const auto widened = widen(interval, 5); + BOOST_REQUIRE(widened.ok()); + BOOST_CHECK_EQUAL(widened.interval.begin, 5); + BOOST_CHECK_EQUAL(widened.interval.end, 25); + BOOST_CHECK_EQUAL(widened.interval.sourceROF, 3u); + // The base interval itself is never mutated by widen(). + BOOST_CHECK_EQUAL(interval.begin, 10); + BOOST_CHECK_EQUAL(interval.end, 20); +} + +BOOST_AUTO_TEST_CASE(WidenRejectsInvalidInterval) +{ + constexpr ROFIntervalBC invalidInterval{}; + const auto widened = widen(invalidInterval, 5); + BOOST_CHECK(!widened.ok()); + BOOST_CHECK(widened.error == WidenError::InvalidInterval); +} + +BOOST_AUTO_TEST_CASE(WidenRejectsNegativeMargin) +{ + const ROFIntervalBC interval{10, 20, 3, 0}; + const auto widened = widen(interval, -1); + BOOST_CHECK(!widened.ok()); + BOOST_CHECK(widened.error == WidenError::InvalidMargin); +} + +BOOST_AUTO_TEST_CASE(WidenDetectsLowerBoundOverflow) +{ + const ROFIntervalBC interval{std::numeric_limits::min() + 5, 20, 3, 0}; + const auto widened = widen(interval, 10); + BOOST_CHECK(!widened.ok()); + BOOST_CHECK(widened.error == WidenError::LowerBoundOverflow); +} + +BOOST_AUTO_TEST_CASE(WidenDetectsUpperBoundOverflow) +{ + const ROFIntervalBC interval{10, std::numeric_limits::max() - 5, 3, 0}; + const auto widened = widen(interval, 10); + BOOST_CHECK(!widened.ok()); + BOOST_CHECK(widened.error == WidenError::UpperBoundOverflow); +} + +BOOST_AUTO_TEST_CASE(EmptyInputIsNotUniform) +{ + const auto result = deriveUniformROFTimingConfig({}); + BOOST_CHECK(!result.uniform); +} + +BOOST_AUTO_TEST_CASE(SingleLayerIsTriviallyUniform) +{ + const std::array layers{LayerTiming{.mNROFsTF = 10, .mROFLength = 40, .mROFDelay = 5, .mROFBias = 2, .mROFAddTimeErr = 1}}; + const auto result = deriveUniformROFTimingConfig(layers); + BOOST_REQUIRE(result.uniform); + BOOST_CHECK_EQUAL(result.config.rofLength, 40); + BOOST_CHECK_EQUAL(result.config.rofDelay, 5); + BOOST_CHECK_EQUAL(result.config.rofBias, 2); + BOOST_CHECK_EQUAL(result.config.rofAddTimeErr, 1); +} + +BOOST_AUTO_TEST_CASE(MatchedPerLayerValuesAreUniform) +{ + // Mirrors real ITS/MFT production defaults: every layer resolves to the + // same shared global value because DPLAlpideParam's per-layer staggering + // overrides all default to zero. + std::array layers{}; + for (auto& lt : layers) { + lt = LayerTiming{.mNROFsTF = 100, .mROFLength = 594, .mROFDelay = 0, .mROFBias = 64, .mROFAddTimeErr = 0}; + } + const auto result = deriveUniformROFTimingConfig(layers); + BOOST_REQUIRE(result.uniform); + BOOST_CHECK_EQUAL(result.config.rofLength, 594); + BOOST_CHECK_EQUAL(result.config.rofBias, 64); +} + +BOOST_AUTO_TEST_CASE(DivergentROFLengthIsRejected) +{ + std::array layers{ + LayerTiming{.mROFLength = 40, .mROFDelay = 0, .mROFBias = 0, .mROFAddTimeErr = 0}, + LayerTiming{.mROFLength = 40, .mROFDelay = 0, .mROFBias = 0, .mROFAddTimeErr = 0}, + LayerTiming{.mROFLength = 44, .mROFDelay = 0, .mROFBias = 0, .mROFAddTimeErr = 0}}; // staggered length + BOOST_CHECK(!deriveUniformROFTimingConfig(layers).uniform); +} + +BOOST_AUTO_TEST_CASE(DivergentROFDelayIsRejected) +{ + std::array layers{ + LayerTiming{.mROFLength = 40, .mROFDelay = 0, .mROFBias = 0, .mROFAddTimeErr = 0}, + LayerTiming{.mROFLength = 40, .mROFDelay = 3, .mROFBias = 0, .mROFAddTimeErr = 0}}; + BOOST_CHECK(!deriveUniformROFTimingConfig(layers).uniform); +} + +BOOST_AUTO_TEST_CASE(DivergentROFBiasIsRejected) +{ + std::array layers{ + LayerTiming{.mROFLength = 40, .mROFDelay = 0, .mROFBias = 64, .mROFAddTimeErr = 0}, + LayerTiming{.mROFLength = 40, .mROFDelay = 0, .mROFBias = 60, .mROFAddTimeErr = 0}}; + BOOST_CHECK(!deriveUniformROFTimingConfig(layers).uniform); +} + +BOOST_AUTO_TEST_CASE(DivergentAddTimeErrIsRejected) +{ + std::array layers{ + LayerTiming{.mROFLength = 40, .mROFDelay = 0, .mROFBias = 0, .mROFAddTimeErr = 0}, + LayerTiming{.mROFLength = 40, .mROFDelay = 0, .mROFBias = 0, .mROFAddTimeErr = 5}}; + BOOST_CHECK(!deriveUniformROFTimingConfig(layers).uniform); +} + +BOOST_AUTO_TEST_CASE(IntersectionIsHalfOpenAndIgnoresROFOrdinal) +{ + const ROFIntervalBC a{0, 10, 5, 0}; + const ROFIntervalBC touching{10, 20, 5, 0}; // same sourceROF as `a`, adjacent + const ROFIntervalBC overlapping{9, 15, 99, 0}; // different sourceROF, overlaps + const ROFIntervalBC disjoint{100, 110, 5, 0}; + + BOOST_CHECK(!intersects(a, touching)); // half-open: touching does not intersect + BOOST_CHECK(intersects(a, overlapping)); // overlap decided by time, not ROF equality + BOOST_CHECK(!intersects(a, disjoint)); + BOOST_CHECK(intersects(a, a)); +} + +BOOST_AUTO_TEST_CASE(InvalidIntervalsNeverIntersect) +{ + constexpr ROFIntervalBC invalidInterval{}; + const ROFIntervalBC valid{0, 10, 0, 0}; + const ROFIntervalBC invalidSourceROF{0, 10, std::numeric_limits::max(), 0}; + const ROFIntervalBC zeroLength{5, 5, 0, 0}; + const ROFIntervalBC reversed{10, 5, 0, 0}; + + BOOST_CHECK(!intersects(invalidInterval, valid)); + BOOST_CHECK(!intersects(valid, invalidInterval)); + BOOST_CHECK(!intersects(invalidSourceROF, valid)); + BOOST_CHECK(!intersects(zeroLength, valid)); + BOOST_CHECK(!intersects(reversed, valid)); +} + +BOOST_AUTO_TEST_CASE(DefaultIntervalIsInvalidSentinel) +{ + constexpr ROFIntervalBC interval{}; + BOOST_CHECK_EQUAL(interval.sourceROF, std::numeric_limits::max()); + BOOST_CHECK(!interval.isValid()); // sourceROF is the sentinel and begin == end + BOOST_CHECK_EQUAL(interval.length(), 0); +} + +BOOST_AUTO_TEST_CASE(ZeroLengthAndReversedIntervalsAreInvalid) +{ + constexpr ROFIntervalBC zeroLength{5, 5, 0, 0}; + constexpr ROFIntervalBC reversed{10, 5, 0, 0}; + BOOST_CHECK(!zeroLength.isValid()); + BOOST_CHECK(!reversed.isValid()); +} + +BOOST_AUTO_TEST_CASE(IntervalWithInvalidSourceROFIsInvalidEvenWithPositiveExtent) +{ + constexpr ROFIntervalBC interval{0, 10, std::numeric_limits::max(), 0}; + BOOST_CHECK(!interval.isValid()); +} + +BOOST_AUTO_TEST_CASE(LengthIsSafeForExtremeSignedRange) +{ + // Signed `end - begin` overflows TFBC here (INT64_MAX - INT64_MIN does not + // fit in int64_t); length() must still return the exact, correct distance + // via unsigned arithmetic instead of invoking signed-overflow UB. + constexpr ROFIntervalBC interval{std::numeric_limits::min(), std::numeric_limits::max(), 0, 0}; + BOOST_CHECK(interval.isValid()); + BOOST_CHECK_EQUAL(interval.length(), std::numeric_limits::max()); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testTimeFrameLifecycle.cxx b/Detectors/ITSMFT/common/tracking/test/testTimeFrameLifecycle.cxx new file mode 100644 index 0000000000000..6db9f2c14205a --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testTimeFrameLifecycle.cxx @@ -0,0 +1,411 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// TimeFrame lifecycle, transactional configuration, and direct loading. +// +// A. Reset lifecycle: TimeFrame::resetTimeFrame() unconditionally clears all +// TimeFrame data while preserving detector configuration and allocator +// identity. Post-reset checks always obtain fresh views. +// +// B. Strong configuration transactionality: a BoundedMemoryResource failure +// while staging a valid replacement must preserve the live configuration, +// workspace, allocator and capacities, as well as an already loaded TimeFrame, +// its allocator-backed storage, navigation, and results. +// +// C. TimeFrame loading resets and fills the configured frame directly. Any +// failure clears partially loaded data. + +#define BOOST_TEST_MODULE ITSMFT TimeFrame lifecycle +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include +#include +#include +#include + +#include + +#include "CommonDataFormat/InteractionRecord.h" +#include "DataFormatsITSMFT/CompCluster.h" +#include "DataFormatsITSMFT/ROFRecord.h" +#include "DataFormatsITSMFT/TopologyDictionary.h" +#include "DetectorsCommonDataFormats/DetID.h" +#include "ITSMFTTracking/DetectorLayout.h" +#include "ITSMFTTracking/detail/TimeFrameScratch.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/ClusterDecoding.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "SimulationDataFormat/MCCompLabel.h" +#include "SimulationDataFormat/MCTruthContainer.h" + +using namespace o2::itsmft; +using namespace o2::itsmft::tracking; + +namespace +{ + +// Deterministic, geometry-free stand-in for GeometryClusterDecoder +// (same construction as testTimeFrameNormalizedSource.cxx / testMultiSourceLoading.cxx): +// sensorID is used directly as the detector-local layer, global/frame +// coordinates are pure functions of (sensorID, row, col), and pattern +// consumption goes through the real production helper so cursor bookkeeping +// is exercised identically to GeometryClusterDecoder. +class LegacyLikeDecoder final : public ClusterDecoder +{ + public: + explicit LegacyLikeDecoder(o2::detectors::DetID::ID detector) : mDetector(detector) {} + + o2::itsmft::tracking::ClusterDecodeResult decode( + const CompClusterExt& cluster, + BoundedPatternCursor& patterns, + const TopologyDictionary* dict, + uint32_t, + bool applySysErrors) const override + { + const auto clusterData = o2::itsmft::ioutils::extractClusterDataBounded(cluster, patterns, dict); + if (!clusterData.ok()) { + o2::itsmft::tracking::ClusterDecodeResult result; + result.error = clusterData.error; + return result; + } + + o2::itsmft::tracking::ClusterDecodeResult result; + const int sensorID = cluster.getSensorID(); + auto& decoded = result.decoded; + decoded.global = {static_cast(sensorID) * 10.f, static_cast(cluster.getRow()), static_cast(cluster.getCol())}; + decoded.cylinderFrame = {static_cast(sensorID) + 100.f, static_cast(cluster.getRow()) + 1.f, static_cast(cluster.getCol()) + 2.f, 0.01f * sensorID}; + decoded.rowColumnCovariance = {clusterData.sig2Row, 0.f, clusterData.sig2Col}; + decoded.shape = clusterData.shape; + decoded.layer = sensorID; + // Counts only clusters this decoder actually turned into a measurement + // (the early-return failure paths above never reach here), so a test can + // prove every cluster of a given input was successfully decoded by + // checking how much this counter advanced across that call. + ++decodeCount; + return result; + } + + mutable int decodeCount{0}; + + private: + o2::detectors::DetID::ID mDetector; +}; + +const TopologyDictionary& dict() +{ + static const TopologyDictionary d; + return d; +} + +constexpr std::array onePixelPattern{1, 1, 0x80}; // 1x1, 1 pixel +constexpr std::array threePixelPattern{1, 3, 0xE0}; // 1x3, 3 pixels + +std::vector concatPatterns(std::initializer_list> parts) +{ + std::vector bytes; + for (const auto& p : parts) { + bytes.insert(bytes.end(), p.begin(), p.end()); + } + return bytes; +} + +std::vector makeITSTestCatalog() +{ + std::vector surfaces; + surfaces.reserve(ITSNLayers); + for (uint16_t i = 0; i < ITSNLayers; ++i) { + surfaces.push_back(SurfaceDescriptor{i, static_cast(o2::detectors::DetID::ITS), SurfaceKind::Cylinder}); + } + return surfaces; +} + +std::vector identitySurfaces(uint16_t nLayers) +{ + std::vector mapping; + mapping.reserve(nLayers); + for (uint16_t i = 0; i < nLayers; ++i) { + mapping.push_back(LayerId{i}); + } + return mapping; +} + +DetectorLayout catalogLayout(SurfaceCatalogView catalog) +{ + return DetectorLayout{gsl::span{catalog.surfaces, catalog.nSurfaces}, + makeDetectorLayout()}; +} + +GlobalPoint3F expectedGlobal(int sensorID, int row, int col) +{ + return {static_cast(sensorID) * 10.f, static_cast(row), static_cast(col)}; +} + +struct Fixture { + std::vector clusters; + std::vector patterns; + std::vector rofs; + o2::dataformats::MCTruthContainer labels; +}; + +// 4 clusters on layers {0,1,0,2}, partitioned into 3 ROFs: ROF0={c0,c1}, +// ROF1={c2}, ROF2={c3}. Identical shape to testTimeFrameNormalizedSource.cxx's +// fixture, so parity with that accepted test coverage is preserved. +Fixture makeFixture() +{ + Fixture f; + f.clusters = { + CompClusterExt{10, 20, CompCluster::InvalidPatternID, 0}, // sensor 0 -> layer 0 + CompClusterExt{11, 21, CompCluster::InvalidPatternID, 1}, // sensor 1 -> layer 1 + CompClusterExt{12, 22, CompCluster::InvalidPatternID, 0}, // sensor 0 -> layer 0 + CompClusterExt{13, 23, CompCluster::InvalidPatternID, 2}, // sensor 2 -> layer 2 + }; + f.patterns = concatPatterns({onePixelPattern, threePixelPattern, onePixelPattern, threePixelPattern}); + f.rofs = { + ROFRecord{{100, 5}, 0, 0, 2}, + ROFRecord{{140, 5}, 1, 2, 1}, + ROFRecord{{1000, 6}, 2, 3, 1}}; + for (uint32_t i = 0; i < f.clusters.size(); ++i) { + f.labels.addElement(i, o2::MCCompLabel{static_cast(i) + 1, 0, 0}); + } + return f; +} + +// A second, distinct, independently valid fixture: different sensors/layers +// (3,4,3,5,3 instead of 0,1,0,2), different rows/columns, a different +// pattern arrangement, a different ROF partition (3 ROFs over 5 clusters +// instead of 4), and its own separate MCTruthContainer with different label +// values. Used as the *replacement* load in the strong-exception-safety +// test, so that if any partial commit ever leaked through, it would be +// observable as foreign data (wrong layer, wrong coordinates, wrong label) +// rather than being masked by coincidentally reloading the same values. +Fixture makeReplacementFixture() +{ + Fixture f; + f.clusters = { + CompClusterExt{50, 60, CompCluster::InvalidPatternID, 3}, // sensor 3 -> layer 3 + CompClusterExt{51, 61, CompCluster::InvalidPatternID, 4}, // sensor 4 -> layer 4 + CompClusterExt{52, 62, CompCluster::InvalidPatternID, 3}, // sensor 3 -> layer 3 + CompClusterExt{53, 63, CompCluster::InvalidPatternID, 5}, // sensor 5 -> layer 5 + CompClusterExt{54, 64, CompCluster::InvalidPatternID, 3}, // sensor 3 -> layer 3 + }; + f.patterns = concatPatterns({threePixelPattern, threePixelPattern, onePixelPattern, threePixelPattern, onePixelPattern}); + f.rofs = { + ROFRecord{{500, 1}, 0, 0, 3}, + ROFRecord{{540, 1}, 1, 3, 1}, + ROFRecord{{2000, 2}, 2, 4, 1}}; + for (uint32_t i = 0; i < f.clusters.size(); ++i) { + f.labels.addElement(i, o2::MCCompLabel{static_cast(i) + 101, 1, 1}); + } + return f; +} + +struct Expected { + uint32_t externalIndex; + int layer; + int sensorID; + int row, col; + uint32_t sourceROF; + uint32_t nPixels; +}; + +const std::vector expectedClusters{ + {0, 0, 0, 10, 20, 0, 1}, + {1, 1, 1, 11, 21, 0, 3}, + {2, 0, 0, 12, 22, 1, 1}, + {3, 2, 2, 13, 23, 2, 3}, +}; + +void verifyFixtureLoaded(const TimeFrame& frame, const Fixture& f) +{ + BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{0}).size(), 2u); + BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{1}).size(), 1u); + BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{2}).size(), 1u); + for (int l = 3; l < ITSNLayers; ++l) { + BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{static_cast(l)}).size(), 0u); + BOOST_CHECK_EQUAL(frame.getNrof(l), static_cast(f.rofs.size())); + } + + BOOST_CHECK_EQUAL(frame.getNrof(0), static_cast(f.rofs.size())); + + for (std::size_t expectedIndex = 0; expectedIndex < expectedClusters.size(); ++expectedIndex) { + const auto& e = expectedClusters[expectedIndex]; + const auto localClusterId = static_cast(std::count_if( + expectedClusters.begin(), expectedClusters.begin() + expectedIndex, + [&](const auto& previous) { return previous.layer == e.layer; })); + const GlobalMeasurement* globalMeasurement = nullptr; + const SurfaceMeasurement* measurement = nullptr; + const auto surface = LayerId{static_cast(e.layer)}; + const auto globals = frame.getGlobalMeasurements(surface); + for (size_t index = 0; index < globals.size(); ++index) { + if (globals[index].clusterId == localClusterId) { + globalMeasurement = &globals[index]; + measurement = frame.getSurfaceMeasurement(surface, localClusterId); + break; + } + } + BOOST_REQUIRE(globalMeasurement != nullptr); + BOOST_REQUIRE(measurement != nullptr); + + const auto g = expectedGlobal(e.sensorID, e.row, e.col); + BOOST_CHECK_EQUAL(globalMeasurement->position.x, g.x); + BOOST_CHECK_EQUAL(globalMeasurement->position.y, g.y); + BOOST_CHECK_EQUAL(globalMeasurement->position.z, g.z); + + BOOST_CHECK_EQUAL(measurement->frame.q, static_cast(e.sensorID) + 100.f); + BOOST_CHECK_EQUAL(measurement->frame.u, static_cast(e.row) + 1.f); + BOOST_CHECK_EQUAL(measurement->frame.v, static_cast(e.col) + 2.f); + BOOST_CHECK_EQUAL(measurement->frame.frameAngle, 0.01f * e.sensorID); + + BOOST_CHECK_EQUAL(measurement->covariance.uu, o2::itsmft::ioutils::DefClusError2Row); + BOOST_CHECK_EQUAL(measurement->covariance.uv, 0.f); + BOOST_CHECK_EQUAL(measurement->covariance.vv, o2::itsmft::ioutils::DefClusError2Col); + + BOOST_CHECK_EQUAL(globalMeasurement->clusterId, localClusterId); + const auto normalizedLabels = frame.getLabels(surface, localClusterId); + BOOST_REQUIRE_EQUAL(normalizedLabels.size(), 1u); + BOOST_CHECK(normalizedLabels[0] == o2::MCCompLabel(static_cast(e.externalIndex) + 1, 0, 0)); + } +} + +void configureFrame(TimeFrame& frame, SurfaceCatalogView catalog, + std::shared_ptr pool = std::make_shared()) +{ + auto layout = catalogLayout(catalog); + BOOST_REQUIRE(frame.configure(std::move(layout), 0, 0, std::move(pool))); +} + +} // namespace + +// --- A. Wipe lifecycle ------------------------------------------------- + +BOOST_AUTO_TEST_CASE(WipeClearsNormalizedFrameButPreservesDetId) +{ + const auto catalog = makeITSTestCatalog(); + const auto orderedSurfaces = identitySurfaces(ITSNLayers); + const SurfaceCatalogView catalogView{catalog.data(), static_cast(catalog.size())}; + LegacyLikeDecoder decoder{o2::detectors::DetID::ITS}; + const o2::InteractionRecord origin{50, 5}; + const ROFTimingConfig timing{40, 0, 0, 0}; + + TimeFrame frame; + const auto plan = catalogLayout(catalogView); + configureFrame(frame, catalogView); + const auto estimatorKey = CapacityEstimator::makeKey(SlabSite::Cells, 2, 0, CellPathId{3}); + frame.getCapacityEstimator().update(estimatorKey, 1000., 8000, 8000, false, false); + const auto learnedCapacity = frame.getCapacityEstimator().capacity(estimatorKey, 1000.); + BOOST_REQUIRE_GT(learnedCapacity, 1024u); + + const auto f = makeFixture(); + const auto result = loadTimeFrameSource(frame, decoder, origin, timing, f.clusters, f.patterns, f.rofs, &dict(), &f.labels, o2::detectors::DetID::ITS, + gsl::span{orderedSurfaces}, plan.getSurfaceCatalog()); + BOOST_REQUIRE(result.ok()); + // Sanity: the successful load itself has the expected content, matching + // the accepted parity coverage in testTimeFrameNormalizedSource.cxx. + verifyFixtureLoaded(frame, f); + + frame.resetTimeFrame(); + BOOST_CHECK_EQUAL(frame.getCapacityEstimator().capacity(estimatorKey, 1000.), learnedCapacity); + + // --- inspect only freshly obtained normalized accessors/views --- + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); + BOOST_CHECK_EQUAL(frame.getNMeasurementSurfaces(), ITSNLayers); + for (uint16_t s = 0; s < ITSNLayers; ++s) { + BOOST_CHECK(frame.getGlobalMeasurements(LayerId{s}).empty()); + } + BOOST_CHECK(frame.getLabels(LayerId{0}, 0).empty()); + + // Gate 4 B3.1: neither owner stores mDetId any more -- the plan lives on + // `plan` above, entirely outside both TimeFrame and LegacyTrackerScratch, + // so resetTimeFrame() has no detector-identity state to preserve or clear. +} + +BOOST_AUTO_TEST_CASE(FailedConfigurationAllocationLeavesClearedFrame) +{ + const auto catalog = makeITSTestCatalog(); + const auto orderedSurfaces = identitySurfaces(ITSNLayers); + const SurfaceCatalogView catalogView{catalog.data(), static_cast(catalog.size())}; + TimeFrame frame; + const auto estimatorKey = CapacityEstimator::makeKey(SlabSite::Tracklets, 1, 0, EdgeId{2}); + frame.getCapacityEstimator().update(estimatorKey, 1000., 9000, 9000, false, false); + const auto learnedCapacity = frame.getCapacityEstimator().capacity(estimatorKey, 1000.); + const auto* const scratch = &frame.getScratch(); + auto layout = catalogLayout(catalogView); + auto failingPool = std::make_shared(0); + + BOOST_CHECK(!frame.configure(std::move(layout), 1, 1, failingPool)); + BOOST_CHECK_EQUAL(frame.getCapacityEstimator().capacity(estimatorKey, 1000.), learnedCapacity); + BOOST_CHECK_EQUAL(failingPool->getThrowCount(), 1u); + BOOST_CHECK_EQUAL(failingPool->getUsedMemory(), 0u); + + BOOST_CHECK(!frame.isConfigured()); + BOOST_CHECK(&frame.getScratch() == scratch); + BOOST_CHECK(frame.getMemoryPool().get() == failingPool.get()); + BOOST_CHECK(frame.getScratch().getMemoryPool().get() == failingPool.get()); + BOOST_CHECK_EQUAL(frame.getScratch().getNEdges(), 0u); + BOOST_CHECK_EQUAL(frame.getScratch().getNCells(), 0u); + BOOST_CHECK(frame.getLayout().empty()); + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); + BOOST_CHECK(frame.getGenericTracks().empty()); + BOOST_CHECK(frame.getTrackClusterIndices().empty()); + BOOST_CHECK_EQUAL(frame.getPrimaryVerticesNum(), 0u); +} + +BOOST_AUTO_TEST_CASE(ConfigurationAdoptionResetsIncompatibleCapacityEstimates) +{ + const auto catalog = makeITSTestCatalog(); + const SurfaceCatalogView catalogView{catalog.data(), static_cast(catalog.size())}; + TimeFrame frame; + const auto key = CapacityEstimator::makeKey(SlabSite::Roads, 3, + CapacityEstimator::makeVariant(5, 3), CellPathId{7}); + frame.getCapacityEstimator().update(key, 1000., 12000, 12000, false, false); + BOOST_REQUIRE_GT(frame.getCapacityEstimator().capacity(key, 1000.), 1024u); + + configureFrame(frame, catalogView); + + BOOST_CHECK_EQUAL(frame.getCapacityEstimator().capacity(key, 1000.), 1024u); +} + +BOOST_AUTO_TEST_CASE(MalformedTimeFrameLoadLeavesTheFrameEmpty) +{ + const auto catalog = makeITSTestCatalog(); + const auto orderedSurfaces = identitySurfaces(ITSNLayers); + const SurfaceCatalogView catalogView{catalog.data(), static_cast(catalog.size())}; + LegacyLikeDecoder decoder{o2::detectors::DetID::ITS}; + const o2::InteractionRecord origin{50, 5}; + const ROFTimingConfig timing{40, 0, 0, 0}; + const auto baselineFixture = makeFixture(); + auto malformedReplacement = makeReplacementFixture(); + const auto plan = catalogLayout(catalogView); + TimeFrame frame; + configureFrame(frame, catalogView); + const auto baseline = loadTimeFrameSource(frame, decoder, origin, timing, baselineFixture.clusters, + baselineFixture.patterns, baselineFixture.rofs, &dict(), + &baselineFixture.labels, o2::detectors::DetID::ITS, + gsl::span{orderedSurfaces}, plan.getSurfaceCatalog()); + BOOST_REQUIRE(baseline.ok()); + verifyFixtureLoaded(frame, baselineFixture); + + malformedReplacement.rofs.front().setFirstEntry(1); + const auto failed = loadTimeFrameSource(frame, decoder, origin, timing, malformedReplacement.clusters, + malformedReplacement.patterns, malformedReplacement.rofs, &dict(), + &malformedReplacement.labels, o2::detectors::DetID::ITS, + gsl::span{orderedSurfaces}, plan.getSurfaceCatalog()); + BOOST_CHECK(!failed.ok()); + BOOST_CHECK(failed.error == MultiSourceLoadError::InvalidROFRange); + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); + BOOST_CHECK_EQUAL(frame.getNMeasurementSurfaces(), ITSNLayers); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testTimeFrameLoadFailure.cxx b/Detectors/ITSMFT/common/tracking/test/testTimeFrameLoadFailure.cxx new file mode 100644 index 0000000000000..28bb843bdfce1 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testTimeFrameLoadFailure.cxx @@ -0,0 +1,122 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// Pure classification/typed-exception tests for the workflow loading boundary +// loading boundary (ITSMFTTracking/IOUtils.h). No geometry +// singleton, DPL, or CCDB dependency: isRecoverableLoadError() and the two +// exception types are plain host-only code. + +#define BOOST_TEST_MODULE ITSMFT TimeFrameLoadFailure +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include + +#include "ITSMFTTracking/IOUtils.h" + +using namespace o2::itsmft::tracking; + +namespace +{ +// Hand-maintained, exhaustive list of every MultiSourceLoadError enumerator +// as declared in IOUtils.h, paired with the classification this +// design's failure taxonomy requires. This -- not the absence of `default:` +// in isRecoverableLoadError()'s switch -- is the actual, checked coverage +// guarantee: if a new enumerator is added there without a corresponding +// entry here, this list's size assertion below fails. +struct Case { + MultiSourceLoadError error; + bool recoverable; // meaningless when error == TimingError; see the dedicated TimingError cases below +}; + +constexpr std::array kAllNonTimingCases{{ + {MultiSourceLoadError::None, false}, + {MultiSourceLoadError::NonDenseSourceIds, false}, + {MultiSourceLoadError::DuplicateSourceId, false}, + {MultiSourceLoadError::UnsupportedDetector, false}, + {MultiSourceLoadError::MissingDecoder, false}, + {MultiSourceLoadError::InvalidROFRange, true}, + {MultiSourceLoadError::InvalidLayerMapping, false}, + {MultiSourceLoadError::DetectorSurfaceMismatch, false}, + {MultiSourceLoadError::InconsistentDecoderMetadata, false}, + {MultiSourceLoadError::TimingError, false}, // placeholder entry: real classification is TimingBuildError-dependent, see below + {MultiSourceLoadError::SurfaceCatalogNotConfigured, false}, + {MultiSourceLoadError::SurfaceCatalogStale, false}, + {MultiSourceLoadError::MissingDictionary, false}, + {MultiSourceLoadError::TruncatedExplicitPattern, true}, + {MultiSourceLoadError::MalformedExplicitPattern, true}, + {MultiSourceLoadError::InvalidPatternId, true}, + {MultiSourceLoadError::InvalidSensor, true}, + {MultiSourceLoadError::InvalidDecodedLayer, true}, + {MultiSourceLoadError::GeometryUnavailable, false}, + {MultiSourceLoadError::OtherMalformedInput, true}, + {MultiSourceLoadError::TrailingPatternData, true}, +}}; +} // namespace + +BOOST_AUTO_TEST_CASE(ClassifyEveryMultiSourceLoadErrorExceptTiming) +{ + // Bump this count, and the list above, whenever MultiSourceLoadError + // gains or loses an enumerator -- that is the mechanism that actually + // catches a classification gap, not the switch's missing `default:`. + static_assert(kAllNonTimingCases.size() == 22); + for (const auto& c : kAllNonTimingCases) { + if (c.error == MultiSourceLoadError::TimingError) { + continue; // covered exhaustively below, per TimingBuildError value + } + BOOST_CHECK_MESSAGE(isRecoverableLoadError(c.error, TimingBuildError::None) == c.recoverable, + "error=" << static_cast(c.error)); + } +} + +BOOST_AUTO_TEST_CASE(ClassifyTimingErrorForEveryTimingBuildErrorValue) +{ + // Overflow is a genuine per-TF BC-arithmetic overflow caused by the + // incoming ROF data: recoverable. InvalidROFLength and InvalidSourceROF + // are configuration problems, and None must never be paired with + // MultiSourceLoadError::TimingError by a real caller (a successful + // computeROFIntervalBC() never reaches this classification at all) -- but + // isRecoverableLoadError() still classifies it structurally, safe-by- + // default, exactly like the other two. + BOOST_CHECK(isRecoverableLoadError(MultiSourceLoadError::TimingError, TimingBuildError::Overflow) == true); + BOOST_CHECK(isRecoverableLoadError(MultiSourceLoadError::TimingError, TimingBuildError::None) == false); + BOOST_CHECK(isRecoverableLoadError(MultiSourceLoadError::TimingError, TimingBuildError::InvalidROFLength) == false); + BOOST_CHECK(isRecoverableLoadError(MultiSourceLoadError::TimingError, TimingBuildError::InvalidSourceROF) == false); +} + +BOOST_AUTO_TEST_CASE(RecoverableLoadFailureRetainsCompleteResult) +{ + const LoadSourcesResult result{.error = MultiSourceLoadError::MalformedExplicitPattern, .source = ClusterSourceId{2}, .rof = 3, .clusterIndex = 4}; + const RecoverableLoadFailure failure{result}; + BOOST_CHECK(failure.error() == MultiSourceLoadError::MalformedExplicitPattern); + BOOST_CHECK(failure.result().error == result.error); + BOOST_CHECK(failure.result().source == result.source); + BOOST_CHECK_EQUAL(failure.result().rof, result.rof); + BOOST_CHECK_EQUAL(failure.result().clusterIndex, result.clusterIndex); +} + +BOOST_AUTO_TEST_CASE(TimeFrameLoadExceptionDistinguishesReasonsWithoutStringMatching) +{ + const TimeFrameLoadException dictionaryNotConfigured{TimeFrameLoadFailureReason::DictionaryNotConfigured, "cluster dictionary not configured"}; + BOOST_CHECK(dictionaryNotConfigured.reason() == TimeFrameLoadFailureReason::DictionaryNotConfigured); + BOOST_CHECK(dictionaryNotConfigured.loadResult().error == MultiSourceLoadError::None); + + const TimeFrameLoadException nonUniformTiming{TimeFrameLoadFailureReason::NonUniformROFTiming, "per-layer ROF timing configuration is not uniform"}; + BOOST_CHECK(nonUniformTiming.reason() == TimeFrameLoadFailureReason::NonUniformROFTiming); + BOOST_CHECK(nonUniformTiming.loadResult().error == MultiSourceLoadError::None); + + const LoadSourcesResult structuralResult{.error = MultiSourceLoadError::SurfaceCatalogStale, .source = ClusterSourceId{0}, .rof = 0, .clusterIndex = 0}; + const TimeFrameLoadException loadSourcesFailure{structuralResult}; + BOOST_CHECK(loadSourcesFailure.reason() == TimeFrameLoadFailureReason::LoadSourcesFailure); + BOOST_CHECK(loadSourcesFailure.loadResult().error == MultiSourceLoadError::SurfaceCatalogStale); + BOOST_CHECK(loadSourcesFailure.loadResult().source == structuralResult.source); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testTrackerFailureContract.cxx b/Detectors/ITSMFT/common/tracking/test/testTrackerFailureContract.cxx new file mode 100644 index 0000000000000..e834a23f591aa --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testTrackerFailureContract.cxx @@ -0,0 +1,1001 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// Tracker failure contract: Tracker::run() +// exception classification, wipe-on-every-failure, and the exact drop +// sentinel. +// +// Contract under test (see Tracker.h/Tracker.cxx): +// - TraversalException (structural/configuration failure): TimeFrame is +// wiped, then the exception always rethrows, regardless of +// DropTFUponFailure. +// - BoundedMemoryResource::MemoryLimitExceeded and std::bad_alloc +// (recoverable, per-TF resource failures): TimeFrame is wiped; +// DropTFUponFailure=true returns TrackingOutcome::RecoverableDropped +// sentinel, DropTFUponFailure=false rethrows. +// - Any other std::exception (e.g. std::runtime_error): treated as +// unclassified/structural, wiped, always rethrows regardless of the +// flag -- it must never be silently converted into a dropped-TF result. +// - Valid empty input (a real layout/topology with zero loaded clusters) +// completes without throwing and returns a non-negative, non-sentinel +// result. +// - A tracker instance that dropped one TimeFrame can immediately process a +// following one successfully. +// +// The std::bad_alloc and unclassified-std::exception cases use a real MFT +// road and a test-owned upstream memory resource that throws during normal +// traversal allocation. This keeps failure injection outside the production +// Tracker and refit APIs. +// +// Every fixture below establishes a real layout/plan and selected workspace +// and then loads a normalized source -- even the structural-failure cases, +// and even when that source carries zero clusters/ROFs -- before running +// tracking. This is load-bearing, not incidental: TimeFrame::initialise() +// unconditionally calls getNrof(layer) = mROFramesClusters[layer].size()-1 +// on every layer, and a never-loaded (default-constructed, size-0) +// mROFramesClusters underflows that subtraction, corrupting memory deep +// inside prepareClusters() rather than throwing a clean exception. +// loadNormalizedSource() sizes mROFramesClusters[layer] to rofs.size()+1 for +// every layer regardless of whether clusters/rofs are empty, which is what +// makes that call, and every "iterate 0..getNrof()" loop reached afterward, +// safe. The structural-failure cases below produce their TraversalException +// through an invalid TrackingParameters/index-table configuration, not +// through a missing/stale plan: Gate 4 B2 Slice 2 removed the plan-currency +// concept entirely (initialiseTimeFrame() now takes the plan as an explicit +// layout/topology view parameter, so "no plan" is no longer a state a +// caller can even construct) -- see the removed +// StructuralFailureViaStaleLayoutAlwaysRethrowsAndWipes test's replacement +// note below for what covers the "always rethrows and wipes" contract now. +// +// The recoverable-failure fixtures tighten the already-used frame allocator +// to its current usage. The next tracking allocation then exercises the +// normal bounded-resource failure/reset contract without changing config. + +#define BOOST_TEST_MODULE ITSMFT Tracker failure contract +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include "TrackingParameterTestSupport.h" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include "Field/MagneticField.h" + +#include "CommonDataFormat/InteractionRecord.h" +#include "DataFormatsITSMFT/CompCluster.h" +#include "DataFormatsITSMFT/ROFRecord.h" +#include "DataFormatsITSMFT/TopologyDictionary.h" +#include "DetectorsCommonDataFormats/DetID.h" +#include "ITSMFTTracking/Tracker.h" +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/detail/ITSSharedClusterCompatibility.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "ITSMFTTracking/detail/MFTFwdTrackHelpers.h" +#include "ITSMFTTracking/SurfaceDescriptor.h" +#include "ITSMFTTracking/detail/TimeFrameScratch.h" +#include "ITSMFTTracking/ClusterDecoding.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/TimeFrame.h" +#include "ITSMFTTracking/TrackerTraits.h" +#include "ITSMFTTracking/TrackingConfigParam.h" +#include "ITSMFTTracking/Constants.h" +#include "ITSMFTTracking/ROFLookupTables.h" +#include "SimulationDataFormat/MCCompLabel.h" +#include "SimulationDataFormat/MCTruthContainer.h" + +using namespace o2::itsmft; +using namespace o2::itsmft::tracking; + +namespace +{ + +// Deterministic, geometry-free stand-in for GeometryClusterDecoder, +// identical construction to testTimeFrameLifecycle.cxx / +// testTimeFrameNormalizedSource.cxx / testMultiSourceLoading.cxx. +class LegacyLikeDecoder final : public ClusterDecoder +{ + public: + explicit LegacyLikeDecoder(o2::detectors::DetID::ID detector) : mDetector(detector) {} + + o2::itsmft::tracking::ClusterDecodeResult decode( + const CompClusterExt& cluster, + BoundedPatternCursor& patterns, + const TopologyDictionary* dict, + uint32_t, + bool applySysErrors) const override + { + const auto clusterData = o2::itsmft::ioutils::extractClusterDataBounded(cluster, patterns, dict); + if (!clusterData.ok()) { + o2::itsmft::tracking::ClusterDecodeResult result; + result.error = clusterData.error; + return result; + } + + o2::itsmft::tracking::ClusterDecodeResult result; + const int sensorID = cluster.getSensorID(); + auto& decoded = result.decoded; + decoded.global = {static_cast(sensorID) * 10.f, static_cast(cluster.getRow()), static_cast(cluster.getCol())}; + decoded.cylinderFrame = {static_cast(sensorID) + 100.f, static_cast(cluster.getRow()) + 1.f, static_cast(cluster.getCol()) + 2.f, 0.01f * sensorID}; + decoded.rowColumnCovariance = {clusterData.sig2Row, 0.f, clusterData.sig2Col}; + decoded.shape = clusterData.shape; + decoded.layer = sensorID; + return result; + } + + private: + o2::detectors::DetID::ID mDetector; +}; + +const TopologyDictionary& dict() +{ + static const TopologyDictionary d; + return d; +} + +// TrackerTraits::findRoads() unconditionally touches the global +// o2::base::Propagator singleton on first use, which in turn requires +// TGeoGlobalMagField to already hold a real o2::field::MagneticField +// object -- with none set (the state of every other test in this suite, +// none of which calls Tracker::run() end to end), Propagator falls +// back to a legacy FairRunAna singleton that also does not exist in this +// process and segfaults dereferencing it. Only the tests that expect a +// genuinely successful Tracker::run() (valid empty input, +// continued processing after a drop) reach findRoads(); the +// structural/recoverable-failure tests throw/return before ever getting +// there and do not need this. A trivial default-constructed +// MagneticField (no field map file, zero solenoid current) is sufficient +// -- these tests never fit or propagate an actual trajectory since there +// are no clusters. TGeoGlobalMagField::Instance()->Lock() only allows one +// SetField() call per process, so this must run at most once. +void ensureTrivialMagneticFieldIsSet() +{ + static const bool done = [] { + TGeoGlobalMagField::Instance()->SetField(new o2::field::MagneticField()); + TGeoGlobalMagField::Instance()->Lock(); + return true; + }(); + (void)done; +} + +constexpr std::array onePixelPattern{1, 1, 0x80}; +constexpr std::array threePixelPattern{1, 3, 0xE0}; + +std::vector concatPatterns(std::initializer_list> parts) +{ + std::vector bytes; + for (const auto& p : parts) { + bytes.insert(bytes.end(), p.begin(), p.end()); + } + return bytes; +} + +std::vector makeITSTestCatalog() +{ + std::vector surfaces; + surfaces.reserve(ITSNLayers); + for (uint16_t i = 0; i < ITSNLayers; ++i) { + surfaces.push_back(SurfaceDescriptor{i, static_cast(o2::detectors::DetID::ITS), SurfaceKind::Cylinder}); + surfaces.back().chartRange = {-20.f, 20.f}; + // Matches o2::itsmft::resetDetectorDefaults(..., DetID::ITS)'s LayerxX0 + // default, so TrackerTraits::initialiseTimeFrame()'s LegacyMaterialMismatch + // compatibility check passes for these unperturbed fixtures. + const float xOverX0 = kNominalITSLayerX0[i]; + surfaces.back().material.xOverX0 = xOverX0; + surfaces.back().material.arealDensityGPerCm2 = xOverX0 * o2::its::constants::Radl * o2::its::constants::Rho; + } + return surfaces; +} + +std::vector identitySurfaces(uint16_t nLayers) +{ + std::vector mapping; + mapping.reserve(nLayers); + for (uint16_t i = 0; i < nLayers; ++i) { + mapping.push_back(LayerId{i}); + } + return mapping; +} + +struct Fixture { + std::vector clusters; + std::vector patterns; + std::vector rofs; + o2::dataformats::MCTruthContainer labels; +}; + +// 4 clusters on layers {0,1,0,2}, partitioned into 3 ROFs. Same shape as +// testTimeFrameLifecycle.cxx's fixture -- only needed to give the +// recoverable-failure fixture genuine per-event content to wipe. +Fixture makeFixture() +{ + Fixture f; + f.clusters = { + CompClusterExt{10, 20, CompCluster::InvalidPatternID, 0}, + CompClusterExt{11, 21, CompCluster::InvalidPatternID, 1}, + CompClusterExt{12, 22, CompCluster::InvalidPatternID, 0}, + CompClusterExt{13, 23, CompCluster::InvalidPatternID, 2}, + }; + f.patterns = concatPatterns({onePixelPattern, threePixelPattern, onePixelPattern, threePixelPattern}); + f.rofs = { + ROFRecord{{100, 5}, 0, 0, 2}, + ROFRecord{{140, 5}, 1, 2, 1}, + ROFRecord{{1000, 6}, 2, 3, 1}}; + for (uint32_t i = 0; i < f.clusters.size(); ++i) { + f.labels.addElement(i, o2::MCCompLabel{static_cast(i) + 1, 0, 0}); + } + return f; +} + +std::vector makeOneIterationITSParams(bool dropTFUponFailure, size_t maxMemory = std::numeric_limits::max()) +{ + std::vector params(1); + resetDetectorDefaults(params[0], o2::detectors::DetID::ITS); + params[0].DropTFUponFailure = dropTFUponFailure; + params[0].MaxMemory = maxMemory; + return params; +} + +// A valid FirstPass iteration 0 followed by a non-FirstPass (RebuildClusterLUT +// only, matching the legacy ITS async-iteration-3 shape) iteration 1, both ITS +// defaults -- callers mutate params[1]'s index-table fields to construct a +// deliberate mismatch against the configuration iteration 0 will commit. +std::vector makeTwoIterationITSParams(bool dropTFUponFailure) +{ + std::vector params(2); + resetDetectorDefaults(params[0], o2::detectors::DetID::ITS); + resetDetectorDefaults(params[1], o2::detectors::DetID::ITS); + params[1].PassFlags = IterationSteps{IterationStep::RebuildClusterLUT}; + for (auto& p : params) { + p.DropTFUponFailure = dropTFUponFailure; + } + return params; +} + +enum class AllocationFailure { None, + BadAlloc, + UnclassifiedRuntimeError }; + +class ControlledMemoryResource final : public std::pmr::memory_resource +{ + public: + using FailurePredicate = std::function; + + void arm(AllocationFailure failure, FailurePredicate predicate = {}) + { + mFailureCount = 0; + mPredicate = std::move(predicate); + mFailure = failure; + } + + void disarm() + { + mFailure = AllocationFailure::None; + mPredicate = {}; + } + + int failureCount() const noexcept { return mFailureCount; } + + private: + void* do_allocate(std::size_t bytes, std::size_t alignment) final + { + if (mFailure != AllocationFailure::None && (!mPredicate || mPredicate())) { + ++mFailureCount; + if (mFailure == AllocationFailure::BadAlloc) { + throw std::bad_alloc{}; + } + throw std::runtime_error{"controlled upstream allocation failure"}; + } + return mUpstream->allocate(bytes, alignment); + } + + void do_deallocate(void* pointer, std::size_t bytes, std::size_t alignment) final + { + mUpstream->deallocate(pointer, bytes, alignment); + } + + bool do_is_equal(const std::pmr::memory_resource& other) const noexcept final + { + return this == &other; + } + + AllocationFailure mFailure{AllocationFailure::None}; + FailurePredicate mPredicate; + int mFailureCount{0}; + std::pmr::memory_resource* mUpstream{BoundedMemoryResource::cachingUpstream()}; +}; + +// Bundles a TimeFrame, real backend, Tracker, and bounded memory pool -- the +// minimal wiring Tracker::run() needs for the ITS configuration tests below. +struct Rig { + explicit Rig(bool dropTFUponFailure, size_t maxMemory = std::numeric_limits::max()) + : pool(std::make_shared()), + params(makeOneIterationITSParams(dropTFUponFailure, maxMemory)), + tracker() + { + traits.setNThreads(1, arena); + frame.setBz(0.5f); + } + + // Stages one pending sidecar entry and one GenericTrack/TrackClusterReference + // pair directly on `frame` -- deliberately not through a real CA seed (out + // of scope here): only frame.resetTimeFrame()'s unconditional clear of these two + // containers and the workflow-edge sidecar reset are under test. + void stageStaleState() + { + ITSSharedClusterCompatibilityTransaction txn{sidecar}; + BOOST_REQUIRE(txn.validate(0)); + txn.reserve(); + txn.append(0); + BOOST_REQUIRE_EQUAL(sidecar.pendingSize(), 1u); + + frame.getTrackClusterIndices().push_back(TrackClusterReference{LayerId{0}, 0, 0}); + GenericTrack track{}; + track.clusterRefEnd = static_cast(frame.getTrackClusterIndices().size()); + frame.getGenericTracks().push_back(track); + BOOST_REQUIRE(!frame.getGenericTracks().empty()); + BOOST_REQUIRE(!frame.getTrackClusterIndices().empty()); + } + + void resetPublication() noexcept { sidecar.clear(); } + + std::shared_ptr pool; + std::vector params; + TimeFrame frame; + TrackerTraits traits; + Tracker tracker; + ITSSharedClusterCompatibility sidecar; + // Scratch carries non-owning runtime ROF views. Keep these adapter-edge + // builders alive across load, initialise, and failure/replacement calls. + std::optional> rofTable; + std::optional> vertexTable; + std::optional> mask; + std::shared_ptr arena; + std::vector catalog; + + // Builds and atomically installs the complete static configuration. + void establishValidLayout() + { + catalog = makeITSTestCatalog(); + const SurfaceCatalogView catalogView{catalog.data(), static_cast(catalog.size())}; + TrackerInitialization configuration; + configuration.catalog = catalogView; + configuration.memoryPool = pool; + const auto orderedSurfaces = identitySurfaces(ITSNLayers); + configuration.layout = makeDetectorLayout(); + configuration.plan = o2::itsmft::tracking::test::makeTrackingPlan(params); + const auto result = tracker.initialize(frame, configuration); + BOOST_REQUIRE(result.ok()); + BOOST_REQUIRE_EQUAL(frame.getLayout().size(), orderedSurfaces.size()); + } + + // Loads clusters (or, with an empty Fixture, zero clusters -- still a + // valid load that sizes every per-layer ROF boundary table to a real, + // if trivial, state) through the same normalized-loading path production + // code uses. This sizing is load-bearing: TimeFrame::initialise() calls + // getNrof(layer) = mROFramesClusters[layer].size() - 1 unconditionally, + // and a never-loaded (default-constructed, size-0) mROFramesClusters + // underflows that subtraction, crashing deep inside prepareClusters() + // before any failure-contract check ever runs. loadNormalizedSource() + // sizes mROFramesClusters[layer] to rofs.size()+1 for every layer even + // when rofs/clusters are empty, so calling it with an empty Fixture is + // the only proven-safe way to reach a genuinely valid, still-empty + // TimeFrame state. + void loadSource(const Fixture& f) + { + LegacyLikeDecoder decoder{o2::detectors::DetID::ITS}; + const o2::InteractionRecord origin{50, 5}; + const ROFTimingConfig timing{40, 0, 0, 0}; + const auto& layout = frame.getLayout(); + const auto layerMapping = identitySurfaces(ITSNLayers); + const auto result = loadTimeFrameSource(frame, decoder, origin, timing, f.clusters, f.patterns, f.rofs, &dict(), + f.labels.getIndexedSize() > 0 ? &f.labels : nullptr, o2::detectors::DetID::ITS, + gsl::span{layerMapping}, layout.getSurfaceCatalog()); + BOOST_REQUIRE(result.ok()); + + // TrackerTraits::computeLayerTracklets() reads per-layer ROF counts + // from mROFOverlapTableView (o2::its::LayerTiming), a separate table + // from mROFramesClusters/getNrof() -- it is never populated by + // loadNormalizedSource() and defaults to an unconfigured/garbage view. + // A traversal that reaches computeLayerTracklets() without this being + // set derives its ROF loop bound from that garbage view and walks out + // of bounds. Mirrors the workflow timing-table construction's + // shape, but with every layer given the same trivial timing matching + // this fixture's single combined ROF stream (real production input has + // per-detector-param ROF length/delay/bias; none of that is exercised + // by the failure-contract cases here, only the ROF *count* is load + // -bearing). + o2::its::LayerTiming timing2{}; + timing2.mNROFsTF = static_cast(f.rofs.size()); + timing2.mROFLength = 40; + rofTable.emplace(); + for (int iLayer = 0; iLayer < ITSNLayers; ++iLayer) { + rofTable->defineLayer(iLayer, timing2); + } + rofTable->init(); + vertexTable.emplace(); + for (int iLayer = 0; iLayer < ITSNLayers; ++iLayer) { + vertexTable->defineLayer(iLayer, timing2); + } + vertexTable->init(); + + mask.emplace(*rofTable); + mask->resetMask(); + for (int iLayer = 0; iLayer < ITSNLayers; ++iLayer) { + mask->setROFsEnabled(iLayer, 0, timing2.mNROFsTF, 1); + } + frame.setROFViews(RuntimeROFViews{rofTable->getView(), vertexTable->getView(), mask->getView(), {}}); + } + + // Set the event-local budget at the current usage; the next allocation is + // the controlled recoverable failure. + void forceMemoryLimitAtCurrentUsage() + { + const auto used = pool->getUsedMemory(); + pool->setMaxMemory(used); + } + + void restoreUnboundedMemory() + { + pool->setMaxMemory(std::numeric_limits::max()); + } +}; + +class MftRoadDecoder final : public ClusterDecoder +{ + public: + explicit MftRoadDecoder(std::vector clusters) : mClusters{std::move(clusters)} {} + + ClusterDecodeResult decode(const CompClusterExt& cluster, BoundedPatternCursor& patterns, + const TopologyDictionary* dictionary, uint32_t externalIndex, bool) const final + { + const auto clusterData = ioutils::extractClusterDataBounded(cluster, patterns, dictionary); + if (!clusterData.ok()) { + ClusterDecodeResult result; + result.error = clusterData.error; + return result; + } + ClusterDecodeResult result; + if (externalIndex >= mClusters.size()) { + return result; + } + auto decoded = mClusters[externalIndex]; + decoded.shape = clusterData.shape; + result.decoded = decoded; + return result; + } + + private: + std::vector mClusters; +}; + +std::vector makeMftRoad(const TrackingParameters& parameters, float bz) +{ + std::vector result; + result.reserve(MFTNLayers); + float x = 3.f; + float y = 1.5f; + float z = detail::mftLayerZ(0); + for (int layer = 0; layer < MFTNLayers; ++layer) { + DecodedCluster cluster{}; + cluster.global = {x, y, z}; + cluster.rowColumnCovariance = {1.e-2f, 0.f, 1.e-2f}; + cluster.layer = layer; + result.push_back(cluster); + if (layer + 1 == MFTNLayers) { + break; + } + const float nextZ = detail::mftLayerZ(layer + 1); + float nextX = 0.f; + float nextY = 0.f; + detail::mftTrackletProject(x, y, z, parameters.Diamond[0], parameters.Diamond[1], parameters.Diamond[2], + layer, layer + 1, bz, parameters.TrackletMinPt, nextX, nextY); + x = nextX; + y = nextY; + z = nextZ; + } + return result; +} + +std::vector makeMftCatalog() +{ + std::vector catalog; + catalog.reserve(MFTNLayers); + for (uint16_t layer = 0; layer < MFTNLayers; ++layer) { + SurfaceDescriptor surface{layer, static_cast(o2::detectors::DetID::MFT), SurfaceKind::Disk}; + surface.chartRange = {kMFTLookupRMin[layer], kMFTLookupRMax[layer]}; + surface.referenceCoordinate = detail::mftLayerZ(layer); + const float xOverX0 = kNominalMFTLayerX0[layer]; + surface.material.xOverX0 = xOverX0; + surface.material.arealDensityGPerCm2 = xOverX0 * o2::its::constants::Radl * o2::its::constants::Rho; + catalog.push_back(surface); + } + return catalog; +} + +// This fixture forms the smallest established full MFT chain: one hit on +// every disk surface. Its test-owned upstream resource can inject failures +// at selected normal traversal allocations without altering production APIs. +struct MftFailureRig { + explicit MftFailureRig(bool dropTFUponFailure) + : pool(std::make_shared(std::numeric_limits::max(), &controlledMemory)) + { + resetDetectorDefaults(parameters, o2::detectors::DetID::MFT); + parameters.UseDiamond = true; + parameters.CreateArtefactLabels = false; + parameters.DropTFUponFailure = dropTFUponFailure; + frame.setBz(.5f); + traits.setNThreads(1, arena); + } + + void configure(std::size_t iterations = 1) + { + catalog = makeMftCatalog(); + TrackerInitialization configuration; + configuration.catalog = {catalog.data(), static_cast(catalog.size())}; + configuration.memoryPool = pool; + const auto surfaces = identitySurfaces(MFTNLayers); + configuration.layout = makeDetectorLayout(); + configuration.plan = o2::itsmft::tracking::test::makeTrackingPlan(parameters); + configuration.plan.iterations.assign(iterations, parameters); + BOOST_REQUIRE(tracker.initialize(frame, configuration).ok()); + const auto key = CapacityEstimator::makeKey(SlabSite::Roads, 7, + CapacityEstimator::makeVariant(5, 3), CellPathId{7}); + frame.getCapacityEstimator().update(key, 1000., 12000, 12000, false, false); + BOOST_REQUIRE_GT(frame.getCapacityEstimator().capacity(key, 1000.), 1024u); + } + + void loadRoad() + { + const auto decoded = makeMftRoad(parameters, frame.getBz()); + std::vector compact; + std::vector patterns; + compact.reserve(decoded.size()); + patterns.reserve(decoded.size() * onePixelPattern.size()); + for (const auto& cluster : decoded) { + compact.emplace_back(0, 0, CompCluster::InvalidPatternID, cluster.layer); + patterns.insert(patterns.end(), onePixelPattern.begin(), onePixelPattern.end()); + } + const std::vector rofs{ROFRecord{{100, 5}, 0, 0, static_cast(compact.size())}}; + MftRoadDecoder decoder{decoded}; + const auto& layout = frame.getLayout(); + const auto layerMapping = identitySurfaces(MFTNLayers); + BOOST_REQUIRE(loadTimeFrameSource(frame, decoder, o2::InteractionRecord{50, 5}, ROFTimingConfig{40, 0, 0, 0}, + compact, patterns, rofs, &dict(), nullptr, o2::detectors::DetID::MFT, + gsl::span{layerMapping}, layout.getSurfaceCatalog()) + .ok()); + o2::its::LayerTiming timing{}; + timing.mNROFsTF = 1; + timing.mROFLength = 40; + rofTable.emplace(); + vertexTable.emplace(); + for (int layer = 0; layer < MFTNLayers; ++layer) { + rofTable->defineLayer(layer, timing); + vertexTable->defineLayer(layer, timing); + } + rofTable->init(); + vertexTable->init(); + mask.emplace(*rofTable); + mask->resetMask(); + for (int layer = 0; layer < MFTNLayers; ++layer) { + mask->setROFsEnabled(layer, 0, 1, 1); + } + frame.setROFViews(RuntimeROFViews{rofTable->getView(), vertexTable->getView(), mask->getView(), {}}); + } + + void assertReset() const + { + BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); + BOOST_CHECK(frame.getGenericTracks().empty()); + BOOST_CHECK(frame.getTrackClusterIndices().empty()); + const auto key = CapacityEstimator::makeKey(SlabSite::Roads, 7, + CapacityEstimator::makeVariant(5, 3), CellPathId{7}); + BOOST_CHECK_GT(frame.getCapacityEstimator().capacity(key, 1000.), 1024u); + } + + void stageStaleState() + { + ITSSharedClusterCompatibilityTransaction txn{sidecar}; + BOOST_REQUIRE(txn.validate(0)); + txn.reserve(); + txn.append(0); + frame.getTrackClusterIndices().push_back(TrackClusterReference{LayerId{0}, 0, 0}); + GenericTrack track{}; + track.clusterRefEnd = static_cast(frame.getTrackClusterIndices().size()); + frame.getGenericTracks().push_back(track); + } + + void resetPublication() noexcept { sidecar.clear(); } + + void armFailure(AllocationFailure failure, ControlledMemoryResource::FailurePredicate predicate = {}) + { + controlledMemory.arm(failure, std::move(predicate)); + } + + void disarmFailure() { controlledMemory.disarm(); } + + int failureCount() const noexcept { return controlledMemory.failureCount(); } + + ControlledMemoryResource controlledMemory; + std::shared_ptr pool; + TrackingParameters parameters{}; + TimeFrame frame; + TrackerTraits traits; + Tracker tracker; + ITSSharedClusterCompatibility sidecar; + std::shared_ptr arena; + std::vector catalog; + std::optional> rofTable; + std::optional> vertexTable; + std::optional> mask; +}; + +Fixture emptyFixture() +{ + return Fixture{}; +} + +} // namespace + +// --- Structural failure: always rethrows, always wipes ------------------- +// +// Gate 4 B2 Slice 2 removed this section's original mechanism +// (StructuralFailureViaStaleLayoutAlwaysRethrowsAndWipes: establish a valid +// layout, then TimeFrame::invalidateTraversalState() right before running +// tracking to deterministically produce TraversalException{StaleLayout}). +// Neither invalidateTraversalState() nor TraversalFailureReason::StaleLayout +// is reachable any more: initialiseTimeFrame() now takes the plan as an +// explicit topology parameter with no TimeFrame-owned +// currency concept to invalidate. The "TraversalException (structural/ +// configuration failure): TimeFrame is wiped, then the exception always +// rethrows, regardless of DropTFUponFailure" contract this test protected is +// still covered below, through a different structural-failure reason +// (InvalidIndexTableConfigurationAlwaysRethrowsAndWipesRegardlessOfFlag / +// IndexTableConfigurationMismatchAlwaysRethrowsAndWipesRegardlessOfFlag): the +// contract under test is about TraversalException as a *category*, not about +// any one specific TraversalFailureReason value. + +// --- Recoverable failure: DropTFUponFailure decides, always wipes -------- + +BOOST_AUTO_TEST_CASE(RecoverableFailureDroppedReturnsExactSentinelAndWipes) +{ + Rig rig{/*dropTFUponFailure=*/true}; + rig.establishValidLayout(); + rig.loadSource(makeFixture()); + BOOST_REQUIRE(rig.frame.getTotalMeasurements() > 0u); + + rig.forceMemoryLimitAtCurrentUsage(); + + const auto result = rig.tracker.run(rig.frame, rig.traits); + + BOOST_CHECK(result.outcome == TrackingOutcome::RecoverableDropped); + BOOST_CHECK_EQUAL(rig.frame.getTotalMeasurements(), 0u); + BOOST_CHECK(rig.frame.getGenericTracks().empty()); +} + +BOOST_AUTO_TEST_CASE(RecoverableFailureNotDroppedRethrowsButStillWipesFirst) +{ + Rig rig{/*dropTFUponFailure=*/false}; + rig.establishValidLayout(); + rig.loadSource(makeFixture()); + BOOST_REQUIRE(rig.frame.getTotalMeasurements() > 0u); + + rig.forceMemoryLimitAtCurrentUsage(); + + BOOST_CHECK_THROW(rig.tracker.run(rig.frame, rig.traits), BoundedMemoryResource::MemoryLimitExceeded); + + // Wipe must have already happened before the exception propagated -- not + // "the process is going down anyway". + BOOST_CHECK_EQUAL(rig.frame.getTotalMeasurements(), 0u); + BOOST_CHECK(rig.frame.getGenericTracks().empty()); +} + +// --- std::bad_alloc: recoverable, same drop-or-rethrow policy ------------ +// +// A real ten-disk MFT event exercises Tracker::run() while a test-owned +// upstream resource injects the plain-heap failure category. + +BOOST_AUTO_TEST_CASE(BadAllocDroppedReturnsExactSentinelAndWipes) +{ + ensureTrivialMagneticFieldIsSet(); + MftFailureRig rig{/*dropTFUponFailure=*/true}; + rig.configure(); + rig.loadRoad(); + BOOST_REQUIRE(rig.frame.getTotalMeasurements() > 0u); + + rig.armFailure(AllocationFailure::BadAlloc); + const auto result = rig.tracker.run(rig.frame, rig.traits); + rig.disarmFailure(); + + BOOST_CHECK(result.outcome == TrackingOutcome::RecoverableDropped); + BOOST_CHECK_GT(rig.failureCount(), 0); + rig.assertReset(); +} + +BOOST_AUTO_TEST_CASE(BadAllocNotDroppedRethrowsButStillWipesFirst) +{ + ensureTrivialMagneticFieldIsSet(); + MftFailureRig rig{/*dropTFUponFailure=*/false}; + rig.configure(); + rig.loadRoad(); + BOOST_REQUIRE(rig.frame.getTotalMeasurements() > 0u); + + rig.armFailure(AllocationFailure::BadAlloc); + BOOST_CHECK_THROW(rig.tracker.run(rig.frame, rig.traits), std::bad_alloc); + rig.disarmFailure(); + + BOOST_CHECK_GT(rig.failureCount(), 0); + rig.assertReset(); +} + +BOOST_AUTO_TEST_CASE(EstimatorLearningRollsBackAfterFailureAndNextEventCommits) +{ + ensureTrivialMagneticFieldIsSet(); + MftFailureRig rig{/*dropTFUponFailure=*/true}; + rig.configure(); + auto& estimator = rig.frame.getCapacityEstimator(); + const auto key = CapacityEstimator::makeKey(SlabSite::Tracklets, 0, 0, EdgeId{0}); + constexpr double scale = 1.; + estimator.update(key, scale, 17, 15, 13, 2, true, false); + const auto beforeStats = estimator.statistics(key); + const auto beforeCapacity = estimator.capacity(key, scale); + const auto beforePeak = estimator.peakCapacity(key); + const auto beforeExpected = estimator.expected(key, scale); + + rig.loadRoad(); + rig.armFailure(AllocationFailure::BadAlloc, [&] { + return estimator.statistics(key).samples > beforeStats.samples; + }); + const auto dropped = rig.tracker.run(rig.frame, rig.traits); + rig.disarmFailure(); + BOOST_REQUIRE(dropped.outcome == TrackingOutcome::RecoverableDropped); + BOOST_REQUIRE_GT(rig.failureCount(), 0); + rig.assertReset(); + + const auto rolledBack = estimator.statistics(key); + BOOST_TEST(rolledBack.requested == beforeStats.requested); + BOOST_TEST(rolledBack.granted == beforeStats.granted); + BOOST_TEST(rolledBack.emitted == beforeStats.emitted); + BOOST_TEST(rolledBack.spilled == beforeStats.spilled); + BOOST_TEST(rolledBack.samples == beforeStats.samples); + BOOST_TEST(rolledBack.overflowEvents == beforeStats.overflowEvents); + BOOST_TEST(estimator.capacity(key, scale) == beforeCapacity); + BOOST_TEST(estimator.peakCapacity(key) == beforePeak); + BOOST_TEST(estimator.expected(key, scale) == beforeExpected); + + // A successful event on the same Tracker/TimeFrame must be able to start a + // new transaction and retain the update made at this same production site. + rig.loadRoad(); + TrackingResult succeeded{TrackingOutcome::Structural, std::numeric_limits::quiet_NaN()}; + BOOST_CHECK_NO_THROW(succeeded = rig.tracker.run(rig.frame, rig.traits)); + BOOST_REQUIRE(succeeded.outcome == TrackingOutcome::Success); + const auto committed = estimator.statistics(key); + BOOST_TEST(committed.samples > beforeStats.samples); + BOOST_TEST(committed.requested > beforeStats.requested); +} + +// --- Unclassified std::exception: always structural, never a sentinel ---- +// +// A plain std::runtime_error (or any std::exception that is neither +// TraversalException, BoundedMemoryResource::MemoryLimitExceeded, nor +// std::bad_alloc) must always rethrow and never be silently converted into +// a dropped-TF result, regardless of DropTFUponFailure. + +BOOST_AUTO_TEST_CASE(UnclassifiedExceptionAlwaysRethrowsAndWipesRegardlessOfFlag) +{ + for (const bool dropFlag : {false, true}) { + ensureTrivialMagneticFieldIsSet(); + MftFailureRig rig{dropFlag}; + rig.configure(); + rig.loadRoad(); + BOOST_REQUIRE(rig.frame.getTotalMeasurements() > 0u); + + rig.armFailure(AllocationFailure::UnclassifiedRuntimeError); + BOOST_CHECK_THROW(rig.tracker.run(rig.frame, rig.traits), std::runtime_error); + rig.disarmFailure(); + + BOOST_CHECK_GT(rig.failureCount(), 0); + rig.assertReset(); + } +} + +BOOST_AUTO_TEST_CASE(LaterIterationFailureWipesEveryIterationWorkspace) +{ + ensureTrivialMagneticFieldIsSet(); + MftFailureRig rig{/*dropTFUponFailure=*/true}; + rig.configure(/*iterations=*/2); + rig.loadRoad(); + + const auto secondIterationKey = CapacityEstimator::makeKey(SlabSite::Tracklets, 1, 0, EdgeId{0}); + const auto secondIterationSamples = rig.frame.getCapacityEstimator().statistics(secondIterationKey).samples; + rig.armFailure(AllocationFailure::UnclassifiedRuntimeError, [&] { + return rig.frame.getCapacityEstimator().statistics(secondIterationKey).samples > secondIterationSamples; + }); + BOOST_CHECK_THROW(rig.tracker.run(rig.frame, rig.traits), std::runtime_error); + rig.disarmFailure(); + + // Failure is armed only after the second iteration's first tracklet update, + // so the first iteration completed before the injected exception. No + // iteration workspace may remain selectable by a later adapter pass. + BOOST_CHECK_GT(rig.failureCount(), 0); + rig.assertReset(); +} + +// --- Index-table configuration failures: structural, always rethrow ------- +// +// Both new TraversalFailureReason values (InvalidIndexTableConfiguration, +// IndexTableConfigurationMismatch; TrackerTraits.cxx::initialiseTimeFrame()) +// are TraversalException, the same structural-failure category the removed +// StaleLayout test above used to cover -- so they must follow the identical +// always-rethrow-and-wipe contract, regardless of DropTFUponFailure. + +BOOST_AUTO_TEST_CASE(InvalidIndexTableConfigurationIsRejectedBeforeTimeFrameConfiguration) +{ + for (const bool dropFlag : {false, true}) { + Rig rig{dropFlag}; + rig.params[0].RowBins = 0; // structurally invalid + rig.catalog = makeITSTestCatalog(); + const auto orderedSurfaces = identitySurfaces(ITSNLayers); + TrackerInitialization configuration; + configuration.catalog = {rig.catalog.data(), static_cast(rig.catalog.size())}; + configuration.memoryPool = rig.pool; + configuration.layout = makeDetectorLayout(); + configuration.plan = o2::itsmft::tracking::test::makeTrackingPlan(rig.params); + const auto result = rig.tracker.initialize(rig.frame, configuration); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(!rig.frame.isConfigured()); + } +} + +BOOST_AUTO_TEST_CASE(IterationSpecificInvalidKernelIsRejectedBeforeCommit) +{ + for (const bool dropFlag : {false, true}) { + Rig rig{dropFlag}; + rig.params = makeTwoIterationITSParams(dropFlag); + rig.params[1].TrackletMinPt = -1.f; + rig.catalog = makeITSTestCatalog(); + TrackerInitialization configuration; + configuration.catalog = {rig.catalog.data(), static_cast(rig.catalog.size())}; + configuration.memoryPool = rig.pool; + configuration.layout = makeDetectorLayout(); + configuration.plan = o2::itsmft::tracking::test::makeTrackingPlan(rig.params); + const auto result = rig.tracker.initialize(rig.frame, configuration); + BOOST_CHECK(!result.ok()); + BOOST_CHECK_EQUAL(result.failedIteration, 1u); + BOOST_CHECK(!rig.frame.isConfigured()); + BOOST_CHECK(rig.frame.getGenericTracks().empty()); + } +} + +// --- Valid empty input ----------------------------------------------------- + +BOOST_AUTO_TEST_CASE(ValidEmptyInputCompletesWithoutErrorAndProducesNoTracks) +{ + ensureTrivialMagneticFieldIsSet(); + Rig rig{/*dropTFUponFailure=*/false}; + rig.establishValidLayout(); + rig.loadSource(emptyFixture()); + BOOST_REQUIRE_EQUAL(rig.frame.getTotalMeasurements(), 0u); + + TrackingResult result{TrackingOutcome::Structural, std::numeric_limits::quiet_NaN()}; + BOOST_CHECK_NO_THROW(result = rig.tracker.run(rig.frame, rig.traits)); + + BOOST_CHECK(result.outcome == TrackingOutcome::Success); + BOOST_CHECK(result.elapsedMs >= 0.f); + BOOST_CHECK_EQUAL(rig.frame.getGenericTracks().size(), 0u); +} + +// --- Direct outcome classification ---------------------------------------- +// +// TrackingOutcome::Structural is part of this type's vocabulary for a future +// caller that catches Tracker::run()'s propagated exception itself -- run() +// never constructs it via a +// normal return, since every structural/unclassified/non-dropped-recoverable +// failure keeps the exact "retain exceptions" contract already proven above +// (UnclassifiedExceptionAlwaysRethrowsAndWipesRegardlessOfFlag, +// InvalidIndexTableConfigurationAlwaysRethrowsAndWipesRegardlessOfFlag, +// BadAllocNotDroppedRethrowsButStillWipesFirst, +// RecoverableFailureNotDroppedRethrowsButStillWipesFirst): those tests *are* +// this outcome's structural-failure classification evidence, expressed the +// only way it is currently observable (a thrown exception, never a returned +// value). This test only proves the three values the type actually defines +// are distinct and that TrackingResult's fields carry what each documented +// path above already relies on. +BOOST_AUTO_TEST_CASE(TrackingOutcomeValuesAreDistinct) +{ + BOOST_CHECK(TrackingOutcome::Success != TrackingOutcome::RecoverableDropped); + BOOST_CHECK(TrackingOutcome::Success != TrackingOutcome::Structural); + BOOST_CHECK(TrackingOutcome::RecoverableDropped != TrackingOutcome::Structural); + + constexpr TrackingResult defaulted{}; + BOOST_CHECK(defaulted.outcome == TrackingOutcome::Success); + BOOST_CHECK_EQUAL(defaulted.elapsedMs, 0.f); +} + +// --- No stale TimeFrame/GenericTrack/sidecar state survives ----------------- +// +// Both non-success return paths from Tracker::run() (structural-rethrow +// and recoverable-dropped) must leave the shared TimeFrame's GenericTrack +// storage and the tracker's adopted compatibility sidecar exactly as empty +// as a freshly wiped/cleared TimeFrame would -- not merely the normalized +// frame and legacy tracks storage the tests above already check. + +BOOST_AUTO_TEST_CASE(RecoverableDroppedLeavesNoStaleGenericTrackOrSidecarState) +{ + Rig rig{/*dropTFUponFailure=*/true}; + rig.establishValidLayout(); + rig.loadSource(makeFixture()); + rig.stageStaleState(); + + rig.forceMemoryLimitAtCurrentUsage(); + const auto result = rig.tracker.run(rig.frame, rig.traits); + rig.resetPublication(); + + BOOST_CHECK(result.outcome == TrackingOutcome::RecoverableDropped); + BOOST_CHECK(rig.frame.getGenericTracks().empty()); + BOOST_CHECK(rig.frame.getTrackClusterIndices().empty()); + BOOST_CHECK_EQUAL(rig.sidecar.pendingSize(), 0u); +} + +BOOST_AUTO_TEST_CASE(StructuralFailureLeavesNoStaleGenericTrackOrSidecarState) +{ + for (const bool dropFlag : {false, true}) { + ensureTrivialMagneticFieldIsSet(); + MftFailureRig rig{dropFlag}; + rig.configure(); + rig.loadRoad(); + rig.stageStaleState(); + + rig.armFailure(AllocationFailure::UnclassifiedRuntimeError); + BOOST_CHECK_THROW(rig.tracker.run(rig.frame, rig.traits), std::runtime_error); + rig.disarmFailure(); + rig.resetPublication(); + + BOOST_CHECK_GT(rig.failureCount(), 0); + rig.assertReset(); + BOOST_CHECK_EQUAL(rig.sidecar.pendingSize(), 0u); + } +} + +// --- Continued processing after a drop ------------------------------------ + +BOOST_AUTO_TEST_CASE(TrackerRemainsUsableAfterADroppedTimeFrame) +{ + ensureTrivialMagneticFieldIsSet(); + Rig rig{/*dropTFUponFailure=*/true}; + rig.establishValidLayout(); + rig.loadSource(makeFixture()); + + rig.forceMemoryLimitAtCurrentUsage(); + const auto dropped = rig.tracker.run(rig.frame, rig.traits); + BOOST_REQUIRE(dropped.outcome == TrackingOutcome::RecoverableDropped); + + // Restore headroom and process a fresh (here, empty) TimeFrame on the + // SAME Tracker/TrackerTraits instance -- proving the tracker/device stays + // usable after a drop, matching the DPL device staying alive. + rig.restoreUnboundedMemory(); + rig.loadSource(emptyFixture()); + + TrackingResult result{TrackingOutcome::Structural, std::numeric_limits::quiet_NaN()}; + BOOST_CHECK_NO_THROW(result = rig.tracker.run(rig.frame, rig.traits)); + BOOST_CHECK(result.outcome == TrackingOutcome::Success); + BOOST_CHECK(result.elapsedMs >= 0.f); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testTrackletFinding.cxx b/Detectors/ITSMFT/common/tracking/test/testTrackletFinding.cxx new file mode 100644 index 0000000000000..79746fc75a300 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testTrackletFinding.cxx @@ -0,0 +1,835 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE ITSMFT TrackletFinding +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "DataFormatsITS/Vertex.h" +#include "DetectorsCommonDataFormats/DetID.h" +#include "Field/MagneticField.h" +#include "GPUCommonMath.h" +#include "ITSMFTTracking/detail/MFTFwdTrackHelpers.h" +#include "ITSMFTTracking/detail/CandidateFinding.h" +#include "ITSMFTTracking/detail/TrackingKernelParameters.h" +#include "ITSMFTTracking/detail/TrackerTraversalPreparation.h" +#include "ITStracking/TrackHelpers.h" + +#include "TrackingParameterTestSupport.h" + +using o2::itsmft::tracking::test::ReferenceTrackingParameters; +using namespace o2::itsmft; +using namespace o2::itsmft::tracking; + +struct PropagatorFieldFixture { + PropagatorFieldFixture() + { + if (!TGeoGlobalMagField::Instance()->GetField()) { + TGeoGlobalMagField::Instance()->SetField(o2::field::MagneticField::createNominalField(5, true)); + TGeoGlobalMagField::Instance()->Lock(); + } + } +}; + +BOOST_GLOBAL_FIXTURE(PropagatorFieldFixture); + +/// Focused numerical-parity coverage for the first D007 surface-kind boundary +/// operation migrated off the legacy per-detector branch (Architecture.md +/// §10, cellsAreCompatible). These tests do not exercise TrackerTraits' +/// production traversal -- see the handoff note on scope. + +namespace +{ + +constexpr float Bz = 0.5f; + +o2::its::TrackingFrameInfo makeBarrelHit(float xTF, float alpha, float y, float z, float sigma2Y = 1.e-4f, float sigma2Z = 1.e-4f) +{ + return o2::its::TrackingFrameInfo{xTF, y, z, xTF, alpha, {y, z}, {sigma2Y, 0.f, sigma2Z}}; +} + +o2::its::TrackingFrameInfo makeDiskHit(float z, float x, float y, float sigma2X = 1.e-2f, float sigma2Y = 1.e-2f) +{ + return o2::its::TrackingFrameInfo{x, y, z, 0.f, 0.f, {x, y}, {sigma2X, 0.f, sigma2Y}}; +} + +o2::its::Vertex makeVertex(float x, float y, float z, + float sigma2X, float sigma2Y, float sigma2Z, + unsigned short contributors = 1) +{ + const float position[3]{x, y, z}; + const float covariance[6]{sigma2X, 0.f, sigma2Y, 0.f, 0.f, sigma2Z}; + return o2::its::Vertex{position, covariance, contributors, 1.f}; +} + +GlobalMeasurement makeGlobalCluster(float x, float y, float z, int id = 0) +{ + GlobalMeasurement measurement{}; + measurement.position = {x, y, z}; + measurement.radius = std::hypot(x, y); + measurement.phi = o2::its::math_utils::computePhi(x, y); + measurement.clusterId = static_cast(id); + return measurement; +} + +GlobalMeasurement makeMeasurement(float x, float y, float z, float uu = 1.e-4f, float vv = 1.e-4f, float uv = 0.f) +{ + GlobalMeasurement measurement{}; + measurement.position = {x, y, z}; + measurement.radius = std::hypot(x, y); + measurement.covariance = {uu, uv, 0.f, vv, 0.f, 0.f}; + return measurement; +} + +GlobalMeasurement makeMeasurement(const GlobalMeasurement& cluster, float uu = 1.e-4f, float vv = 1.e-4f, float uv = 0.f) +{ + auto measurement = cluster; + measurement.covariance = {uu, uv, 0.f, vv, 0.f, 0.f}; + return measurement; +} + +TrackletProjectionCache makeCylinderProjectionCache(int fromLayer, int toLayer, float fromRadius, float toRadius, + float targetMinR, float targetMaxR, float sourcePositionResolution, + float edgeMSAngle, float edgePhiCut) +{ + return {fromLayer, toLayer, fromRadius, toRadius, targetMinR, targetMaxR, 0.f, 0.f, + sourcePositionResolution, edgeMSAngle, edgePhiCut}; +} + +TrackletProjectionCache makeDiskProjectionCache(int fromLayer, int toLayer, float fromRadius, + float, float targetMinZ, float targetMaxZ, + float edgeMSAngle, float edgePhiCut) +{ + return {fromLayer, toLayer, fromRadius, 0.f, 0.f, 0.f, targetMinZ, targetMaxZ, + 0.f, edgeMSAngle, edgePhiCut}; +} + +// CandidateFinding exposes one descriptor-selected projection operation. +// Keep the numerical fixtures readable without exporting coordinate leaves. +bool projectCylinderSearchWindow(const GlobalMeasurement& sourceMeasurement, + const GlobalMeasurement&, + const o2::its::Vertex& vertex, + const TrackletProjectionCache& edgeCache, + const o2::itsmft::IndexTableUtilsCore& indexUtils, + const TrackingKernelParameters& params, + TrackletSearchWindow& out) +{ + return projectTrackletSearchWindow(sourceMeasurement, vertex, 0.f, SurfaceKind::Cylinder, + edgeCache, indexUtils, params.nSigmaCut, out); +} + +bool projectDiskSearchWindow(const GlobalMeasurement& sourceMeasurement, + const GlobalMeasurement&, + const o2::its::Vertex& vertex, + const TrackletProjectionCache& edgeCache, + const o2::itsmft::IndexTableUtilsCore& indexUtils, + const TrackingKernelParameters& params, + TrackletSearchWindow& out) +{ + return projectTrackletSearchWindow(sourceMeasurement, vertex, 0.f, SurfaceKind::Disk, + edgeCache, indexUtils, params.nSigmaCut, out); +} + +void setDiskLookup(IndexTableUtilsCore& indexUtils, const ReferenceTrackingParameters& params, + float radialMin = 0.1f, float radialMax = 20.f) +{ + std::array minima{}; + std::array maxima{}; + minima.fill(radialMin); + maxima.fill(radialMax); + indexUtils.setIndexTableParams(IndexTableCoordType::PhiR, params.RowBins, params.ColBins, + 0.f, o2::constants::math::TwoPI, minima, maxima); +} + +void checkSearchWindowEqual(const TrackletSearchWindow& lhs, const TrackletSearchWindow& rhs) +{ + BOOST_CHECK_EQUAL(lhs.bins.x, rhs.bins.x); + BOOST_CHECK_EQUAL(lhs.bins.y, rhs.bins.y); + BOOST_CHECK_EQUAL(lhs.bins.z, rhs.bins.z); + BOOST_CHECK_EQUAL(lhs.bins.w, rhs.bins.w); + BOOST_CHECK_EQUAL(lhs.sourceReferenceCoordinate, rhs.sourceReferenceCoordinate); + BOOST_CHECK_EQUAL(lhs.sourceProjectedCoordinate, rhs.sourceProjectedCoordinate); + BOOST_CHECK_EQUAL(lhs.slope, rhs.slope); + BOOST_CHECK_EQUAL(lhs.varianceConstant, rhs.varianceConstant); + BOOST_CHECK_EQUAL(lhs.varianceLinear, rhs.varianceLinear); + BOOST_CHECK_EQUAL(lhs.varianceQuadratic, rhs.varianceQuadratic); + BOOST_CHECK_EQUAL(lhs.phiPrediction, rhs.phiPrediction); + BOOST_CHECK_EQUAL(lhs.phiVariance, rhs.phiVariance); +} + +std::pair evaluateSearchWindowAt(const TrackletSearchWindow& window, float targetReferenceCoordinate) +{ + const float delta = targetReferenceCoordinate - window.sourceReferenceCoordinate; + return {window.sourceProjectedCoordinate + window.slope * delta, + window.varianceConstant + delta * (window.varianceLinear + delta * window.varianceQuadratic)}; +} + +NominalSurfaceMaterial toMaterial(float xOverX0) +{ + return NominalSurfaceMaterial{xOverX0, xOverX0 * o2::its::constants::Radl * o2::its::constants::Rho}; +} + +std::array toMaterial(const std::array& xOverX0) +{ + return {toMaterial(xOverX0[0]), toMaterial(xOverX0[1]), toMaterial(xOverX0[2])}; +} + +std::vector toCatalog(const std::vector& xOverX0) +{ + std::vector material; + material.reserve(xOverX0.size()); + for (const float x0 : xOverX0) { + SurfaceDescriptor descriptor; + descriptor.material = toMaterial(x0); + material.push_back(descriptor); + } + return material; +} + +TrackingKernelParameters makeKernelParameters(const ReferenceTrackingParameters& params, SurfaceKind kind) +{ + (void)kind; + TrackingKernelParameters out; + out.trackletMinPt = params.TrackletMinPt; + out.nSigmaCut = params.NSigmaCut; + out.maxChi2ClusterAttachment = params.MaxChi2ClusterAttachment; + out.maxChi2NDF = params.MaxChi2NDF; + out.pvResolution = params.PVres; + return out; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(BindingCopiesEveryFieldToTheCorrectSlot) +{ + // Distinct sentinel per field so a field-swap bug in the binding is caught. + ReferenceTrackingParameters legacy; + legacy.TrackletMinPt = 1.11f; + legacy.NSigmaCut = 3.33f; + legacy.MaxChi2ClusterAttachment = 4.44f; + legacy.MaxChi2NDF = 5.55f; + legacy.PVres = 8.88f; + legacy.LayerxX0 = {0.011f, 0.022f, 0.033f}; + legacy.CorrType = o2::base::PropagatorF::MatCorrType::USEMatCorrLUT; + + const auto barrel = makeKernelParameters(legacy, SurfaceKind::Cylinder); + BOOST_CHECK_CLOSE(barrel.trackletMinPt, 1.11f, 1e-6); + BOOST_CHECK_CLOSE(barrel.nSigmaCut, 3.33f, 1e-6); + BOOST_CHECK_CLOSE(barrel.maxChi2ClusterAttachment, 4.44f, 1e-6); + BOOST_CHECK_CLOSE(barrel.maxChi2NDF, 5.55f, 1e-6); + BOOST_CHECK_CLOSE(barrel.pvResolution, 8.88f, 1e-6); + BOOST_CHECK(barrel.isValid()); + + const auto disk = makeKernelParameters(legacy, SurfaceKind::Disk); + BOOST_CHECK_CLOSE(disk.trackletMinPt, 1.11f, 1e-6); + BOOST_CHECK_CLOSE(disk.nSigmaCut, 3.33f, 1e-6); + BOOST_CHECK_CLOSE(disk.maxChi2ClusterAttachment, 4.44f, 1e-6); + BOOST_CHECK_CLOSE(disk.maxChi2NDF, 5.55f, 1e-6); + BOOST_CHECK(disk.isValid()); + + const auto legacyMaterial = toCatalog(legacy.LayerxX0); + const auto attach = bindAttachHitConfig(SurfaceCatalogView{legacyMaterial.data(), static_cast(legacyMaterial.size())}, legacy); + BOOST_REQUIRE_EQUAL(attach.catalog.nSurfaces, 3u); + BOOST_CHECK_CLOSE(attach.catalog.surfaces[0].material.xOverX0, 0.011f, 1e-6); + BOOST_CHECK_CLOSE(attach.catalog.surfaces[1].material.xOverX0, 0.022f, 1e-6); + BOOST_CHECK_CLOSE(attach.catalog.surfaces[2].material.xOverX0, 0.033f, 1e-6); + BOOST_CHECK(attach.corrType == o2::base::PropagatorF::MatCorrType::USEMatCorrLUT); + BOOST_CHECK(attach.isValid(3)); + BOOST_CHECK(!attach.isValid(4)); +} + +BOOST_AUTO_TEST_CASE(BoundConfigurationRejectsInvalidCorrectionType) +{ + ReferenceTrackingParameters legacy; + legacy.TrackletMinPt = 1.11f; + legacy.NSigmaCut = 3.33f; + legacy.MaxChi2ClusterAttachment = 4.44f; + legacy.MaxChi2NDF = 5.55f; + + auto invalidCorrection = legacy; + invalidCorrection.CorrType = static_cast(99); + const auto invalidCorrectionMaterial = toCatalog(invalidCorrection.LayerxX0); + BOOST_CHECK(!bindAttachHitConfig(SurfaceCatalogView{invalidCorrectionMaterial.data(), static_cast(invalidCorrectionMaterial.size())}, invalidCorrection) + .isValid(invalidCorrection.LayerxX0.size())); +} + +BOOST_AUTO_TEST_CASE(CylinderProjectSearchWindowUsesCandidateRadiusAndBoundsTheFullTargetInterval) +{ + ReferenceTrackingParameters legacy; + legacy.PVres = 0.f; + const auto params = makeKernelParameters(legacy, SurfaceKind::Cylinder); + BOOST_REQUIRE(params.isValid()); + + IndexTableUtilsCore indexUtils; + indexUtils.setTrackingParameters(legacy); + + const auto source = makeGlobalCluster(2.f, 0.f, 0.5f); + const auto sourceMeasurement = makeMeasurement(source); + const auto vertex = makeVertex(0.f, 0.f, 0.f, 1.e-4f, 1.e-4f, 4.e-4f, 4); + const auto state = makeCylinderProjectionCache(0, 3, 2.f, 4.f, 3.8f, 4.2f, 5.e-4f, 2.e-3f, 0.08f); + + TrackletSearchWindow window{}; + BOOST_REQUIRE((projectCylinderSearchWindow( + sourceMeasurement, source, vertex, state, indexUtils, params, window))); + + const float tanLambda = (source.z - vertex.getZ()) / source.radius; + const float targetMeanRadius = 0.5f * (state.targetMinR + state.targetMaxR); + const float deltaRadius = targetMeanRadius - source.radius; + const float zAtTargetMeanR = tanLambda * deltaRadius + source.z; + const float projectionScale = 1.f + deltaRadius / source.radius; + const float originScale = projectionScale - 1.f; + const float sourceCoordinateVariance = o2::its::math_utils::Sq(state.sourcePositionResolution); + const float varianceZ = + o2::its::math_utils::Sq(projectionScale) * sourceCoordinateVariance + + o2::its::math_utils::Sq(tanLambda * projectionScale) * sourceCoordinateVariance + + o2::its::math_utils::Sq(originScale) * vertex.getSigmaZ2() + + o2::its::math_utils::Sq(deltaRadius * state.edgeMSAngle); + const auto predictionAndVarianceAt = [&](float radius) { + const float deltaR = radius - source.radius; + const float scale = 1.f + deltaR / source.radius; + const float origin = scale - 1.f; + const float candidateVariance = + o2::its::math_utils::Sq(scale) * sourceCoordinateVariance + + o2::its::math_utils::Sq(tanLambda * scale) * sourceCoordinateVariance + + o2::its::math_utils::Sq(origin) * vertex.getSigmaZ2() + + o2::its::math_utils::Sq(deltaR * state.edgeMSAngle); + return std::pair{source.z + tanLambda * deltaR, candidateVariance}; + }; + const auto [minPrediction, minVariance] = predictionAndVarianceAt(state.targetMinR); + const auto [maxPrediction, maxVariance] = predictionAndVarianceAt(state.targetMaxR); + const float lowerBound = std::min(minPrediction - params.nSigmaCut * std::sqrt(minVariance), + maxPrediction - params.nSigmaCut * std::sqrt(maxVariance)); + const float upperBound = std::max(minPrediction + params.nSigmaCut * std::sqrt(minVariance), + maxPrediction + params.nSigmaCut * std::sqrt(maxVariance)); + const auto directBins = getBinsPhiColumn(source.phi, state.toLayer, 0.5f * (lowerBound + upperBound), + 0.5f * (upperBound - lowerBound), state.edgePhiCut, indexUtils); + + BOOST_CHECK_EQUAL(window.bins.x, directBins.x); + BOOST_CHECK_EQUAL(window.bins.y, directBins.y); + BOOST_CHECK_EQUAL(window.bins.z, directBins.z); + BOOST_CHECK_EQUAL(window.bins.w, directBins.w); + const auto [midpointPrediction, midpointVariance] = evaluateSearchWindowAt(window, targetMeanRadius); + BOOST_CHECK_EQUAL(midpointPrediction, zAtTargetMeanR); + BOOST_CHECK_CLOSE_FRACTION(midpointVariance, varianceZ, 1.e-6f); + const auto [evaluatedMinPrediction, evaluatedMinVariance] = evaluateSearchWindowAt(window, state.targetMinR); + BOOST_CHECK_EQUAL(evaluatedMinPrediction, minPrediction); + BOOST_CHECK_CLOSE_FRACTION(evaluatedMinVariance, minVariance, 1.e-6f); + const auto [evaluatedMaxPrediction, evaluatedMaxVariance] = evaluateSearchWindowAt(window, state.targetMaxR); + BOOST_CHECK_EQUAL(evaluatedMaxPrediction, maxPrediction); + BOOST_CHECK_CLOSE_FRACTION(evaluatedMaxVariance, maxVariance, 1.e-6f); + + TrackletSearchWindow beamUncertaintyWindow{}; + BOOST_REQUIRE(projectTrackletSearchWindow(sourceMeasurement, vertex, 1.e-3f, + SurfaceKind::Cylinder, state, indexUtils, params.nSigmaCut, + beamUncertaintyWindow)); + const auto [beamPrediction, beamVariance] = evaluateSearchWindowAt(beamUncertaintyWindow, targetMeanRadius); + BOOST_CHECK_EQUAL(beamPrediction, zAtTargetMeanR); + BOOST_CHECK_CLOSE_FRACTION(beamVariance, + varianceZ + o2::its::math_utils::Sq(tanLambda * originScale) * 1.e-3f, 1.e-6f); + + legacy.PVres = 0.025f; + const auto differentConfiguredPVParams = makeKernelParameters(legacy, SurfaceKind::Cylinder); + BOOST_REQUIRE(differentConfiguredPVParams.isValid()); + TrackletSearchWindow differentConfiguredPVWindow{}; + BOOST_REQUIRE((projectCylinderSearchWindow( + sourceMeasurement, source, vertex, state, indexUtils, differentConfiguredPVParams, differentConfiguredPVWindow))); + checkSearchWindowEqual(differentConfiguredPVWindow, window); +} + +BOOST_AUTO_TEST_CASE(DiskProjectSearchWindowBuildsPeriodicPhiRCoordinates) +{ + ReferenceTrackingParameters legacy; + const auto params = makeKernelParameters(legacy, SurfaceKind::Disk); + BOOST_REQUIRE(params.isValid()); + + IndexTableUtilsCore indexUtils; + setDiskLookup(indexUtils, legacy); + + constexpr int fromLayer = 1; + constexpr int toLayer = 4; // deliberately skipped/nonadjacent edge + const float fromZ = detail::mftLayerZ(fromLayer); + const float toZ = detail::mftLayerZ(toLayer); + const auto source = makeGlobalCluster(1.2f, 0.7f, fromZ); + const auto sourceMeasurement = makeMeasurement(source, 2.e-4f, 3.e-4f); + const auto vertex = makeVertex(0.01f, -0.02f, 0.1f, 4.e-4f, 5.e-4f, 0.04f, 3); + const auto state = makeDiskProjectionCache(fromLayer, toLayer, 2.f, fromZ, toZ, toZ, 3.e-3f, 0.04f); + + TrackletSearchWindow window{}; + BOOST_REQUIRE((projectDiskSearchWindow( + sourceMeasurement, source, vertex, state, indexUtils, params, window))); + + const float slope = source.radius / (source.z - vertex.getZ()); + const float deltaZ = toZ - source.z; + const float expectedRadius = source.radius + slope * deltaZ; + const float radialScale = expectedRadius / source.radius; + const float expectedX = radialScale * source.x; + const float expectedY = radialScale * source.y; + const float projectionScale = 1.f + deltaZ / (source.z - vertex.getZ()); + const float originScale = projectionScale - 1.f; + const float sourceCoordinateVariance = o2::its::math_utils::Sq(state.sourcePositionResolution); + const float varianceR = + o2::its::math_utils::Sq(projectionScale) * sourceCoordinateVariance + + o2::its::math_utils::Sq(slope * projectionScale) * sourceCoordinateVariance + + o2::its::math_utils::Sq(slope * originScale) * vertex.getSigmaZ2() + + o2::its::math_utils::Sq(deltaZ * state.edgeMSAngle); + + const auto [evaluatedRadius, evaluatedVariance] = evaluateSearchWindowAt(window, toZ); + BOOST_CHECK_EQUAL(evaluatedRadius, expectedRadius); + BOOST_CHECK_CLOSE_FRACTION(evaluatedVariance, varianceR, 1.e-6f); + BOOST_CHECK_EQUAL(window.phiPrediction, source.phi); + BOOST_CHECK_EQUAL(window.phiVariance, o2::its::math_utils::Sq(state.edgePhiCut / params.nSigmaCut)); + + TrackletSearchWindow beamUncertaintyWindow{}; + BOOST_REQUIRE(projectTrackletSearchWindow(sourceMeasurement, vertex, 1.e-3f, + SurfaceKind::Disk, state, indexUtils, params.nSigmaCut, + beamUncertaintyWindow)); + const auto [beamRadius, beamVariance] = evaluateSearchWindowAt(beamUncertaintyWindow, toZ); + BOOST_CHECK_EQUAL(beamRadius, expectedRadius); + BOOST_CHECK_CLOSE_FRACTION(beamVariance, + varianceR + o2::its::math_utils::Sq(originScale) * 1.e-3f, 1.e-6f); + BOOST_CHECK_EQUAL(beamUncertaintyWindow.phiVariance, window.phiVariance); +} + +BOOST_AUTO_TEST_CASE(DiskProjectSearchWindowUsesCandidateZAndBoundsTheFullTargetInterval) +{ + ReferenceTrackingParameters legacy; + const auto params = makeKernelParameters(legacy, SurfaceKind::Disk); + BOOST_REQUIRE(params.isValid()); + + IndexTableUtilsCore indexUtils; + setDiskLookup(indexUtils, legacy); + + constexpr int fromLayer = 0; + constexpr int toLayer = 1; + const float fromZ = detail::mftLayerZ(fromLayer); + const float toZ = detail::mftLayerZ(toLayer); + const auto source = makeGlobalCluster(1.2f, 0.7f, fromZ); + const auto measurement = makeMeasurement(source, 2.e-4f, 3.e-4f); + const auto vertex = makeVertex(0.01f, -0.02f, 0.1f, 4.e-4f, 5.e-4f, 0.04f, 3); + + const auto pointTarget = makeDiskProjectionCache(fromLayer, toLayer, 2.f, fromZ, toZ, toZ, 3.e-3f, 0.04f); + const auto intervalTarget = makeDiskProjectionCache(fromLayer, toLayer, 2.f, fromZ, toZ - 0.5f, toZ + 0.5f, 3.e-3f, 0.04f); + TrackletSearchWindow pointWindow{}; + TrackletSearchWindow intervalWindow{}; + BOOST_REQUIRE((projectDiskSearchWindow(measurement, source, vertex, pointTarget, indexUtils, params, pointWindow))); + BOOST_REQUIRE((projectDiskSearchWindow(measurement, source, vertex, intervalTarget, indexUtils, params, intervalWindow))); + + const float slope = source.radius / (source.z - vertex.getZ()); + const float sourceCoordinateVariance = o2::its::math_utils::Sq(intervalTarget.sourcePositionResolution); + const float sourceVarianceScale = (1.f + o2::its::math_utils::Sq(slope)) * sourceCoordinateVariance; + const float originVarianceScale = o2::its::math_utils::Sq(slope) * vertex.getSigmaZ2(); + const float edgeMSVarianceScale = o2::its::math_utils::Sq(intervalTarget.edgeMSAngle); + const auto predictionAndVarianceAt = [&](float z) { + const float deltaZ = z - source.z; + const float originScale = deltaZ / (source.z - vertex.getZ()); + const float projectionScale = 1.f + originScale; + const float candidateVariance = + o2::its::math_utils::Sq(projectionScale) * sourceVarianceScale + + o2::its::math_utils::Sq(originScale) * originVarianceScale + + o2::its::math_utils::Sq(deltaZ) * edgeMSVarianceScale; + return std::pair{source.radius + slope * deltaZ, candidateVariance}; + }; + const auto [minPrediction, minVariance] = predictionAndVarianceAt(intervalTarget.targetMinZ); + const auto [maxPrediction, maxVariance] = predictionAndVarianceAt(intervalTarget.targetMaxZ); + const float lowerBound = std::min(minPrediction - params.nSigmaCut * std::sqrt(minVariance), + maxPrediction - params.nSigmaCut * std::sqrt(maxVariance)); + const float upperBound = std::max(minPrediction + params.nSigmaCut * std::sqrt(minVariance), + maxPrediction + params.nSigmaCut * std::sqrt(maxVariance)); + const auto directBins = getBinsPhiColumn(source.phi, intervalTarget.toLayer, 0.5f * (lowerBound + upperBound), + 0.5f * (upperBound - lowerBound), intervalTarget.edgePhiCut, indexUtils); + + BOOST_CHECK_EQUAL(intervalWindow.bins.x, directBins.x); + BOOST_CHECK_EQUAL(intervalWindow.bins.y, directBins.y); + BOOST_CHECK_EQUAL(intervalWindow.bins.z, directBins.z); + BOOST_CHECK_EQUAL(intervalWindow.bins.w, directBins.w); + const auto [pointPrediction, pointVariance] = evaluateSearchWindowAt(pointWindow, toZ); + const auto [intervalPrediction, intervalVariance] = evaluateSearchWindowAt(intervalWindow, toZ); + BOOST_CHECK_CLOSE_FRACTION(intervalPrediction, pointPrediction, 1.e-6f); + BOOST_CHECK_CLOSE_FRACTION(intervalVariance, pointVariance, 1.e-6f); + BOOST_CHECK_CLOSE_FRACTION(intervalWindow.phiPrediction, pointWindow.phiPrediction, 1.e-6f); + BOOST_CHECK_SMALL(intervalWindow.phiVariance - pointWindow.phiVariance, 1.e-9f); + + const auto [evaluatedMinPrediction, evaluatedMinVariance] = evaluateSearchWindowAt(intervalWindow, intervalTarget.targetMinZ); + BOOST_CHECK_EQUAL(evaluatedMinPrediction, minPrediction); + BOOST_CHECK_CLOSE_FRACTION(evaluatedMinVariance, minVariance, 1.e-6f); + const auto [evaluatedMaxPrediction, evaluatedMaxVariance] = evaluateSearchWindowAt(intervalWindow, intervalTarget.targetMaxZ); + BOOST_CHECK_EQUAL(evaluatedMaxPrediction, maxPrediction); + BOOST_CHECK_CLOSE_FRACTION(evaluatedMaxVariance, maxVariance, 1.e-6f); +} + +BOOST_AUTO_TEST_CASE(ProjectSearchWindowInvalidBinsLeaveEveryOutputFieldUnchanged) +{ + ReferenceTrackingParameters legacy; + + IndexTableUtilsCore cylinderIndexUtils; + cylinderIndexUtils.setTrackingParameters(legacy); + const auto cylinderParams = makeKernelParameters(legacy, SurfaceKind::Cylinder); + const auto cylinderSource = makeGlobalCluster(2.f, 0.f, 100.f); + const auto cylinderMeasurement = makeMeasurement(cylinderSource); + const auto cylinderVertex = makeVertex(0.f, 0.f, 0.f, 0.f, 0.f, 0.f); + const auto cylinderState = makeCylinderProjectionCache(0, 3, 2.f, 4.f, 3.8f, 4.2f, 5.e-4f, 2.e-3f, 0.08f); + const TrackletSearchWindow cylinderSentinel{ + {101, 102, 103, 104}, 105.f, 106.f, 107.f, 108.f, 109.f, 110.f, 111.f, 112.f}; + auto cylinderOut = cylinderSentinel; + BOOST_CHECK(!(projectCylinderSearchWindow( + cylinderMeasurement, cylinderSource, cylinderVertex, cylinderState, cylinderIndexUtils, cylinderParams, cylinderOut))); + checkSearchWindowEqual(cylinderOut, cylinderSentinel); + + IndexTableUtilsCore diskIndexUtils; + setDiskLookup(diskIndexUtils, legacy, 0.1f, 0.01f); + const auto diskParams = makeKernelParameters(legacy, SurfaceKind::Disk); + constexpr int fromLayer = 0; + constexpr int toLayer = 1; + const float fromZ = detail::mftLayerZ(fromLayer); + const float toZ = detail::mftLayerZ(toLayer); + const auto diskSource = makeGlobalCluster(1.f, 0.5f, fromZ); + const auto diskMeasurement = makeMeasurement(diskSource); + const auto diskVertex = makeVertex(0.f, 0.f, 0.f, 0.f, 0.f, 0.f); + const auto diskState = makeDiskProjectionCache(fromLayer, toLayer, 2.f, fromZ, toZ, toZ, 3.e-3f, 0.04f); + const TrackletSearchWindow diskSentinel{ + {201, 202, 203, 204}, 205.f, 206.f, 207.f, 208.f, 209.f, 210.f, 211.f, 212.f}; + auto diskOut = diskSentinel; + BOOST_CHECK(!(projectDiskSearchWindow( + diskMeasurement, diskSource, diskVertex, diskState, diskIndexUtils, diskParams, diskOut))); + checkSearchWindowEqual(diskOut, diskSentinel); +} + +BOOST_AUTO_TEST_CASE(DiskProjectionUsesBeamCenteredPolarCoordinatesAndIgnoresVertexXY) +{ + ReferenceTrackingParameters legacy; + const auto params = makeKernelParameters(legacy, SurfaceKind::Disk); + constexpr int fromLayer = 0; + constexpr int toLayer = 1; + const float fromZ = detail::mftLayerZ(fromLayer); + const float toZ = detail::mftLayerZ(toLayer); + const auto source = makeGlobalCluster(1.f, 0.5f, fromZ); + const auto sourceMeasurement = makeMeasurement(source); + const auto state = makeDiskProjectionCache(fromLayer, toLayer, 2.f, fromZ, toZ, toZ, 3.e-3f, 0.04f); + + IndexTableUtilsCore indexUtils; + setDiskLookup(indexUtils, legacy); + + const auto straightVertex = makeVertex(0.1f, -0.2f, 0.3f, 4.e-4f, 5.e-4f, 0.04f); + TrackletSearchWindow straightWindow{}; + BOOST_REQUIRE((projectDiskSearchWindow( + sourceMeasurement, source, straightVertex, state, indexUtils, params, straightWindow))); + const float slope = source.radius / (source.z - straightVertex.getZ()); + const float expectedRadius = source.radius + slope * (toZ - source.z); + const auto [straightPrediction, straightVariance] = evaluateSearchWindowAt(straightWindow, toZ); + BOOST_CHECK_EQUAL(straightPrediction, expectedRadius); + BOOST_CHECK(straightVariance > 0.f); + BOOST_CHECK_EQUAL(straightWindow.phiPrediction, source.phi); + + const auto displacedVertex = makeVertex(-3.f, 4.f, straightVertex.getZ(), 8.f, 9.f, straightVertex.getSigmaZ2()); + TrackletSearchWindow displacedWindow{}; + BOOST_REQUIRE((projectDiskSearchWindow( + sourceMeasurement, source, displacedVertex, state, indexUtils, params, displacedWindow))); + checkSearchWindowEqual(displacedWindow, straightWindow); + + const auto fallbackVertex = makeVertex(0.1f, -0.2f, fromZ, 4.e-4f, 5.e-4f, 0.f); + TrackletSearchWindow fallbackWindow{}; + const TrackletSearchWindow sentinel{{1, 2, 3, 4}, 5.f, 6.f, 7.f, 8.f, 9.f, 10.f, 11.f, 12.f}; + fallbackWindow = sentinel; + BOOST_CHECK(!(projectDiskSearchWindow( + sourceMeasurement, source, fallbackVertex, state, indexUtils, params, fallbackWindow))); + checkSearchWindowEqual(fallbackWindow, sentinel); +} + +BOOST_AUTO_TEST_CASE(GlobalMeasurementsAreTheSoleCoordinateAuthority) +{ + ReferenceTrackingParameters cylinderParameters; + cylinderParameters.PVres = 0.f; + const auto cylinderKernelParameters = makeKernelParameters(cylinderParameters, SurfaceKind::Cylinder); + IndexTableUtilsCore cylinderIndex; + cylinderIndex.setTrackingParameters(cylinderParameters); + const auto vertex = makeVertex(0.f, 0.f, 0.f, 1.e-4f, 1.e-4f, 4.e-4f, 4); + const auto cylinderState = makeCylinderProjectionCache(0, 1, 2.f, 4.f, 3.8f, 4.2f, 5.e-4f, 2.e-3f, 0.08f); + const auto sourceMeasurement = makeMeasurement(2.f, 0.f, 0.5f); + const auto source = makeGlobalCluster(2.f, 0.f, 0.5f); + + TrackletSearchWindow baseline{}; + BOOST_REQUIRE((projectCylinderSearchWindow( + sourceMeasurement, source, vertex, cylinderState, cylinderIndex, cylinderKernelParameters, baseline))); + + auto poisonedSource = source; + poisonedSource.x = -999.f; + poisonedSource.y = 888.f; + poisonedSource.z = -777.f; + TrackletSearchWindow poisonedWindow{}; + BOOST_REQUIRE((projectCylinderSearchWindow( + sourceMeasurement, poisonedSource, vertex, cylinderState, cylinderIndex, cylinderKernelParameters, poisonedWindow))); + checkSearchWindowEqual(poisonedWindow, baseline); + + auto poisonedNavigationCache = source; + poisonedNavigationCache.radius = 4.f; + TrackletSearchWindow cachePoisonedWindow{}; + BOOST_REQUIRE((projectCylinderSearchWindow( + sourceMeasurement, poisonedNavigationCache, vertex, cylinderState, cylinderIndex, cylinderKernelParameters, cachePoisonedWindow))); + checkSearchWindowEqual(cachePoisonedWindow, baseline); + + ReferenceTrackingParameters diskParameters; + const auto diskKernelParameters = makeKernelParameters(diskParameters, SurfaceKind::Disk); + IndexTableUtilsCore diskIndex; + setDiskLookup(diskIndex, diskParameters); + const float fromZ = detail::mftLayerZ(0); + const float toZ = detail::mftLayerZ(1); + const auto diskMeasurement = makeMeasurement(1.f, 0.5f, fromZ, 2.e-4f, 3.e-4f, 7.f); + auto diskLocator = makeGlobalCluster(1.f, 0.5f, fromZ); + const auto diskState = makeDiskProjectionCache(0, 1, 2.f, fromZ, toZ, toZ, 3.e-3f, 0.04f); + TrackletSearchWindow diskBaseline{}; + BOOST_REQUIRE((projectDiskSearchWindow( + diskMeasurement, diskLocator, vertex, diskState, diskIndex, diskKernelParameters, diskBaseline))); + diskLocator.x = 123.f; + diskLocator.y = -321.f; + diskLocator.z = 456.f; + auto uvPoisoned = diskMeasurement; + uvPoisoned.covariance.xy = -12345.f; + TrackletSearchWindow diskPoisoned{}; + BOOST_REQUIRE((projectDiskSearchWindow( + uvPoisoned, diskLocator, vertex, diskState, diskIndex, diskKernelParameters, diskPoisoned))); + checkSearchWindowEqual(diskPoisoned, diskBaseline); +} + +/// Gate 3 edge-preparation slice coverage (relocated from +/// TimeFrame::initialise() into TrackerTraits::initialiseTimeFrame(); see +/// CandidateFinding.h family scattering leaves and +/// prepareEdgeScatteringAndBending. These tests verify +/// exact legacy-formula parity, the family-specific arithmetic literal that +/// integration review required preserved (not canonicalized), and the +/// order-sensitive oneOverR ratchet -- independently of TrackerTraits' +/// production traversal (covered separately in +/// testComputeLayerTrackletsOrchestration.cxx). + +namespace +{ +/// Independent re-transcription of the shared arithmetic in the frozen +/// ITS-only TimeFrame::initialise() (ITS/tracking/src/TimeFrame.cxx:352-370), +/// which the (now-removed) common-CA non-MFT branch reproduced verbatim. +/// Deliberately re-derived here rather than calling +/// prepareEdgeScatteringAndBending, so a transcription mistake in +/// either the operation or this reference would show up as a mismatch. +EdgeScatteringBendingPrep referenceEdgeScatteringAndBending( + gsl::span perLayerMSAngle, int fromLayer, int toLayer, + float r1, float r2, float clampedOneOverR, float res1, float res2) +{ + float ms2 = 0.f; + for (int layer = fromLayer; layer < toLayer; ++layer) { + ms2 += o2::its::math_utils::Sq(perLayerMSAngle[layer]); + } + const float msAngle = o2::gpu::CAMath::Sqrt(ms2); + const float cosTheta1half = o2::gpu::CAMath::Sqrt(1.f - o2::its::math_utils::Sq(0.5f * r1 * clampedOneOverR)); + const float cosTheta2half = o2::gpu::CAMath::Sqrt(1.f - o2::its::math_utils::Sq(0.5f * r2 * clampedOneOverR)); + const float x = (r2 * cosTheta1half) - (r1 * cosTheta2half); + const float delta = o2::gpu::CAMath::Sqrt(1.f / (1.f - 0.25f * o2::its::math_utils::Sq(x * clampedOneOverR)) * + (o2::its::math_utils::Sq((0.25f * r1 * r2 * o2::its::math_utils::Sq(clampedOneOverR) / cosTheta2half) + cosTheta1half) * o2::its::math_utils::Sq(res1) + + o2::its::math_utils::Sq((0.25f * r1 * r2 * o2::its::math_utils::Sq(clampedOneOverR) / cosTheta1half) + cosTheta2half) * o2::its::math_utils::Sq(res2))); + const float phiCut = o2::gpu::CAMath::Min(o2::gpu::CAMath::ASin(0.5f * x * clampedOneOverR) + 2.f * msAngle + delta, o2::constants::math::PI * 0.5f); + return EdgeScatteringBendingPrep{msAngle, phiCut}; +} +} // namespace + +BOOST_AUTO_TEST_CASE(CylinderScatteringAngleMatchesFrozenITSFormula) +{ + // Bit-exact vs the frozen ITS expression (ITS/tracking/src/TimeFrame.cxx:347): + // math_utils::MSangle(0.14f, trkParam.TrackletMinPt, trkParam.LayerxX0[iLayer]). + const std::array xX0Values{0.f, -0.001f, 5.e-3f, 1.e-2f}; + const std::array trackletMinPtValues{0.1f, 0.3f, 2.5f}; + for (float xX0 : xX0Values) { + for (float trackletMinPt : trackletMinPtValues) { + const float reference = o2::its::math_utils::MSangle(0.14f, trackletMinPt, xX0); + const float actual = cylinderLayerMultipleScatteringAngle( + CylinderLayerScatteringInputs{xX0}, trackletMinPt); + BOOST_CHECK_EQUAL(actual, reference); + } + } + // xX0 <= 0 behavior, explicit: legacy MSangle maps this to zero, not a + // rejection; the typed operation must not add validation beyond it. + BOOST_CHECK_EQUAL(cylinderLayerMultipleScatteringAngle( + CylinderLayerScatteringInputs{0.f}, 0.3f), + 0.f); +} + +BOOST_AUTO_TEST_CASE(DiskScatteringAngleMatchesLegacyMftFormulaWithExplicitReferenceZ) +{ + // Bit-exact vs the legacy detail::mftLayerMSAngle(layer, params), except + // the Disk operation receives referenceCoordinate/layerRadius + // explicitly instead of calling mftLayerZ()/LayerZCoordinate() internally. + // mftLayerZ() is used here only to construct the *expected* legacy value, + // exactly as this operation's caller (TrackerTraits::initialiseTimeFrame(), + // from the detector layout is required to do. + ReferenceTrackingParameters legacy; + resetDetectorDefaults(legacy, o2::detectors::DetID::MFT); + for (int layer : {0, 3, o2::mft::constants::mft::LayersNumber - 1}) { + const float referenceZ = detail::mftLayerZ(layer); + const float radius = legacy.LayerRadii[layer]; + const float xX0 = legacy.LayerxX0[layer]; + + const float reference = detail::mftLayerMSAngle(layer, legacy); + const float actual = diskLayerMultipleScatteringAngle( + DiskLayerScatteringInputs{xX0, radius, referenceZ}, legacy.TrackletMinPt); + BOOST_CHECK_EQUAL(actual, reference); + } + + // xX0 == 0 behavior, explicit: the legacy formula has no special case for + // it (sqrt(0 * cscLambda) == 0), and this operation must not add one. + const float referenceZ = detail::mftLayerZ(0); + const float zeroX0Actual = diskLayerMultipleScatteringAngle( + DiskLayerScatteringInputs{0.f, legacy.LayerRadii[0], referenceZ}, legacy.TrackletMinPt); + BOOST_CHECK_EQUAL(zeroX0Actual, 0.f); +} + +BOOST_AUTO_TEST_CASE(DiskScatteringAngleNearZeroReferenceRadiusFallback) +{ + // Legacy fallback: |rRef| <= 1e-6 => tanlRef = 0 (detail::mftLayerMSAngle), + // rather than dividing by a near-zero radius. + ReferenceTrackingParameters legacy; + resetDetectorDefaults(legacy, o2::detectors::DetID::MFT); + legacy.LayerRadii[0] = 1.e-9f; // below the legacy 1e-6 fallback threshold + const float referenceZ = detail::mftLayerZ(0); + + const float reference = detail::mftLayerMSAngle(0, legacy); + const float actual = diskLayerMultipleScatteringAngle( + DiskLayerScatteringInputs{legacy.LayerxX0[0], legacy.LayerRadii[0], referenceZ}, + legacy.TrackletMinPt); + BOOST_CHECK_EQUAL(actual, reference); + + // Cross-check the fallback actually engages: tanlRef == 0 (rRef below the + // 1e-6 threshold) makes absTanl == 0, which is *not* > 1e-6 either, so + // cscLambda takes the near-parallel-incidence sentinel 1e6f, not 1 -- i.e. + // this input is genuinely exercising the near-zero-radius branch, not + // merely reproducing an unrelated formula. + const float expectedWithSentinelCscLambda = 0.0136f * (1.f / legacy.TrackletMinPt) * std::sqrt(legacy.LayerxX0[0] * 1.e6f); + BOOST_CHECK_EQUAL(reference, expectedWithSentinelCscLambda); +} + +BOOST_AUTO_TEST_CASE(ClampEdgeCurvatureUsesOneCoordinateNeutralExpression) +{ + const std::array, 5> samples{{ + {0.001f, 50.f}, // clamp does not trigger + {3.0f, 1.0f}, // clamp triggers + {0.02f, 25.f}, + {0.5f, 0.9f}, + {0.0001f, 4.f}, + }}; + for (const auto& sample : samples) { + const float oneOverR = sample.first; + const float r2 = sample.second; + + const float actual = clampEdgeCurvature(oneOverR, r2); + const float reference = (0.5f * oneOverR >= 1.f / r2) ? (2.f / r2) - o2::constants::math::Almost0 : oneOverR; + BOOST_CHECK_EQUAL(actual, reference); + } +} + +BOOST_AUTO_TEST_CASE(CurvatureClampIsEdgeLocal) +{ + constexpr float initialOneOverR = 3.f; + const std::array outerRadii{1.f, 4.f, 0.5f}; + for (const auto outerRadius : outerRadii) { + const auto forward = clampEdgeCurvature(initialOneOverR, outerRadius); + const auto repeated = clampEdgeCurvature(initialOneOverR, outerRadius); + BOOST_CHECK_EQUAL(forward, repeated); + } +} + +BOOST_AUTO_TEST_CASE(PrepareEdgeScatteringAndBendingMatchesFrozenFormulaForITSAndMFTShapedInputs) +{ + // ITS-shaped (cm-scale barrel radii from ReferenceTrackingParameters defaults). + { + const std::array msAngles{1.e-3f, 1.1e-3f, 1.2e-3f, 2.e-3f, 2.1e-3f, 2.2e-3f, 2.3e-3f}; + constexpr int fromLayer = 0; + constexpr int toLayer = 3; // half-open: sums layers 0,1,2 only + constexpr float r1 = 2.33959f; + constexpr float r2 = 19.6213f; + constexpr float res1 = 5.e-4f; + constexpr float res2 = 5.e-4f; + const float oneOverR = clampEdgeCurvature( + 0.001f * 0.3f * std::abs(Bz) / 0.3f, r2); + const gsl::span msSpan(msAngles.data(), msAngles.size()); + const auto actual = prepareEdgeScatteringAndBending(msSpan, fromLayer, toLayer, r1, r2, oneOverR, res1, res2); + const auto reference = referenceEdgeScatteringAndBending(msSpan, fromLayer, toLayer, r1, r2, oneOverR, res1, res2); + BOOST_CHECK_EQUAL(actual.msAngle, reference.msAngle); + BOOST_CHECK_EQUAL(actual.phiCut, reference.phiCut); + + // Half-open range: layer index `toLayer` itself must not contribute. + const auto includingToLayer = referenceEdgeScatteringAndBending(msSpan, fromLayer, toLayer + 1, r1, r2, oneOverR, res1, res2); + BOOST_CHECK_NE(actual.msAngle, includingToLayer.msAngle); + } + + // MFT-shaped, deliberately skipped/non-adjacent edge (fromLayer=1, + // toLayer=4: sums layers 1,2,3, skipping layer 4 itself as the endpoint). + { + ReferenceTrackingParameters mft; + resetDetectorDefaults(mft, o2::detectors::DetID::MFT); + std::array msAngles{}; + for (int layer = 0; layer < o2::mft::constants::mft::LayersNumber; ++layer) { + msAngles[layer] = diskLayerMultipleScatteringAngle( + DiskLayerScatteringInputs{mft.LayerxX0[layer], mft.LayerRadii[layer], detail::mftLayerZ(layer)}, + mft.TrackletMinPt); + } + constexpr int fromLayer = 1; + constexpr int toLayer = 4; + const float r1 = mft.LayerRadii[fromLayer]; + const float r2 = mft.LayerRadii[toLayer]; + constexpr float res1 = 5.e-4f; + constexpr float res2 = 6.e-4f; + const float oneOverR = clampEdgeCurvature( + 0.001f * 0.3f * std::abs(Bz) / mft.TrackletMinPt, r2); + const gsl::span msSpan(msAngles.data(), msAngles.size()); + const auto actual = prepareEdgeScatteringAndBending(msSpan, fromLayer, toLayer, r1, r2, oneOverR, res1, res2); + const auto reference = referenceEdgeScatteringAndBending(msSpan, fromLayer, toLayer, r1, r2, oneOverR, res1, res2); + BOOST_CHECK_EQUAL(actual.msAngle, reference.msAngle); + BOOST_CHECK_EQUAL(actual.phiCut, reference.phiCut); + } +} + +BOOST_AUTO_TEST_CASE(PrepareEdgeScatteringAndBendingZeroFieldAndDegenerateRadiusMatchLegacyFormula) +{ + const std::array msAngles{1.e-3f, 1.2e-3f, 1.4e-3f}; + const gsl::span msSpan(msAngles.data(), msAngles.size()); + + // Zero field: oneOverR's initial value (before any clamp) is exactly 0, + // matching the legacy `0.001f * 0.3f * std::abs(mBz) / trkParam.TrackletMinPt`. + { + const float zeroFieldOneOverR = 0.001f * 0.3f * std::abs(0.f) / 0.3f; + BOOST_CHECK_EQUAL(zeroFieldOneOverR, 0.f); + const float clamped = clampEdgeCurvature(zeroFieldOneOverR, 5.f); + BOOST_CHECK_EQUAL(clamped, 0.f); // 0.5*0 >= 1/5 is false: clamp does not trigger + const auto actual = prepareEdgeScatteringAndBending(msSpan, 0, 2, 2.f, 5.f, clamped, 5.e-4f, 5.e-4f); + const auto reference = referenceEdgeScatteringAndBending(msSpan, 0, 2, 2.f, 5.f, clamped, 5.e-4f, 5.e-4f); + BOOST_CHECK_EQUAL(actual.msAngle, reference.msAngle); + BOOST_CHECK_EQUAL(actual.phiCut, reference.phiCut); + } + + // Degenerate radius (r2 == 0): legacy does not reject this -- it flows + // through to whatever the floating-point expression produces. This test + // asserts parity with that expression, not any particular finiteness. + { + const float oneOverR = clampEdgeCurvature(0.01f, 0.f); + const auto actual = prepareEdgeScatteringAndBending(msSpan, 0, 2, 2.f, 0.f, oneOverR, 5.e-4f, 5.e-4f); + const auto reference = referenceEdgeScatteringAndBending(msSpan, 0, 2, 2.f, 0.f, oneOverR, 5.e-4f, 5.e-4f); + // BOOST_CHECK_EQUAL on NaN is always false (NaN != NaN); compare the bit + // pattern so a NaN-vs-NaN legacy-parity match is still recognized as a pass. + BOOST_CHECK(std::memcmp(&actual.msAngle, &reference.msAngle, sizeof(float)) == 0); + BOOST_CHECK(std::memcmp(&actual.phiCut, &reference.phiCut, sizeof(float)) == 0); + } +} diff --git a/Detectors/ITSMFT/common/tracking/test/testTraversalTopology.cxx b/Detectors/ITSMFT/common/tracking/test/testTraversalTopology.cxx new file mode 100644 index 0000000000000..6b9abfd71ae45 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testTraversalTopology.cxx @@ -0,0 +1,224 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE ITSMFT TraversalTopology +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include +#include + +#include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/TraversalTopology.h" + +namespace +{ +using namespace o2::itsmft::tracking; +using o2::itsmft::TrackingParameters; + +std::vector catalog(uint16_t count) +{ + std::vector result; + result.reserve(count); + for (uint16_t id = 0; id < count; ++id) { + result.push_back(SurfaceDescriptor{id, 0, SurfaceKind::Cylinder}); + } + return result; +} + +DetectorLayout makeLayout(uint16_t layerCount, + std::vector componentOffsets = {0}, + LayerMask holeLayers = {}) +{ + const auto surfaces = catalog(layerCount); + DetectorLayoutDefinition definition; + definition.componentOffsets = std::move(componentOffsets); + definition.holeLayers = holeLayers; + return DetectorLayout{surfaces, std::move(definition)}; +} + +LayerMask mask(std::initializer_list ids) +{ + LayerMask result; + for (const auto id : ids) { + result.set(id); + } + return result; +} + +LayerMask layerMask(std::initializer_list positions) +{ + LayerMask result; + for (const auto position : positions) { + result.set(position); + } + return result; +} + +TrackingParameters parametersFor(const DetectorLayout& layout) +{ + TrackingParameters result; + result.NLayers = static_cast(layout.size()); + result.StartLayerMask = LayerMask::span(0, result.NLayers - 1); + return result; +} + +const Edge* findEdge(const TraversalTopology& topology, LayerId from, LayerId to) +{ + const auto edge = std::find_if(topology.edges.begin(), topology.edges.end(), [&](const auto& candidate) { + return candidate.from == from && candidate.to == to; + }); + return edge == topology.edges.end() ? nullptr : &*edge; +} +} // namespace + +BOOST_AUTO_TEST_CASE(CellPathContainsOnlyTwoEdgeIds) +{ + static_assert(std::is_standard_layout_v); + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(sizeof(CellPath) == sizeof(EdgeId) + sizeof(EdgeId)); + BOOST_CHECK_EQUAL(sizeof(CellPath), 4u); +} + +BOOST_AUTO_TEST_CASE(EdgeContainsOnlySurfaceEndpoints) +{ + static_assert(std::is_standard_layout_v); + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(sizeof(Edge) == sizeof(LayerId) + sizeof(LayerId)); + BOOST_CHECK_EQUAL(sizeof(Edge), 4u); +} + +BOOST_AUTO_TEST_CASE(ComponentBoundariesRejectCrossComponentEdges) +{ + const auto layout = makeLayout(4, {0, 2}); + const auto result = deriveTraversalTopology(layout, parametersFor(layout)); + BOOST_REQUIRE(result.ok()); + BOOST_CHECK_EQUAL(result.topology->edges.size(), 2u); + BOOST_CHECK(findEdge(*result.topology, LayerId{1}, LayerId{2}) == nullptr); +} + +BOOST_AUTO_TEST_CASE(AllActiveChainDerivesEdgesAndCellPaths) +{ + const auto layout = makeLayout(4); + const auto result = deriveTraversalTopology(layout, parametersFor(layout)); + BOOST_REQUIRE(result.ok()); + const auto& topology = *result.topology; + BOOST_CHECK_EQUAL(topology.nLayers, 4u); + BOOST_CHECK_EQUAL(topology.activeSurfaceList.size(), 4u); + BOOST_CHECK_EQUAL(topology.edges.size(), 3u); + BOOST_CHECK_EQUAL(topology.paths.size(), 2u); + BOOST_CHECK(topology.edges[0].from == LayerId{0}); + BOOST_CHECK(topology.edges[0].to == LayerId{1}); + BOOST_CHECK(topology.edges[1].from == LayerId{1}); + BOOST_CHECK(topology.edges[1].to == LayerId{2}); + BOOST_CHECK(topology.edges[2].from == LayerId{2}); + BOOST_CHECK(topology.edges[2].to == LayerId{3}); + BOOST_CHECK(topology.paths[0].first == EdgeId{0}); + BOOST_CHECK(topology.paths[0].second == EdgeId{1}); + BOOST_CHECK(topology.paths[1].first == EdgeId{1}); + BOOST_CHECK(topology.paths[1].second == EdgeId{2}); +} + +BOOST_AUTO_TEST_CASE(SeedingLayersBuildTheGraphWhileStartLayersOnlySelectRoadStarts) +{ + const auto layout = makeLayout(5); + const auto seeding = mask({0, 2, 4}); + auto outerStartParameters = parametersFor(layout); + outerStartParameters.SeedingLayers = layerMask({0, 2, 4}); + outerStartParameters.StartLayerMask = layerMask({4}); + const auto startsAtOuterSurface = deriveTraversalTopology( + layout, outerStartParameters); + BOOST_REQUIRE(startsAtOuterSurface.ok()); + const auto& topology = *startsAtOuterSurface.topology; + BOOST_CHECK(topology.seedingLayers == seeding); + BOOST_CHECK_EQUAL(topology.activeSurfaceList.size(), 5u); + BOOST_REQUIRE_EQUAL(topology.edges.size(), 2u); + BOOST_CHECK(findEdge(topology, LayerId{0}, LayerId{2}) != nullptr); + BOOST_CHECK(findEdge(topology, LayerId{2}, LayerId{4}) != nullptr); + BOOST_REQUIRE_EQUAL(topology.paths.size(), 1u); + BOOST_REQUIRE_EQUAL(topology.roadStartPaths.size(), 1u); + + auto middleStartParameters = outerStartParameters; + middleStartParameters.StartLayerMask = layerMask({2}); + const auto startsAtMiddleSurface = deriveTraversalTopology( + layout, middleStartParameters); + BOOST_REQUIRE(startsAtMiddleSurface.ok()); + BOOST_REQUIRE_EQUAL(startsAtMiddleSurface.topology->edges.size(), topology.edges.size()); + BOOST_REQUIRE_EQUAL(startsAtMiddleSurface.topology->paths.size(), topology.paths.size()); + for (std::size_t i = 0; i < topology.edges.size(); ++i) { + BOOST_CHECK(startsAtMiddleSurface.topology->edges[i].from == topology.edges[i].from); + BOOST_CHECK(startsAtMiddleSurface.topology->edges[i].to == topology.edges[i].to); + } + for (std::size_t i = 0; i < topology.paths.size(); ++i) { + BOOST_CHECK(startsAtMiddleSurface.topology->paths[i].first == topology.paths[i].first); + BOOST_CHECK(startsAtMiddleSurface.topology->paths[i].second == topology.paths[i].second); + } + BOOST_CHECK(startsAtMiddleSurface.topology->roadStartPaths.empty()); +} + +BOOST_AUTO_TEST_CASE(DisabledMiddleSurfaceRetainsAdmittedBridge) +{ + const auto layout = makeLayout(4, {0}, mask({1})); + auto parameters = parametersFor(layout); + parameters.MaxHoles = 1; + parameters.InactiveLayerMask = layerMask({1}); + const auto result = deriveTraversalTopology(layout, parameters); + BOOST_REQUIRE(result.ok()); + const auto& topology = *result.topology; + BOOST_CHECK_EQUAL(topology.activeSurfaceList.size(), 3u); + BOOST_CHECK_EQUAL(topology.edges.size(), 2u); + BOOST_CHECK_EQUAL(topology.paths.size(), 1u); + const auto* bridge = findEdge(topology, LayerId{0}, LayerId{2}); + BOOST_REQUIRE(bridge != nullptr); + BOOST_CHECK(bridge->from == LayerId{0}); + BOOST_CHECK(bridge->to == LayerId{2}); + BOOST_CHECK(topology.activeSurfaceList[1] == LayerId{2}); + BOOST_CHECK(topology.paths[0].first == EdgeId{0}); + BOOST_CHECK(topology.paths[0].second == EdgeId{1}); +} + +BOOST_AUTO_TEST_CASE(DisabledEndpointOmitsItsEdges) +{ + const auto layout = makeLayout(4, {0}, mask({1})); + auto parameters = parametersFor(layout); + parameters.MaxHoles = 1; + parameters.InactiveLayerMask = layerMask({0}); + const auto result = deriveTraversalTopology(layout, parameters); + BOOST_REQUIRE(result.ok()); + for (const auto& edge : result.topology->edges) { + BOOST_CHECK(edge.from != LayerId{0}); + BOOST_CHECK(edge.to != LayerId{0}); + } + BOOST_CHECK(findEdge(*result.topology, LayerId{1}, LayerId{2}) != nullptr); +} + +BOOST_AUTO_TEST_CASE(InvalidDerivationIsTransactional) +{ + const auto layout = makeLayout(4); + auto parameters = parametersFor(layout); + parameters.NLayers = 7; + const auto result = deriveTraversalTopology(layout, parameters); + BOOST_CHECK(!result.ok()); + BOOST_CHECK(!result.topology.has_value()); + BOOST_CHECK(result.error == TraversalTopologyError::LayerCountMismatch); + + DetectorLayout invalid; + const auto invalidResult = deriveTraversalTopology(invalid, TrackingParameters{}); + BOOST_CHECK(!invalidResult.ok()); + BOOST_CHECK(!invalidResult.topology.has_value()); + BOOST_CHECK(invalidResult.error == TraversalTopologyError::InvalidLayout); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testTripletFitting.cxx b/Detectors/ITSMFT/common/tracking/test/testTripletFitting.cxx new file mode 100644 index 0000000000000..dd0cf1f502733 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testTripletFitting.cxx @@ -0,0 +1,327 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE ITSMFTTrackingTripletFitting +#include + +#include +#include +#include +#include +#include + +#include "ITSMFTTracking/TripletFitting.h" + +using namespace o2::itsmft::tracking; + +namespace +{ + +constexpr double Radius = 50.; +constexpr double TanLambda = 0.4; + +GlobalCovariance3F makeCovariance() +{ + // Positive definite, non-axis-aligned covariance in cm^2. + return {4.e-6f, 0.8e-6f, -0.4e-6f, 3.e-6f, 0.3e-6f, 5.e-6f}; +} + +GlobalMeasurement makeMeasurement(float x, float y, float z, + GlobalCovariance3F covariance = makeCovariance()) +{ + GlobalMeasurement measurement{}; + measurement.position = {x, y, z}; + measurement.covariance = covariance; + return measurement; +} + +std::array makeHelixMeasurements() +{ + const std::array angles{0.1, 0.16, 0.25}; + std::array measurements{}; + for (std::size_t i = 0; i < measurements.size(); ++i) { + measurements[i].position = {static_cast(3. + Radius * std::cos(angles[i])), + static_cast(-2. + Radius * std::sin(angles[i])), + static_cast(1.5 + Radius * angles[i] * TanLambda)}; + measurements[i].covariance = makeCovariance(); + } + return measurements; +} + +std::array makeAdjacentHelixMeasurements() +{ + const std::array angles{0.1, 0.16, 0.25, 0.33}; + std::array measurements{}; + for (std::size_t i = 0; i < measurements.size(); ++i) { + measurements[i].position = {static_cast(3. + Radius * std::cos(angles[i])), + static_cast(-2. + Radius * std::sin(angles[i])), + static_cast(1.5 + Radius * angles[i] * TanLambda)}; + measurements[i].covariance = makeCovariance(); + } + return measurements; +} + +std::array fitAdjacentFactors( + const std::array& measurements) +{ + const std::array first{ + measurements[0], measurements[1], measurements[2]}; + const std::array second{ + measurements[1], measurements[2], measurements[3]}; + std::array factors{}; + BOOST_REQUIRE(makeTripletFitFactor(first, factors[0])); + BOOST_REQUIRE(makeTripletFitFactor(second, factors[1])); + return factors; +} + +GlobalCovariance3F rotateCovarianceAroundZ(const GlobalCovariance3F& covariance, + double angle) +{ + const double cosine = std::cos(angle); + const double sine = std::sin(angle); + return {static_cast(cosine * cosine * covariance.xx - 2. * sine * cosine * covariance.xy + sine * sine * covariance.yy), + static_cast(sine * cosine * covariance.xx + (cosine * cosine - sine * sine) * covariance.xy - + sine * cosine * covariance.yy), + static_cast(cosine * covariance.xz - sine * covariance.yz), + static_cast(sine * sine * covariance.xx + 2. * sine * cosine * covariance.xy + cosine * cosine * covariance.yy), + static_cast(sine * covariance.xz + cosine * covariance.yz), + covariance.zz}; +} + +void checkClose(double actual, double expected, double relativeTolerance, double absoluteTolerance = 0.) +{ + BOOST_CHECK_SMALL(actual - expected, + std::max(absoluteTolerance, relativeTolerance * std::max(std::abs(actual), std::abs(expected)))); +} + +double factorCovariance(const TripletFitFactor& factor, + const std::array& measurements, + bool leftTheta, bool rightTheta) +{ + double covariance = 0.; + for (std::size_t hit = 0; hit < measurements.size(); ++hit) { + const auto& left = leftTheta ? factor.h[hit].theta : factor.h[hit].phi; + const auto& right = rightTheta ? factor.h[hit].theta : factor.h[hit].phi; + const auto& v = measurements[hit].covariance; + covariance += left[0] * (v.xx * right[0] + v.xy * right[1] + v.xz * right[2]) + + left[1] * (v.xy * right[0] + v.yy * right[1] + v.yz * right[2]) + + left[2] * (v.xz * right[0] + v.yz * right[1] + v.zz * right[2]); + } + return covariance; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(ExactHelixProducesAConsistentFactor) +{ + const auto measurements = makeHelixMeasurements(); + TripletFitFactor factor{}; + BOOST_REQUIRE(makeTripletFitFactor(measurements, factor)); + BOOST_REQUIRE(factor.isValid()); + const double referenceCurvature = -static_cast(factor.psi.phi) / factor.rho.phi; + const double expectedCurvature = (1. / Radius) / std::sqrt(1. + TanLambda * TanLambda); + checkClose(referenceCurvature, expectedCurvature, 4.e-4); + // Native float hit coordinates leave this residual after the otherwise + // double-precision geometry calculation. + BOOST_CHECK_SMALL(static_cast(factor.psi.theta) + + static_cast(factor.rho.theta) * referenceCurvature, + 2.e-7); + BOOST_CHECK_GT(factorCovariance(factor, measurements, true, true), 0.); + BOOST_CHECK_NE(factorCovariance(factor, measurements, true, false), 0.); +} + +BOOST_AUTO_TEST_CASE(AdjacentFactorsImplementEquation19ClosedForm) +{ + const GlobalCovariance3F exact{}; + const std::array measurements{{ + makeMeasurement(0.f, 0.f, 0.f, exact), + makeMeasurement(1.f, 0.f, 0.f, exact), + makeMeasurement(2.f, 0.f, 0.f, exact), + makeMeasurement(3.f, 0.f, 0.f, exact), + }}; + std::array factors{}; + factors[0].psi = {1.f, 2.f}; + factors[0].rho = {1.f, 1.f}; + factors[1].psi = {3.f, 4.f}; + factors[1].rho = {1.f, 1.f}; + + AdjacentTripletFitResult result{}; + BOOST_REQUIRE(fitAdjacentTripletFactors(factors[0], factors[1], measurements, {4.f, 9.f}, result)); + const double rhoKpsi = 1. / 4. + 2. / 4. + 3. / 9. + 4. / 9.; + const double rhoKrho = 1. / 4. + 1. / 4. + 1. / 9. + 1. / 9.; + const double psiKpsi = 1. / 4. + 4. / 4. + 9. / 9. + 16. / 9.; + checkClose(result.curvature, -rhoKpsi / rhoKrho, 2.e-6); + checkClose(result.curvatureVariance, 1. / rhoKrho, 2.e-6); + checkClose(result.chi2, psiKpsi - rhoKpsi * rhoKpsi / rhoKrho, 2.e-6); +} + +BOOST_AUTO_TEST_CASE(AdjacentFactorsRetainSharedHitCrossCovariance) +{ + const GlobalCovariance3F exact{}; + std::array measurements{{ + makeMeasurement(0.f, 0.f, 0.f, exact), + makeMeasurement(1.f, 0.f, 0.f, {2.f, 0.5f, 0.f, 3.f, 0.f, 0.f}), + makeMeasurement(2.f, 0.f, 0.f, exact), + makeMeasurement(3.f, 0.f, 0.f, exact), + }}; + std::array factors{}; + factors[0].rho.phi = 1.f; + factors[1].rho.phi = 1.f; + factors[0].h[1].theta = {1.f, 2.f, 0.f}; + factors[0].h[1].phi = {-1.f, 1.f, 0.f}; + factors[1].h[0].theta = {3.f, -2.f, 0.f}; + factors[1].h[0].phi = {2.f, 4.f, 0.f}; + + AdjacentTripletFitResult correlated{}; + BOOST_REQUIRE(fitAdjacentTripletFactors(factors[0], factors[1], measurements, + {100.f, 100.f}, correlated)); + + // Move the second factor's identical covariance contribution from shared + // hit 1 to private hit 3. Diagonal blocks stay equal; only H V H^T's + // cross-triplet block disappears. + auto independentFactors = factors; + auto independentMeasurements = measurements; + independentFactors[1].h[2] = independentFactors[1].h[0]; + independentFactors[1].h[0] = {}; + independentMeasurements[3].covariance = measurements[1].covariance; + AdjacentTripletFitResult independent{}; + BOOST_REQUIRE(fitAdjacentTripletFactors(independentFactors[0], independentFactors[1], + independentMeasurements, {100.f, 100.f}, independent)); + BOOST_CHECK_NE(correlated.curvatureVariance, independent.curvatureVariance); +} + +BOOST_AUTO_TEST_CASE(AdjacentFactorsApplySpaceAngleMSGeometry) +{ + const GlobalCovariance3F exact{}; + const std::array measurements{{ + makeMeasurement(0.f, 0.f, 0.f, exact), + makeMeasurement(1.f, 0.f, 1.f, exact), + makeMeasurement(2.f, 0.f, 2.f, exact), + makeMeasurement(3.f, 0.f, 3.f, exact), + }}; + std::array factors{}; + factors[0].rho = {1.f, 1.f}; + factors[1].rho = {1.f, 1.f}; + AdjacentTripletFitResult result{}; + BOOST_REQUIRE(fitAdjacentTripletFactors(factors[0], factors[1], measurements, {4.f, 9.f}, result)); + const double expectedPrecision = 1. / 4. + 1. / 8. + 1. / 9. + 1. / 18.; + checkClose(result.curvatureVariance, 1. / expectedPrecision, 2.e-6); +} + +BOOST_AUTO_TEST_CASE(AdjacentExactHelixHasCommonCurvatureAndZeroQuality) +{ + const auto measurements = makeAdjacentHelixMeasurements(); + const std::array angularVariance{1.e-8f, 2.e-8f}; + const auto factors = fitAdjacentFactors(measurements); + AdjacentTripletFitResult result{}; + BOOST_REQUIRE(fitAdjacentTripletFactors(factors[0], factors[1], measurements, angularVariance, result)); + const double expectedCurvature = (1. / Radius) / std::sqrt(1. + TanLambda * TanLambda); + checkClose(result.curvature, expectedCurvature, 4.e-4); + // Native float measurements and persisted float factors leave only this + // numerical residue in an otherwise exactly common-curvature helix. + BOOST_CHECK_SMALL(result.chi2, 2.e-6f); + BOOST_CHECK_GT(result.curvatureVariance, 0.); +} + +BOOST_AUTO_TEST_CASE(AdjacentFactorFitIsRotationInvariant) +{ + const auto original = makeAdjacentHelixMeasurements(); + auto rotated = original; + const double angle = 0.73; + const double cosine = std::cos(angle); + const double sine = std::sin(angle); + for (auto& measurement : rotated) { + const double x = measurement.x; + const double y = measurement.y; + measurement.x = static_cast(cosine * x - sine * y); + measurement.y = static_cast(sine * x + cosine * y); + measurement.covariance = rotateCovarianceAroundZ(measurement.covariance, angle); + } + const std::array angularVariance{2.e-8f, 3.e-8f}; + const auto originalFactors = fitAdjacentFactors(original); + const auto rotatedFactors = fitAdjacentFactors(rotated); + AdjacentTripletFitResult first{}; + AdjacentTripletFitResult second{}; + BOOST_REQUIRE(fitAdjacentTripletFactors(originalFactors[0], originalFactors[1], original, angularVariance, first)); + BOOST_REQUIRE(fitAdjacentTripletFactors(rotatedFactors[0], rotatedFactors[1], rotated, angularVariance, second)); + // Rotating and storing the coordinates and covariance back into floats + // limits the invariance of the derived Jacobian and covariance. + checkClose(second.curvature, first.curvature, 2.e-6); + checkClose(second.curvatureVariance, first.curvatureVariance, 5.e-2); + checkClose(second.chi2, first.chi2, 1.e-5, 5.e-7); +} + +BOOST_AUTO_TEST_CASE(StraightTripletUsesTheRemovableZeroBendingLimit) +{ + const GlobalCovariance3F covariance{1.e-6f, 0.f, 0.f, 1.e-6f, 0.f, 1.e-6f}; + const std::array measurements{{ + makeMeasurement(1.f, 2.f, 3.f, covariance), + makeMeasurement(2.f, 2.f, 3.5f, covariance), + makeMeasurement(4.f, 2.f, 4.5f, covariance), + }}; + TripletFitFactor factor{}; + BOOST_REQUIRE(makeTripletFitFactor(measurements, factor)); + BOOST_REQUIRE(factor.isValid()); + BOOST_CHECK_SMALL(-static_cast(factor.psi.phi) / factor.rho.phi, 1.e-15); +} + +BOOST_AUTO_TEST_CASE(FactorConstructionGeometryFailureIsTransactional) +{ + TripletFitFactor sentinel{}; + sentinel.psi = {1.f, 2.f}; + sentinel.rho = {3.f, 4.f}; + auto measurements = makeHelixMeasurements(); + TripletFitFactor result = sentinel; + + measurements[1].position = measurements[0].position; + BOOST_CHECK(!makeTripletFitFactor(measurements, result)); + BOOST_CHECK_EQUAL(std::memcmp(&result, &sentinel, sizeof(result)), 0); +} + +BOOST_AUTO_TEST_CASE(CharacterizeFactorConstructionHostCost) +{ + const auto measurements = makeHelixMeasurements(); + constexpr int Repetitions = 20000; + double checksum = 0.; + const auto start = std::chrono::steady_clock::now(); + for (int iteration = 0; iteration < Repetitions; ++iteration) { + TripletFitFactor factor{}; + BOOST_REQUIRE(makeTripletFitFactor(measurements, factor)); + checksum += factor.psi.theta + factor.psi.phi + factor.rho.theta + factor.rho.phi; + } + const auto elapsed = std::chrono::steady_clock::now() - start; + const double nanosecondsPerFit = + std::chrono::duration_cast(elapsed).count() / + static_cast(Repetitions); + BOOST_TEST_MESSAGE("triplet-factor construction host cost: " << nanosecondsPerFit << " ns/factor; checksum=" << checksum); + BOOST_CHECK_NE(checksum, 0.); +} + +BOOST_AUTO_TEST_CASE(CharacterizeAdjacentFactorHostCost) +{ + const auto measurements = makeAdjacentHelixMeasurements(); + const std::array angularVariance{2.e-7f, 3.e-7f}; + const auto factors = fitAdjacentFactors(measurements); + constexpr int Repetitions = 20000; + double checksum = 0.; + const auto start = std::chrono::steady_clock::now(); + for (int iteration = 0; iteration < Repetitions; ++iteration) { + AdjacentTripletFitResult result{}; + BOOST_REQUIRE(fitAdjacentTripletFactors(factors[0], factors[1], measurements, angularVariance, result)); + checksum += result.curvature + result.chi2; + } + const auto elapsed = std::chrono::steady_clock::now() - start; + const double nanosecondsPerFit = + std::chrono::duration_cast(elapsed).count() / + static_cast(Repetitions); + BOOST_TEST_MESSAGE("adjacent triplet-factor fit host cost: " << nanosecondsPerFit << " ns/fit; checksum=" << checksum); + BOOST_CHECK_GT(checksum, 0.); +} diff --git a/Detectors/ITSMFT/common/tracking/test/testWorkflowSession.cxx b/Detectors/ITSMFT/common/tracking/test/testWorkflowSession.cxx new file mode 100644 index 0000000000000..d495a0aba4774 --- /dev/null +++ b/Detectors/ITSMFT/common/tracking/test/testWorkflowSession.cxx @@ -0,0 +1,485 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#define BOOST_TEST_MODULE ITSMFT workflow session +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include +#include +#include +#include +#include +#include +#include +#include +#include "ITSMFTTracking/WorkflowSession.h" +#include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" +#include "TrackingParameterTestSupport.h" + +using namespace o2::itsmft; +using namespace o2::itsmft::tracking; +using LayerCounts = boost::mpl::list, std::integral_constant>; +namespace +{ +struct FieldFixture { + FieldFixture() { o2::base::Propagator::initFieldFromGRP(0.f, 0.f, true, false); } +}; +BOOST_GLOBAL_FIXTURE(FieldFixture); + +template +struct Rig { + static constexpr auto Detector = N == ITSNLayers ? o2::detectors::DetID::ITS : o2::detectors::DetID::MFT; + WorkflowSession session{N == ITSNLayers ? "ITS" : "MFT", N}; + Tracker tracker; + TrackerTraits traits; + std::shared_ptr arena; + TopologyDictionary dictionary; + std::array mapping{}; + std::vector rofs{{{100, 5}, 0, 0, 0}}; + std::vector clusters; + struct Decoder : ClusterDecoder { + ClusterDecodeResult decode(const CompClusterExt&, BoundedPatternCursor&, const TopologyDictionary*, uint32_t, bool) const override + { + ClusterDecodeResult result; + const float radius = N == ITSNLayers ? kITSStaticSurfaceCatalog[0].referenceCoordinate : 3.f; + const float z = N == ITSNLayers ? 0.f : kMFTStaticSurfaceCatalog[0].referenceCoordinate; + result.decoded.global = {radius, 0.f, z}; + result.decoded.cylinderFrame = {radius, 0.f, z, 0.f}; + result.decoded.rowColumnCovariance = {1.e-4f, 0.f, 1.e-4f}; + result.decoded.layer = 0; + return result; + } + } decoder; + + explicit Rig(bool drop = false, size_t memory = std::numeric_limits::max()) + { + TrackingParameters parameters; + resetDetectorDefaults(parameters, Detector); + parameters.UseDiamond = true; + auto plan = test::makeTrackingPlan(parameters); + plan.execution = {memory, drop}; + SurfaceCatalogView catalog = N == ITSNLayers ? SurfaceCatalogView{kITSStaticSurfaceCatalog.data(), ITSNLayers} + : SurfaceCatalogView{kMFTStaticSurfaceCatalog.data(), MFTNLayers}; + TrackerInitialization init{catalog, {}, std::move(plan), std::make_shared()}; + BOOST_REQUIRE(tracker.initialize(session.frame, init).ok()); + traits.setNThreads(1, arena); + for (int layer = 0; layer < N; ++layer) { + mapping[layer] = LayerId{static_cast(layer)}; + } + configure(); + } + void configure() + { + std::vector timings(N); + std::fill(timings.begin(), timings.end(), o2::its::LayerTiming{.mNROFsTF = 1, .mROFLength = 40}); + session.configureTiming(timings, [](int) { return true; }); + } + ClusterSourceInput source() + { + ClusterSourceInput input; + input.detector = Detector; + input.id = ClusterSourceId{0}; + input.rofs = rofs; + input.clusters = clusters; + input.dictionary = &dictionary; + input.decoder = &decoder; + input.layerToSurface = mapping; + return input; + } + void checkClean() + { + BOOST_CHECK_EQUAL(session.frame.getTotalMeasurements(), 0u); + BOOST_CHECK(session.externalIndices.empty()); + BOOST_CHECK(session.clusterSizes.empty()); + BOOST_CHECK(!session.publicationClock); + BOOST_CHECK_EQUAL(session.frame.getROFViews().overlap.mLayerCount, 0); + } +}; +} // namespace + +BOOST_AUTO_TEST_CASE_TEMPLATE(SuccessAndValidEmptyInputCompleteBeforeCleanup, Count, LayerCounts) +{ + for (bool withCluster : {false, true}) { + Rig rig; + if (withCluster) { + rig.clusters.emplace_back(0, 0, CompCluster::InvalidPatternID, 0); + rig.rofs[0].setNEntries(1); + } + int loaded = 0, completed = 0; + { + auto cleanup = rig.session.cleanupOnExit(); + const auto outcome = rig.session.process(rig.tracker, rig.traits, rig.source(), [&](const o2::InteractionRecord& origin) { + ++loaded; + BOOST_CHECK(origin == rig.rofs.front().getBCData()); + BOOST_CHECK_EQUAL(rig.session.frame.getTotalMeasurements(), withCluster ? 1u : 0u); + BOOST_CHECK_EQUAL(rig.session.frame.getROFViews().overlap.mLayerCount, Count::value); }, [&](const TrackingResult& result) { + ++completed; + BOOST_CHECK(result.outcome == TrackingOutcome::Success); + BOOST_REQUIRE_EQUAL(result.acceptedTrackCounts.size(), 1u); + BOOST_CHECK_EQUAL(result.acceptedTrackCounts[0], 0u); }); + BOOST_CHECK(decideCATrackerPublicationAction(true, outcome) == CATrackerPublicationAction::PublishActiveResult); + rig.session.publicationClock.emplace(rig.session.overlap.getView().getClockLayer()); + BOOST_CHECK(rig.session.publicationClock); + } + BOOST_CHECK_EQUAL(loaded, 1); + BOOST_CHECK_EQUAL(completed, 1); + rig.checkClean(); + } +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(MalformedInputDropsOnlyUnderTheConfiguredPolicy, Count, LayerCounts) +{ + for (bool drop : {false, true}) { + Rig rig{drop}; + rig.rofs[0].setNEntries(1); // Claims a missing cluster: recoverable InvalidROFRange. + int completed = 0; + const auto run = [&] { + auto cleanup = rig.session.cleanupOnExit(); + const auto result = rig.session.process(rig.tracker, rig.traits, rig.source(), [](const o2::InteractionRecord&) {}, [&](const TrackingResult&) { ++completed; }); + BOOST_CHECK(decideCATrackerPublicationAction(true, result) == CATrackerPublicationAction::SkipDroppedTimeFrame); + cleanup.frameAlreadyReset(); + }; + if (drop) { + BOOST_CHECK_NO_THROW(run()); + } else { + BOOST_CHECK_THROW(run(), RecoverableLoadFailure); + } + BOOST_CHECK_EQUAL(completed, 0); + rig.checkClean(); + } +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(StructuralLoadingAndPublicationExceptionsAlwaysCleanUp, Count, LayerCounts) +{ + for (bool drop : {false, true}) { + Rig rig{drop}; + auto source = rig.source(); + source.dictionary = nullptr; + const auto run = [&] { + auto cleanup = rig.session.cleanupOnExit(); + rig.session.process(rig.tracker, rig.traits, source, [](const o2::InteractionRecord&) {}, [](const TrackingResult&) {}); + }; + BOOST_CHECK_THROW(run(), TimeFrameLoadException); + rig.checkClean(); + rig.configure(); + const auto publish = [&] { + auto cleanup = rig.session.cleanupOnExit(); + rig.session.process(rig.tracker, rig.traits, rig.source(), [](const o2::InteractionRecord&) {}, [](const TrackingResult&) { throw std::runtime_error{"publication failed"}; }); + }; + BOOST_CHECK_THROW(publish(), std::runtime_error); + rig.checkClean(); + } +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(ResourceExceptionsInPostLoadHookFollowLoadingPolicy, Count, LayerCounts) +{ + for (bool drop : {false, true}) { + for (bool bounded : {false, true}) { + Rig rig{drop}; + const auto run = [&] { + auto cleanup = rig.session.cleanupOnExit(); + const auto outcome = rig.session.process(rig.tracker, rig.traits, rig.source(), [&](const o2::InteractionRecord&) { + if (bounded) { throw BoundedMemoryResource::MemoryLimitExceeded{2, 1, 1}; } + throw std::bad_alloc{}; }, [](const TrackingResult&) { BOOST_FAIL("must not track after failed loading"); }); + BOOST_CHECK(outcome == TrackingOutcome::RecoverableDropped); + cleanup.frameAlreadyReset(); + }; + if (drop) { + BOOST_CHECK_NO_THROW(run()); + } else { + BOOST_CHECK_THROW(run(), std::bad_alloc); + } + rig.checkClean(); + } + } +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(TrackingResourceFailureSkipsCompletionAndPublication, Count, LayerCounts) +{ + Rig rig{true, 1}; + auto cleanup = rig.session.cleanupOnExit(); + const auto outcome = rig.session.process(rig.tracker, rig.traits, rig.source(), [](const o2::InteractionRecord&) {}, [](const TrackingResult&) { BOOST_FAIL("must not complete a dropped TF"); }); + BOOST_CHECK(outcome == TrackingOutcome::RecoverableDropped); + BOOST_CHECK(rig.session.frame.getGenericTracks().empty()); + cleanup.frameAlreadyReset(); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(TimingViewsBelongToTheSessionAndFilteringSurvivesMoves, Count, LayerCounts) +{ + Rig rig; + { + auto cleanup = rig.session.cleanupOnExit(); + std::vector timings(Count::value); + std::fill(timings.begin(), timings.end(), o2::its::LayerTiming{.mNROFsTF = 3, .mROFLength = 40}); + rig.session.configureTiming(timings, [](int rof) { return rof != 1; }); + std::fill(timings.begin(), timings.end(), o2::its::LayerTiming{}); // No view may refer to the caller's timing storage. + const auto views = rig.session.frame.getROFViews(); + BOOST_CHECK_EQUAL(views.overlap.getLayer(0).mROFLength, 40u); + for (int layer = 0; layer < Count::value; ++layer) { + BOOST_CHECK(views.mask.isROFEnabled(layer, 0)); + BOOST_CHECK(!views.mask.isROFEnabled(layer, 1)); + BOOST_CHECK(views.mask.isROFEnabled(layer, 2)); + } + std::fill(timings.begin(), timings.end(), o2::its::LayerTiming{.mNROFsTF = 3, .mROFLength = 40}); + timings[1].mROFLength = 41; + BOOST_CHECK_THROW(rig.session.configureTiming(timings, [](int) { return true; }), TimeFrameLoadException); + BOOST_CHECK_EQUAL(rig.session.frame.getROFViews().overlap.getLayer(1).mROFLength, 40u); + } + rig.checkClean(); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(InactivePublicationRetainsTheEchoedEmptyContract, Count, LayerCounts) +{ + for (auto outcome : {TrackingOutcome::Success, TrackingOutcome::RecoverableDropped, TrackingOutcome::Structural}) { + BOOST_CHECK(decideCATrackerPublicationAction(false, outcome) == CATrackerPublicationAction::PublishInactiveEmpty); + } +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(UnclassifiedExceptionsDoNotBecomeRecoverableDrops, Count, LayerCounts) +{ + for (bool drop : {false, true}) { + for (bool standard : {false, true}) { + Rig rig{drop}; + const auto run = [&] { + auto cleanup = rig.session.cleanupOnExit(); + rig.session.process(rig.tracker, rig.traits, rig.source(), [&](const o2::InteractionRecord&) { + if (standard) { throw std::logic_error{"unexpected loading failure"}; } + throw 7; }, [](const TrackingResult&) { BOOST_FAIL("must not complete after an exception"); }); + }; + if (standard) { + BOOST_CHECK_THROW(run(), std::logic_error); + } else { + BOOST_CHECK_THROW(run(), int); + } + rig.checkClean(); + } + } +} + +namespace +{ +struct TestOutputAllocator { + std::map values; + template + Vector& make(int output, Iterator first, Iterator last) + { + values[output] = Vector(first, last); + return std::any_cast(values.at(output)); + } +}; +} // namespace +BOOST_AUTO_TEST_CASE_TEMPLATE(PublishedCommonColumnsOwnTheirStorageAfterSessionCleanup, Count, LayerCounts) +{ + using Staged = std::conditional_t; + Rig rig; + TestOutputAllocator outputs; + { + auto cleanup = rig.session.cleanupOnExit(); + Staged staged; + staged.tracks.resize(1); + staged.trackROFs.emplace_back(o2::InteractionRecord{123, 45}, 0, 0, 1); + staged.clusterIndices = {17, 23}; + copyTrackingOutputColumns(outputs, 0, 1, 2, staged); + staged.clusterIndices[0] = 99; + staged.trackROFs[0].setNEntries(0); + staged.tracks.clear(); + } + rig.checkClean(); + const auto& rofs = std::any_cast&>(outputs.values.at(0)); + BOOST_REQUIRE_EQUAL(rofs.size(), 1u); + BOOST_CHECK_EQUAL(rofs[0].getNEntries(), 1); + BOOST_CHECK((rofs[0].getBCData() == o2::InteractionRecord{123, 45})); + BOOST_CHECK_EQUAL(std::any_cast(outputs.values.at(1)).size(), 1u); + const auto& indices = std::any_cast&>(outputs.values.at(2)); + const std::vector expected{17, 23}; + BOOST_CHECK_EQUAL_COLLECTIONS(indices.begin(), indices.end(), expected.begin(), expected.end()); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(DetectorTimingConstructionRetainsValidationAndUnits, Count, LayerCounts) +{ + struct AlpideTiming { + int length = 40; + int getROFLengthInBC(int) const { return length; } + int getROFDelayInBC(int) const { return 3; } + int getROFBiasInBC(int) const { return 4; } + } alpide; + Rig rig; + const std::vector timeErrors(Count::value, 5); + const auto timings = rig.session.layerTimings(alpide, 2, timeErrors); + for (const auto& timing : timings) { + BOOST_CHECK_EQUAL(timing.mROFLength, 40u); + BOOST_CHECK_EQUAL(timing.mROFDelay, 3u); + BOOST_CHECK_EQUAL(timing.mROFBias, 4u); + BOOST_CHECK_EQUAL(timing.mROFAddTimeErr, 5u); + BOOST_CHECK_EQUAL(timing.mNROFsTF, 178u); + } + BOOST_CHECK_EXCEPTION(rig.session.layerTimings(alpide, 0, timeErrors), TimeFrameLoadException, + [](const TimeFrameLoadException& error) { return error.reason() == TimeFrameLoadFailureReason::ZeroROFCount; }); + BOOST_CHECK_THROW(rig.session.layerTimings(alpide, 2, std::vector(Count::value - 1)), TimeFrameLoadException); + alpide.length = 0; + BOOST_CHECK_EXCEPTION(rig.session.layerTimings(alpide, 2, timeErrors), TimeFrameLoadException, + [](const TimeFrameLoadException& error) { return error.reason() == TimeFrameLoadFailureReason::NonUniformROFTiming; }); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(UnchangedTimingReusesStorageButRefreshesEventData, Count, LayerCounts) +{ + WorkflowSession session{"test", Count::value}; + std::vector timings(Count::value); + std::fill(timings.begin(), timings.end(), o2::its::LayerTiming{.mNROFsTF = 3, .mROFLength = 40}); + session.configureTiming(timings, [](int rof) { return rof == 0; }); + o2::its::Vertex vertex; + vertex.getTimeStamp().setTimeStamp(20); + vertex.getTimeStamp().setTimeStampError(5); + session.vertices.update(&vertex, 1); + const auto overlapStorage = session.overlap.getView().mFlatTable; + const auto vertexStorage = session.vertices.getView().mFlatTable; + const auto maskStorage = session.mask.getView().mFlatMask; + BOOST_REQUIRE_EQUAL(session.vertices.getView().getVertices(0, 0).getEntries(), 1u); + session.publicationClock.emplace(session.overlap.getView().getClockLayer()); + session.reset(); + session.invalidatePublication(); + BOOST_CHECK_EQUAL(session.frame.getROFViews().overlap.mLayerCount, 0); + + int calls = 0; + session.configureTiming(timings, [&](int rof) { ++calls; return rof == 2; }); + std::fill(timings.begin(), timings.end(), o2::its::LayerTiming{}); // The key and table definitions own their timing values. + BOOST_CHECK_EQUAL(calls, 3); + BOOST_CHECK(session.overlap.getView().mFlatTable == overlapStorage); + BOOST_CHECK(session.vertices.getView().mFlatTable == vertexStorage); + BOOST_CHECK(session.mask.getView().mFlatMask == maskStorage); + BOOST_CHECK(!session.publicationClock); + for (int layer = 0; layer < Count::value; ++layer) { + for (int rof = 0; rof < 3; ++rof) { + const auto range = session.vertices.getView().getVertices(layer, rof); + BOOST_CHECK_EQUAL(range.getFirstEntry(), 0u); + BOOST_CHECK_EQUAL(range.getEntries(), 0u); + BOOST_CHECK_EQUAL(session.frame.getROFViews().mask.isROFEnabled(layer, rof), rof == 2); + } + } + // New truth contents can be bound after a cache hit, then cleared again. + vertex.getTimeStamp().setTimeStamp(100); + session.vertices.update(&vertex, 1); + BOOST_CHECK_EQUAL(session.vertices.getView().getVertices(0, 2).getEntries(), 1u); + std::fill(timings.begin(), timings.end(), o2::its::LayerTiming{.mNROFsTF = 3, .mROFLength = 40}); + session.configureTiming(timings, [](int) { return false; }); + BOOST_CHECK_EQUAL(session.vertices.getView().getVertices(0, 2).getEntries(), 0u); + BOOST_CHECK(!session.frame.getROFViews().mask.isROFEnabled(0, 2)); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(EveryTimingFieldAndLayerExtentInvalidateTheCache, Count, LayerCounts) +{ + using Timing = o2::its::LayerTiming; + constexpr std::array fields{&Timing::mNROFsTF, &Timing::mROFLength, &Timing::mROFDelay, + &Timing::mROFBias, &Timing::mROFAddTimeErr}; + std::vector baseline(Count::value); + std::fill(baseline.begin(), baseline.end(), Timing{.mNROFsTF = 3, .mROFLength = 40}); + WorkflowSession session{"test", Count::value}; + const auto accept = [](int rof) { return rof < 3 && rof != 1; }; + const auto compareWithFresh = [&](const auto& timings) { + session.configureTiming(timings, accept); + WorkflowSession fresh{"oracle", Count::value}; + fresh.configureTiming(timings, accept); + const auto actual = session.overlap.getView(); + const auto expected = fresh.overlap.getView(); + for (int layer = 0; layer < Count::value; ++layer) { + for (auto field : fields) { + BOOST_CHECK_EQUAL(actual.getLayer(layer).*field, timings[layer].*field); + BOOST_CHECK_EQUAL(session.vertices.getView().getLayer(layer).*field, timings[layer].*field); + } + for (uint32_t rof = 0; rof < timings[layer].mNROFsTF; ++rof) { + BOOST_CHECK_EQUAL(session.mask.getView().isROFEnabled(layer, rof), fresh.mask.getView().isROFEnabled(layer, rof)); + BOOST_CHECK_EQUAL(session.vertices.getView().getVertices(layer, rof).getEntries(), 0u); + for (int to = 0; to < Count::value; ++to) { + if (layer == to) { + continue; + } + BOOST_CHECK_EQUAL(actual.getOverlap(layer, to, rof).getFirstEntry(), expected.getOverlap(layer, to, rof).getFirstEntry()); + BOOST_CHECK_EQUAL(actual.getOverlap(layer, to, rof).getEntries(), expected.getOverlap(layer, to, rof).getEntries()); + } + } + } + }; + for (auto field : fields) { + compareWithFresh(baseline); + auto changed = baseline; + for (auto& timing : changed) { + timing.*field += 1; + } + compareWithFresh(changed); + compareWithFresh(changed); // Reuse must match the fresh oracle as well. + compareWithFresh(baseline); // Includes shrinking the TF again. + if (field != &Timing::mNROFsTF) { + for (int layer = 0; layer < Count::value; ++layer) { + auto nonuniform = baseline; + nonuniform[layer].*field += 1; + BOOST_CHECK_THROW(session.configureTiming(nonuniform, accept), TimeFrameLoadException); + } + } + } + // Uniformity constrains the four BC fields, but each layer has its own extent. + for (int layer = 0; layer < Count::value; ++layer) { + auto changed = baseline; + changed[layer].mNROFsTF += 1; + compareWithFresh(changed); + compareWithFresh(baseline); + } +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(FilterFailureLeavesNoEventViewsAndDoesNotPoisonTimingReuse, Count, LayerCounts) +{ + WorkflowSession session{"test", Count::value}; + std::vector timings(Count::value); + std::fill(timings.begin(), timings.end(), o2::its::LayerTiming{.mNROFsTF = 3, .mROFLength = 40}); + session.configureTiming(timings, [](int) { return true; }); + for (bool changeTiming : {false, true}) { + if (changeTiming) { + for (auto& timing : timings) { + timing.mROFLength += 1; + } + } + session.publicationClock.emplace(session.overlap.getView().getClockLayer()); + BOOST_CHECK_THROW(session.configureTiming(timings, [](int rof) { + if (rof == 1) { + throw std::runtime_error{"filter failed"}; + } + return true; + }), + std::runtime_error); + BOOST_CHECK_EQUAL(session.frame.getROFViews().overlap.mLayerCount, 0); + BOOST_CHECK(!session.publicationClock); + const auto storage = session.overlap.getView().mFlatTable; + session.configureTiming(timings, [](int rof) { return rof == 2; }); + BOOST_CHECK(session.overlap.getView().mFlatTable == storage); + for (int layer = 0; layer < Count::value; ++layer) { + BOOST_CHECK(!session.frame.getROFViews().mask.isROFEnabled(layer, 0)); + BOOST_CHECK(!session.frame.getROFViews().mask.isROFEnabled(layer, 1)); + BOOST_CHECK(session.frame.getROFViews().mask.isROFEnabled(layer, 2)); + BOOST_CHECK_EQUAL(session.vertices.getView().getVertices(layer, 0).getEntries(), 0u); + } + } +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(InvalidTimingLayerCountPreservesCachedConfiguration, Count, LayerCounts) +{ + WorkflowSession session{"test", Count::value}; + std::vector timings(Count::value, {.mNROFsTF = 3, .mROFLength = 40}); + const auto accept = [](int) { return true; }; + session.configureTiming(timings, accept); + const auto cached = session.overlap.getView().mFlatTable; + for (auto count : {0, Count::value - 1, Count::value + 1}) { + auto invalid = timings; + invalid.resize(count, timings.front()); + BOOST_CHECK_THROW(session.configureTiming(invalid, accept), TimeFrameLoadException); + BOOST_CHECK(session.frame.getROFViews().overlap.mFlatTable == cached); + } + session.configureTiming(timings, accept); + BOOST_CHECK(session.overlap.getView().mFlatTable == cached); +} diff --git a/Detectors/ITSMFT/common/workflow-ca-writer/CMakeLists.txt b/Detectors/ITSMFT/common/workflow-ca-writer/CMakeLists.txt new file mode 100644 index 0000000000000..7a0a5a63adc20 --- /dev/null +++ b/Detectors/ITSMFT/common/workflow-ca-writer/CMakeLists.txt @@ -0,0 +1,26 @@ +# Copyright 2019-2020 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. + +o2_add_library(ITSMFTCAWriter + SOURCES src/ITSCATrackWriterSpec.cxx + src/MFTCATrackWriterSpec.cxx + PUBLIC_LINK_LIBRARIES O2::Framework + O2::SimulationDataFormat + O2::DataFormatsITS + O2::DataFormatsITSMFT + O2::DataFormatsMFT + O2::MFTTracking) + +o2_add_test(itsmft-ca-writer-contract + COMPONENT_NAME itsmft + LABELS "itsmft;workflow" + SOURCES test/testITSMFTCAWriterContract.cxx + PUBLIC_LINK_LIBRARIES O2::ITSMFTCAWriter) diff --git a/Detectors/ITSMFT/common/workflow-ca-writer/include/ITSMFTCAWriter/ITSCATrackWriterSpec.h b/Detectors/ITSMFT/common/workflow-ca-writer/include/ITSMFTCAWriter/ITSCATrackWriterSpec.h new file mode 100644 index 0000000000000..e32117c9b9dc4 --- /dev/null +++ b/Detectors/ITSMFT/common/workflow-ca-writer/include/ITSMFTCAWriter/ITSCATrackWriterSpec.h @@ -0,0 +1,29 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// +/// \file ITSCATrackWriterSpec.h +/// \brief Vertex-free ITS common-CA track writer. Writes a distinct file +/// (o2trac_its_ca.root) with no vertex branches. + +#ifndef O2_ITSMFT_CAWRITER_ITSCATRACKWRITERSPEC_H_ +#define O2_ITSMFT_CAWRITER_ITSCATRACKWRITERSPEC_H_ + +#include "Framework/DataProcessorSpec.h" + +namespace o2::its::ca +{ + +/// Write ITS CA tracks to o2trac_its_ca.root without vertex branches. +o2::framework::DataProcessorSpec getTrackWriterSpec(bool useMC); + +} // namespace o2::its::ca + +#endif // O2_ITSMFT_CAWRITER_ITSCATRACKWRITERSPEC_H_ diff --git a/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/TrackWriterSpec.h b/Detectors/ITSMFT/common/workflow-ca-writer/include/ITSMFTCAWriter/MFTCATrackWriterSpec.h similarity index 61% rename from Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/TrackWriterSpec.h rename to Detectors/ITSMFT/common/workflow-ca-writer/include/ITSMFTCAWriter/MFTCATrackWriterSpec.h index 5a8d50939a25a..cc885c8f33041 100644 --- a/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/TrackWriterSpec.h +++ b/Detectors/ITSMFT/common/workflow-ca-writer/include/ITSMFTCAWriter/MFTCATrackWriterSpec.h @@ -9,26 +9,19 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -/// @file TrackWriterSpec.h +/// @file MFTCATrackWriterSpec.h -#ifndef O2_MFT_TRACKWRITER_H_ -#define O2_MFT_TRACKWRITER_H_ - -#include "TFile.h" +#ifndef O2_ITSMFT_CAWRITER_MFTCATRACKWRITERSPEC_H_ +#define O2_ITSMFT_CAWRITER_MFTCATRACKWRITERSPEC_H_ #include "Framework/DataProcessorSpec.h" -#include "Framework/Task.h" -namespace o2 -{ -namespace mft +namespace o2::mft { -/// create a processor spec -/// write MFT tracks a root file -o2::framework::DataProcessorSpec getTrackWriterSpec(bool useMC); +/// Write MFT tracks to a ROOT file. +o2::framework::DataProcessorSpec getTrackWriterSpec(bool useMC, bool useCA = false); -} // namespace mft -} // namespace o2 +} // namespace o2::mft -#endif /* O2_MFT_TRACKWRITER_H_ */ +#endif // O2_ITSMFT_CAWRITER_MFTCATRACKWRITERSPEC_H_ diff --git a/Detectors/ITSMFT/common/workflow-ca-writer/src/ITSCATrackWriterSpec.cxx b/Detectors/ITSMFT/common/workflow-ca-writer/src/ITSCATrackWriterSpec.cxx new file mode 100644 index 0000000000000..f50aba2192a06 --- /dev/null +++ b/Detectors/ITSMFT/common/workflow-ca-writer/src/ITSCATrackWriterSpec.cxx @@ -0,0 +1,62 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "ITSMFTCAWriter/ITSCATrackWriterSpec.h" + +#include + +#include "DPLUtils/MakeRootTreeWriterSpec.h" +#include "DataFormatsITS/TrackITS.h" +#include "DataFormatsITSMFT/ROFRecord.h" +#include "SimulationDataFormat/MCCompLabel.h" +#include "SimulationDataFormat/MCTruthContainer.h" + +using namespace o2::framework; + +namespace o2::its::ca +{ + +template +using BranchDefinition = MakeRootTreeWriterSpec::BranchDefinition; +using LabelsType = std::vector; + +DataProcessorSpec getTrackWriterSpec(bool useMC) +{ + // Spectators for logging; mirrors ITSWorkflow/TrackWriterSpec.cxx. + auto tracksSize = std::make_shared(0); + auto tracksSizeGetter = [tracksSize](std::vector const& tracks) { + *tracksSize = tracks.size(); + }; + auto logger = [tracksSize](std::vector const& rofs) { + LOG(info) << "ITSCATrackWriter pulled " << *tracksSize << " tracks, in " << rofs.size() << " RO frames"; + }; + // Deliberately no VERTICES/VERTICESROF/VERTICESMCTR/VERTICESMCPUR branch: + // this opt-in tracker-only workflow never publishes those OutputSpecs (see + // CATrackerSpec.cxx), so a writer branch consuming them would simply never + // fire. + return MakeRootTreeWriterSpec("its-ca-track-writer", + "o2trac_its_ca.root", + MakeRootTreeWriterSpec::TreeAttributes{"o2sim", "Tree with ITS common-CA tracks"}, + BranchDefinition>{InputSpec{"tracks", "ITS", "TRACKS", 0}, + "ITSTrack", + tracksSizeGetter}, + BranchDefinition>{InputSpec{"trackClIdx", "ITS", "TRACKCLSID", 0}, + "ITSTrackClusIdx"}, + BranchDefinition>{InputSpec{"ROframes", "ITS", "ITSTrackROF", 0}, + "ITSTracksROF", + logger}, + BranchDefinition{InputSpec{"labels", "ITS", "TRACKSMCTR", 0}, + "ITSTrackMCTruth", + (useMC ? 1 : 0), // one branch if mc labels enabled + ""})(); +} + +} // namespace o2::its::ca diff --git a/Detectors/ITSMFT/MFT/workflow/src/TrackWriterSpec.cxx b/Detectors/ITSMFT/common/workflow-ca-writer/src/MFTCATrackWriterSpec.cxx similarity index 85% rename from Detectors/ITSMFT/MFT/workflow/src/TrackWriterSpec.cxx rename to Detectors/ITSMFT/common/workflow-ca-writer/src/MFTCATrackWriterSpec.cxx index f8a848f6fde32..1706317778dc9 100644 --- a/Detectors/ITSMFT/MFT/workflow/src/TrackWriterSpec.cxx +++ b/Detectors/ITSMFT/common/workflow-ca-writer/src/MFTCATrackWriterSpec.cxx @@ -9,11 +9,9 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -/// @file TrackWriterSpec.cxx - #include -#include "MFTWorkflow/TrackWriterSpec.h" +#include "ITSMFTCAWriter/MFTCATrackWriterSpec.h" #include "DPLUtils/MakeRootTreeWriterSpec.h" #include "MFTTracking/TrackCA.h" @@ -34,7 +32,7 @@ template using BranchDefinition = MakeRootTreeWriterSpec::BranchDefinition; using namespace o2::header; -DataProcessorSpec getTrackWriterSpec(bool useMC) +DataProcessorSpec getTrackWriterSpec(bool useMC, bool useCA) { // Spectators for logging // this is only to restore the original behavior @@ -53,6 +51,10 @@ DataProcessorSpec getTrackWriterSpec(bool useMC) tracksSizeGetter}, BranchDefinition>{InputSpec{"trackClIdx", "MFT", "TRACKCLSID", 0}, "MFTTrackClusIdx"}, + BranchDefinition>{InputSpec{"trackSeedPat", "MFT", "TRACKSEEDPAT", 0}, + "MFTTrackSeedPattern", + (useCA ? 1 : 0), + ""}, BranchDefinition>{InputSpec{"ROframes", "MFT", "MFTTrackROF", 0}, "MFTTracksROF", logger}, diff --git a/Detectors/ITSMFT/common/workflow-ca-writer/test/testITSMFTCAWriterContract.cxx b/Detectors/ITSMFT/common/workflow-ca-writer/test/testITSMFTCAWriterContract.cxx new file mode 100644 index 0000000000000..ef9d32c0d2566 --- /dev/null +++ b/Detectors/ITSMFT/common/workflow-ca-writer/test/testITSMFTCAWriterContract.cxx @@ -0,0 +1,106 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +// Pin the shared ITS and MFT common-CA writer specifications. + +#define BOOST_TEST_MODULE ITSMFT ITSMFTCAWriterContract +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include +#include + +#include "Framework/DataProcessorSpec.h" +#include "Framework/DataSpecUtils.h" +#include "ITSMFTCAWriter/ITSCATrackWriterSpec.h" +#include "ITSMFTCAWriter/MFTCATrackWriterSpec.h" + +using namespace o2::framework; + +namespace +{ +bool hasInput(const std::vector& specs, const std::string& binding) +{ + return std::any_of(specs.begin(), specs.end(), [&binding](const InputSpec& s) { return s.binding == binding; }); +} + +bool sameShape(const DataProcessorSpec& a, const DataProcessorSpec& b) +{ + if (a.name != b.name || a.inputs.size() != b.inputs.size() || a.outputs.size() != b.outputs.size()) { + return false; + } + for (size_t i = 0; i < a.inputs.size(); ++i) { + if (a.inputs[i].binding != b.inputs[i].binding || DataSpecUtils::describe(a.inputs[i]) != DataSpecUtils::describe(b.inputs[i])) { + return false; + } + } + return true; +} +} // namespace + +BOOST_AUTO_TEST_CASE(ITSWriterSpecContract) +{ + const auto spec = o2::its::ca::getTrackWriterSpec(false); + BOOST_CHECK_EQUAL(spec.name, "its-ca-track-writer"); + BOOST_CHECK(hasInput(spec.inputs, "tracks")); + BOOST_CHECK(hasInput(spec.inputs, "trackClIdx")); + BOOST_CHECK(hasInput(spec.inputs, "ROframes")); + BOOST_CHECK(!hasInput(spec.inputs, "labels")); +} + +BOOST_AUTO_TEST_CASE(ITSWriterSpecMCContractAddsLabels) +{ + const auto spec = o2::its::ca::getTrackWriterSpec(true); + BOOST_CHECK(hasInput(spec.inputs, "labels")); +} + +BOOST_AUTO_TEST_CASE(ITSWriterSpecIsDeterministicAcrossCallers) +{ + const auto first = o2::its::ca::getTrackWriterSpec(true); + const auto second = o2::its::ca::getTrackWriterSpec(true); + BOOST_CHECK(sameShape(first, second)); +} + +BOOST_AUTO_TEST_CASE(MFTWriterSpecContract) +{ + const auto spec = o2::mft::getTrackWriterSpec(false); + BOOST_CHECK_EQUAL(spec.name, "mft-track-writer"); + BOOST_CHECK(hasInput(spec.inputs, "tracks")); + BOOST_CHECK(hasInput(spec.inputs, "trackClIdx")); + BOOST_CHECK(!hasInput(spec.inputs, "trackSeedPat")); + BOOST_CHECK(hasInput(spec.inputs, "ROframes")); + BOOST_CHECK(!hasInput(spec.inputs, "labels")); +} + +BOOST_AUTO_TEST_CASE(MFTWriterSpecMCContractAddsLabels) +{ + const auto spec = o2::mft::getTrackWriterSpec(true); + BOOST_CHECK(hasInput(spec.inputs, "labels")); +} + +BOOST_AUTO_TEST_CASE(MFTWriterSpecDefaultUseCAIsFalseMatchingLegacyCaller) +{ + // RecoWorkflow.cxx (legacy o2-mft-reco-workflow) calls + // getTrackWriterSpec(useMC) with useCA left at its default -- must stay + // false so the legacy writer's own contract is unchanged. + const auto legacyShape = o2::mft::getTrackWriterSpec(false); + const auto explicitFalse = o2::mft::getTrackWriterSpec(false, false); + BOOST_CHECK(sameShape(legacyShape, explicitFalse)); +} + +BOOST_AUTO_TEST_CASE(MFTWriterSpecIsDeterministicAcrossCallersUseCATrue) +{ + const auto first = o2::mft::getTrackWriterSpec(true, true); + const auto second = o2::mft::getTrackWriterSpec(true, true); + BOOST_CHECK(sameShape(first, second)); + BOOST_CHECK(hasInput(first.inputs, "trackSeedPat")); +} From fd6d4eac21dda6983a6fdca2f4b4bbd096c414f2 Mon Sep 17 00:00:00 2001 From: ALICE Action Bot Date: Wed, 9 Sep 2026 09:52:42 +0000 Subject: [PATCH 2/2] Please consider the following formatting changes --- .../ITSMFTTracking/TrackingConfigParam.h | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackingConfigParam.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackingConfigParam.h index 9abd75dc558c8..21d8d1a5bc7dd 100644 --- a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackingConfigParam.h +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/TrackingConfigParam.h @@ -78,12 +78,12 @@ struct TrackerParamConfig : public o2::conf::ConfigurableParamHelper