diff --git a/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/CATrackerSpec.h b/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/CATrackerSpec.h index ad08081fad5fe..d78320d84aa6e 100644 --- a/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/CATrackerSpec.h +++ b/Detectors/ITSMFT/ITS/workflow-ca/include/ITSCAWorkflow/CATrackerSpec.h @@ -28,7 +28,6 @@ #include "Framework/Task.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" @@ -77,7 +76,6 @@ class CATrackerDPL : public o2::framework::Task 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; PublicationAdapter mPublication; }; diff --git a/Detectors/ITSMFT/ITS/workflow-ca/src/CATrackerSpec.cxx b/Detectors/ITSMFT/ITS/workflow-ca/src/CATrackerSpec.cxx index be918788f01e9..6747a50a6d59b 100644 --- a/Detectors/ITSMFT/ITS/workflow-ca/src/CATrackerSpec.cxx +++ b/Detectors/ITSMFT/ITS/workflow-ca/src/CATrackerSpec.cxx @@ -232,7 +232,6 @@ bool completePublication(PublicationAdapter& publication, CATrackerDPL::CATrackerDPL(std::shared_ptr gr, WorkflowOptions options) : mGGCCDBRequest(std::move(gr)), mUseMC(options.useMC), mOptions(std::move(options)) { - mClusterDecoder = std::make_unique(); } void CATrackerDPL::addTruthSeedingVertices(const o2::InteractionRecord& origin, gsl::span rofs) @@ -363,7 +362,6 @@ o2::itsmft::tracking::TrackingOutcome CATrackerDPL::processTimeFrame( 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); diff --git a/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CATrackerSpec.h b/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CATrackerSpec.h index 3d4b6b0e549ca..6f8ede458139b 100644 --- a/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CATrackerSpec.h +++ b/Detectors/ITSMFT/MFT/workflow/include/MFTWorkflow/CATrackerSpec.h @@ -25,7 +25,6 @@ #include "Framework/Task.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" @@ -74,7 +73,6 @@ class CATrackerDPL : public o2::framework::Task 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; }; diff --git a/Detectors/ITSMFT/MFT/workflow/src/CATrackerSpec.cxx b/Detectors/ITSMFT/MFT/workflow/src/CATrackerSpec.cxx index 148fbb2d47c0a..1d3e584195868 100644 --- a/Detectors/ITSMFT/MFT/workflow/src/CATrackerSpec.cxx +++ b/Detectors/ITSMFT/MFT/workflow/src/CATrackerSpec.cxx @@ -235,7 +235,6 @@ bool rofOverlapsIRFrames(const o2::itsmft::ROFRecord& rof, int rofLengthInBC, 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, @@ -307,7 +306,6 @@ o2::itsmft::tracking::TrackingOutcome CATrackerDPL::processTimeFrame( 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&) {}); } diff --git a/Detectors/ITSMFT/common/tracking/CMakeLists.txt b/Detectors/ITSMFT/common/tracking/CMakeLists.txt index a987fd4e51639..f154dcf6964d3 100644 --- a/Detectors/ITSMFT/common/tracking/CMakeLists.txt +++ b/Detectors/ITSMFT/common/tracking/CMakeLists.txt @@ -36,7 +36,6 @@ o2_add_library(ITSMFTTracking src/PropagatorBarrelOperations.cxx src/PropagatorForwardOperations.cxx src/MaterialPhysics.cxx - src/FamilyMaterialOperations.cxx src/IndexTableConfiguration.cxx src/TraversalTopology.cxx src/Tracker.cxx diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ClusterDecoding.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ClusterDecoding.h deleted file mode 100644 index 3271c2a7ae8d0..0000000000000 --- a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/ClusterDecoding.h +++ /dev/null @@ -1,239 +0,0 @@ -// 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/IOUtils.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IOUtils.h index a34e4f9fe22be..0ccf62a559125 100644 --- a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IOUtils.h +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/IOUtils.h @@ -16,11 +16,11 @@ #ifndef ALICEO2_ITSMFT_TRACKING_IOUTILS_H_ #define ALICEO2_ITSMFT_TRACKING_IOUTILS_H_ -#include +#include #include -#include #ifndef GPUCA_GPUCODE +#include #include #include #endif @@ -33,7 +33,7 @@ #include "DataFormatsITSMFT/CompCluster.h" #include "DataFormatsITSMFT/ROFRecord.h" #include "DataFormatsITSMFT/TopologyDictionary.h" -#include "ITSMFTTracking/ClusterDecoding.h" +#include "ITSMFTTracking/GlobalMeasurement.h" #include "ITSMFTTracking/Configuration.h" #include "ITSMFTTracking/ROFViews.h" #include "ITSMFTTracking/SurfaceDescriptor.h" @@ -42,178 +42,74 @@ #include "MathUtils/Cartesian.h" #include "SimulationDataFormat/MCTruthContainer.h" -namespace o2::itsmft::ioutils -{ - -namespace detail -{ -constexpr bool isSensorInGeometry(int sensor, int geometrySize) noexcept +namespace o2::itsmft::tracking { - return sensor >= 0 && sensor < geometrySize; -} -constexpr bool isLayerInDetector(int layer, int detectorLayers) noexcept -{ - return layer >= 0 && layer < detectorLayers; -} +// 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{}; + uint32_t nPixels{0}; + int layer{-1}; +}; -/// 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; - } -} +} // namespace o2::itsmft::tracking -/// Add configured systematic-error corrections to `sigma2Row` and `sigma2Col`. -/// ITS is a no-op. -template -void addSysErrors(int layerId, float& sigma2Row, float& sigma2Col) +namespace o2::itsmft::ioutils { - 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 { +struct ClusterData { 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; } + uint32_t nPixels{0}; }; -// Bounded counterpart: acquire pattern bytes only after validating the encoding. +// Decode using dictionary coordinates, errors and pixel counts. Grouped and +// explicit patterns require their actual bitmap; the group entry is representative. +// As in ITS tracking, the explicit-pattern stream is assumed to be valid. template -ClusterDataDecodeResult extractClusterDataBounded( +ClusterData extractClusterData( const CompClusterExt& c, - o2::itsmft::tracking::BoundedPatternCursor& patterns, + gsl::span::iterator& patterns, const TopologyDictionary* dict) { - ClusterDataDecodeResult result; + ClusterData result; if (dict == nullptr) { - result.error = o2::itsmft::tracking::ClusterDecodeError::MissingDictionary; - return result; + throw std::runtime_error("Cluster dictionary is not available"); } const auto pattID = c.getPatternID(); if (pattID != CompCluster::InvalidPatternID) { if (pattID >= dict->getSize()) { - result.error = o2::itsmft::tracking::ClusterDecodeError::InvalidPatternId; - return result; + throw std::runtime_error("Cluster pattern ID is outside the topology dictionary"); } 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.nPixels = static_cast(dict->getNpixels(pattID)); 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); + const o2::itsmft::ClusterPattern pattern{patterns}; + result.nPixels = static_cast(pattern.getNPixels()); + result.coordinates = TopologyDictionary::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 @@ -231,102 +127,66 @@ struct ClusterSourceInput { 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); +/// Clear and fill a configured frame. On failure, the caller must reset the +/// frame before reuse; partially loaded data must not be published. +void 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); +/// Invalid input throws. On failure, the caller must reset the frame before +/// reuse; partially loaded data must not be published. +void 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 +namespace detail { - 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 +void prepareSources(TimeFrame&, const SurfaceCatalogView&, gsl::span, + std::vector>*, std::vector>*); +void validateSource(const ClusterSourceInput&, const o2::InteractionRecord&); +void appendCluster(TimeFrame&, const SurfaceCatalogView&, const ClusterSourceInput&, const DecodedCluster&, + uint32_t, uint32_t, std::vector>&, std::vector>&); +void finishTimeFrameLoading(TimeFrame&, const SurfaceCatalogView&, gsl::span, + const std::vector>&); + +// Internal loading loop; geometry decoding and synthetic fixtures share the +// same stream consumption, diagnostics and measurement insertion. +template +void loadDecodedSource(TimeFrame& frame, const SurfaceCatalogView& catalog, const ClusterSourceInput& src, + const Decode& decode, std::vector>& externalIndices, + std::vector>& clusterSizes) { - 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 + auto patterns = src.patterns.begin(); + 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); + DecodedCluster decoded; + try { + decoded = decode(cluster, patterns); + } catch (const std::runtime_error& error) { + throw std::runtime_error(std::format("Cluster decoding failed: source={} rof={} clusterIndex={}: {}", + src.id.value(), r, externalIndex, error.what())); + } + appendCluster(frame, catalog, src, decoded, r, externalIndex, externalIndices, clusterSizes); + } + } + if (patterns != src.patterns.end()) { + throw std::runtime_error(std::format("Trailing cluster pattern data source={} rof={} clusterIndex={}", src.id.value(), static_cast(src.rofs.size()), static_cast(src.clusters.size()))); + } +} +} // namespace detail } // namespace o2::itsmft::tracking diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/MaterialPhysics.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/MaterialPhysics.h index f92442b8bfda1..05613777a5aca 100644 --- a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/MaterialPhysics.h +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/MaterialPhysics.h @@ -36,11 +36,10 @@ struct IntegratedMaterialBudget { float arealDensityGPerCm2; ///< crossed length*density, g/cm^2 }; -// Detector-neutral, PID/absCharge-aware scalar material-physics kernel. +// Scalar material-physics kernel for charged particles. // 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. +// scattering scale factors. absCharge must be nonzero and need not equal +// PID::getCharge(). // // Validation precedence (first failure wins): invalid direction, negative // material, non-positive momentum, invalid PID, then a charged massless PID. diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Propagator.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Propagator.h index 4e2a7f08aa342..fbc22ab0c1f5e 100644 --- a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Propagator.h +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/Propagator.h @@ -71,6 +71,13 @@ class Propagator float bz, material::MaterialTraversalDirection direction, bool chi2GateEnabled, float maxChi2, float& chi2, bool shiftReferenceToMeasurement) noexcept; + + private: + // Called only after propagation validates matching Cylinder/Disk kinds for + // the state and incidence reference. Select material formulas from state.kind. + static bool correctForMaterial(SurfaceTrackState& state, SurfaceTrackParameters& incidenceReference, + material::IntegratedMaterialBudget materialBudget, + material::MaterialTraversalDirection direction) noexcept; }; } // namespace o2::itsmft::tracking diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceTrackState.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceTrackState.h index e62e045cac788..8712e7507555b 100644 --- a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceTrackState.h +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/SurfaceTrackState.h @@ -28,7 +28,8 @@ namespace o2::itsmft::tracking // 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 +// Fitted surface state for charged particles; valid tracks have absCharge > 0. +// The field order keeps the device-facing representation // compact while the parameter-only linearization state remains independent. struct SurfaceTrackState { float parameters[5]{}; @@ -40,22 +41,14 @@ struct SurfaceTrackState { uint8_t absCharge{0}; o2::track::PID pid{o2::track::PID::Pion}; + GPUhdi() float getP() const noexcept + { + return absCharge * std::sqrt(1.f + parameters[3] * parameters[3]) / std::abs(parameters[4]); + } + 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 { @@ -76,15 +69,6 @@ struct SurfaceTrackParameters { 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; diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/WorkflowSession.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/WorkflowSession.h index f0aa14e848383..9f8b8df59d8b0 100644 --- a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/WorkflowSession.h +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/WorkflowSession.h @@ -110,15 +110,13 @@ class WorkflowSession { 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"}; + throw std::runtime_error{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"}; + throw std::runtime_error{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), @@ -127,8 +125,7 @@ class WorkflowSession .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"}; + throw std::runtime_error{std::string(mDetectorName) + " CA per-layer ROF timing yields zero ROFs per TimeFrame"}; } } return timings; @@ -139,8 +136,7 @@ class WorkflowSession { 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"}; + throw std::runtime_error{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. @@ -183,12 +179,6 @@ class WorkflowSession 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(); @@ -201,12 +191,8 @@ class WorkflowSession 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()); + LOGP(error, "{} CA loading failed: {}", mDetectorName, error.what()); reset(); throw; } @@ -225,24 +211,16 @@ class WorkflowSession 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"}; + throw std::runtime_error{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"}; + throw std::runtime_error{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}; - } + loadTimeFrameSources(frame, gsl::span{&source, 1}, + frame.getLayout().getSurfaceCatalog(), origin, &externalIndices, &clusterSizes); afterLoad(origin); })) { return TrackingOutcome::RecoverableDropped; diff --git a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/SurfaceStateOperations.h b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/SurfaceStateOperations.h index 4647540a479ba..6bd40075f2f0e 100644 --- a/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/SurfaceStateOperations.h +++ b/Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/detail/SurfaceStateOperations.h @@ -12,7 +12,6 @@ #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" @@ -27,11 +26,6 @@ bool rotate(SurfaceTrackState& state, float targetAlpha) noexcept; bool propagate(SurfaceTrackState& state, float targetX, float bz) noexcept; bool predictedChi2(const SurfaceTrackState& state, const SurfaceMeasurement& measurement, float& chi2) noexcept; bool update(SurfaceTrackState& state, const SurfaceMeasurement& measurement, float& chi2) noexcept; -bool correctForMaterial(SurfaceTrackState& state, material::IntegratedMaterialBudget materialBudget, - material::MaterialTraversalDirection direction) noexcept; -bool correctForMaterial(SurfaceTrackState& state, SurfaceTrackParameters& linRef, - material::IntegratedMaterialBudget materialBudget, - material::MaterialTraversalDirection direction) noexcept; bool stateChi2(const SurfaceTrackState& reference, const SurfaceTrackState& candidate, float& chi2) noexcept; #ifndef GPUCA_GPUCODE @@ -48,17 +42,6 @@ bool propagate(SurfaceTrackState& state, SurfaceTrackParameters& linRef, float targetZ, float bz) noexcept; bool predictedChi2(const SurfaceTrackState& state, const SurfaceMeasurement& measurement, float& chi2) noexcept; bool update(SurfaceTrackState& state, const SurfaceMeasurement& measurement, float& chi2) 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) noexcept; -bool correctForMaterial(SurfaceTrackState& state, material::IntegratedMaterialBudget materialBudget, - material::MaterialTraversalDirection direction) noexcept; -bool correctForMaterial(SurfaceTrackState& state, SurfaceTrackParameters& linRef, - material::IntegratedMaterialBudget materialBudget, - material::MaterialTraversalDirection direction) noexcept; bool stateChi2(const SurfaceTrackState& reference, const SurfaceTrackState& candidate, float& chi2) noexcept; #ifndef GPUCA_GPUCODE diff --git a/Detectors/ITSMFT/common/tracking/src/FamilyMaterialOperations.cxx b/Detectors/ITSMFT/common/tracking/src/FamilyMaterialOperations.cxx deleted file mode 100644 index 292cc598c52e3..0000000000000 --- a/Detectors/ITSMFT/common/tracking/src/FamilyMaterialOperations.cxx +++ /dev/null @@ -1,321 +0,0 @@ -// 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; -} - -// 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) noexcept -{ - if (state.kind != expectedFamily) { - return false; - } - const float u = state.parameters[4]; - if (!familyCheck(state) || u == 0.f) { - return false; - } - if (state.pid.getID() >= o2::track::PID::NIDsTot) { - return false; - } - if (state.absCharge != 0 && state.pid.getMass() == 0.f) { - return false; - } - if (!covarianceDiagonalsNonNegative(state)) { - return false; - } - return true; -} - -// Complete incidence-aware transactional operation shared by cylinder and disk -// states: 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 succeeds -// 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 -bool correctForMaterialImpl(SurfaceTrackState& state, SurfaceTrackParameters& incidenceReference, - SurfaceKind expectedFamily, - material::IntegratedMaterialBudget materialBudget, - material::MaterialTraversalDirection direction, - FamilyKinematicsCheck&& familyCheck, - ScaleMaterial&& scaleMaterial, - ProjectCovariance&& projectCovariance) noexcept -{ - if (incidenceReference.kind != expectedFamily) { - return false; - } - if (!familyCheck(incidenceReference) || incidenceReference.parameters[4] == 0.f) { - return false; - } - if (!preflightValidate(state, expectedFamily, familyCheck)) { - return false; - } - - float momentumBeforeGeV = 0.f; - if (!derivePhysicalMomentum(state, momentumBeforeGeV)) { - return false; - } - - SurfaceTrackState scratchState = state; - SurfaceTrackParameters scratchReference = incidenceReference; - scaleMaterial(materialBudget, scratchReference); - float momentumAfterGeV = 0.f; - float highlandTheta2Rad2 = 0.f; - float relativeInverseMomentumVariance = 0.f; - if (!material::calculateMaterialPhysics(momentumBeforeGeV, scratchState.pid, scratchState.absCharge, direction, materialBudget, - momentumAfterGeV, highlandTheta2Rad2, relativeInverseMomentumVariance)) { - return false; - } - - const bool isNoopMaterial = (materialBudget.xOverX0 == 0.f && materialBudget.arealDensityGPerCm2 == 0.f); - if (scratchState.absCharge == 0 || isNoopMaterial) { - return true; - } - - const float tBefore = scratchState.parameters[3]; - const float kBefore = scratchState.parameters[4]; - projectCovariance(scratchState, highlandTheta2Rad2, relativeInverseMomentumVariance, 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 = (momentumBeforeGeV == momentumAfterGeV) - ? kBefore - : (kBefore * momentumBeforeGeV) / 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 false; - } - float momentumAfterDerived = 0.f; - if (!derivePhysicalMomentum(scratchState, momentumAfterDerived)) { - return false; - } - if (!covarianceDiagonalsNonNegative(scratchState)) { - return false; - } - - // 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] = (momentumBeforeGeV == momentumAfterGeV) - ? referenceKBefore - : (referenceKBefore * momentumBeforeGeV) / momentumAfterGeV; - if (scratchReference.parameters[4] == 0.f || !std::isfinite(scratchReference.parameters[4])) { - return false; - } - - state = scratchState; - incidenceReference = scratchReference; - return true; -} - -} // namespace -} // namespace o2::itsmft::tracking - -namespace o2::itsmft::tracking::detail::barrel -{ -bool 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, float highlandTheta2Rad2, float relativeInverseMomentumVariance, - 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 = highlandTheta2Rad2; - const float R = 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); -} - -bool 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 -{ -bool 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, float highlandTheta2Rad2, float relativeInverseMomentumVariance, - float t, float k) noexcept { - const float A = 1.f + t * t; - const float h = highlandTheta2Rad2; - const float R = 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); -} - -bool 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 index 23987496b28df..8bbd24590ae8e 100644 --- a/Detectors/ITSMFT/common/tracking/src/IOUtils.cxx +++ b/Detectors/ITSMFT/common/tracking/src/IOUtils.cxx @@ -14,12 +14,11 @@ #include #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" @@ -28,42 +27,64 @@ namespace { -using o2::itsmft::ioutils::detail::addSysErrors; -using o2::itsmft::ioutils::detail::shouldApplySysErrors; +/// 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]; + } +} template -o2::itsmft::tracking::ClusterDecodeResult decodeClusterBounded( +o2::itsmft::tracking::DecodedCluster decodeCluster( GeomT* geom, const o2::itsmft::CompClusterExt& cluster, - o2::itsmft::tracking::BoundedPatternCursor& patterns, + gsl::span::iterator& patterns, const o2::itsmft::TopologyDictionary* dict, bool applySysErrors) { - using o2::itsmft::tracking::ClusterDecodeError; - o2::itsmft::tracking::ClusterDecodeResult result; + o2::itsmft::tracking::DecodedCluster result; if (dict == nullptr) { - result.error = ClusterDecodeError::MissingDictionary; - return result; + throw std::runtime_error("Cluster dictionary is not available"); } if (geom == nullptr) { - result.error = ClusterDecodeError::GeometryUnavailable; - return result; + throw std::runtime_error("Cluster geometry is not available"); } const auto sensorID = cluster.getSensorID(); - if (!o2::itsmft::ioutils::detail::isSensorInGeometry(sensorID, geom->getSize())) { - result.error = ClusterDecodeError::InvalidSensor; - return result; + if (sensorID >= geom->getSize()) { + throw std::runtime_error("Cluster sensor ID is outside the detector geometry"); } const int layer = geom->getLayer(sensorID); - if (!o2::itsmft::ioutils::detail::isLayerInDetector(layer, o2::itsmft::tracking::TrackerParamRef::nLayers())) { - result.error = ClusterDecodeError::InvalidLayer; - return result; + if (layer < 0 || layer >= o2::itsmft::tracking::TrackerParamRef::nLayers()) { + throw std::runtime_error("Cluster layer is outside the detector"); } - const auto clusterData = o2::itsmft::ioutils::extractClusterDataBounded(cluster, patterns, dict); - if (!clusterData.ok()) { - result.error = clusterData.error; - return result; - } + const auto clusterData = o2::itsmft::ioutils::extractClusterData(cluster, patterns, dict); float sigma2Row = clusterData.sig2Row; float sigma2Col = clusterData.sig2Col; if (applySysErrors && shouldApplySysErrors()) { @@ -73,78 +94,133 @@ o2::itsmft::tracking::ClusterDecodeResult decodeClusterBounded( 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}; + result = {{gloXYZ.x(), gloXYZ.y(), gloXYZ.z()}, + {trkXYZ.x(), trkXYZ.y(), trkXYZ.z(), geom->getSensorRefAlpha(sensorID)}, + {sigma2Row, 0.f, sigma2Col}, + clusterData.nPixels, + layer}; } else { if (!geom->getCacheL2G().isFilled() || geom->getCacheL2G().getSize() <= sensorID) { - result.error = ClusterDecodeError::GeometryUnavailable; - return result; + throw std::runtime_error("Cluster geometry is not available"); } const auto gloXYZ = geom->getMatrixL2G(sensorID) * clusterData.coordinates; - result.decoded = {{gloXYZ.x(), gloXYZ.y(), gloXYZ.z()}, {}, {sigma2Row, 0.f, sigma2Col}, clusterData.shape, layer}; + result = {{gloXYZ.x(), gloXYZ.y(), gloXYZ.z()}, {}, {sigma2Row, 0.f, sigma2Col}, clusterData.nPixels, layer}; } return result; } +template +void decodeDetectorSource(const o2::itsmft::tracking::ClusterSourceInput& source, const Consume& consume) +{ + using Geometry = std::conditional_t; + Geometry* geometry = nullptr; + if (!source.clusters.empty()) { + geometry = Geometry::Instance(); + geometry->fillMatrixCache(o2::math_utils::bit2Mask(o2::math_utils::TransformType::T2L, o2::math_utils::TransformType::L2G)); + } + consume([&](const auto& cluster, auto& patterns) { + return decodeCluster(geometry, cluster, patterns, source.dictionary, source.applySysErrors); + }); +} + } // namespace -namespace o2::itsmft::ioutils +namespace o2::itsmft::tracking { - -void fillMatrixCache(o2::detectors::DetID::ID detId) +namespace { - 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)); - } +// Project decoded ITS facts into the accepted cylindrical convention. +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}; } -template -o2::itsmft::tracking::ClusterDecodeResult decodeCluster( - const CompClusterExt& cluster, o2::itsmft::tracking::BoundedPatternCursor& patterns, - const TopologyDictionary* dict, bool applySysErrors) +// 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. +GlobalMeasurement makeDiskGlobalMeasurement(const DecodedCluster& decoded, uint32_t clusterId) { - 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); - } + 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}; } -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 +SurfaceMeasurement makeCylinderSurfaceMeasurement(const DecodedCluster& decoded) +{ + return {decoded.cylinderFrame, decoded.rowColumnCovariance}; +} -namespace o2::itsmft::tracking +SurfaceMeasurement makeDiskSurfaceMeasurement(const DecodedCluster& decoded) { + return {{decoded.global.z, decoded.global.x, decoded.global.y, 0.f}, + decoded.rowColumnCovariance}; +} -namespace +bool covariance2DIsPositiveSemidefinite(float cxx, float cxy, float cyy) noexcept { -class FailedTimeFrameLoadGuard + if (cxx < 0.f || cyy < 0.f) { + return false; + } + const double diagonalProduct = static_cast(cxx) * cyy; + const double cxySquared = static_cast(cxy) * cxy; + const double tolerance = 16. * std::numeric_limits::epsilon() * + std::max(diagonalProduct, cxySquared); + return diagonalProduct - cxySquared >= -tolerance; +} + +bool globalCovarianceIsPositiveSemidefinite(const GlobalCovariance3F& covariance) noexcept { - public: - explicit FailedTimeFrameLoadGuard(TimeFrame& frame) noexcept : mFrame{&frame} {} - ~FailedTimeFrameLoadGuard() - { - if (mFrame != nullptr) { - mFrame->resetTimeFrame(); - } + 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; } - void release() noexcept { mFrame = nullptr; } + 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; +} - private: - TimeFrame* mFrame; -}; +bool decodedMeasurementIsValid(const GlobalMeasurement& global, + const SurfaceMeasurement& local) noexcept +{ + return globalCovarianceIsPositiveSemidefinite(global.covariance) & + covariance2DIsPositiveSemidefinite(local.covariance.uu, local.covariance.uv, local.covariance.vv); +} void clearFrameAndSidecars(TimeFrame& frame, std::vector>* externalIndicesBySurface, @@ -159,37 +235,129 @@ void clearFrameAndSidecars(TimeFrame& frame, } } -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) +namespace detail +{ +void prepareSources(TimeFrame& frame, const SurfaceCatalogView& catalog, + gsl::span sources, + std::vector>* externalIndicesBySurface, + std::vector>* clusterSizesBySurface) { clearFrameAndSidecars(frame, externalIndicesBySurface, clusterSizesBySurface); if (!frame.isConfigured()) { - return {MultiSourceLoadError::FrameNotConfigured}; + throw std::runtime_error("TimeFrame is not configured"); } - if (sources.empty()) { - return {MultiSourceLoadError::OtherMalformedInput}; + 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) { + throw std::runtime_error(std::format("Source IDs must be dense source={}", src.id.value())); + } + if (seen[src.id.value()]) { + throw std::runtime_error(std::format("Duplicate source ID source={}", src.id.value())); + } + seen[src.id.value()] = true; + if (src.detector != o2::detectors::DetID::ITS && src.detector != o2::detectors::DetID::MFT) { + throw std::runtime_error(std::format("Unsupported source detector source={}", src.id.value())); + } + if (!src.clusters.empty() && src.dictionary == nullptr) { + throw std::runtime_error(std::format("Cluster dictionary is not available source={} rof={} clusterIndex={}", src.id.value(), 0, 0)); + } + for (const auto surface : src.layerToSurface) { + if (!surface.isValid() || surface.value() >= catalog.nSurfaces) { + throw std::runtime_error(std::format("Invalid source-to-surface layer mapping source={}", src.id.value())); + } + if (sourceBySurface[surface.value()].isValid()) { + throw std::runtime_error(std::format("Invalid source-to-surface layer mapping source={}", src.id.value())); + } + if (catalog.getSurface(surface).detectorId != static_cast(src.detector)) { + throw std::runtime_error(std::format("Source detector does not match its surface source={}", src.id.value())); + } + sourceBySurface[surface.value()] = src.id; + } + } +} +void validateSource(const ClusterSourceInput& src, const o2::InteractionRecord& origin) +{ + 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) { + throw std::runtime_error(std::format("Invalid ROF cluster range source={} rof={}", src.id.value(), r)); + } + expectedNext = first + n; + if (expectedNext > static_cast(src.clusters.size())) { + throw std::runtime_error(std::format("Invalid ROF cluster range source={} rof={}", src.id.value(), r)); + } } - FailedTimeFrameLoadGuard failedLoad{frame}; - std::vector> loadedExternalIndices; - std::vector> loadedClusterSizes; - const auto loadResult = decodeSources(frame, catalog, sources, origin, - &loadedExternalIndices, &loadedClusterSizes); - if (!loadResult.ok()) { - return loadResult; + if (expectedNext != static_cast(src.clusters.size())) { + throw std::runtime_error(std::format("Invalid ROF cluster range source={} rof={}", src.id.value(), 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()) { + throw std::runtime_error(std::format("Invalid ROF timing: source={} rof={} timingError={}", src.id.value(), r, static_cast(built.error))); + } + } +} +void appendCluster(TimeFrame& frame, const SurfaceCatalogView& catalog, + const ClusterSourceInput& src, const DecodedCluster& decoded, + uint32_t r, uint32_t externalIndex, + std::vector>& externalIndices, + std::vector>& clusterSizes) +{ + if (decoded.layer < 0 || static_cast(decoded.layer) >= src.layerToSurface.size()) { + throw std::runtime_error(std::format("Invalid source-to-surface layer mapping source={} rof={} clusterIndex={}", src.id.value(), r, externalIndex)); + } + const auto expectedSurface = src.layerToSurface[decoded.layer]; + if (!expectedSurface.isValid() || expectedSurface.value() >= catalog.nSurfaces) { + throw std::runtime_error(std::format("Invalid source-to-surface layer mapping source={} rof={} clusterIndex={}", src.id.value(), r, externalIndex)); + } + const auto& surfaceDescriptor = catalog.getSurface(expectedSurface); + if (surfaceDescriptor.detectorId != static_cast(src.detector)) { + throw std::runtime_error(std::format("Source detector does not match its surface source={} rof={} clusterIndex={}", src.id.value(), 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)) { + throw std::runtime_error(std::format("Malformed cluster loading input source={} rof={} clusterIndex={}", src.id.value(), 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); + } + clusterSizes[expectedSurface.value()].push_back(decoded.nPixels); + externalIndices[expectedSurface.value()].push_back(externalIndex); +} +void finishTimeFrameLoading(TimeFrame& frame, const SurfaceCatalogView& catalog, + gsl::span sources, + const std::vector>& loadedExternalIndices) +{ + if (sources.empty()) { + throw std::runtime_error("Malformed cluster loading input"); + } const auto& layout = frame.getLayout(); if (layout.empty()) { - return {MultiSourceLoadError::FrameNotConfigured}; + throw std::runtime_error("TimeFrame is not configured"); } std::array configuredSurfaces{}; for (std::size_t position = 0; position < layout.size(); ++position) { @@ -200,10 +368,10 @@ LoadSourcesResult loadTimeFrameSources(TimeFrame& frame, gsl::span= MaxLayoutSurfaces || mappedSurfaces[surface.value()] || !configuredSurfaces[surface.value()]) { - return {MultiSourceLoadError::InvalidLayerMapping, source.id}; + throw std::runtime_error(std::format("Invalid source-to-surface layer mapping source={}", source.id.value())); } if (catalog.getSurface(surface).detectorId != static_cast(source.detector)) { - return {MultiSourceLoadError::DetectorSurfaceMismatch, source.id}; + throw std::runtime_error(std::format("Source detector does not match its surface source={}", source.id.value())); } mappedSurfaces[surface.value()] = true; } @@ -221,13 +389,13 @@ LoadSourcesResult loadTimeFrameSources(TimeFrame& frame, gsl::span(std::distance(source.layerToSurface.begin(), it)); } if (owner == nullptr) { - return {MultiSourceLoadError::InvalidLayerMapping}; + throw std::runtime_error("Invalid source-to-surface layer mapping"); } const auto globals = frame.getGlobalMeasurements(surface); @@ -261,386 +429,78 @@ LoadSourcesResult loadTimeFrameSources(TimeFrame& frame, gsl::span= loadedExternalIndices.size() || clusterId >= loadedExternalIndices[surface.value()].size()) { - return {MultiSourceLoadError::InconsistentDecoderMetadata, owner->id, - static_cast(rof), clusterId}; + throw std::runtime_error(std::format("Decoded cluster metadata is inconsistent with its ROF source={} rof={} clusterIndex={}", owner->id.value(), 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}; + throw std::runtime_error(std::format("Decoded cluster metadata is inconsistent with its ROF source={} rof={} clusterIndex={}", owner->id.value(), static_cast(rof), externalIndex)); } ++measurement; } boundaries[rof + 1] = static_cast(measurement); } if (measurement != globals.size()) { - return {MultiSourceLoadError::InconsistentDecoderMetadata, owner->id}; + throw std::runtime_error(std::format("Decoded cluster metadata is inconsistent with its ROF source={}", owner->id.value())); } 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; } +} // namespace detail -bool globalCovarianceIsPositiveSemidefinite(const GlobalCovariance3F& covariance) noexcept +void loadSources(TimeFrame& frame, const SurfaceCatalogView& catalog, + gsl::span sources, const o2::InteractionRecord& origin, + std::vector>* externalIndicesBySurface, + std::vector>* clusterSizesBySurface) { - 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); + detail::prepareSources(frame, catalog, sources, externalIndicesBySurface, clusterSizesBySurface); + std::vector> externalIndices(catalog.nSurfaces); + std::vector> clusterSizes(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())}; + for (const auto& source : sources) { + detail::validateSource(source, origin); + const auto load = [&](const auto& decode) { + detail::loadDecodedSource(frame, catalog, source, decode, externalIndices, clusterSizes); + }; + if (source.detector == o2::detectors::DetID::ITS) { + decodeDetectorSource(source, load); + } else { + decodeDetectorSource(source, load); } + hasMCInformation |= source.labels != nullptr; } - frame.setHasMCInformation(hasMCInformation); if (externalIndicesBySurface != nullptr) { - *externalIndicesBySurface = std::move(stagedExternalIndices); + *externalIndicesBySurface = std::move(externalIndices); } if (clusterSizesBySurface != nullptr) { - *clusterSizesBySurface = std::move(perSurfaceClusterSizes); + *clusterSizesBySurface = std::move(clusterSizes); } - return {}; } -} // namespace -LoadSourcesResult loadSources(TimeFrame& frame, const SurfaceCatalogView& catalog, - gsl::span sources, - const o2::InteractionRecord& origin, - std::vector>* externalIndicesBySurface, - std::vector>* clusterSizesBySurface) +void 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 (externalIndicesBySurface != nullptr) { + externalIndicesBySurface->clear(); } - FailedTimeFrameLoadGuard failedLoad{frame}; - const auto result = decodeSources(frame, catalog, sources, origin, - externalIndicesBySurface, clusterSizesBySurface); - if (result.ok()) { - failedLoad.release(); + if (clusterSizesBySurface != nullptr) { + clusterSizesBySurface->clear(); + } + std::vector> externalIndices; + std::vector> clusterSizes; + loadSources(frame, catalog, sources, origin, &externalIndices, &clusterSizes); + detail::finishTimeFrameLoading(frame, catalog, sources, externalIndices); + if (externalIndicesBySurface != nullptr) { + *externalIndicesBySurface = std::move(externalIndices); + } + if (clusterSizesBySurface != nullptr) { + *clusterSizesBySurface = std::move(clusterSizes); } - 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/MaterialPhysics.cxx b/Detectors/ITSMFT/common/tracking/src/MaterialPhysics.cxx index 3bda73a519031..bf869dbed79f6 100644 --- a/Detectors/ITSMFT/common/tracking/src/MaterialPhysics.cxx +++ b/Detectors/ITSMFT/common/tracking/src/MaterialPhysics.cxx @@ -67,17 +67,10 @@ bool calculateMaterialPhysics( return false; } const float mass = pid.getMass(); - if (absCharge != 0 && mass == 0.f) { + if (mass == 0.f) { return false; } - if (absCharge == 0) { - momentumAfterGeV = momentumGeV; - outHighlandTheta2Rad2 = 0.f; - outRelativeInverseMomentumVariance = 0.f; - return true; - } - const float q2 = static_cast(absCharge) * static_cast(absCharge); const float p0 = momentumGeV; const float p0Squared = p0 * p0; diff --git a/Detectors/ITSMFT/common/tracking/src/Propagator.cxx b/Detectors/ITSMFT/common/tracking/src/Propagator.cxx index 585e6ab1d2c31..777d77c686d1c 100644 --- a/Detectors/ITSMFT/common/tracking/src/Propagator.cxx +++ b/Detectors/ITSMFT/common/tracking/src/Propagator.cxx @@ -12,8 +12,11 @@ #include "ITSMFTTracking/Propagator.h" #include +#include +#include "ITSMFTTracking/MaterialPhysics.h" #include "ITSMFTTracking/detail/SurfaceStateOperations.h" +#include "ReconstructionDataFormats/PID.h" #include "ReconstructionDataFormats/TrackParametrization.h" namespace o2::itsmft::tracking @@ -91,7 +94,7 @@ bool barrelToForward(SurfaceTrackState& state, float bz) noexcept // 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 curvature = 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}, @@ -143,7 +146,7 @@ bool forwardToBarrel(SurfaceTrackState& state, float bz) noexcept // 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 curvature = 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}, @@ -188,8 +191,141 @@ bool acceptsAttachmentChi2(float predictedChi2, bool gateEnabled, float maxChi2) return true; } +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; +} + +// 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}; + } // namespace +// Work on copies so that any rejection leaves both the fitted state and its +// incidence reference unchanged. +bool Propagator::correctForMaterial(SurfaceTrackState& state, SurfaceTrackParameters& incidenceReference, + material::IntegratedMaterialBudget materialBudget, + material::MaterialTraversalDirection direction) noexcept +{ + if (state.parameters[4] == 0.f || incidenceReference.parameters[4] == 0.f) { + return false; + } + if (state.kind == SurfaceKind::Cylinder) { + if (!(std::abs(state.parameters[2]) < 1.f) || !(std::abs(incidenceReference.parameters[2]) < 1.f)) { + return false; + } + } else if (state.parameters[3] == 0.f || incidenceReference.parameters[3] == 0.f) { + return false; + } + if (state.pid.getID() >= o2::track::PID::NIDsTot) { + return false; + } + if (state.pid.getMass() == 0.f) { + return false; + } + if (!covarianceDiagonalsNonNegative(state)) { + return false; + } + + float momentumBeforeGeV = state.getP(); + SurfaceTrackState scratchState = state; + SurfaceTrackParameters scratchReference = incidenceReference; + // Layer budgets describe normal incidence. Use the reference trajectory to + // scale both radiation length and areal density to the crossed path length. + const float tgl = scratchReference.parameters[3]; + float incidenceScale; + if (state.kind == SurfaceKind::Cylinder) { + const float snp = scratchReference.parameters[2]; + const float cosPhi2 = (1.f - snp) * (1.f + snp); + const float inverseCosLambda2 = 1.f + tgl * tgl; + incidenceScale = std::sqrt(inverseCosLambda2 / cosPhi2); + } else { + incidenceScale = std::sqrt(1.f + tgl * tgl) / std::abs(tgl); + } + materialBudget.xOverX0 *= incidenceScale; + materialBudget.arealDensityGPerCm2 *= incidenceScale; + float momentumAfterGeV = 0.f; + float highlandTheta2Rad2 = 0.f; + float relativeInverseMomentumVariance = 0.f; + if (!material::calculateMaterialPhysics(momentumBeforeGeV, scratchState.pid, scratchState.absCharge, direction, materialBudget, + momentumAfterGeV, highlandTheta2Rad2, relativeInverseMomentumVariance)) { + return false; + } + + // No material must also bypass covariance limiting. + const bool isNoopMaterial = (materialBudget.xOverX0 == 0.f && materialBudget.arealDensityGPerCm2 == 0.f); + if (isNoopMaterial) { + return true; + } + + const float tBefore = scratchState.parameters[3]; + const float kBefore = scratchState.parameters[4]; + const float A = 1.f + tBefore * tBefore; + const float h = highlandTheta2Rad2; + const float R = relativeInverseMomentumVariance; + if (state.kind == SurfaceKind::Cylinder) { + // Barrel slot 2 is sin(phi); disk slot 2 is phi itself. + const float snp = scratchState.parameters[2]; + const float c2 = 1.f - snp * snp; + scratchState.covariance[packedCovarianceIndex(2, 2)] += h * A * c2; + } else { + scratchState.covariance[packedCovarianceIndex(2, 2)] += h * A; + } + scratchState.covariance[packedCovarianceIndex(3, 3)] += h * A * A; + scratchState.covariance[packedCovarianceIndex(4, 3)] += h * A * tBefore * kBefore; + scratchState.covariance[packedCovarianceIndex(4, 4)] += h * (tBefore * kBefore) * (tBefore * kBefore) + kBefore * kBefore * R; + if (state.kind == SurfaceKind::Cylinder) { + sanitizeCovariance(scratchState, kBarrelMaxDiagonal); + } + + // 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 = (momentumBeforeGeV == momentumAfterGeV) + ? kBefore + : (kBefore * momentumBeforeGeV) / momentumAfterGeV; + scratchState.parameters[4] = kAfter; + + // Only covariance and inverse transverse momentum changed; the coordinate + // preconditions checked above still hold. + if (scratchState.parameters[4] == 0.f) { + return false; + } + float momentumAfterDerived = scratchState.getP(); + if (!covarianceDiagonalsNonNegative(scratchState)) { + return false; + } + + // 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] = (momentumBeforeGeV == momentumAfterGeV) + ? referenceKBefore + : (referenceKBefore * momentumBeforeGeV) / momentumAfterGeV; + if (scratchReference.parameters[4] == 0.f || !std::isfinite(scratchReference.parameters[4])) { + return false; + } + + state = scratchState; + incidenceReference = scratchReference; + return true; +} + bool Propagator::attachMeasurement(SurfaceTrackState& state, const SurfaceDescriptor& targetSurface, const SurfaceMeasurement& measurement, float bz, material::MaterialTraversalDirection direction, @@ -213,7 +349,8 @@ bool Propagator::attachMeasurement(SurfaceTrackState& state, const SurfaceDescri !detail::barrel::propagate(scratch, measurement.frame.q, bz)) { return false; } - const auto materialResult = detail::barrel::correctForMaterial(scratch, integratedMaterial, direction); + SurfaceTrackParameters incidenceReference{scratch}; + const auto materialResult = correctForMaterial(scratch, incidenceReference, integratedMaterial, direction); if (!materialResult) { return false; } @@ -230,7 +367,8 @@ bool Propagator::attachMeasurement(SurfaceTrackState& state, const SurfaceDescri if (!propagateToReference(scratch, measurement.frame.q, bz)) { return false; } - const auto materialResult = detail::forward::correctForMaterial(scratch, integratedMaterial, direction); + SurfaceTrackParameters incidenceReference{scratch}; + const auto materialResult = correctForMaterial(scratch, incidenceReference, integratedMaterial, direction); if (!materialResult) { return false; } @@ -380,7 +518,7 @@ bool Propagator::propagateToMeasurement(SurfaceTrackState& state, SurfaceTrackPa return false; } clampNegligibleCovarianceNoise(scratchState); - const auto materialResult = detail::barrel::correctForMaterial(scratchState, scratchRef, materialBudget, direction); + const auto materialResult = correctForMaterial(scratchState, scratchRef, materialBudget, direction); if (!materialResult) { return false; } @@ -392,7 +530,7 @@ bool Propagator::propagateToMeasurement(SurfaceTrackState& state, SurfaceTrackPa return false; } clampNegligibleCovarianceNoise(scratchState); - const auto materialResult = detail::forward::correctForMaterial(scratchState, scratchRef, materialBudget, direction); + const auto materialResult = correctForMaterial(scratchState, scratchRef, materialBudget, direction); if (!materialResult) { return false; } diff --git a/Detectors/ITSMFT/common/tracking/src/PropagatorBarrelOperations.cxx b/Detectors/ITSMFT/common/tracking/src/PropagatorBarrelOperations.cxx index 2ab1e3480a9b4..5f54e6fde7659 100644 --- a/Detectors/ITSMFT/common/tracking/src/PropagatorBarrelOperations.cxx +++ b/Detectors/ITSMFT/common/tracking/src/PropagatorBarrelOperations.cxx @@ -38,7 +38,7 @@ static_assert(o2::math_utils::MatRepSym::kSize == 15, "packed symmetri 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. +// match the barrel limits used by Propagator::correctForMaterial(). constexpr float kBarrelMaxDiagonal[5] = {o2::track::kCY2max, o2::track::kCZ2max, o2::track::kCSnp2max, o2::track::kCTgl2max, o2::track::kC1Pt2max}; @@ -177,7 +177,7 @@ bool propagate(SurfaceTrackState& state, float targetX, float bz) noexcept 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 curvature = 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) { return false; @@ -355,10 +355,8 @@ 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) noexcept +// TrackParametrization::propagateParamTo formula for charged particles. +bool propagateReferenceParams(SurfaceTrackParameters& ref, float targetX, float bz) noexcept { const float dx = targetX - ref.referenceCoordinate; if (dx == 0.f) { @@ -366,7 +364,7 @@ bool propagateReferenceParams(SurfaceTrackParameters& ref, uint8_t stateAbsCharg 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 curvature = 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) { return false; @@ -455,7 +453,7 @@ bool rotate(SurfaceTrackState& state, SurfaceTrackParameters& linRef, float targ // 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)) { + if (!propagateReferenceParams(scratchRef, trackX, bz)) { return false; } @@ -505,7 +503,7 @@ bool rotate(SurfaceTrackState& state, SurfaceTrackParameters& linRef, float targ 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 j5 = scratchRef.parameters[4] * bz * o2::constants::math::B2C; const float hXSigY = cXSigY + cSigX2 * j3; const float hXSigZ = cXSigZ + cSigX2 * j4; @@ -548,7 +546,6 @@ bool propagate(SurfaceTrackState& state, SurfaceTrackParameters& linRef, float t 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; @@ -565,7 +562,7 @@ bool propagate(SurfaceTrackState& state, SurfaceTrackParameters& linRef, float t const float cspRef0 = std::sqrt((1.f - snpRef0) * (1.f + snpRef0)); const float tglRef0 = scratchRef.parameters[3]; - if (!propagateReferenceParams(scratchRef, state.absCharge, targetX, effectiveBz)) { + if (!propagateReferenceParams(scratchRef, targetX, bz)) { return false; } const float snpRef1 = scratchRef.parameters[2]; @@ -574,7 +571,7 @@ bool propagate(SurfaceTrackState& state, SurfaceTrackParameters& linRef, float t return false; } - const float kb = effectiveBz * o2::constants::math::B2C; + const float kb = bz * o2::constants::math::B2C; const float cspRef0Inv = 1.f / cspRef0; const float cspRef1Inv = 1.f / cspRef1; const float cc = cspRef0 + cspRef1; diff --git a/Detectors/ITSMFT/common/tracking/src/PropagatorForwardOperations.cxx b/Detectors/ITSMFT/common/tracking/src/PropagatorForwardOperations.cxx index 3a0d62dee91f3..b29c30238ed25 100644 --- a/Detectors/ITSMFT/common/tracking/src/PropagatorForwardOperations.cxx +++ b/Detectors/ITSMFT/common/tracking/src/PropagatorForwardOperations.cxx @@ -332,31 +332,6 @@ bool update(SurfaceTrackState& state, const SurfaceMeasurement& measurement, flo return true; } -bool correctForMaterial(SurfaceTrackState& state, float xOverX0) noexcept -{ - if (!validateSource(state)) { - return false; - } - if (xOverX0 == 0.f) { - return true; - } - const float tanl = state.parameters[3]; - if (tanl == 0.f) { - 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) noexcept { if (reference.kind != SurfaceKind::Disk || candidate.kind != SurfaceKind::Disk) { diff --git a/Detectors/ITSMFT/common/tracking/test/CMakeLists.txt b/Detectors/ITSMFT/common/tracking/test/CMakeLists.txt index b7f92fc87249e..253e26241eb9e 100644 --- a/Detectors/ITSMFT/common/tracking/test/CMakeLists.txt +++ b/Detectors/ITSMFT/common/tracking/test/CMakeLists.txt @@ -50,7 +50,6 @@ 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) diff --git a/Detectors/ITSMFT/common/tracking/test/CombinedTrackingTestSupport.h b/Detectors/ITSMFT/common/tracking/test/CombinedTrackingTestSupport.h index 4576a11f3c330..d0b66bf3a36bf 100644 --- a/Detectors/ITSMFT/common/tracking/test/CombinedTrackingTestSupport.h +++ b/Detectors/ITSMFT/common/tracking/test/CombinedTrackingTestSupport.h @@ -168,33 +168,18 @@ class CombinedTrackingPlan mLastResult.reset(); } - std::optional validateSources(const ClusterSourceInput& itsSource, - const ClusterSourceInput& mftSource) const noexcept + void validateSources(const ClusterSourceInput& itsSource, + const ClusterSourceInput& mftSource) const { if (itsSource.id != ClusterSourceId{0} || itsSource.detector != o2::detectors::DetID::ITS) { - return LoadSourcesResult{MultiSourceLoadError::UnsupportedDetector, itsSource.id}; + throw std::runtime_error("Invalid ITS source"); } if (mftSource.id != ClusterSourceId{1} || mftSource.detector != o2::detectors::DetID::MFT) { - return LoadSourcesResult{MultiSourceLoadError::UnsupportedDetector, mftSource.id}; + throw std::runtime_error("Invalid MFT source"); } - 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) { diff --git a/Detectors/ITSMFT/common/tracking/test/TrackingParameterTestSupport.h b/Detectors/ITSMFT/common/tracking/test/TrackingParameterTestSupport.h index 33163d5c15887..f30ba3efe288e 100644 --- a/Detectors/ITSMFT/common/tracking/test/TrackingParameterTestSupport.h +++ b/Detectors/ITSMFT/common/tracking/test/TrackingParameterTestSupport.h @@ -12,6 +12,9 @@ #ifndef ALICEO2_ITSMFT_TRACKING_PARAMETER_TEST_SUPPORT_H_ #define ALICEO2_ITSMFT_TRACKING_PARAMETER_TEST_SUPPORT_H_ #include "ITSMFTTracking/Configuration.h" +#include "ITSMFTTracking/IOUtils.h" +#include "ITSMFTTracking/TimeFrame.h" +#include #include "ITSMFTTracking/ITSMFTDetectorDefinitions.h" namespace o2::itsmft::tracking::test @@ -74,5 +77,107 @@ inline std::vector referenceTrackingParameters(o2::detectors { return expandTrackingPlan(TrackingMode::getTrackingPlan(detector, mode)); } +// Synthetic decoding is confined to tests. Exercise the same normalization +// and ROF bookkeeping as production without constructing detector geometry. +struct TestClusterSourceInput : ClusterSourceInput { + std::function::iterator&, + const itsmft::TopologyDictionary*, uint32_t, bool)> + decode; + + template + void setDecoder(const Decoder& decoder) + { + decode = [&decoder](const auto& cluster, auto& patterns, const auto* dictionary, uint32_t index, bool sysErrors) { + return decoder.decode(cluster, patterns, dictionary, index, sysErrors); + }; + } +}; + +inline void loadSources(TimeFrame& frame, const SurfaceCatalogView& catalog, + gsl::span sources, const o2::InteractionRecord& origin, + std::vector>* indices = nullptr, + std::vector>* sizes = nullptr) +{ + const std::vector inputs(sources.begin(), sources.end()); + detail::prepareSources(frame, catalog, inputs, indices, sizes); + std::vector> externalIndices(catalog.nSurfaces); + std::vector> clusterSizes(catalog.nSurfaces); + bool hasMCInformation = false; + for (const auto& source : sources) { + detail::validateSource(source, origin); + detail::loadDecodedSource(frame, catalog, source, [&](const auto& cluster, auto& patterns) { + const auto index = static_cast(&cluster - source.clusters.data()); + return source.decode(cluster, patterns, source.dictionary, index, source.applySysErrors); }, externalIndices, clusterSizes); + hasMCInformation |= source.labels != nullptr; + } + frame.setHasMCInformation(hasMCInformation); + if (indices != nullptr) { + *indices = std::move(externalIndices); + } + if (sizes != nullptr) { + *sizes = std::move(clusterSizes); + } +} + +inline void loadTimeFrameSources(TimeFrame& frame, gsl::span sources, + SurfaceCatalogView catalog, const o2::InteractionRecord& origin, + std::vector>* indices = nullptr, + std::vector>* sizes = nullptr) +{ + if (indices != nullptr) { + indices->clear(); + } + if (sizes != nullptr) { + sizes->clear(); + } + std::vector> externalIndices; + std::vector> clusterSizes; + loadSources(frame, catalog, sources, origin, &externalIndices, &clusterSizes); + const std::vector inputs(sources.begin(), sources.end()); + detail::finishTimeFrameLoading(frame, catalog, inputs, externalIndices); + if (indices != nullptr) { + *indices = std::move(externalIndices); + } + if (sizes != nullptr) { + *sizes = std::move(clusterSizes); + } +} + +template +void loadTimeFrameSource( + TimeFrame& frame, + const Decoder& 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 = true, + std::vector>* externalIndicesBySurface = nullptr, + std::vector>* clusterSizesBySurface = nullptr) +{ + constexpr ClusterSourceId sourceId{0}; + TestClusterSourceInput 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.setDecoder(decoder); + source.applySysErrors = applySysErrors; + source.rofViews = frame.getROFViews(); + loadTimeFrameSources(frame, gsl::span{&source, 1}, catalog, origin, + externalIndicesBySurface, clusterSizesBySurface); +} + } // namespace o2::itsmft::tracking::test #endif diff --git a/Detectors/ITSMFT/common/tracking/test/testCellFinding.cxx b/Detectors/ITSMFT/common/tracking/test/testCellFinding.cxx index 397e8a0cbba83..535e634ac4ca2 100644 --- a/Detectors/ITSMFT/common/tracking/test/testCellFinding.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testCellFinding.cxx @@ -278,29 +278,6 @@ BOOST_AUTO_TEST_CASE(AttachHitBarrelNegativeChi2IsRejectedMatchingLegacyInclusiv 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; - - TrackingKernelParameters permissive; - permissive.maxChi2ClusterAttachment = 1.e6f; - BOOST_REQUIRE(attachMeasurement(neutral, hit, material, BarrelAttachBz, neutralChi2, permissive)); - BOOST_REQUIRE(attachMeasurement(charged, hit, material, BarrelAttachBz, chargedChi2, permissive)); - BOOST_CHECK(!bitEqual(neutral, charged)); -} - BOOST_AUTO_TEST_CASE(AttachHitBarrelIsByteDeterministic) { auto first = barrelAttachState(); @@ -380,25 +357,6 @@ BOOST_AUTO_TEST_CASE(AttachHitDiskEachFailureStagePreservesStateTransactionally) } } -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; - - TrackingKernelParameters permissive; - permissive.maxChi2ClusterAttachment = 1.e6f; - BOOST_REQUIRE(attachMeasurement(neutral, hit, material, DiskAttachBz, neutralChi2, permissive)); - BOOST_REQUIRE(attachMeasurement(charged, hit, material, DiskAttachBz, chargedChi2, permissive)); - BOOST_CHECK(!bitEqual(neutral, charged)); -} - BOOST_AUTO_TEST_CASE(AttachHitDiskActivatesEnergyLossUnlikeLegacyMcsOnlyPath) { auto noLossMaterial = diskAttachMaterial(); diff --git a/Detectors/ITSMFT/common/tracking/test/testCombinedTrackingComposition.cxx b/Detectors/ITSMFT/common/tracking/test/testCombinedTrackingComposition.cxx index 0e951d1c57803..f65a870049941 100644 --- a/Detectors/ITSMFT/common/tracking/test/testCombinedTrackingComposition.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testCombinedTrackingComposition.cxx @@ -40,10 +40,8 @@ #include "ITSMFTTracking/Tracker.h" #include "ITSMFTTracking/Configuration.h" #include "ITSMFTTracking/detail/TimeFrameScratch.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" @@ -93,7 +91,7 @@ std::vector ordered(uint16_t first, uint16_t count) return result; } -class PrescribedDecoder final : public ClusterDecoder +class PrescribedDecoder { public: PrescribedDecoder(o2::detectors::DetID::ID detector, SurfaceKind kind, std::vector clusters) @@ -101,27 +99,21 @@ class PrescribedDecoder final : public ClusterDecoder { } - o2::itsmft::tracking::ClusterDecodeResult decode( + o2::itsmft::tracking::DecodedCluster decode( const CompClusterExt& cluster, - BoundedPatternCursor& patterns, + gsl::span::iterator& patterns, const TopologyDictionary* dictionary, uint32_t externalIndex, - bool) const final + bool) const { - 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; + const auto clusterData = o2::itsmft::ioutils::extractClusterData(cluster, patterns, dictionary); + o2::itsmft::tracking::DecodedCluster result; if (externalIndex >= mClusters.size()) { return result; } auto decoded = mClusters[externalIndex]; - decoded.shape = clusterData.shape; - result.decoded = decoded; + decoded.nPixels = clusterData.nPixels; + result = decoded; return result; } @@ -214,13 +206,13 @@ TrackingParameters makeMftParams() } /// Encodes `decoded` as compact/pattern input and returns a -/// ClusterSourceInput referencing `decoder`/`compactOut`/`patternsOut`/ +/// test::TestClusterSourceInput 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) +test::TestClusterSourceInput 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()); @@ -230,7 +222,7 @@ ClusterSourceInput makeSource(ClusterSourceId id, o2::detectors::DetID::ID det, } rofsOut = {ROFRecord{{100, 5}, 0, 0, static_cast(compactOut.size())}}; - ClusterSourceInput source{}; + test::TestClusterSourceInput source{}; source.id = id; source.detector = det; source.clusters = compactOut; @@ -239,23 +231,23 @@ ClusterSourceInput makeSource(ClusterSourceId id, o2::detectors::DetID::ID det, source.dictionary = &dict(); source.layerToSurface = surfaces; source.timing = ROFTimingConfig{40, 0, 0, 0}; - source.decoder = &decoder; + source.setDecoder(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) +test::TestClusterSourceInput makeEmptySource(ClusterSourceId id, o2::detectors::DetID::ID det, const std::vector& surfaces, + const PrescribedDecoder& decoder) { - ClusterSourceInput source{}; + test::TestClusterSourceInput source{}; source.id = id; source.detector = det; source.dictionary = &dict(); source.layerToSurface = surfaces; source.timing = ROFTimingConfig{40, 0, 0, 0}; - source.decoder = &decoder; + source.setDecoder(decoder); return source; } @@ -315,11 +307,10 @@ struct StandaloneRun { 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()); + test::loadTimeFrameSource(frame, decoder, o2::InteractionRecord{50, 5}, ROFTimingConfig{rofLength, 0, 0, 0}, + compact, patterns, rofs, &dict(), nullptr, det, + gsl::span{layerMapping}, + frame.getLayout().getSurfaceCatalog()); o2::its::LayerTiming layerTiming{}; layerTiming.mNROFsTF = 1; @@ -349,7 +340,7 @@ struct StandaloneRun { /// 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 +/// plus Tracker + test::loadTimeFrameSources() together the same way the /// DPL task does, without a DPL ProcessingContext. struct CombinedTrackingComposer { struct Result { @@ -410,7 +401,7 @@ struct CombinedTrackingComposer { plan.getMFTLayerMapping()}; } - Result process(const ClusterSourceInput& itsSource, const ClusterSourceInput& mftSource, const o2::InteractionRecord& origin) + Result process(const test::TestClusterSourceInput& itsSource, const test::TestClusterSourceInput& mftSource, const o2::InteractionRecord& origin) { invalidatePublication(); clearPublicationSidecars(); @@ -420,24 +411,15 @@ struct CombinedTrackingComposer { 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; + try { + plan.validateSources(itsSource, mftSource); + const std::array sources{itsInput, mftInput}; + test::loadTimeFrameSources(*frame, gsl::span{sources}, plan.catalogView(), origin); + } catch (const std::runtime_error&) { plan.clearPublicationSidecars(); frame->resetTimeFrame(); invalidatePublication(); - return {outcome, 0, 0}; + return {TrackingOutcome::Structural, 0, 0}; } try { @@ -676,7 +658,7 @@ BOOST_AUTO_TEST_CASE(LoadFailureResetsWholeCombinedTFExactlyOnceAndInvalidatesPu BOOST_REQUIRE(composer.getMFTPublicationExport().has_value()); // Malformed MFT ROF partition (a gap before the second cluster): a - // structural load failure loadTimeFrameSources() must + // structural load failure test::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; @@ -750,8 +732,8 @@ struct MinimalFixture { std::vector itsCompact, mftCompact; std::vector itsPatterns, mftPatterns; std::vector itsRofs, mftRofs; - ClusterSourceInput itsSource; - ClusterSourceInput mftSource; + test::TestClusterSourceInput itsSource; + test::TestClusterSourceInput mftSource; MinimalFixture() { @@ -761,10 +743,8 @@ struct MinimalFixture { }; /// 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. +/// source, reproducing an unrecoverable ROF range error +/// without touching the other detector's valid source. void makeRofGap(std::vector& rofs) { rofs = {ROFRecord{{100, 5}, 0, 0, 1}, ROFRecord{{140, 5}, 0, 2, 1}}; @@ -772,7 +752,7 @@ void makeRofGap(std::vector& rofs) } // namespace -BOOST_AUTO_TEST_CASE(RecoverableITSLoadFailureIsDroppedOnlyWhenITSDropTFAllows) +BOOST_AUTO_TEST_CASE(MalformedITSInputIsAlwaysStructural) { ensureTrivialMagneticFieldIsSet(); @@ -790,7 +770,7 @@ BOOST_AUTO_TEST_CASE(RecoverableITSLoadFailureIsDroppedOnlyWhenITSDropTFAllows) composer.setNThreads(1); const auto result = composer.process(fixture.itsSource, fixture.mftSource, o2::InteractionRecord{50, 5}); - const auto expected = itsDropTF ? TrackingOutcome::RecoverableDropped : TrackingOutcome::Structural; + const auto expected = 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 @@ -803,7 +783,7 @@ BOOST_AUTO_TEST_CASE(RecoverableITSLoadFailureIsDroppedOnlyWhenITSDropTFAllows) } } -BOOST_AUTO_TEST_CASE(RecoverableMFTLoadFailureUsesSharedCombinedDropPolicy) +BOOST_AUTO_TEST_CASE(MalformedMFTInputIsAlwaysStructural) { ensureTrivialMagneticFieldIsSet(); @@ -823,7 +803,7 @@ BOOST_AUTO_TEST_CASE(RecoverableMFTLoadFailureUsesSharedCombinedDropPolicy) composer.setNThreads(1); const auto result = composer.process(fixture.itsSource, fixture.mftSource, o2::InteractionRecord{50, 5}); - const auto expected = combinedDropTF ? TrackingOutcome::RecoverableDropped : TrackingOutcome::Structural; + const auto expected = 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); @@ -837,9 +817,7 @@ 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. + // A missing dictionary must not turn into a dropped TF. MinimalFixture fixture; fixture.itsSource.dictionary = nullptr; @@ -861,13 +839,7 @@ 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. + // Unknown source IDs are rejected even when DropTFUponFailure is enabled. MinimalFixture fixture; fixture.itsSource.id = ClusterSourceId{5}; diff --git a/Detectors/ITSMFT/common/tracking/test/testComputeLayerCellsOrchestration.cxx b/Detectors/ITSMFT/common/tracking/test/testComputeLayerCellsOrchestration.cxx index c762b34ba583e..0108f4c907fcb 100644 --- a/Detectors/ITSMFT/common/tracking/test/testComputeLayerCellsOrchestration.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testComputeLayerCellsOrchestration.cxx @@ -49,8 +49,6 @@ #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" @@ -78,14 +76,14 @@ constexpr float Bz = 0.5f; // 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 +class NeverDecodedDecoder { 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 + o2::itsmft::tracking::DecodedCluster decode( + const CompClusterExt&, gsl::span::iterator&, const TopologyDictionary*, + uint32_t, bool) const { return {}; } @@ -103,7 +101,7 @@ class NeverDecodedDecoder final : public ClusterDecoder // 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 +class FixedMeasurementDecoder { public: struct MeasurementPair { @@ -114,19 +112,19 @@ class FixedMeasurementDecoder final : public ClusterDecoder void setMeasurement(int layer, const MeasurementPair& measurement) { mByLayer[layer] = measurement; } - o2::itsmft::tracking::ClusterDecodeResult decode( + o2::itsmft::tracking::DecodedCluster decode( const CompClusterExt& cluster, - BoundedPatternCursor&, + gsl::span::iterator&, const TopologyDictionary*, uint32_t, - bool) const override + bool) const { - o2::itsmft::tracking::ClusterDecodeResult result; + o2::itsmft::tracking::DecodedCluster 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; + result = it->second.decoded; + result.layer = layer; return result; } @@ -365,9 +363,8 @@ struct Rig : RigFrameStorage { 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()); + BOOST_REQUIRE_NO_THROW(test::loadTimeFrameSource(frame, decoder, origin, timing, noClusters, noPatterns, noRofs, &dict(), nullptr, mDet, + gsl::span{orderedSurfaces}, layout.getSurfaceCatalog())); } o2::detectors::DetID::ID detector() const noexcept { return mDet; } @@ -435,9 +432,8 @@ void loadCandidateClusters(Rig& rig, 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()); + BOOST_REQUIRE_NO_THROW(test::loadTimeFrameSource(rig.frame, decoder, origin, timing, compClusters, noPatterns, rofs, &dict(), nullptr, rig.detector(), + gsl::span{layerMapping}, rig.frame.getLayout().getSurfaceCatalog())); } // Finds the cellIndex whose two edges span exactly @@ -531,9 +527,8 @@ void loadCandidateClustersAtLayers(Rig& rig, 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()); + BOOST_REQUIRE_NO_THROW(test::loadTimeFrameSource(rig.frame, decoder, origin, timing, compClusters, noPatterns, rofs, &dict(), nullptr, rig.detector(), + gsl::span{layerMapping}, rig.frame.getLayout().getSurfaceCatalog())); } // Finds the edgeId spanning exactly from->to, mirroring diff --git a/Detectors/ITSMFT/common/tracking/test/testComputeLayerTrackletsOrchestration.cxx b/Detectors/ITSMFT/common/tracking/test/testComputeLayerTrackletsOrchestration.cxx index 24bf0b2c7f536..a6a8d3494d479 100644 --- a/Detectors/ITSMFT/common/tracking/test/testComputeLayerTrackletsOrchestration.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testComputeLayerTrackletsOrchestration.cxx @@ -32,11 +32,8 @@ #include "DataFormatsITSMFT/TopologyDictionary.h" #include "DetectorsCommonDataFormats/DetID.h" #include "ITSMFTTracking/Configuration.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" @@ -97,7 +94,7 @@ std::vector makeCatalog(uint16_t nLayers, o2::detectors::DetI return surfaces; } -class PrescribedDecoder final : public ClusterDecoder +class PrescribedDecoder { public: PrescribedDecoder(o2::detectors::DetID::ID detector, SurfaceKind kind, std::vector clusters) @@ -105,27 +102,21 @@ class PrescribedDecoder final : public ClusterDecoder { } - o2::itsmft::tracking::ClusterDecodeResult decode( + o2::itsmft::tracking::DecodedCluster decode( const CompClusterExt& cluster, - BoundedPatternCursor& patterns, + gsl::span::iterator& patterns, const TopologyDictionary* dictionary, uint32_t externalIndex, - bool) const final + bool) const { - 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; + const auto clusterData = o2::itsmft::ioutils::extractClusterData(cluster, patterns, dictionary); + o2::itsmft::tracking::DecodedCluster result; if (externalIndex >= mClusters.size()) { return result; } auto decoded = mClusters[externalIndex]; - decoded.shape = clusterData.shape; - result.decoded = decoded; + decoded.nPixels = clusterData.nPixels; + result = decoded; return result; } @@ -201,10 +192,9 @@ TrackletSnapshot runFixture(o2::detectors::DetID::ID detector, } 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()); + BOOST_REQUIRE_NO_THROW(test::loadTimeFrameSource(frame, decoder, o2::InteractionRecord{50, 5}, ROFTimingConfig{40, 0, 0, 0}, + compactClusters, patterns, rofs, &dict(), nullptr, detector, + gsl::span{orderedSurfaces}, layout.getSurfaceCatalog())); o2::its::LayerTiming layerTiming{}; layerTiming.mNROFsTF = 1; @@ -474,10 +464,9 @@ BOOST_AUTO_TEST_CASE(PerTimeFrameValidationFailureLeavesEdgeArraysZeroFilledNotP } 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()); + BOOST_REQUIRE_NO_THROW(test::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())); auto layer0 = frame.getGlobalMeasurements(LayerId{0}); BOOST_REQUIRE_EQUAL(layer0.size(), 2u); layer0[1].clusterId = layer0[0].clusterId; diff --git a/Detectors/ITSMFT/common/tracking/test/testCovarianceSanitization.cxx b/Detectors/ITSMFT/common/tracking/test/testCovarianceSanitization.cxx index bdaeef95b747c..a8373a09311f0 100644 --- a/Detectors/ITSMFT/common/tracking/test/testCovarianceSanitization.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testCovarianceSanitization.cxx @@ -581,59 +581,7 @@ BOOST_AUTO_TEST_CASE(ForwardUpdateSanitizesReproducer) 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); -} - -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); -} - -// --- 7. Operation failure remains transactional: a failing rotate/propagate +// --- 6. 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. diff --git a/Detectors/ITSMFT/common/tracking/test/testGenericTrack.cxx b/Detectors/ITSMFT/common/tracking/test/testGenericTrack.cxx index 7d0796f8f1f14..e9d6d4ae010e0 100644 --- a/Detectors/ITSMFT/common/tracking/test/testGenericTrack.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testGenericTrack.cxx @@ -50,8 +50,7 @@ #include "DetectorsCommonDataFormats/DetID.h" #include "ITSMFTTracking/GenericTrack.h" #include "ITSMFTTracking/DetectorLayout.h" -#include "ITSMFTTracking/IOUtils.h" -#include "ITSMFTTracking/ClusterDecoding.h" +#include "TrackingParameterTestSupport.h" #include "ITSMFTTracking/detail/TimeFrameScratch.h" #include "ITSMFTTracking/TrackPublicationHelpers.h" #include "ITSMFTTracking/TimeFrame.h" @@ -202,32 +201,26 @@ 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 +class FakeClusterDecoder { public: FakeClusterDecoder(o2::detectors::DetID::ID detector, bool disk) : mDetector(detector), mDisk(disk) {} - o2::itsmft::tracking::ClusterDecodeResult decode( + o2::itsmft::tracking::DecodedCluster decode( const CompClusterExt& cluster, - BoundedPatternCursor& patterns, + gsl::span::iterator& patterns, const TopologyDictionary* dict, uint32_t, - bool applySysErrors) const override + bool applySysErrors) const { - 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 clusterData = o2::itsmft::ioutils::extractClusterData(cluster, patterns, dict); + o2::itsmft::tracking::DecodedCluster result; const int sensorID = cluster.getSensorID(); - auto& decoded = result.decoded; + auto& decoded = result; 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.nPixels = clusterData.nPixels; decoded.layer = sensorID; return result; } @@ -312,7 +305,7 @@ void loadThreeMeasurementFrame(TimeFrame& frame, const BuiltLayout& layout, const std::array mftLayerToSurface{LayerId{3}}; static const FakeClusterDecoder mftDecoder{o2::detectors::DetID::MFT, true}; - std::array sources{}; + std::array sources{}; sources[0].id = ClusterSourceId{0}; sources[0].detector = o2::detectors::DetID::ITS; sources[0].clusters = itsClusters; @@ -321,7 +314,7 @@ void loadThreeMeasurementFrame(TimeFrame& frame, const BuiltLayout& layout, sources[0].dictionary = &dict(); sources[0].layerToSurface = itsLayerToSurface; sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; - sources[0].decoder = &itsDecoder; + sources[0].setDecoder(itsDecoder); sources[1].id = ClusterSourceId{1}; sources[1].detector = o2::detectors::DetID::MFT; @@ -331,11 +324,10 @@ void loadThreeMeasurementFrame(TimeFrame& frame, const BuiltLayout& layout, sources[1].dictionary = &dict(); sources[1].layerToSurface = mftLayerToSurface; sources[1].timing = ROFTimingConfig{50, 0, 0, 0}; - sources[1].decoder = &mftDecoder; + sources[1].setDecoder(mftDecoder); - BOOST_REQUIRE(loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}, - externalIndicesBySurface, clusterSizesBySurface) - .ok()); + BOOST_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}, + externalIndicesBySurface, clusterSizesBySurface)); } } // namespace @@ -474,35 +466,29 @@ BOOST_AUTO_TEST_CASE(HitSurfacesEqualsUnionAndEachMeasurementSurfaceMatchesItsRe namespace { -// Deterministic, geometry-free stand-in for GeometryClusterDecoder +// Deterministic, geometry-free stand-in for detector geometry decoding // (same construction as testTimeFrameLifecycle.cxx): sensorID is used // directly as the detector-local layer. -class LegacyLikeDecoder final : public ClusterDecoder +class LegacyLikeDecoder { public: explicit LegacyLikeDecoder(o2::detectors::DetID::ID detector) : mDetector(detector) {} - o2::itsmft::tracking::ClusterDecodeResult decode( + o2::itsmft::tracking::DecodedCluster decode( const CompClusterExt& cluster, - BoundedPatternCursor& patterns, + gsl::span::iterator& patterns, const TopologyDictionary* dict, uint32_t, - bool applySysErrors) const override + bool applySysErrors) const { - 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 clusterData = o2::itsmft::ioutils::extractClusterData(cluster, patterns, dict); + o2::itsmft::tracking::DecodedCluster result; const int sensorID = cluster.getSensorID(); - auto& decoded = result.decoded; + auto& decoded = result; 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.nPixels = clusterData.nPixels; decoded.layer = sensorID; return result; } @@ -551,14 +537,14 @@ struct TimeFrameFixture { } // One cluster on layer 0, one ROF: the minimal input that succeeds. - LoadSourcesResult load() + void 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); + test::loadTimeFrameSource(tf, decoder, origin, timing, clusters, patterns, rofs, &dict(), nullptr, o2::detectors::DetID::ITS, + gsl::span{layerMapping}, tf.getLayout().getSurfaceCatalog(), true, + &externalIndicesBySurface, &clusterSizesBySurface); } }; @@ -608,7 +594,7 @@ void populateCommonResults(TimeFrame& tf) BOOST_AUTO_TEST_CASE(SuccessfulReloadClearsCommonTrackResults) { TimeFrameFixture fixture; - BOOST_REQUIRE(fixture.load().ok()); + BOOST_REQUIRE_NO_THROW(fixture.load()); populateCommonResults(fixture.tf); BOOST_REQUIRE_EQUAL(fixture.tf.getGenericTracks().size(), 1u); @@ -618,7 +604,7 @@ BOOST_AUTO_TEST_CASE(SuccessfulReloadClearsCommonTrackResults) // 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_REQUIRE_NO_THROW(fixture.load()); BOOST_CHECK(fixture.tf.getGenericTracks().empty()); BOOST_CHECK(fixture.tf.getTrackLabels().empty()); BOOST_CHECK(fixture.tf.getTrackClusterIndices().empty()); @@ -627,7 +613,7 @@ BOOST_AUTO_TEST_CASE(SuccessfulReloadClearsCommonTrackResults) BOOST_AUTO_TEST_CASE(FailedLoadClearsCommonTrackResults) { TimeFrameFixture fixture; - BOOST_REQUIRE(fixture.load().ok()); + BOOST_REQUIRE_NO_THROW(fixture.load()); populateCommonResults(fixture.tf); BOOST_REQUIRE_EQUAL(fixture.tf.getGenericTracks().size(), 1u); @@ -641,11 +627,10 @@ BOOST_AUTO_TEST_CASE(FailedLoadClearsCommonTrackResults) 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_EXCEPTION(test::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()), + std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Unsupported source detector") != std::string::npos; }); BOOST_CHECK_EQUAL(fixture.tf.getTotalMeasurements(), 0u); BOOST_CHECK(fixture.tf.getGenericTracks().empty()); @@ -696,7 +681,7 @@ BOOST_AUTO_TEST_CASE(TrackPublicationTimestampIsSymmetricAndClamped) BOOST_AUTO_TEST_CASE(TrackPublicationUsesLegacyPublicationOrder) { TimeFrameFixture fixture; - BOOST_REQUIRE(fixture.load().ok()); + BOOST_REQUIRE_NO_THROW(fixture.load()); auto later = makeTestGenericTrack(); later.track.timestamp = {200, 240}; @@ -751,7 +736,7 @@ BOOST_AUTO_TEST_CASE(ClockTimingPublicationViewDelegatesLegacyClockSemantics) BOOST_AUTO_TEST_CASE(TrackPublicationSelectionRejectsMalformedReferences) { TimeFrameFixture fixture; - BOOST_REQUIRE(fixture.load().ok()); + BOOST_REQUIRE_NO_THROW(fixture.load()); const auto record = makeTestGenericTrack(); storeTestGenericTrack(fixture.tf, record); const auto surfaces = gsl::span{fixture.layerMapping}; diff --git a/Detectors/ITSMFT/common/tracking/test/testMaterialPhysics.cxx b/Detectors/ITSMFT/common/tracking/test/testMaterialPhysics.cxx index bdd1943851555..ff4bc3d83f9b3 100644 --- a/Detectors/ITSMFT/common/tracking/test/testMaterialPhysics.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testMaterialPhysics.cxx @@ -126,21 +126,6 @@ float energyChange(float before, float after, PID pid) } // namespace -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)); - - float resultMomentum = 0.f; - float resultTheta2 = 0.f; - float resultVariance = 0.f; - const bool result = calculateMaterialPhysics(1.f, pid, 0, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance); - BOOST_CHECK_MESSAGE(result, "PID id " << static_cast(id) << " failed"); - BOOST_CHECK_EQUAL(resultMomentum, 1.f); - } -} - BOOST_AUTO_TEST_CASE(EveryValidMassivePidIdChargedSucceeds) { IntegratedMaterialBudget material{0.01f, 0.05f}; @@ -164,15 +149,6 @@ BOOST_AUTO_TEST_CASE(InvalidPidIdsRejectedBeforeMassLookup) for (uint8_t id : {static_cast(PID::NIDsTot), static_cast(255)}) { PID pid(static_cast(id)); - float neutralMomentum = 0.f; - float neutralTheta2 = 0.f; - float neutralVariance = 0.f; - const bool neutral = calculateMaterialPhysics(1.f, pid, 0, MaterialTraversalDirection::AlongMomentum, material, neutralMomentum, neutralTheta2, neutralVariance); - BOOST_CHECK(!neutral); - BOOST_CHECK_EQUAL(neutralMomentum, 0.f); - BOOST_CHECK_EQUAL(neutralTheta2, 0.f); - BOOST_CHECK_EQUAL(neutralVariance, 0.f); - float chargedMomentum = 0.f; float chargedTheta2 = 0.f; float chargedVariance = 0.f; @@ -205,23 +181,6 @@ BOOST_AUTO_TEST_CASE(PidAndChargeAreIndependent) BOOST_CHECK(closeTo(q2resultTheta2, 4.f * q1Theta2)); } -BOOST_AUTO_TEST_CASE(NeutralMassiveAndMasslessAccepted) -{ - IntegratedMaterialBudget material{0.2f, 5.f}; - for (PID pid : {PID(PID::K0), PID(PID::Photon)}) { - - float resultMomentum = 0.f; - float resultTheta2 = 0.f; - float resultVariance = 0.f; - const bool result = calculateMaterialPhysics(3.f, pid, 0, MaterialTraversalDirection::OppositeMomentum, material, resultMomentum, resultTheta2, resultVariance); - BOOST_REQUIRE(result); - BOOST_CHECK_EQUAL(resultMomentum, 3.f); - BOOST_CHECK_EQUAL(energyChange(3.f, resultMomentum, pid), 0.f); - BOOST_CHECK_EQUAL(resultTheta2, 0.f); - BOOST_CHECK_EQUAL(resultVariance, 0.f); - } -} - BOOST_AUTO_TEST_CASE(ChargedMasslessRejected) { IntegratedMaterialBudget material{0.f, 0.f}; diff --git a/Detectors/ITSMFT/common/tracking/test/testMultiSourceLoading.cxx b/Detectors/ITSMFT/common/tracking/test/testMultiSourceLoading.cxx index 01dbcb5614c63..d880fe2d46c5c 100644 --- a/Detectors/ITSMFT/common/tracking/test/testMultiSourceLoading.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testMultiSourceLoading.cxx @@ -26,9 +26,8 @@ #include "DataFormatsITSMFT/TopologyDictionary.h" #include "DetectorsCommonDataFormats/DetID.h" #include "ITSMFTTracking/DetectorLayout.h" -#include "ITSMFTTracking/IOUtils.h" +#include "TrackingParameterTestSupport.h" #include "ITSMFTTracking/TimeFrame.h" -#include "ITSMFTTracking/ClusterDecoding.h" #include "SimulationDataFormat/MCCompLabel.h" #include "SimulationDataFormat/MCTruthContainer.h" @@ -48,7 +47,7 @@ enum class Corruption { LayerOutOfRange }; -class FakeClusterDecoder final : public ClusterDecoder +class FakeClusterDecoder { public: FakeClusterDecoder(o2::detectors::DetID::ID detector, std::vector sensorToLayer, bool disk, Corruption corruption = Corruption::None) @@ -56,39 +55,33 @@ class FakeClusterDecoder final : public ClusterDecoder { } - o2::itsmft::tracking::ClusterDecodeResult decode( + o2::itsmft::tracking::DecodedCluster decode( const CompClusterExt& cluster, - BoundedPatternCursor& patterns, + gsl::span::iterator& patterns, const TopologyDictionary* dict, uint32_t, - bool) const override + bool) const { if (mCorruption == Corruption::NegativeLayer) { - o2::itsmft::tracking::ClusterDecodeResult result; - result.decoded.layer = -1; + o2::itsmft::tracking::DecodedCluster result; + result.layer = -1; return result; } if (mCorruption == Corruption::LayerOutOfRange) { - o2::itsmft::tracking::ClusterDecodeResult result; - result.decoded.layer = std::numeric_limits::max(); + o2::itsmft::tracking::DecodedCluster result; + result.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 clusterData = o2::itsmft::ioutils::extractClusterData(cluster, patterns, dict); + o2::itsmft::tracking::DecodedCluster result; const auto sensorID = cluster.getSensorID(); const int layer = (sensorID >= 0 && static_cast(sensorID) < mSensorToLayer.size()) ? mSensorToLayer[sensorID] : -1; - auto& decoded = result.decoded; + auto& decoded = result; 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.nPixels = clusterData.nPixels; decoded.layer = layer; return result; } @@ -105,38 +98,31 @@ class FakeClusterDecoder final : public ClusterDecoder // 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 +class PatternContractDecoder { public: - o2::itsmft::tracking::ClusterDecodeResult decode( + o2::itsmft::tracking::DecodedCluster decode( const CompClusterExt& cluster, - BoundedPatternCursor& patterns, + gsl::span::iterator& patterns, const TopologyDictionary* dictionary, uint32_t, - bool) const override + bool) const { - o2::itsmft::tracking::ClusterDecodeResult result; + o2::itsmft::tracking::DecodedCluster result; if (dictionary == nullptr) { - result.error = ClusterDecodeError::MissingDictionary; - return result; + throw std::runtime_error("Cluster dictionary is not available"); } - ClusterShape shape{1, 1, 1}; + uint32_t nPixels = 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())}; + const o2::itsmft::ClusterPattern pattern{patterns}; + nPixels = static_cast(pattern.getNPixels()); } - auto& decoded = result.decoded; + auto& decoded = result; 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.nPixels = nPixels; decoded.layer = 0; return result; } @@ -221,7 +207,7 @@ BOOST_AUTO_TEST_CASE(SingleITSSourceLoadsIntoExpectedSurfaces) const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 2}}; FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0, 1}, false}; - ClusterSourceInput src; + test::TestClusterSourceInput src; src.id = ClusterSourceId{0}; src.detector = o2::detectors::DetID::ITS; src.clusters = clusters; @@ -230,17 +216,13 @@ BOOST_AUTO_TEST_CASE(SingleITSSourceLoadsIntoExpectedSurfaces) src.dictionary = &dict(); src.layerToSurface = itsLayerToSurface; src.timing = ROFTimingConfig{40, 0, 0, 0}; - src.decoder = &decoder; + src.setDecoder(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_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}, + &externalIndicesBySurface)); BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{0}).size(), 1u); BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{1}).size(), 1u); @@ -250,15 +232,7 @@ BOOST_AUTO_TEST_CASE(SingleITSSourceLoadsIntoExpectedSurfaces) 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. + // Invalid timing must throw with source and ROF context before decoding. const auto layout = makeCombinedLayout(); BOOST_REQUIRE(layout.valid()); @@ -267,7 +241,7 @@ BOOST_AUTO_TEST_CASE(InvalidTimingConfigurationIsReportedWithBuildErrorDetail) const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; - ClusterSourceInput src; + test::TestClusterSourceInput src; src.id = ClusterSourceId{0}; src.detector = o2::detectors::DetID::ITS; src.clusters = clusters; @@ -276,13 +250,11 @@ BOOST_AUTO_TEST_CASE(InvalidTimingConfigurationIsReportedWithBuildErrorDetail) src.dictionary = &dict(); src.layerToSurface = itsLayerToSurface; src.timing = ROFTimingConfig{0, 0, 0, 0}; // rofLength <= 0 - src.decoder = &decoder; + src.setDecoder(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_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Invalid ROF timing: source=0 rof=0") != std::string::npos; }); BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); } @@ -299,7 +271,7 @@ BOOST_AUTO_TEST_CASE(SingleMFTSourceLoadsIntoExpectedSurfaces) const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 2}}; FakeClusterDecoder decoder{o2::detectors::DetID::MFT, {0, 1}, true}; - ClusterSourceInput src; + test::TestClusterSourceInput src; src.id = ClusterSourceId{0}; src.detector = o2::detectors::DetID::MFT; src.clusters = clusters; @@ -308,14 +280,13 @@ BOOST_AUTO_TEST_CASE(SingleMFTSourceLoadsIntoExpectedSurfaces) src.dictionary = &dict(); src.layerToSurface = mftLayerToSurface; src.timing = ROFTimingConfig{40, 0, 0, 0}; - src.decoder = &decoder; + src.setDecoder(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_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}, + &externalIndicesBySurface)); BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{2}).size(), 1u); BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{3}).size(), 1u); @@ -337,7 +308,7 @@ BOOST_AUTO_TEST_CASE(CombinedITSAndMFTSourcesLoadTogether) 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{}; + std::array sources{}; sources[0].id = ClusterSourceId{0}; sources[0].detector = o2::detectors::DetID::ITS; sources[0].clusters = itsClusters; @@ -346,7 +317,7 @@ BOOST_AUTO_TEST_CASE(CombinedITSAndMFTSourcesLoadTogether) sources[0].dictionary = &dict(); sources[0].layerToSurface = itsLayerToSurface; sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; - sources[0].decoder = &itsDecoder; + sources[0].setDecoder(itsDecoder); sources[1].id = ClusterSourceId{1}; sources[1].detector = o2::detectors::DetID::MFT; @@ -356,12 +327,11 @@ BOOST_AUTO_TEST_CASE(CombinedITSAndMFTSourcesLoadTogether) sources[1].dictionary = &dict(); sources[1].layerToSurface = mftLayerToSurface; sources[1].timing = ROFTimingConfig{50, 0, 0, 0}; - sources[1].decoder = &mftDecoder; + sources[1].setDecoder(mftDecoder); TimeFrame frame; configureFrame(frame, layout); - const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); - BOOST_REQUIRE(result.ok()); + BOOST_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0})); BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{0}).size(), 1u); BOOST_CHECK_EQUAL(frame.getGlobalMeasurements(LayerId{3}).size(), 1u); @@ -382,7 +352,7 @@ BOOST_AUTO_TEST_CASE(TwoSourcesCannotOwnTheSameSurface) FakeClusterDecoder decoderA{o2::detectors::DetID::ITS, {0}, false}; FakeClusterDecoder decoderB{o2::detectors::DetID::ITS, {0}, false}; - std::array sources{}; + std::array sources{}; sources[0].id = ClusterSourceId{0}; sources[0].detector = o2::detectors::DetID::ITS; sources[0].clusters = clustersA; @@ -391,7 +361,7 @@ BOOST_AUTO_TEST_CASE(TwoSourcesCannotOwnTheSameSurface) sources[0].dictionary = &dict(); sources[0].layerToSurface = itsLayerToSurface; sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; - sources[0].decoder = &decoderA; + sources[0].setDecoder(decoderA); sources[1].id = ClusterSourceId{1}; sources[1].detector = o2::detectors::DetID::ITS; @@ -401,12 +371,11 @@ BOOST_AUTO_TEST_CASE(TwoSourcesCannotOwnTheSameSurface) sources[1].dictionary = &dict(); sources[1].layerToSurface = itsLayerToSurface; sources[1].timing = ROFTimingConfig{40, 0, 0, 0}; - sources[1].decoder = &decoderB; + sources[1].setDecoder(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_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Invalid source-to-surface layer mapping") != std::string::npos; }); BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); } @@ -430,7 +399,7 @@ BOOST_AUTO_TEST_CASE(IdenticalExternalIndicesInDifferentSourcesDoNotCollide) FakeClusterDecoder decoderA{o2::detectors::DetID::ITS, {0}, false}; FakeClusterDecoder decoderB{o2::detectors::DetID::ITS, {0}, false}; - std::array sources{}; + std::array sources{}; sources[0].id = ClusterSourceId{0}; sources[0].detector = o2::detectors::DetID::ITS; sources[0].clusters = clustersA; @@ -440,7 +409,7 @@ BOOST_AUTO_TEST_CASE(IdenticalExternalIndicesInDifferentSourcesDoNotCollide) sources[0].labels = &labelsA; sources[0].layerToSurface = firstITSSurface; sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; - sources[0].decoder = &decoderA; + sources[0].setDecoder(decoderA); sources[1].id = ClusterSourceId{1}; sources[1].detector = o2::detectors::DetID::ITS; @@ -451,12 +420,11 @@ BOOST_AUTO_TEST_CASE(IdenticalExternalIndicesInDifferentSourcesDoNotCollide) sources[1].labels = &labelsB; sources[1].layerToSurface = secondITSSurface; sources[1].timing = ROFTimingConfig{40, 0, 0, 0}; - sources[1].decoder = &decoderB; + sources[1].setDecoder(decoderB); TimeFrame frame; configureFrame(frame, layout); - const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); - BOOST_REQUIRE(result.ok()); + BOOST_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0})); const auto onSurfaceZero = frame.getGlobalMeasurements(LayerId{0}); BOOST_REQUIRE_EQUAL(onSurfaceZero.size(), 1u); @@ -484,7 +452,7 @@ BOOST_AUTO_TEST_CASE(OriginalClusterIdResolvesLabelsAndCompactGlobal) labels.addElement(0, o2::MCCompLabel{1, 0, 0}); FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; - ClusterSourceInput src; + test::TestClusterSourceInput src; src.id = ClusterSourceId{0}; src.detector = o2::detectors::DetID::ITS; src.clusters = clusters; @@ -494,12 +462,11 @@ BOOST_AUTO_TEST_CASE(OriginalClusterIdResolvesLabelsAndCompactGlobal) src.labels = &labels; src.layerToSurface = itsLayerToSurface; src.timing = ROFTimingConfig{40, 0, 0, 0}; - src.decoder = &decoder; + src.setDecoder(decoder); TimeFrame frame; configureFrame(frame, layout); - const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); - BOOST_REQUIRE(result.ok()); + BOOST_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0})); constexpr uint32_t clusterId = 0; const auto labelPlain = frame.getLabels(LayerId{0}, clusterId); @@ -533,7 +500,7 @@ BOOST_AUTO_TEST_CASE(IndependentROFCountsAcrossSourcesAreAllowed) FakeClusterDecoder decoderA{o2::detectors::DetID::ITS, {0}, false}; FakeClusterDecoder decoderB{o2::detectors::DetID::ITS, {0}, false}; - std::array sources{}; + std::array sources{}; sources[0].id = ClusterSourceId{0}; sources[0].detector = o2::detectors::DetID::ITS; sources[0].clusters = clustersA; @@ -542,7 +509,7 @@ BOOST_AUTO_TEST_CASE(IndependentROFCountsAcrossSourcesAreAllowed) sources[0].dictionary = &dict(); sources[0].layerToSurface = firstITSSurface; sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; - sources[0].decoder = &decoderA; + sources[0].setDecoder(decoderA); sources[1].id = ClusterSourceId{1}; sources[1].detector = o2::detectors::DetID::ITS; @@ -552,12 +519,11 @@ BOOST_AUTO_TEST_CASE(IndependentROFCountsAcrossSourcesAreAllowed) sources[1].dictionary = &dict(); sources[1].layerToSurface = secondITSSurface; sources[1].timing = ROFTimingConfig{100, 0, 0, 0}; - sources[1].decoder = &decoderB; + sources[1].setDecoder(decoderB); TimeFrame frame; configureFrame(frame, layout); - const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); - BOOST_REQUIRE(result.ok()); + BOOST_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0})); BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 4u); } @@ -584,7 +550,7 @@ BOOST_AUTO_TEST_CASE(OverlappingAndNonOverlappingSourceTimingIntervals) FakeClusterDecoder decoderB{o2::detectors::DetID::ITS, {0}, false}; FakeClusterDecoder decoderC{o2::detectors::DetID::MFT, {0}, true}; - std::array sources{}; + std::array sources{}; sources[0].id = ClusterSourceId{0}; sources[0].detector = o2::detectors::DetID::ITS; sources[0].clusters = clustersA; @@ -593,7 +559,7 @@ BOOST_AUTO_TEST_CASE(OverlappingAndNonOverlappingSourceTimingIntervals) sources[0].dictionary = &dict(); sources[0].layerToSurface = firstITSSurface; sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; - sources[0].decoder = &decoderA; + sources[0].setDecoder(decoderA); sources[1].id = ClusterSourceId{1}; sources[1].detector = o2::detectors::DetID::ITS; @@ -603,7 +569,7 @@ BOOST_AUTO_TEST_CASE(OverlappingAndNonOverlappingSourceTimingIntervals) sources[1].dictionary = &dict(); sources[1].layerToSurface = secondITSSurface; sources[1].timing = ROFTimingConfig{40, 0, 0, 0}; - sources[1].decoder = &decoderB; + sources[1].setDecoder(decoderB); sources[2].id = ClusterSourceId{2}; sources[2].detector = o2::detectors::DetID::MFT; @@ -613,12 +579,11 @@ BOOST_AUTO_TEST_CASE(OverlappingAndNonOverlappingSourceTimingIntervals) sources[2].dictionary = &dict(); sources[2].layerToSurface = firstMFTSurface; sources[2].timing = ROFTimingConfig{40, 0, 0, 0}; - sources[2].decoder = &decoderC; + sources[2].setDecoder(decoderC); TimeFrame frame; configureFrame(frame, layout); - const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); - BOOST_REQUIRE(result.ok()); + BOOST_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0})); BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 3u); } @@ -662,7 +627,7 @@ BOOST_AUTO_TEST_CASE(TriggeredAndContinuousReadoutAreBothSupportedTogether) FakeClusterDecoder continuousDecoder{o2::detectors::DetID::ITS, {0}, false}; FakeClusterDecoder triggeredDecoder{o2::detectors::DetID::ITS, {0}, false}; - std::array sources{}; + std::array sources{}; sources[0].id = ClusterSourceId{0}; sources[0].detector = o2::detectors::DetID::ITS; sources[0].clusters = continuousClusters; @@ -671,7 +636,7 @@ BOOST_AUTO_TEST_CASE(TriggeredAndContinuousReadoutAreBothSupportedTogether) sources[0].dictionary = &dict(); sources[0].layerToSurface = firstITSSurface; sources[0].timing = ROFTimingConfig{continuousRofLength, 0, 0, 0}; - sources[0].decoder = &continuousDecoder; + sources[0].setDecoder(continuousDecoder); sources[1].id = ClusterSourceId{1}; sources[1].detector = o2::detectors::DetID::ITS; @@ -681,12 +646,11 @@ BOOST_AUTO_TEST_CASE(TriggeredAndContinuousReadoutAreBothSupportedTogether) sources[1].dictionary = &dict(); sources[1].layerToSurface = secondITSSurface; sources[1].timing = ROFTimingConfig{triggeredRofLength, 0, 0, 0}; - sources[1].decoder = &triggeredDecoder; + sources[1].setDecoder(triggeredDecoder); TimeFrame frame; configureFrame(frame, layout); - const auto result = loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}); - BOOST_REQUIRE(result.ok()); + BOOST_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0})); BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 6u); } @@ -710,7 +674,7 @@ BOOST_AUTO_TEST_CASE(SourceSpecificPatternCursorsAreIndependent) FakeClusterDecoder decoderA{o2::detectors::DetID::ITS, {0}, false}; FakeClusterDecoder decoderB{o2::detectors::DetID::ITS, {0}, false}; - std::array sources{}; + std::array sources{}; sources[0].id = ClusterSourceId{0}; sources[0].detector = o2::detectors::DetID::ITS; sources[0].clusters = clustersA; @@ -719,7 +683,7 @@ BOOST_AUTO_TEST_CASE(SourceSpecificPatternCursorsAreIndependent) sources[0].dictionary = &dict(); sources[0].layerToSurface = firstITSSurface; sources[0].timing = ROFTimingConfig{40, 0, 0, 0}; - sources[0].decoder = &decoderA; + sources[0].setDecoder(decoderA); sources[1].id = ClusterSourceId{1}; sources[1].detector = o2::detectors::DetID::ITS; @@ -729,14 +693,13 @@ BOOST_AUTO_TEST_CASE(SourceSpecificPatternCursorsAreIndependent) sources[1].dictionary = &dict(); sources[1].layerToSurface = secondITSSurface; sources[1].timing = ROFTimingConfig{40, 0, 0, 0}; - sources[1].decoder = &decoderB; + sources[1].setDecoder(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()); + BOOST_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}, + nullptr, &clusterSizesBySurface)); // Every cluster consumed exactly one 1-pixel pattern regardless of source. for (const auto layer : {LayerId{0}, LayerId{1}}) { @@ -756,7 +719,7 @@ BOOST_AUTO_TEST_CASE(CommonDictionaryPatternDoesNotConsumeExplicitBytes) const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 2}}; PatternContractDecoder decoder; - ClusterSourceInput src; + test::TestClusterSourceInput src; src.id = ClusterSourceId{0}; src.detector = o2::detectors::DetID::ITS; src.clusters = clusters; @@ -765,73 +728,18 @@ BOOST_AUTO_TEST_CASE(CommonDictionaryPatternDoesNotConsumeExplicitBytes) src.dictionary = &dict(); src.layerToSurface = itsLayerToSurface; src.timing = ROFTimingConfig{40, 0, 0, 0}; - src.decoder = &decoder; + src.setDecoder(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_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}, + nullptr, &clusterSizesBySurface)); 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(); @@ -840,7 +748,7 @@ BOOST_AUTO_TEST_CASE(ExactPatternConsumptionSucceedsAndTrailingBytesAreRejected) const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; auto makeSource = [&](gsl::span patterns) { - ClusterSourceInput src; + test::TestClusterSourceInput src; src.id = ClusterSourceId{0}; src.detector = o2::detectors::DetID::ITS; src.clusters = clusters; @@ -849,7 +757,7 @@ BOOST_AUTO_TEST_CASE(ExactPatternConsumptionSucceedsAndTrailingBytesAreRejected) src.dictionary = &dict(); src.layerToSurface = itsLayerToSurface; src.timing = ROFTimingConfig{40, 0, 0, 0}; - src.decoder = &decoder; + src.setDecoder(decoder); return src; }; @@ -857,36 +765,38 @@ BOOST_AUTO_TEST_CASE(ExactPatternConsumptionSucceedsAndTrailingBytesAreRejected) auto exactSource = makeSource(exact); TimeFrame frame; configureFrame(frame, layout); - BOOST_REQUIRE(loadSources(frame, layout.getCatalog(), gsl::span(&exactSource, 1), {0, 0}).ok()); + BOOST_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(&exactSource, 1), {0, 0})); 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_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(&trailingSource, 1), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Trailing cluster pattern data") != std::string::npos; }); + frame.resetTimeFrame(); 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_EXCEPTION(test::loadSources( + frame, layout.getCatalog(), + gsl::span(&missingDictionarySource, 1), {0, 0}), + std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Cluster dictionary is not available") != std::string::npos; }); BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); } -BOOST_AUTO_TEST_CASE(MissingDictionaryIsTypedBeforeProductionGeometryDecode) +BOOST_AUTO_TEST_CASE(MissingDictionaryThrowsBeforeProductionGeometryDecode) { - 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); + const auto layout = makeCombinedLayout(); + TimeFrame frame; + configureFrame(frame, layout); + const std::array clusters{{{1, 1, CompCluster::InvalidPatternID, 0}}}; + ClusterSourceInput source; + source.id = ClusterSourceId{0}; + source.clusters = clusters; + source.patterns = onePixelPattern; + source.layerToSurface = itsLayerToSurface; + BOOST_CHECK_EXCEPTION(loadSources(frame, layout.getCatalog(), gsl::span{&source, 1}, {0, 0}), + std::runtime_error, [](const std::runtime_error& error) { + return std::string(error.what()).find("Cluster dictionary is not available source=0") != std::string::npos; + }); } BOOST_AUTO_TEST_CASE(AbsentLabelsAreLegal) @@ -899,7 +809,7 @@ BOOST_AUTO_TEST_CASE(AbsentLabelsAreLegal) const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; - ClusterSourceInput src; + test::TestClusterSourceInput src; src.id = ClusterSourceId{0}; src.detector = o2::detectors::DetID::ITS; src.clusters = clusters; @@ -909,12 +819,11 @@ BOOST_AUTO_TEST_CASE(AbsentLabelsAreLegal) src.labels = nullptr; // no MC labels for this source src.layerToSurface = itsLayerToSurface; src.timing = ROFTimingConfig{40, 0, 0, 0}; - src.decoder = &decoder; + src.setDecoder(decoder); TimeFrame frame; configureFrame(frame, layout); - const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); - BOOST_REQUIRE(result.ok()); + BOOST_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0})); BOOST_CHECK(frame.getLabels(LayerId{0}, 0).empty()); BOOST_CHECK(frame.getLabels(LayerId{}, 0).empty()); @@ -931,7 +840,7 @@ BOOST_AUTO_TEST_CASE(NonDenseAndDuplicateAndInvalidSourceIdsAreRejected) FakeClusterDecoder decoderB{o2::detectors::DetID::ITS, {0}, false}; auto makeSource = [&](ClusterSourceId id, FakeClusterDecoder& decoder) { - ClusterSourceInput src; + test::TestClusterSourceInput src; src.id = id; src.detector = o2::detectors::DetID::ITS; src.clusters = clusters; @@ -940,36 +849,30 @@ BOOST_AUTO_TEST_CASE(NonDenseAndDuplicateAndInvalidSourceIdsAreRejected) src.dictionary = &dict(); src.layerToSurface = itsLayerToSurface; src.timing = ROFTimingConfig{40, 0, 0, 0}; - src.decoder = &decoder; + src.setDecoder(decoder); return src; }; { // Non-dense: ids {0, 2} for two sources. - std::array sources{makeSource(ClusterSourceId{0}, decoderA), makeSource(ClusterSourceId{2}, decoderB)}; + 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); + BOOST_CHECK_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Source IDs must be dense") != std::string::npos; }); } { // Duplicate ids {0, 0}. - std::array sources{makeSource(ClusterSourceId{0}, decoderA), makeSource(ClusterSourceId{0}, decoderB)}; + 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); + BOOST_CHECK_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Duplicate source ID") != std::string::npos; }); } { // Explicitly invalid id. - std::array sources{makeSource(ClusterSourceId::invalid(), decoderA)}; + 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_CHECK_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Source IDs must be dense") != std::string::npos; }); } } @@ -984,7 +887,7 @@ BOOST_AUTO_TEST_CASE(InvalidROFClusterRangesAreRejected) FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; auto makeSrc = [&](const std::vector& rofs) { - ClusterSourceInput src; + test::TestClusterSourceInput src; src.id = ClusterSourceId{0}; src.detector = o2::detectors::DetID::ITS; src.clusters = clusters; @@ -993,7 +896,7 @@ BOOST_AUTO_TEST_CASE(InvalidROFClusterRangesAreRejected) src.dictionary = &dict(); src.layerToSurface = itsLayerToSurface; src.timing = ROFTimingConfig{40, 0, 0, 0}; - src.decoder = &decoder; + src.setDecoder(decoder); return src; }; @@ -1003,9 +906,7 @@ BOOST_AUTO_TEST_CASE(InvalidROFClusterRangesAreRejected) 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_CHECK_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Invalid ROF cluster range") != std::string::npos; }); } { // Overlapping ranges. @@ -1013,9 +914,7 @@ BOOST_AUTO_TEST_CASE(InvalidROFClusterRangesAreRejected) 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_CHECK_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Invalid ROF cluster range") != std::string::npos; }); } { // Leading gap: first ROF does not begin at cluster index 0. @@ -1023,9 +922,7 @@ BOOST_AUTO_TEST_CASE(InvalidROFClusterRangesAreRejected) 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_CHECK_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Invalid ROF cluster range") != std::string::npos; }); } { // Internal gap: rof0 covers [0,1), rof1 covers [2,2) i.e. starts at 2 @@ -1035,9 +932,7 @@ BOOST_AUTO_TEST_CASE(InvalidROFClusterRangesAreRejected) 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_CHECK_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Invalid ROF cluster range") != std::string::npos; }); } { // Trailing cluster: the ROFs cover only the first cluster, leaving the @@ -1046,9 +941,7 @@ BOOST_AUTO_TEST_CASE(InvalidROFClusterRangesAreRejected) 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_CHECK_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Invalid ROF cluster range") != std::string::npos; }); } { // Clusters without ROFs: zero ROFs is only valid when clusters is also @@ -1057,9 +950,7 @@ BOOST_AUTO_TEST_CASE(InvalidROFClusterRangesAreRejected) 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_CHECK_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Invalid ROF cluster range") != std::string::npos; }); } } @@ -1072,7 +963,7 @@ BOOST_AUTO_TEST_CASE(ZeroROFsIsValidWithZeroClusters) const std::vector rofs{}; FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; - ClusterSourceInput src; + test::TestClusterSourceInput src; src.id = ClusterSourceId{0}; src.detector = o2::detectors::DetID::ITS; src.clusters = clusters; @@ -1081,12 +972,11 @@ BOOST_AUTO_TEST_CASE(ZeroROFsIsValidWithZeroClusters) src.dictionary = &dict(); src.layerToSurface = itsLayerToSurface; src.timing = ROFTimingConfig{40, 0, 0, 0}; - src.decoder = &decoder; + src.setDecoder(decoder); TimeFrame frame; configureFrame(frame, layout); - const auto result = loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}); - BOOST_CHECK(result.ok()); + BOOST_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0})); BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); } @@ -1099,7 +989,7 @@ BOOST_AUTO_TEST_CASE(InvalidLayerToSurfaceMappingIsRejected) const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {-1, 1}, false}; - ClusterSourceInput src; + test::TestClusterSourceInput src; src.id = ClusterSourceId{0}; src.detector = o2::detectors::DetID::ITS; src.clusters = clusters; @@ -1108,13 +998,11 @@ BOOST_AUTO_TEST_CASE(InvalidLayerToSurfaceMappingIsRejected) 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; + src.setDecoder(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_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Invalid source-to-surface layer mapping") != std::string::npos; }); } BOOST_AUTO_TEST_CASE(DetectorSurfaceMismatchIsRejected) @@ -1126,7 +1014,7 @@ BOOST_AUTO_TEST_CASE(DetectorSurfaceMismatchIsRejected) const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; - ClusterSourceInput src; + test::TestClusterSourceInput src; src.id = ClusterSourceId{0}; src.detector = o2::detectors::DetID::ITS; src.clusters = clusters; @@ -1137,13 +1025,11 @@ BOOST_AUTO_TEST_CASE(DetectorSurfaceMismatchIsRejected) const std::array wrongMapping{LayerId{2}}; src.layerToSurface = wrongMapping; src.timing = ROFTimingConfig{40, 0, 0, 0}; - src.decoder = &decoder; + src.setDecoder(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_CHECK_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Source detector does not match its surface") != std::string::npos; }); } BOOST_AUTO_TEST_CASE(UnsafeDecodedLayerIsRejected) @@ -1160,7 +1046,7 @@ BOOST_AUTO_TEST_CASE(UnsafeDecodedLayerIsRejected) Corruption::NegativeLayer, Corruption::LayerOutOfRange}; for (const auto corruption : corruptions) { FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false, corruption}; - ClusterSourceInput src; + test::TestClusterSourceInput src; src.id = ClusterSourceId{0}; src.detector = o2::detectors::DetID::ITS; src.clusters = clusters; @@ -1169,18 +1055,16 @@ BOOST_AUTO_TEST_CASE(UnsafeDecodedLayerIsRejected) src.dictionary = &dict(); src.layerToSurface = itsLayerToSurface; src.timing = ROFTimingConfig{40, 0, 0, 0}; - src.decoder = &decoder; + src.setDecoder(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_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(&src, 1), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Invalid source-to-surface layer mapping") != std::string::npos; }); BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); } } -BOOST_AUTO_TEST_CASE(FailedLoadLeavesNoPartialState) +BOOST_AUTO_TEST_CASE(LoadingClearsPreviousDataBeforeValidation) { const auto layout = makeCombinedLayout(); BOOST_REQUIRE(layout.valid()); @@ -1190,7 +1074,7 @@ BOOST_AUTO_TEST_CASE(FailedLoadLeavesNoPartialState) const std::vector rofs{ROFRecord{{0, 0}, 0, 0, 1}}; FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; - ClusterSourceInput goodSrc; + test::TestClusterSourceInput goodSrc; goodSrc.id = ClusterSourceId{0}; goodSrc.detector = o2::detectors::DetID::ITS; goodSrc.clusters = clusters; @@ -1199,27 +1083,24 @@ BOOST_AUTO_TEST_CASE(FailedLoadLeavesNoPartialState) goodSrc.dictionary = &dict(); goodSrc.layerToSurface = itsLayerToSurface; goodSrc.timing = ROFTimingConfig{40, 0, 0, 0}; - goodSrc.decoder = &decoder; + goodSrc.setDecoder(decoder); TimeFrame frame; configureFrame(frame, layout); - BOOST_REQUIRE(loadSources(frame, layout.getCatalog(), gsl::span(&goodSrc, 1), {0, 0}).ok()); + BOOST_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(&goodSrc, 1), {0, 0})); 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()); + std::array badSources{goodSrc, goodSrc}; // both id==0 + BOOST_CHECK_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(badSources), {0, 0}), std::runtime_error); BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); } -BOOST_AUTO_TEST_CASE(FailedLoadAfterFirstSourceDecodedLeavesNoPartialState) +BOOST_AUTO_TEST_CASE(CallerResetsAfterPartialLoadFailure) { - // 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. + // The second source returns an invalid layer after the first source has + // already populated the frame. A direct caller owns cleanup after the throw. const auto layout = makeCombinedLayout(); BOOST_REQUIRE(layout.valid()); @@ -1230,7 +1111,7 @@ BOOST_AUTO_TEST_CASE(FailedLoadAfterFirstSourceDecodedLeavesNoPartialState) labels.addElement(0, o2::MCCompLabel{1, 0, 0}); FakeClusterDecoder decoder{o2::detectors::DetID::ITS, {0}, false}; - ClusterSourceInput goodSrc; + test::TestClusterSourceInput goodSrc; goodSrc.id = ClusterSourceId{0}; goodSrc.detector = o2::detectors::DetID::ITS; goodSrc.clusters = clusters; @@ -1240,42 +1121,37 @@ BOOST_AUTO_TEST_CASE(FailedLoadAfterFirstSourceDecodedLeavesNoPartialState) goodSrc.labels = &labels; goodSrc.layerToSurface = itsLayerToSurface; goodSrc.timing = ROFTimingConfig{40, 0, 0, 0}; - goodSrc.decoder = &decoder; + goodSrc.setDecoder(decoder); TimeFrame frame; configureFrame(frame, layout); - BOOST_REQUIRE(loadSources(frame, layout.getCatalog(), gsl::span(&goodSrc, 1), {0, 0}).ok()); + BOOST_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span(&goodSrc, 1), {0, 0})); 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}; + FakeClusterDecoder decoderB{o2::detectors::DetID::MFT, {0}, true, Corruption::NegativeLayer}; - ClusterSourceInput srcA = goodSrc; - srcA.decoder = &decoderA; + test::TestClusterSourceInput srcA = goodSrc; + srcA.setDecoder(decoderA); - ClusterSourceInput srcB; + test::TestClusterSourceInput srcB; srcB.id = ClusterSourceId{1}; - srcB.detector = o2::detectors::DetID::ITS; + srcB.detector = o2::detectors::DetID::MFT; 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; + const std::array mapping{LayerId{2}}; + srcB.layerToSurface = mapping; srcB.timing = ROFTimingConfig{40, 0, 0, 0}; - srcB.decoder = &decoderB; + srcB.setDecoder(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}); + std::array sources{srcA, srcB}; + BOOST_CHECK_EXCEPTION(test::loadSources(frame, layout.getCatalog(), gsl::span(sources), {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Invalid source-to-surface layer mapping") != std::string::npos; }); + frame.resetTimeFrame(); BOOST_CHECK(frame.getGlobalMeasurements(LayerId{0}).empty()); BOOST_CHECK(frame.getLabels(LayerId{0}, 0).empty()); BOOST_CHECK(frame.getLabels(LayerId{2}, 0).empty()); @@ -1293,8 +1169,7 @@ BOOST_AUTO_TEST_CASE(EmptyFrameAccessorsAvoidNullPointerArithmetic) 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_REQUIRE_NO_THROW(test::loadSources(frame, layout.getCatalog(), gsl::span{}, {0, 0})); BOOST_CHECK_EQUAL(frame.getTotalMeasurements(), 0u); BOOST_CHECK(frame.getSurfaceMeasurement(LayerId{0}, 0) == nullptr); @@ -1309,8 +1184,7 @@ BOOST_AUTO_TEST_CASE(UnconfiguredFrameRejectsEvenAnEmptyLoad) const SurfaceCatalogView emptyCatalog{}; TimeFrame frame; - const auto result = loadSources(frame, emptyCatalog, gsl::span{}, {0, 0}); - BOOST_CHECK(result.error == MultiSourceLoadError::FrameNotConfigured); + BOOST_CHECK_EXCEPTION(test::loadSources(frame, emptyCatalog, gsl::span{}, {0, 0}), std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("TimeFrame is not configured") != std::string::npos; }); 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 index 21a35d181291f..54813d96d0b08 100644 --- a/Detectors/ITSMFT/common/tracking/test/testPropagator.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testPropagator.cxx @@ -141,6 +141,31 @@ SurfaceDescriptor diskDescriptor(NominalSurfaceMaterial material) return descriptor; } +// A stationary, zero-residual measurement leaves momentum unchanged by the +// transport/update, exposing material effects through the public API. +bool propagateThroughMaterial(SurfaceTrackState& state, SurfaceTrackParameters& reference, + material::IntegratedMaterialBudget budget, + material::MaterialTraversalDirection direction) +{ + SurfaceDescriptor surface{}; + surface.kind = state.kind; + surface.referenceCoordinate = state.referenceCoordinate; + surface.material = {budget.xOverX0, budget.arealDensityGPerCm2}; + SurfaceMeasurement measurement{}; + measurement.frame = {state.referenceCoordinate, state.parameters[0], state.parameters[1], state.alpha}; + measurement.covariance = {0.04f, 0.f, 0.09f}; + float chi2 = 0.f; + return Propagator::propagateToMeasurement(state, reference, surface, measurement, 0.f, + direction, false, 0.f, chi2, false); +} + +bool propagateThroughMaterial(SurfaceTrackState& state, material::IntegratedMaterialBudget budget, + material::MaterialTraversalDirection direction) +{ + SurfaceTrackParameters reference{state}; + return propagateThroughMaterial(state, reference, budget, direction); +} + // 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, @@ -154,7 +179,7 @@ std::array intersectConversionPlane(const SurfaceTrackState& source, z = p[1]; phi = source.alpha + std::asin(p[2]); } - const double curvature = source.absCharge == 0 ? 0. : p[4] * bz * o2::constants::math::B2C; + const double curvature = 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; @@ -280,17 +305,16 @@ BOOST_AUTO_TEST_CASE(PropagatorSelectsCompatibilityFromStateKind) BOOST_CHECK(!Propagator::stateChi2(cylinderReference, diskCandidate, chi2)); } -// --- 3: compatible family never converts -- exact agreement with a direct -// detail::barrel::rotate/propagate/correctForMaterial/predictedChi2/update replay --- +// --- 3: compatible-family propagation and material effects ----------------- -BOOST_AUTO_TEST_CASE(CompatibleFamilyMatchesDirectBarrelPrimitiveReplay) +BOOST_AUTO_TEST_CASE(CompatibleFamilyMatchesDirectBarrelPrimitiveReplayWithoutMaterial) { 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 material = NominalSurfaceMaterial{0.f, 0.f}; const auto descriptor = cylinderDescriptor(material); float chi2Propagator = 0.f; float chi2Direct = 0.f; @@ -301,10 +325,6 @@ BOOST_AUTO_TEST_CASE(CompatibleFamilyMatchesDirectBarrelPrimitiveReplay) BOOST_REQUIRE(detail::barrel::rotate(viaDirect, viaDirectRef, measurement.frame.frameAngle, BarrelBz)); BOOST_REQUIRE(detail::barrel::propagate(viaDirect, viaDirectRef, measurement.frame.q, BarrelBz)); - const auto materialResult = detail::barrel::correctForMaterial( - viaDirect, viaDirectRef, material::IntegratedMaterialBudget{material.xOverX0, material.arealDensityGPerCm2}, - material::MaterialTraversalDirection::OppositeMomentum); - BOOST_REQUIRE(materialResult); float predChi2 = 0.f; BOOST_REQUIRE(detail::barrel::predictedChi2(viaDirect, measurement, predChi2)); float updateChi2 = 0.f; @@ -347,8 +367,8 @@ BOOST_AUTO_TEST_CASE(BarrelMaterialUsesLegacyIncidencePathLength) const bool uncorrected = material::calculateMaterialPhysics(momentum, original.pid, original.absCharge, material::MaterialTraversalDirection::AlongMomentum, nominalMaterial, uncorrectedMomentum, uncorrectedTheta2, uncorrectedVariance); - const auto result = detail::barrel::correctForMaterial(state, nominalMaterial, - material::MaterialTraversalDirection::AlongMomentum); + const auto result = propagateThroughMaterial(state, nominalMaterial, + material::MaterialTraversalDirection::AlongMomentum); BOOST_REQUIRE(expected); BOOST_REQUIRE(uncorrected); @@ -387,8 +407,8 @@ BOOST_AUTO_TEST_CASE(LinearizedBarrelMaterialUsesLegacyReferenceIncidence) const bool expected = material::calculateMaterialPhysics(momentum, state.pid, state.absCharge, material::MaterialTraversalDirection::AlongMomentum, legacyMaterial, expectedMomentum, expectedTheta2, expectedVariance); - const auto result = detail::barrel::correctForMaterial(state, linRef, nominalMaterial, - material::MaterialTraversalDirection::AlongMomentum); + const auto result = propagateThroughMaterial(state, linRef, nominalMaterial, + material::MaterialTraversalDirection::AlongMomentum); BOOST_REQUIRE(expected); BOOST_REQUIRE(result); @@ -405,7 +425,7 @@ BOOST_AUTO_TEST_CASE(LinearizedBarrelMaterialKeepsReferenceQ2PtForMCSOnly) auto linRef = barrelLinRef(state); const auto referenceBefore = linRef; - const auto result = detail::barrel::correctForMaterial( + const auto result = propagateThroughMaterial( state, linRef, material::IntegratedMaterialBudget{0.01f, 0.f}, material::MaterialTraversalDirection::AlongMomentum); @@ -421,7 +441,7 @@ BOOST_AUTO_TEST_CASE(FailingLinearizedBarrelMaterialLeavesStateAndReferenceUncha const auto stateBefore = state; const auto referenceBefore = linRef; - const auto result = detail::barrel::correctForMaterial( + const auto result = propagateThroughMaterial( state, linRef, material::IntegratedMaterialBudget{1.e8f, 0.f}, material::MaterialTraversalDirection::AlongMomentum); @@ -431,14 +451,14 @@ BOOST_AUTO_TEST_CASE(FailingLinearizedBarrelMaterialLeavesStateAndReferenceUncha BOOST_CHECK(bitEqual(linRef, referenceBefore)); } -BOOST_AUTO_TEST_CASE(CompatibleFamilyMatchesDirectForwardPrimitiveReplay) +BOOST_AUTO_TEST_CASE(CompatibleFamilyMatchesDirectForwardPrimitiveReplayWithoutMaterial) { 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 material = NominalSurfaceMaterial{0.f, 0.f}; const auto descriptor = diskDescriptor(material); float chi2Propagator = 0.f; float chi2Direct = 0.f; @@ -448,10 +468,6 @@ BOOST_AUTO_TEST_CASE(CompatibleFamilyMatchesDirectForwardPrimitiveReplay) false, 0.f, chi2Propagator, true)); BOOST_REQUIRE(detail::forward::propagate(viaDirect, viaDirectRef, measurement.frame.q, DiskBz)); - const auto materialResult = detail::forward::correctForMaterial( - viaDirect, viaDirectRef, material::IntegratedMaterialBudget{material.xOverX0, material.arealDensityGPerCm2}, - material::MaterialTraversalDirection::OppositeMomentum); - BOOST_REQUIRE(materialResult); float predChi2 = 0.f; BOOST_REQUIRE(detail::forward::predictedChi2(viaDirect, measurement, predChi2)); float updateChi2 = 0.f; @@ -492,8 +508,8 @@ BOOST_AUTO_TEST_CASE(ForwardMaterialUsesLegacyIncidencePathLength) const bool uncorrected = material::calculateMaterialPhysics(momentum, original.pid, original.absCharge, material::MaterialTraversalDirection::AlongMomentum, nominalMaterial, uncorrectedMomentum, uncorrectedTheta2, uncorrectedVariance); - const auto result = detail::forward::correctForMaterial(state, nominalMaterial, - material::MaterialTraversalDirection::AlongMomentum); + const auto result = propagateThroughMaterial(state, nominalMaterial, + material::MaterialTraversalDirection::AlongMomentum); BOOST_REQUIRE(expected); BOOST_REQUIRE(uncorrected); @@ -528,8 +544,8 @@ BOOST_AUTO_TEST_CASE(LinearizedForwardMaterialUsesReferenceIncidence) const bool expected = material::calculateMaterialPhysics(momentum, state.pid, state.absCharge, material::MaterialTraversalDirection::AlongMomentum, scaledMaterial, expectedMomentum, expectedTheta2, expectedVariance); - const auto result = detail::forward::correctForMaterial(state, linRef, nominalMaterial, - material::MaterialTraversalDirection::AlongMomentum); + const auto result = propagateThroughMaterial(state, linRef, nominalMaterial, + material::MaterialTraversalDirection::AlongMomentum); BOOST_REQUIRE(expected); BOOST_REQUIRE(result); @@ -546,7 +562,7 @@ BOOST_AUTO_TEST_CASE(LinearizedForwardMaterialKeepsReferenceQ2PtForMCSOnly) auto linRef = diskLinRef(state); const auto referenceBefore = linRef; - const auto result = detail::forward::correctForMaterial( + const auto result = propagateThroughMaterial( state, linRef, material::IntegratedMaterialBudget{0.01f, 0.f}, material::MaterialTraversalDirection::AlongMomentum); @@ -562,7 +578,7 @@ BOOST_AUTO_TEST_CASE(FailingLinearizedForwardMaterialLeavesStateAndReferenceUnch const auto stateBefore = state; const auto referenceBefore = linRef; - const auto result = detail::forward::correctForMaterial( + const auto result = propagateThroughMaterial( state, linRef, material::IntegratedMaterialBudget{1.e8f, 0.f}, material::MaterialTraversalDirection::AlongMomentum); @@ -572,6 +588,20 @@ BOOST_AUTO_TEST_CASE(FailingLinearizedForwardMaterialLeavesStateAndReferenceUnch BOOST_CHECK(bitEqual(linRef, referenceBefore)); } +BOOST_AUTO_TEST_CASE(MaterialPropagationRejectsMismatchedReferenceKinds) +{ + for (const auto original : {barrelState(), diskState()}) { + auto state = original; + SurfaceTrackParameters reference{state}; + reference.kind = state.kind == SurfaceKind::Cylinder ? SurfaceKind::Disk : SurfaceKind::Cylinder; + const auto referenceBefore = reference; + BOOST_CHECK(!propagateThroughMaterial(state, reference, {0.01f, 0.001f}, + material::MaterialTraversalDirection::AlongMomentum)); + BOOST_CHECK(bitEqual(state, original)); + BOOST_CHECK(bitEqual(reference, referenceBefore)); + } +} + // --- 4: incompatible family converts, then propagates ----------------------- BOOST_AUTO_TEST_CASE(BarrelStateConvertsToForwardThenPropagatesToDiskMeasurement) diff --git a/Detectors/ITSMFT/common/tracking/test/testTimeFrameLifecycle.cxx b/Detectors/ITSMFT/common/tracking/test/testTimeFrameLifecycle.cxx index 6db9f2c14205a..8984c47f0f5eb 100644 --- a/Detectors/ITSMFT/common/tracking/test/testTimeFrameLifecycle.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testTimeFrameLifecycle.cxx @@ -20,8 +20,8 @@ // 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. +// C. TimeFrame loading resets and fills the configured frame directly. +// Callers reset the frame after catching loading failures. #define BOOST_TEST_MODULE ITSMFT TimeFrame lifecycle #define BOOST_TEST_MAIN @@ -43,10 +43,9 @@ #include "DetectorsCommonDataFormats/DetID.h" #include "ITSMFTTracking/DetectorLayout.h" #include "ITSMFTTracking/detail/TimeFrameScratch.h" -#include "ITSMFTTracking/IOUtils.h" +#include "TrackingParameterTestSupport.h" #include "ITSMFTTracking/SurfaceDescriptor.h" -#include "ITSMFTTracking/ClusterDecoding.h" -#include "ITSMFTTracking/IOUtils.h" +#include "TrackingParameterTestSupport.h" #include "ITSMFTTracking/TimeFrame.h" #include "ITSMFTTracking/TrackingConfigParam.h" #include "SimulationDataFormat/MCCompLabel.h" @@ -58,38 +57,32 @@ using namespace o2::itsmft::tracking; namespace { -// Deterministic, geometry-free stand-in for GeometryClusterDecoder +// Deterministic, geometry-free stand-in for detector geometry decoding // (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 +// is exercised identically to production decoding. +class LegacyLikeDecoder { public: explicit LegacyLikeDecoder(o2::detectors::DetID::ID detector) : mDetector(detector) {} - o2::itsmft::tracking::ClusterDecodeResult decode( + o2::itsmft::tracking::DecodedCluster decode( const CompClusterExt& cluster, - BoundedPatternCursor& patterns, + gsl::span::iterator& patterns, const TopologyDictionary* dict, uint32_t, - bool applySysErrors) const override + bool applySysErrors) const { - 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 clusterData = o2::itsmft::ioutils::extractClusterData(cluster, patterns, dict); + o2::itsmft::tracking::DecodedCluster result; const int sensorID = cluster.getSensorID(); - auto& decoded = result.decoded; + auto& decoded = result; 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.nPixels = clusterData.nPixels; 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 @@ -310,9 +303,8 @@ BOOST_AUTO_TEST_CASE(WipeClearsNormalizedFrameButPreservesDetId) 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()); + BOOST_REQUIRE_NO_THROW(test::loadTimeFrameSource(frame, decoder, origin, timing, f.clusters, f.patterns, f.rofs, &dict(), &f.labels, o2::detectors::DetID::ITS, + gsl::span{orderedSurfaces}, plan.getSurfaceCatalog())); // Sanity: the successful load itself has the expected content, matching // the accepted parity coverage in testTimeFrameNormalizedSource.cxx. verifyFixtureLoaded(frame, f); @@ -379,7 +371,7 @@ BOOST_AUTO_TEST_CASE(ConfigurationAdoptionResetsIncompatibleCapacityEstimates) BOOST_CHECK_EQUAL(frame.getCapacityEstimator().capacity(key, 1000.), 1024u); } -BOOST_AUTO_TEST_CASE(MalformedTimeFrameLoadLeavesTheFrameEmpty) +BOOST_AUTO_TEST_CASE(CallerResetsAfterMalformedTimeFrameLoad) { const auto catalog = makeITSTestCatalog(); const auto orderedSurfaces = identitySurfaces(ITSNLayers); @@ -392,20 +384,19 @@ BOOST_AUTO_TEST_CASE(MalformedTimeFrameLoadLeavesTheFrameEmpty) 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()); + BOOST_REQUIRE_NO_THROW(test::loadTimeFrameSource(frame, decoder, origin, timing, baselineFixture.clusters, + baselineFixture.patterns, baselineFixture.rofs, &dict(), + &baselineFixture.labels, o2::detectors::DetID::ITS, + gsl::span{orderedSurfaces}, plan.getSurfaceCatalog())); 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_EXCEPTION(test::loadTimeFrameSource(frame, decoder, origin, timing, malformedReplacement.clusters, + malformedReplacement.patterns, malformedReplacement.rofs, &dict(), + &malformedReplacement.labels, o2::detectors::DetID::ITS, + gsl::span{orderedSurfaces}, plan.getSurfaceCatalog()), + std::runtime_error, [](const std::runtime_error& error) { return std::string(error.what()).find("Invalid ROF cluster range") != std::string::npos; }); + frame.resetTimeFrame(); 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 deleted file mode 100644 index 28bb843bdfce1..0000000000000 --- a/Detectors/ITSMFT/common/tracking/test/testTimeFrameLoadFailure.cxx +++ /dev/null @@ -1,122 +0,0 @@ -// 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 index bdafbef94dbb4..54cb08f22e822 100644 --- a/Detectors/ITSMFT/common/tracking/test/testTrackerFailureContract.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testTrackerFailureContract.cxx @@ -76,12 +76,9 @@ #include "DetectorsCommonDataFormats/DetID.h" #include "ITSMFTTracking/Tracker.h" #include "ITSMFTTracking/Configuration.h" -#include "ITSMFTTracking/IOUtils.h" #include "ITSMFTTracking/ITSMFTDetectorDefinitions.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" @@ -96,35 +93,29 @@ using namespace o2::itsmft::tracking; namespace { -// Deterministic, geometry-free stand-in for GeometryClusterDecoder, +// Deterministic, geometry-free stand-in for detector geometry decoding, // identical construction to testTimeFrameLifecycle.cxx / // testTimeFrameNormalizedSource.cxx / testMultiSourceLoading.cxx. -class LegacyLikeDecoder final : public ClusterDecoder +class LegacyLikeDecoder { public: explicit LegacyLikeDecoder(o2::detectors::DetID::ID detector) : mDetector(detector) {} - o2::itsmft::tracking::ClusterDecodeResult decode( + o2::itsmft::tracking::DecodedCluster decode( const CompClusterExt& cluster, - BoundedPatternCursor& patterns, + gsl::span::iterator& patterns, const TopologyDictionary* dict, uint32_t, - bool applySysErrors) const override + bool applySysErrors) const { - 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 clusterData = o2::itsmft::ioutils::extractClusterData(cluster, patterns, dict); + o2::itsmft::tracking::DecodedCluster result; const int sensorID = cluster.getSensorID(); - auto& decoded = result.decoded; + auto& decoded = result; 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.nPixels = clusterData.nPixels; decoded.layer = sensorID; return result; } @@ -329,10 +320,9 @@ struct Rig { 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()); + BOOST_REQUIRE_NO_THROW(test::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())); // TrackerTraits::computeLayerTracklets() reads per-layer ROF counts // from mROFOverlapTableView (o2::its::LayerTiming), a separate table diff --git a/Detectors/ITSMFT/common/tracking/test/testWorkflowSession.cxx b/Detectors/ITSMFT/common/tracking/test/testWorkflowSession.cxx index 62f10dd7d1ef3..3e0eadcd5529d 100644 --- a/Detectors/ITSMFT/common/tracking/test/testWorkflowSession.cxx +++ b/Detectors/ITSMFT/common/tracking/test/testWorkflowSession.cxx @@ -45,19 +45,6 @@ struct Rig { 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()) { @@ -90,7 +77,6 @@ struct Rig { input.rofs = rofs; input.clusters = clusters; input.dictionary = &dictionary; - input.decoder = &decoder; input.layerToSurface = mapping; return input; } @@ -105,21 +91,17 @@ struct Rig { }; } // namespace -BOOST_AUTO_TEST_CASE_TEMPLATE(SuccessAndValidEmptyInputCompleteBeforeCleanup, Count, LayerCounts) +BOOST_AUTO_TEST_CASE_TEMPLATE(ValidEmptyInputCompletesBeforeCleanup, 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.getTotalMeasurements(), 0u); BOOST_CHECK_EQUAL(rig.session.frame.getROFViews().overlap.mLayerCount, Count::value); }, [&](const TrackingResult& result) { ++completed; BOOST_CHECK(result.outcome == TrackingOutcome::Success); @@ -135,11 +117,11 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(SuccessAndValidEmptyInputCompleteBeforeCleanup, Co } } -BOOST_AUTO_TEST_CASE_TEMPLATE(MalformedInputDropsOnlyUnderTheConfiguredPolicy, Count, LayerCounts) +BOOST_AUTO_TEST_CASE_TEMPLATE(MalformedInputAlwaysThrows, Count, LayerCounts) { for (bool drop : {false, true}) { Rig rig{drop}; - rig.rofs[0].setNEntries(1); // Claims a missing cluster: recoverable InvalidROFRange. + rig.rofs[0].setNEntries(1); // Claims a missing cluster: unrecoverable InvalidROFRange. int completed = 0; const auto run = [&] { auto cleanup = rig.session.cleanupOnExit(); @@ -147,16 +129,58 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(MalformedInputDropsOnlyUnderTheConfiguredPolicy, C BOOST_CHECK(decideCATrackerPublicationAction(true, result) == CATrackerPublicationAction::SkipDroppedTimeFrame); cleanup.frameAlreadyReset(); }; - if (drop) { - BOOST_CHECK_NO_THROW(run()); - } else { - BOOST_CHECK_THROW(run(), RecoverableLoadFailure); - } + BOOST_CHECK_THROW(run(), std::runtime_error); BOOST_CHECK_EQUAL(completed, 0); rig.checkClean(); } } +BOOST_AUTO_TEST_CASE_TEMPLATE(DecodingFailureAlwaysThrowsAndClearsFrame, Count, LayerCounts) +{ + for (bool drop : {false, true}) { + Rig rig{drop}; + const auto run = [&] { + rig.session.loadWithRecovery(drop, [&] { + // A decoding failure after an insertion must clear partial frame data. + GlobalMeasurement global{}; + global.x = 3.f; + global.radius = 3.f; + rig.session.frame.addMeasurement(LayerId{0}, global, SurfaceMeasurement{}); + BOOST_REQUIRE_EQUAL(rig.session.frame.getTotalMeasurements(), 1u); + const CompClusterExt cluster{1, 1, 0, 0}; // Absent from the empty dictionary. + auto patterns = gsl::span{}.begin(); + o2::itsmft::ioutils::extractClusterData(cluster, patterns, &rig.dictionary); + }); + }; + BOOST_CHECK_EXCEPTION(run(), std::runtime_error, [](const std::runtime_error& error) { + return std::string(error.what()).find("Cluster pattern ID is outside the topology dictionary") != std::string::npos; + }); + rig.checkClean(); + } +} + +BOOST_AUTO_TEST_CASE_TEMPLATE(TimingOverflowAlwaysThrowsAndClearsFrame, Count, LayerCounts) +{ + for (bool drop : {false, true}) { + Rig rig{drop}; + auto source = rig.source(); + source.timing = {40, std::numeric_limits::max(), 0, 0}; + const auto run = [&] { + rig.session.loadWithRecovery(drop, [&] { + loadSources(rig.session.frame, rig.session.frame.getLayout().getSurfaceCatalog(), + gsl::span{&source, 1}, {0, 0}, + &rig.session.externalIndices, &rig.session.clusterSizes); + }); + }; + BOOST_CHECK_EXCEPTION(run(), std::runtime_error, [](const std::runtime_error& error) { + const std::string message = error.what(); + return message.find("Invalid ROF timing: source=0 rof=0") != std::string::npos && + message.find("timingError=" + std::to_string(static_cast(TimingBuildError::Overflow))) != std::string::npos; + }); + rig.checkClean(); + } +} + BOOST_AUTO_TEST_CASE_TEMPLATE(StructuralLoadingAndPublicationExceptionsAlwaysCleanUp, Count, LayerCounts) { for (bool drop : {false, true}) { @@ -167,7 +191,7 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(StructuralLoadingAndPublicationExceptionsAlwaysCle auto cleanup = rig.session.cleanupOnExit(); rig.session.process(rig.tracker, rig.traits, source, [](const o2::InteractionRecord&) {}, [](const TrackingResult&) {}); }; - BOOST_CHECK_THROW(run(), TimeFrameLoadException); + BOOST_CHECK_THROW(run(), std::runtime_error); rig.checkClean(); rig.configure(); const auto publish = [&] { @@ -230,7 +254,7 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(TimingViewsBelongToTheSessionAndFilteringSurvivesM } 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_THROW(rig.session.configureTiming(timings, [](int) { return true; }), std::runtime_error); BOOST_CHECK_EQUAL(rig.session.frame.getROFViews().overlap.getLayer(1).mROFLength, 40u); } rig.checkClean(); @@ -325,12 +349,12 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(DetectorTimingConstructionRetainsValidationAndUnit 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); + BOOST_CHECK_EXCEPTION(rig.session.layerTimings(alpide, 0, timeErrors), std::runtime_error, + [](const std::runtime_error& error) { return std::string(error.what()).find("zero ROFs") != std::string::npos; }); + BOOST_CHECK_THROW(rig.session.layerTimings(alpide, 2, std::vector(Count::value - 1)), std::runtime_error); alpide.length = 0; - BOOST_CHECK_EXCEPTION(rig.session.layerTimings(alpide, 2, timeErrors), TimeFrameLoadException, - [](const TimeFrameLoadException& error) { return error.reason() == TimeFrameLoadFailureReason::NonUniformROFTiming; }); + BOOST_CHECK_EXCEPTION(rig.session.layerTimings(alpide, 2, timeErrors), std::runtime_error, + [](const std::runtime_error& error) { return std::string(error.what()).find("non-positive ROF length") != std::string::npos; }); } BOOST_AUTO_TEST_CASE_TEMPLATE(UnchangedTimingReusesStorageButRefreshesEventData, Count, LayerCounts) @@ -424,7 +448,7 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(EveryTimingFieldAndLayerExtentInvalidateTheCache, for (int layer = 0; layer < Count::value; ++layer) { auto nonuniform = baseline; nonuniform[layer].*field += 1; - BOOST_CHECK_THROW(session.configureTiming(nonuniform, accept), TimeFrameLoadException); + BOOST_CHECK_THROW(session.configureTiming(nonuniform, accept), std::runtime_error); } } } @@ -481,7 +505,7 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(InvalidTimingLayerCountPreservesCachedConfiguratio 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_THROW(session.configureTiming(invalid, accept), std::runtime_error); BOOST_CHECK(session.frame.getROFViews().overlap.mFlatTable == cached); } session.configureTiming(timings, accept);