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..21d8d1a5bc7dd --- /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")); +}