From bd2220f3000068c0e195b8085044fb86501309f1 Mon Sep 17 00:00:00 2001 From: Felix Weiglhofer <9267733+fweig@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:58:42 +0200 Subject: [PATCH 01/47] GPU/TPC: Add vectorised CPU version of tail filter (#15589) * GPU/TPC: Add vectorised CPU version of tail filter * Format --- GPU/GPUTracking/Definitions/GPUSettingsList.h | 2 +- .../Global/GPUChainTrackingClusterizer.cxx | 6 +- .../GPUTPCCFCheckPadBaseline.cxx | 312 +++++++++++++++--- .../GPUTPCCFCheckPadBaseline.h | 8 - GPU/GPUTracking/utils/VcShim.h | 186 ++++++++++- 5 files changed, 444 insertions(+), 70 deletions(-) diff --git a/GPU/GPUTracking/Definitions/GPUSettingsList.h b/GPU/GPUTracking/Definitions/GPUSettingsList.h index 40f7a34e699c9..cbd8f07752617 100644 --- a/GPU/GPUTracking/Definitions/GPUSettingsList.h +++ b/GPU/GPUTracking/Definitions/GPUSettingsList.h @@ -113,7 +113,7 @@ AddOptionRTC(maxTimeBinAboveThresholdIn1000Bin, uint16_t, 500, "", 0, "Except pa AddOptionRTC(maxConsecTimeBinAboveThreshold, uint16_t, 200, "", 0, "Except pad from cluster finding if number of consecutive charges in a fragment is above this baseline (disable = 0)") AddOptionRTC(noisyPadSaturationThreshold, uint16_t, 700, "", 0, "Threshold where a timebin is considered saturated, disabling the noisy pad check for that pad") AddOptionRTC(hipTailFilter, uint8_t, 0, "", 0, "Enable Highly Ionising Particle tail filter in CheckPadBaseline (0 = disable, 1 = filter tails)") -AddOptionRTC(hipTailFilterMinimum, uint16_t, 1024, "", 0, "Thread signal above this minimum as saturated") +AddOptionRTC(hipTailFilterMinimum, uint16_t, 1023, "", 0, "Thread signal above this minimum as saturated") AddOptionRTC(hipTailFilterThreshold, uint16_t, 100, "", 0, "Threshold that must be exceeded for a timebin to be counted towards Highly Ionising Particle tail") AddOptionRTC(hipTailFilterAlpha, float, 0.5f, "", 0, "Smoothing factor for the exponential Highly Ionising Particle tail filter") AddOptionRTC(occupancyMapTimeBins, uint16_t, 16, "", 0, "Number of timebins per histogram bin of occupancy map (0 = disable occupancy map)") diff --git a/GPU/GPUTracking/Global/GPUChainTrackingClusterizer.cxx b/GPU/GPUTracking/Global/GPUChainTrackingClusterizer.cxx index 462b7798ce337..681914414d401 100644 --- a/GPU/GPUTracking/Global/GPUChainTrackingClusterizer.cxx +++ b/GPU/GPUTracking/Global/GPUChainTrackingClusterizer.cxx @@ -1144,16 +1144,12 @@ int32_t GPUChainTracking::RunTPCClusterizer(bool synchronizeOutput) // TODO Add some warning when re enabling pad filter with this flag, so it's not just silently enabled when disabling was requested checkForNoisyPads |= rec()->GetParam().rec.tpc.hipTailFilter; - if (rec()->GetParam().rec.tpc.hipTailFilter && !doGPU) { - GPUError("HIP tail filter enabled, but this is currently not supported on CPU"); - } - if (checkForNoisyPads) { if (rec()->GetParam().rec.tpc.hipTailFilter) { runKernel({GetGridAutoStep(lane, RecoStep::TPCClusterFinding)}, clustererShadow.mPhipTailsByRow, GPUTPCGeometry::NROWS * sizeof(*clustererShadow.mPhipTailsByRow) * GPUTPCCFHIPClusterizer::MaxHIPTailsPerRow); runKernel({GetGridAutoStep(lane, RecoStep::TPCClusterFinding)}, clustererShadow.mPnHIPTails, GPUTPCGeometry::NROWS * sizeof(*clustererShadow.mPnHIPTails)); } - const int32_t nBlocks = GPUTPCCFCheckPadBaseline::GetNBlocks(doGPU); + const int32_t nBlocks = GPUTPCGeometry::NROWS; runKernel({GetGridBlk(nBlocks, lane), {iSector}}); getKernelTimer(RecoStep::TPCClusterFinding, iSector, TPC_REAL_PADS_IN_SECTOR * fragment.lengthWithoutOverlap() * sizeof(PackedCharge), false); diff --git a/GPU/GPUTracking/TPCClusterFinder/GPUTPCCFCheckPadBaseline.cxx b/GPU/GPUTracking/TPCClusterFinder/GPUTPCCFCheckPadBaseline.cxx index 5a5fb478fe065..7470f86877490 100644 --- a/GPU/GPUTracking/TPCClusterFinder/GPUTPCCFCheckPadBaseline.cxx +++ b/GPU/GPUTracking/TPCClusterFinder/GPUTPCCFCheckPadBaseline.cxx @@ -22,6 +22,7 @@ #ifndef GPUCA_GPUCODE #include "MCLabelAccumulator.h" #include "utils/VcShim.h" +#include #endif #if 0 @@ -344,7 +345,7 @@ GPUd() void GPUTPCCFCheckPadBaseline::CheckBaselineGPU(int32_t nBlocks, int32_t GPUbarrier(); - } // if (hipTriggerFound) + } // if (hasHIPTrigger) } // for (uint16_t t = firstTB; t < lastTB; t += NumOfCachedTBs) @@ -374,60 +375,264 @@ GPUd() void GPUTPCCFCheckPadBaseline::CheckBaselineGPU(int32_t nBlocks, int32_t GPUd() void GPUTPCCFCheckPadBaseline::CheckBaselineCPU(int32_t nBlocks, int32_t nThreads, int32_t iBlock, int32_t iThread, GPUSharedMemory& smem, processorType& clusterer) { #ifndef GPUCA_GPUCODE - const CfFragment& fragment = clusterer.mPmemory->fragment; - CfArray2D chargeMap(reinterpret_cast(clusterer.mPchargeMap)); - - CfChargePos basePos(iBlock * PadsPerCacheline, 0); - - constexpr GPUTPCGeometry geo; - if (basePos.pad() >= geo.NPads(basePos.row())) { + if (iBlock >= (int32_t)GPUTPCGeometry::NROWS) { return; } - constexpr size_t ElemsInTileRow = (size_t)TilingLayout>::WidthInTiles * TimebinsPerCacheline * PadsPerCacheline; + constexpr GPUTPCGeometry geo; + const int32_t row = iBlock; + const int32_t nPads = geo.NPads(row); + const int32_t nVecPads = (nPads + PadsPerCacheline - 1) / PadsPerCacheline; + + const CfFragment& fragment = clusterer.mPmemory->fragment; + const bool hipFilterOn = clusterer.Param().rec.tpc.hipTailFilter; + const Charge hipTailThreshold = clusterer.Param().rec.tpc.hipTailFilterThreshold; + const Charge hipTailFilterAlpha = clusterer.Param().rec.tpc.hipTailFilterAlpha; + auto* nHIPTails = clusterer.mPnHIPTails; + auto* hipTails = GetHIPTails(clusterer, row); + + CfArray2D chargeMap(reinterpret_cast(clusterer.mPchargeMap)); using UShort8 = Vc::fixed_size_simd; + using Short8 = Vc::fixed_size_simd; using Charge8 = Vc::fixed_size_simd; - UShort8 totalCharges{Vc::Zero}; - UShort8 consecCharges{Vc::Zero}; - UShort8 maxConsecCharges{Vc::Zero}; - Charge8 maxCharge{Vc::Zero}; - - tpccf::TPCFragmentTime t = fragment.firstNonOverlapTimeBin(); - - // Access packed charges as raw integers. We throw away the PackedCharge type here to simplify vectorization. - const uint16_t* packedChargeStart = reinterpret_cast(&chargeMap[basePos.delta({0, t})]); - - for (; t < fragment.lastNonOverlapTimeBin(); t += TimebinsPerCacheline) { - for (tpccf::TPCFragmentTime localtime = 0; localtime < TimebinsPerCacheline; localtime++) { - const UShort8 packedCharges{packedChargeStart + PadsPerCacheline * localtime, Vc::Aligned}; - const UShort8::mask_type isCharge = packedCharges != 0; - - if (isCharge.isNotEmpty()) { - totalCharges(isCharge)++; - consecCharges += 1; - consecCharges(not isCharge) = 0; - maxConsecCharges = Vc::max(consecCharges, maxConsecCharges); - - // Manually unpack charges to float. - // Duplicated from PackedCharge::unpack to generate vectorized code: - // Charge unpack() const { return Charge(mVal & ChargeMask) / Charge(1 << DecimalBits); } - // Note that PackedCharge has to cut off the highest 2 bits via ChargeMask as they are used for flags by the cluster finder - // and are not part of the charge value. We can skip this step because the cluster finder hasn't run yet - // and thus these bits are guarenteed to be zero. - const Charge8 unpackedCharges = Charge8(packedCharges) / Charge(1 << PackedCharge::DecimalBits); - maxCharge = Vc::max(maxCharge, unpackedCharges); - } else { - consecCharges = 0; - } + std::vector totalChargesV(nVecPads, UShort8{Vc::Zero}); + std::vector consecChargesV(nVecPads, UShort8{Vc::Zero}); + std::vector maxConsecChargesV(nVecPads, UShort8{Vc::Zero}); + std::vector maxChargeV(nVecPads, Charge8{Vc::Zero}); + + std::vector localHipTbV(nVecPads, -1); + std::vector broadcastHipTbV(nVecPads, -1); + std::vector aboveThresholdStartV(nVecPads, -1); + std::vector activeHIPTailStartV(nVecPads, -1); + std::vector activeHIPTailEndV(nVecPads, -1); + std::vector tailFilterChargeV(nVecPads, Charge8{Vc::Zero}); + + for (int16_t t = 0; t < fragment.length; t += NumOfCachedTBs) { + + bool hasAnyTrigger = false; + + // Run actual noisy pad filter and look for HIP trigger + for (int16_t iVecPad = 0; iVecPad < nVecPads; iVecPad++) { + + auto totalCharges = totalChargesV[iVecPad]; + auto consecCharges = consecChargesV[iVecPad]; + auto maxConsecCharges = maxConsecChargesV[iVecPad]; + auto maxCharge = maxChargeV[iVecPad]; + + auto hipTb = Short8(-1); + auto aboveThresholdStart = aboveThresholdStartV[iVecPad]; + auto activeHIPTailStart = activeHIPTailStartV[iVecPad]; + auto activeHIPTailEnd = activeHIPTailEndV[iVecPad]; + auto tailFilterCharge = tailFilterChargeV[iVecPad]; + + const CfChargePos basePos(row, iVecPad * PadsPerCacheline, t); + + for (tpccf::TPCFragmentTime localtime = 0; localtime < NumOfCachedTBs; localtime++) { + + const uint16_t* packedChargeStart = reinterpret_cast(&chargeMap[basePos.delta({0, localtime})]); + const UShort8 packedCharges = t + localtime < fragment.length + ? UShort8{packedChargeStart, Vc::Aligned} + : UShort8{Vc::Zero}; + const auto isCharge = packedCharges != 0; + + const auto unpackedCharges = Charge8(packedCharges) / Charge(1 << PackedCharge::DecimalBits); + + if (isCharge.isNotEmpty()) { + totalCharges(isCharge)++; + consecCharges += 1; + consecCharges(not isCharge) = 0; + maxConsecCharges = Vc::max(consecCharges, maxConsecCharges); + + // Manually unpack charges to float. + // Duplicated from PackedCharge::unpack to generate vectorized code: + // Charge unpack() const { return Charge(mVal & ChargeMask) / Charge(1 << DecimalBits); } + // Note that PackedCharge has to cut off the highest 2 bits via ChargeMask as they are used for flags by the cluster finder + // and are not part of the charge value. We can skip this step because the cluster finder hasn't run yet + // and thus these bits are guarenteed to be zero. + maxCharge = Vc::max(maxCharge, unpackedCharges); + + const auto aboveRisingEdge = unpackedCharges >= hipTailThreshold; + const auto startRisingEdge = aboveRisingEdge && aboveThresholdStart < 0; + aboveThresholdStart(startRisingEdge) = t + localtime; + aboveThresholdStart(!aboveRisingEdge) = -1; + + const auto hasNewTrigger = hipTb < 0 && unpackedCharges >= Charge(MaxADC); + hipTb(hasNewTrigger) = aboveThresholdStart; + hasAnyTrigger |= hasNewTrigger.isNotEmpty(); + } else { + consecCharges = 0; + aboveThresholdStart = -1; + } + + const auto tailOpen = activeHIPTailStart > -1 && activeHIPTailEnd < 0; + tailFilterCharge(tailOpen) = tailFilterCharge + hipTailFilterAlpha * (unpackedCharges - tailFilterCharge); + activeHIPTailEnd(tailOpen && tailFilterCharge < hipTailThreshold) = t + localtime; + } // for (tpccf::TPCFragmentTime localtime = 0; localtime < TimebinsPerCacheline; localtime++) + + totalChargesV[iVecPad] = totalCharges; + consecChargesV[iVecPad] = consecCharges; + maxConsecChargesV[iVecPad] = maxConsecCharges; + maxChargeV[iVecPad] = maxCharge; + + localHipTbV[iVecPad] = hipTb; + aboveThresholdStartV[iVecPad] = aboveThresholdStart; + activeHIPTailStartV[iVecPad] = activeHIPTailStart; + activeHIPTailEndV[iVecPad] = activeHIPTailEnd; + tailFilterChargeV[iVecPad] = tailFilterCharge; + + } // for (int16_t iVecPad = 0; iVecPad < nVecPads; iVecPad++) + + if (hasAnyTrigger) { + broadcastHipTbV = localHipTbV; } - packedChargeStart += ElemsInTileRow; - } + // Broadcast trigger times to neighboring pads across the whole + for (int16_t iVecPad = 0; iVecPad < nVecPads && hasAnyTrigger; iVecPad++) { + + const auto hipTb = localHipTbV[iVecPad]; + + const auto hasHipTrigger = hipTb > -1; + if (hasHipTrigger.isNotEmpty()) [[unlikely]] { + + // TODO: This could be vectorised, but doesn't seem necessary + for (uint16_t p = 0; p < PadsPerCacheline; p++) { + if (hasHipTrigger[p]) { + const int16_t pad = iVecPad * PadsPerCacheline + p; + const int16_t neighborSt = CAMath::Max(0, pad - SSClusterPadWidth); + const int16_t neighborEnd = CAMath::Min(nPads, pad + SSClusterPadWidth + 1); + for (int16_t np = neighborSt; np < neighborEnd; np++) { + if (np == pad) { + continue; + } + const auto pv = np / PadsPerCacheline; + const auto pi = np % PadsPerCacheline; + // GPU keeps a pad's own trigger time; only pads without a local trigger inherit from neighbors. + if (localHipTbV[pv][pi] < 0) { + broadcastHipTbV[pv][pi] = CAMath::Max(hipTb[p], broadcastHipTbV[pv][pi]); + } + } // for (int16_t np = neighborSt; np < neighborEnd; np++) + } // if (hasHipTrigger[p]) { + } // for (uint16_t p = 0; p < PadsPerCacheline; p++) + } // if (hasHipTrigger.isNotEmpty()) + } // for (int16_t iVecPad = 0; iVecPad < nVecPads; iVecPad++) + + // Close old tails for all pads, open new tails in case of overlap + for (int16_t iVecPad = 0; iVecPad < nVecPads && hasAnyTrigger; iVecPad++) { + + auto hipTb = broadcastHipTbV[iVecPad]; + auto aboveThresholdStart = aboveThresholdStartV[iVecPad]; + auto activeHIPTailStart = activeHIPTailStartV[iVecPad]; + auto activeHIPTailEnd = activeHIPTailEndV[iVecPad]; + auto tailFilterCharge = tailFilterChargeV[iVecPad]; + + const auto shouldCloseTail = hipTb > -1 && activeHIPTailStart > -1; + activeHIPTailEnd(shouldCloseTail && activeHIPTailEnd < 0) = hipTb; + + // Closing tails will store them to global memory and zero the range + // So it's enough to disable this part to fully disable the tail filter + if (hipFilterOn && shouldCloseTail.isNotEmpty()) { + for (int16_t p = 0; p < PadsPerCacheline; p++) { + const int16_t pad = iVecPad * PadsPerCacheline + p; + if (shouldCloseTail[p] && pad < nPads) { + Charge tailQtot = 0; + Charge tailQMax = 0; + for (int16_t tt = activeHIPTailStart[p]; tt < activeHIPTailEnd[p]; tt++) { + const CfChargePos basePos(row, iVecPad * PadsPerCacheline, 0); + const auto pos = basePos.delta({p, tt}); + const auto q = chargeMap[pos].unpack(); + tailQtot += q; + tailQMax = CAMath::Max(tailQMax, q); + chargeMap[pos] = PackedCharge{0}; + } + + if (activeHIPTailEnd[p] > activeHIPTailStart[p]) { // Prune empty tails + const auto tailIdx = CAMath::AtomicAdd(&nHIPTails[row], 1) + 1; + if (tailIdx < GPUTPCCFHIPTailConnector::MaxHIPTailsPerRow) { + hipTails[tailIdx] = { + .iPrev = 0, + .iNext = 0, + .pad = uint16_t(pad), + .tailStart = uint16_t(activeHIPTailStart[p]), + .tailEnd = uint16_t(activeHIPTailEnd[p]), + .qTot = tailQtot, + .qMax = tailQMax, + }; + } + } + + } // if (shouldCloseTail[p] && pad < nPads) + } // for (uint16_t p = 0; p < PadsPerCacheline; p++) + } // if (shouldCloseThipFilterOn && shouldCloseTail.isNotEmpty()) + + activeHIPTailStart(hipTb > -1) = hipTb; + activeHIPTailEnd(hipTb > -1) = -1; + tailFilterCharge(hipTb > -1) = MaxADC; + + aboveThresholdStartV[iVecPad] = aboveThresholdStart; + activeHIPTailStartV[iVecPad] = activeHIPTailStart; + activeHIPTailEndV[iVecPad] = activeHIPTailEnd; + tailFilterChargeV[iVecPad] = tailFilterCharge; + + } // for (int32_t iVecPad = 0; iVecPad < nVecPads; iVecPad++) + } // for (auto t = 0; t < fragment.length; t += TimebinsPerCacheline) + + // Close old tails for all pads, open new tails in case of overlap + for (int16_t iVecPad = 0; iVecPad < nVecPads; iVecPad++) { + + auto activeHIPTailStart = activeHIPTailStartV[iVecPad]; + auto activeHIPTailEnd = activeHIPTailEndV[iVecPad]; + + const auto shouldCloseTail = activeHIPTailStart > -1; + activeHIPTailEnd(shouldCloseTail && activeHIPTailEnd < 0) = fragment.length; + + if (hipFilterOn && shouldCloseTail.isNotEmpty()) { + for (int16_t p = 0; p < PadsPerCacheline; p++) { + const int16_t pad = iVecPad * PadsPerCacheline + p; + if (shouldCloseTail[p] && pad < nPads) { + Charge tailQtot = 0; + Charge tailQMax = 0; + for (int16_t tt = activeHIPTailStart[p]; tt < activeHIPTailEnd[p]; tt++) { + const CfChargePos basePos(row, iVecPad * PadsPerCacheline, 0); + const auto pos = basePos.delta({p, tt}); + const auto q = chargeMap[pos].unpack(); + tailQtot += q; + tailQMax = CAMath::Max(tailQMax, q); + chargeMap[pos] = PackedCharge{0}; + } + + if (activeHIPTailEnd[p] > activeHIPTailStart[p]) { // Prune empty tails + const auto tailIdx = CAMath::AtomicAdd(&nHIPTails[row], 1) + 1; + if (tailIdx < GPUTPCCFHIPTailConnector::MaxHIPTailsPerRow) { + hipTails[tailIdx] = { + .iPrev = 0, + .iNext = 0, + .pad = uint16_t(pad), + .tailStart = uint16_t(activeHIPTailStart[p]), + .tailEnd = uint16_t(activeHIPTailEnd[p]), + .qTot = tailQtot, + .qMax = tailQMax, + }; + } + } + + } // if (shouldCloseTail[p] && pad < nPads) + } // for (uint16_t p = 0; p < PadsPerCacheline; p++) + } // if (hipFilterOn && shouldCloseTail.isNotEmpty()) + } // for (int16_t iVecPad = 0; iVecPad < nVecPads; iVecPad++) + + for (int32_t iVecPad = 0; iVecPad < nVecPads; iVecPad++) { + + const UShort8 totalCharges = totalChargesV[iVecPad]; + const UShort8 maxConsecCharges = maxConsecChargesV[iVecPad]; + const Charge8 maxCharge = maxChargeV[iVecPad]; + + const CfChargePos basePos(row, iVecPad * PadsPerCacheline, 0); - for (tpccf::Pad localpad = 0; localpad < PadsPerCacheline; localpad++) { - updatePadBaseline(basePos.gpad + localpad, clusterer, totalCharges[localpad], maxConsecCharges[localpad], maxCharge[localpad]); + for (tpccf::Pad localpad = 0; localpad < PadsPerCacheline; localpad++) { + updatePadBaseline(basePos.gpad + localpad, clusterer, totalCharges[localpad], maxConsecCharges[localpad], maxCharge[localpad]); + } } #endif } @@ -463,16 +668,23 @@ GPUd() void GPUTPCCFHIPTailConnector::Thread<0>(int32_t nBlocks, int32_t nThread #ifdef GPUCA_DETERMINISTIC_MODE // Races in tail comparisons and atomic swap can lead to slightly different clusters. // So need a sequential fallback for deterministic mode - if (iThread > 0) { - return; - } - nThreads = 1; GPUCommonAlgorithm::sortInBlock(tails + 1, tails + nTails + 1, [](auto&& t1, auto&& t2) { if (t1.pad != t2.pad) { return t1.pad < t2.pad; + } else if (t1.tailStart != t2.tailStart) { + return t1.tailStart < t2.tailStart; + } else if (t1.tailEnd != t2.tailEnd) { + return t1.tailEnd < t2.tailEnd; + } else if (t1.qTot != t2.qTot) { + return t1.qTot < t2.qTot; + } else { + return t1.qMax < t2.qMax; } - return t1.tailStart < t2.tailStart; }); + if (iThread > 0) { + return; + } + nThreads = 1; #endif for (uint32_t iTail = iThread + 1; iTail <= nTails; iTail += nThreads) { diff --git a/GPU/GPUTracking/TPCClusterFinder/GPUTPCCFCheckPadBaseline.h b/GPU/GPUTracking/TPCClusterFinder/GPUTPCCFCheckPadBaseline.h index c2c5a1e339256..08e6110ca2373 100644 --- a/GPU/GPUTracking/TPCClusterFinder/GPUTPCCFCheckPadBaseline.h +++ b/GPU/GPUTracking/TPCClusterFinder/GPUTPCCFCheckPadBaseline.h @@ -126,14 +126,6 @@ class GPUTPCCFCheckPadBaseline : public GPUKernelTemplate return gpudatatypes::RecoStep::TPCClusterFinding; } - static int32_t GetNBlocks(bool isGPU) - { - // Important to exclude rightmost padding from Pad Filter. - // There's nothing to filter there and padding is counted as start of a row, so it causes an overflow in the row count. - const int32_t nBlocksCPU = (TPC_CLUSTERER_STRIDED_PAD_COUNT - GPUCF_PADDING_PAD) / PadsPerCacheline; - return isGPU ? GPUTPCGeometry::NROWS : nBlocksCPU; - } - template GPUd() static void Thread(int32_t nBlocks, int32_t nThreads, int32_t iBlock, int32_t iThread, GPUSharedMemory& smem, processorType& clusterer); diff --git a/GPU/GPUTracking/utils/VcShim.h b/GPU/GPUTracking/utils/VcShim.h index 2bbc1d471bbbb..b51210100c5a6 100644 --- a/GPU/GPUTracking/utils/VcShim.h +++ b/GPU/GPUTracking/utils/VcShim.h @@ -27,6 +27,7 @@ #include #include #include +#include namespace Vc { @@ -59,6 +60,7 @@ class WriteMaskVector V& mVec; public: + using vector_type = V; using value_type = typename V::value_type; WriteMaskVector(V& v, const M& m) : mMask(m), mVec(v) {} @@ -78,6 +80,15 @@ class WriteMaskVector } return *this; } + + WriteMaskVector& operator=(const vector_type& v) + { + for (size_t i = 0; i < mVec.size(); i++) { + if (mMask[i]) + mVec[i] = v[i]; + } + return *this; + } }; inline void prefetchMid(const void*) {} @@ -86,7 +97,7 @@ inline void prefetchForOneRead(const void*) {} } // namespace Common -template +template class fixed_size_simd_mask { private: @@ -95,7 +106,7 @@ class fixed_size_simd_mask public: bool isNotEmpty() const { return mData.any(); } - std::bitset::reference operator[](size_t i) { return mData[i]; } + typename std::bitset::reference operator[](size_t i) { return mData[i]; } bool operator[](size_t i) const { return mData[i]; } fixed_size_simd_mask operator!() const @@ -104,6 +115,13 @@ class fixed_size_simd_mask o.mData.flip(); return o; } + + fixed_size_simd_mask operator&&(const fixed_size_simd_mask& o) const + { + auto r = *this; + r.mData &= o.mData; + return r; + } }; template @@ -115,7 +133,7 @@ class fixed_size_simd public: using vector_type = std::array; using value_type = T; - using mask_type = fixed_size_simd_mask; + using mask_type = fixed_size_simd_mask; static constexpr size_t size() { return N; } @@ -130,6 +148,8 @@ class fixed_size_simd fixed_size_simd(const T* d, AlignedTag) { std::copy_n(d, N, mData.begin()); } + fixed_size_simd(const T& x) { mData.fill(x); } + T& operator[](size_t i) { return mData[i]; } const T& operator[](size_t i) const { return mData[i]; } @@ -149,6 +169,44 @@ class fixed_size_simd return *this; } + template + fixed_size_simd& operator+=(const fixed_size_simd& v) + { + for (size_t i = 0; i < N; i++) + mData[i] += v[i]; + return *this; + } + + fixed_size_simd& operator-=(const T& v) + { + for (auto& x : mData) + x -= v; + return *this; + } + + template + fixed_size_simd& operator-=(const fixed_size_simd& v) + { + for (size_t i = 0; i < N; i++) + mData[i] -= v[i]; + return *this; + } + + fixed_size_simd& operator*=(const T& v) + { + for (auto& x : mData) + x *= v; + return *this; + } + + template + fixed_size_simd& operator*=(const fixed_size_simd& v) + { + for (size_t i = 0; i < N; i++) + mData[i] *= v[i]; + return *this; + } + fixed_size_simd& operator/=(const T& v) { for (auto& x : mData) @@ -156,10 +214,12 @@ class fixed_size_simd return *this; } - fixed_size_simd operator/(const T& v) const + template + fixed_size_simd& operator/=(const fixed_size_simd& v) { - auto x = *this; - return x /= v; + for (size_t i = 0; i < N; i++) + mData[i] /= v[i]; + return *this; } mask_type operator==(const T& v) const @@ -172,10 +232,124 @@ class fixed_size_simd mask_type operator!=(const T& v) const { return !(*this == v); } + mask_type operator>(const T& v) const + { + mask_type m; + for (size_t i = 0; i < N; i++) + m[i] = mData[i] > v; + return m; + } + + mask_type operator>=(const T& v) const + { + mask_type m; + for (size_t i = 0; i < N; i++) + m[i] = mData[i] >= v; + return m; + } + + mask_type operator<(const T& v) const + { + mask_type m; + for (size_t i = 0; i < N; i++) + m[i] = mData[i] < v; + return m; + } + friend vector_type& internal_data<>(fixed_size_simd& x); friend const vector_type& internal_data<>(const fixed_size_simd& x); }; +template +struct is_fixed_size_simd : std::false_type { +}; + +template +struct is_fixed_size_simd> : std::true_type { +}; + +template +using EnableIfScalar = typename std::enable_if_t< + !is_fixed_size_simd>::value && std::is_convertible_v, int>; + +template +fixed_size_simd operator+(fixed_size_simd a, const fixed_size_simd& b) +{ + return a += b; +} + +template = 0> +fixed_size_simd operator+(fixed_size_simd a, const S& b) +{ + return a += static_cast(b); +} + +template = 0> +fixed_size_simd operator+(const S& a, fixed_size_simd b) +{ + return b += static_cast(a); +} + +template +fixed_size_simd operator-(fixed_size_simd a, const fixed_size_simd& b) +{ + return a -= b; +} + +template = 0> +fixed_size_simd operator-(fixed_size_simd a, const S& b) +{ + return a -= static_cast(b); +} + +template = 0> +fixed_size_simd operator-(const S& a, const fixed_size_simd& b) +{ + fixed_size_simd o; + for (size_t i = 0; i < N; i++) + o[i] = static_cast(a) - b[i]; + return o; +} + +template +fixed_size_simd operator*(fixed_size_simd a, const fixed_size_simd& b) +{ + return a *= b; +} + +template = 0> +fixed_size_simd operator*(fixed_size_simd a, const S& b) +{ + return a *= static_cast(b); +} + +template = 0> +fixed_size_simd operator*(const S& a, fixed_size_simd b) +{ + return b *= static_cast(a); +} + +template +fixed_size_simd operator/(fixed_size_simd a, const fixed_size_simd& b) +{ + return a /= b; +} + +template = 0> +fixed_size_simd operator/(fixed_size_simd a, const S& b) +{ + return a /= static_cast(b); +} + +template = 0> +fixed_size_simd operator/(const S& a, const fixed_size_simd& b) +{ + fixed_size_simd o; + for (size_t i = 0; i < N; i++) + o[i] = static_cast(a) / b[i]; + return o; +} + template V max(const V& a, const V& b) { From 19d24d5b06b6b9738f882855fbcec73a1ae2170e Mon Sep 17 00:00:00 2001 From: wiechula Date: Wed, 15 Jul 2026 23:10:07 +0200 Subject: [PATCH 02/47] Make maximum number of electrons per step a configurable --- Detectors/TPC/base/include/TPCBase/ParameterGas.h | 1 + Detectors/TPC/simulation/src/Detector.cxx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Detectors/TPC/base/include/TPCBase/ParameterGas.h b/Detectors/TPC/base/include/TPCBase/ParameterGas.h index 210d8dbd14867..e5e3ab68c3eb9 100644 --- a/Detectors/TPC/base/include/TPCBase/ParameterGas.h +++ b/Detectors/TPC/base/include/TPCBase/ParameterGas.h @@ -41,6 +41,7 @@ struct ParameterGas : public o2::conf::ConfigurableParamHelper { float Pressure = 1013.25f; ///< Pressure [mbar] float Temperature = 20.0f; ///< Temperature [°C] float BetheBlochParam[5] = {0.820172e-1f, 9.94795f, 8.97292e-05f, 2.05873f, 1.65272f}; ///< Parametrization of Bethe-Bloch + int MaxElePerStep = 300; ///< maximum number of electron allowed per step, default is 300 ~10keV O2ParamDef(ParameterGas, "TPCGasParam"); }; diff --git a/Detectors/TPC/simulation/src/Detector.cxx b/Detectors/TPC/simulation/src/Detector.cxx index 1a7c0fc25802b..8c9c45b6c13a8 100644 --- a/Detectors/TPC/simulation/src/Detector.cxx +++ b/Detectors/TPC/simulation/src/Detector.cxx @@ -226,7 +226,7 @@ Bool_t Detector::ProcessHits(FairVolume* vol) const double rndm = fMC->GetRandom()->Rndm(); const double eDep = TMath::Power((kMax - kMin) * rndm + kMin, oneOverAlpha_p1); int nel_step = static_cast(((eDep - eMin) / wIon) + 1); - nel_step = TMath::Min(nel_step, 300); // 300 electrons corresponds to 10 keV + nel_step = TMath::Min(nel_step, gasParam.MaxElePerStep); // 300 electrons corresponds to 10 keV numberOfElectrons += nel_step; } From 56f11931cb2278bb982c675539431927d1f4fa63 Mon Sep 17 00:00:00 2001 From: swenzel Date: Thu, 16 Jul 2026 12:58:10 +0200 Subject: [PATCH 03/47] TPC addHits: remove short type limitation Change type from short to float to remove possible overflow errors There is no deep reason for this to be short in any case. The underlaying storage was already of type float. Fixes a problem observed in monopole simulations inside TPC --- Detectors/TPC/simulation/include/TPCSimulation/Point.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Detectors/TPC/simulation/include/TPCSimulation/Point.h b/Detectors/TPC/simulation/include/TPCSimulation/Point.h index dd477d1d20c33..1ce7fdc9f1a35 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/Point.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/Point.h @@ -117,7 +117,7 @@ class HitGroup : public o2::BaseHit ~HitGroup() = default; - void addHit(float x, float y, float z, float time, short e) + void addHit(float x, float y, float z, float time, float e) { #ifdef HIT_AOS mHits.emplace_back(x, y, z, time, e); From 7d9a516f256b59e91d62f0741113a3a90aa42ea1 Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Wed, 15 Jul 2026 19:05:20 +0200 Subject: [PATCH 04/47] Fix for Loopers identified as primaries --- Generators/src/TPCLoopers.cxx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Generators/src/TPCLoopers.cxx b/Generators/src/TPCLoopers.cxx index f8d001588b8f3..41551ab483660 100644 --- a/Generators/src/TPCLoopers.cxx +++ b/Generators/src/TPCLoopers.cxx @@ -286,15 +286,14 @@ std::vector GenTPCLoopers::importParticles() p_etot = TMath::Sqrt(px_p * px_p + py_p * py_p + pz_p * pz_p + mass_p * mass_p); // Push the electron TParticle electron(11, 1, -1, -1, -1, -1, px_e, py_e, pz_e, e_etot, vx, vy, vz, time / 1e9); - electron.SetStatusCode(o2::mcgenstatus::MCGenStatusEncoding(electron.GetStatusCode(), 0).fullEncoding); - electron.SetBit(ParticleStatus::kToBeDone, // - o2::mcgenstatus::getHepMCStatusCode(electron.GetStatusCode()) == 1); + // Setting HepMC status code != 1 to avoid detecting the loopers as physical primaries + electron.SetStatusCode(o2::mcgenstatus::MCGenStatusEncoding(2, 0).fullEncoding); + electron.SetBit(ParticleStatus::kToBeDone, true); particles.push_back(electron); // Push the positron TParticle positron(-11, 1, -1, -1, -1, -1, px_p, py_p, pz_p, p_etot, vx, vy, vz, time / 1e9); - positron.SetStatusCode(o2::mcgenstatus::MCGenStatusEncoding(positron.GetStatusCode(), 0).fullEncoding); - positron.SetBit(ParticleStatus::kToBeDone, // - o2::mcgenstatus::getHepMCStatusCode(positron.GetStatusCode()) == 1); + positron.SetStatusCode(o2::mcgenstatus::MCGenStatusEncoding(2, 0).fullEncoding); + positron.SetBit(ParticleStatus::kToBeDone, true); particles.push_back(positron); } // Get compton electrons from the event @@ -312,9 +311,9 @@ std::vector GenTPCLoopers::importParticles() etot = TMath::Sqrt(px * px + py * py + pz * pz + mass_e * mass_e); // Push the electron TParticle electron(11, 1, -1, -1, -1, -1, px, py, pz, etot, vx, vy, vz, time / 1e9); - electron.SetStatusCode(o2::mcgenstatus::MCGenStatusEncoding(electron.GetStatusCode(), 0).fullEncoding); - electron.SetBit(ParticleStatus::kToBeDone, // - o2::mcgenstatus::getHepMCStatusCode(electron.GetStatusCode()) == 1); + // Setting HepMC status code != 1 to avoid detecting the loopers as physical primaries + electron.SetStatusCode(o2::mcgenstatus::MCGenStatusEncoding(2, 0).fullEncoding); + electron.SetBit(ParticleStatus::kToBeDone, true); particles.push_back(electron); } From 6ac7a7a863a2ffb588cd7b4d2c89b16b5ba5323e Mon Sep 17 00:00:00 2001 From: shahor02 Date: Fri, 17 Jul 2026 12:49:35 +0400 Subject: [PATCH 05/47] Properly discard ITS hits preceding readout start (#15601) * Properly discard ITS hits preceding readout start * Properly discard ITS3 hits preceding RO start --- .../simulation/include/ITSMFTSimulation/Digitizer.h | 2 +- Detectors/ITSMFT/common/simulation/src/Digitizer.cxx | 10 ++++------ .../ITS3/simulation/include/ITS3Simulation/Digitizer.h | 2 +- Detectors/Upgrades/ITS3/simulation/src/Digitizer.cxx | 8 +++----- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/Detectors/ITSMFT/common/simulation/include/ITSMFTSimulation/Digitizer.h b/Detectors/ITSMFT/common/simulation/include/ITSMFTSimulation/Digitizer.h index c81e2d9476644..6c1caa4a0b1e9 100644 --- a/Detectors/ITSMFT/common/simulation/include/ITSMFTSimulation/Digitizer.h +++ b/Detectors/ITSMFT/common/simulation/include/ITSMFTSimulation/Digitizer.h @@ -127,7 +127,7 @@ class Digitizer : public TObject uint32_t mROFrameMin = 0; ///< lowest RO frame of current digits uint32_t mROFrameMax = 0; ///< highest RO frame of current digits uint32_t mNewROFrame = 0; ///< ROFrame corresponding to provided time - bool mIsBeforeFirstRO = false; + int mROFsWrtFirstRO = 0; uint32_t mEventROFrameMin = 0xffffffff; ///< lowest RO frame for processed events (w/o automatic noise ROFs) uint32_t mEventROFrameMax = 0; ///< highest RO frame forfor processed events (w/o automatic noise ROFs) diff --git a/Detectors/ITSMFT/common/simulation/src/Digitizer.cxx b/Detectors/ITSMFT/common/simulation/src/Digitizer.cxx index b1a92e988968b..5480392600074 100644 --- a/Detectors/ITSMFT/common/simulation/src/Digitizer.cxx +++ b/Detectors/ITSMFT/common/simulation/src/Digitizer.cxx @@ -164,15 +164,13 @@ void Digitizer::setEventTime(const o2::InteractionTimeRecord& irt, int layer) // we might get interactions to digitize from before // the first sampled IR + mROFsWrtFirstRO = std::floor(float(nbc) / mParams.getROFrameLengthInBC(layer)); if (nbc < 0) { - mNewROFrame = 0; - // this event is before the first RO - mIsBeforeFirstRO = true; + mNewROFrame = 0; // this event is before the first RO } else { mNewROFrame = nbc / mParams.getROFrameLengthInBC(layer); - mIsBeforeFirstRO = false; } - LOG(debug) << " NewROFrame " << mNewROFrame << " nbc " << nbc; + LOG(debug) << " NewROFrame " << mNewROFrame << " nbc " << nbc << " ROFsWrtFirstRO " << mROFsWrtFirstRO; // in continuous mode depends on starts of periodic readout frame mCollisionTimeWrtROF += (nbc % mParams.getROFrameLengthInBC(layer)) * o2::constants::lhc::LHCBunchSpacingNS; @@ -279,7 +277,7 @@ void Digitizer::processHit(const o2::itsmft::Hit& hit, uint32_t& maxFr, int evID if (isContinuous()) { timeInROF += mCollisionTimeWrtROF; } - if (mIsBeforeFirstRO && timeInROF < 0) { + if (mROFsWrtFirstRO < -1 || (mROFsWrtFirstRO == -1 && timeInROF < 0)) { // disregard this hit because it comes from an event before readout starts and it does not effect this RO return; } diff --git a/Detectors/Upgrades/ITS3/simulation/include/ITS3Simulation/Digitizer.h b/Detectors/Upgrades/ITS3/simulation/include/ITS3Simulation/Digitizer.h index 78bb9923dae97..90dda6fd67393 100644 --- a/Detectors/Upgrades/ITS3/simulation/include/ITS3Simulation/Digitizer.h +++ b/Detectors/Upgrades/ITS3/simulation/include/ITS3Simulation/Digitizer.h @@ -111,7 +111,7 @@ class Digitizer : public TObject uint32_t mROFrameMin = 0; ///< lowest RO frame of current digits uint32_t mROFrameMax = 0; ///< highest RO frame of current digits uint32_t mNewROFrame = 0; ///< ROFrame corresponding to provided time - bool mIsBeforeFirstRO = false; + int mROFsWrtFirstRO = 0; uint32_t mEventROFrameMin = 0xffffffff; ///< lowest RO frame for processed events (w/o automatic noise ROFs) uint32_t mEventROFrameMax = 0; ///< highest RO frame forfor processed events (w/o automatic noise ROFs) diff --git a/Detectors/Upgrades/ITS3/simulation/src/Digitizer.cxx b/Detectors/Upgrades/ITS3/simulation/src/Digitizer.cxx index 6fbed92bb1400..9d048ea1f31ca 100644 --- a/Detectors/Upgrades/ITS3/simulation/src/Digitizer.cxx +++ b/Detectors/Upgrades/ITS3/simulation/src/Digitizer.cxx @@ -128,15 +128,13 @@ void Digitizer::setEventTime(const o2::InteractionTimeRecord& irt, int layer) } // we might get interactions to digitize from before // the first sampled IR + mROFsWrtFirstRO = std::floor(float(nbc) / mParams.getROFrameLengthInBC(layer)); if (nbc < 0) { mNewROFrame = 0; - // this event is before the first RO - mIsBeforeFirstRO = true; } else { mNewROFrame = nbc / mParams.getROFrameLengthInBC(layer); - mIsBeforeFirstRO = false; } - LOG(debug) << " NewROFrame " << mNewROFrame << " nbc " << nbc; + LOG(debug) << " NewROFrame " << mNewROFrame << " nbc " << nbc << " ROFsWrtFirstRO " << mROFsWrtFirstRO; // in continuous mode depends on starts of periodic readout frame mCollisionTimeWrtROF += (nbc % mParams.getROFrameLengthInBC(layer)) * o2::constants::lhc::LHCBunchSpacingNS; @@ -240,7 +238,7 @@ void Digitizer::processHit(const o2::itsmft::Hit& hit, uint32_t& maxFr, int evID if (isContinuous()) { timeInROF += mCollisionTimeWrtROF; } - if (mIsBeforeFirstRO && timeInROF < 0) { + if (mROFsWrtFirstRO < -1 || (mROFsWrtFirstRO == -1 && timeInROF < 0)) { // disregard this hit because it comes from an event before readout starts and it does not effect this RO return; } From f5f2e146929d02302ecf75214773541b30a84ebb Mon Sep 17 00:00:00 2001 From: David Rohr Date: Tue, 14 Jul 2026 10:42:10 +0200 Subject: [PATCH 06/47] Fix compiler warning --- GPU/GPUTracking/TRDTracking/GPUTRDTracker.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GPU/GPUTracking/TRDTracking/GPUTRDTracker.cxx b/GPU/GPUTracking/TRDTracking/GPUTRDTracker.cxx index 324acc9451f36..f5f8f08b1138e 100644 --- a/GPU/GPUTracking/TRDTracking/GPUTRDTracker.cxx +++ b/GPU/GPUTracking/TRDTracking/GPUTRDTracker.cxx @@ -1056,7 +1056,7 @@ GPUd() void GPUTRDTracker_t::RecalcTrkltCovDy(const float tilt, co { float t2 = tilt * tilt; // tan^2 (tilt) float c2 = 1.f / (1.f + t2); // cos^2 (tilt) - float sy2 = mRecoParam->getRPhiRes(snp, CAMath::Abs(pull), occupancy); + // float sy2 = mRecoParam->getRPhiRes(snp, CAMath::Abs(pull), occupancy); float sdy2 = mRecoParam->getDyRes(snp, occupancy); cov[3] = mRecoParam->getCorrYDy() * CAMath::Sqrt(sdy2 * c2); cov[4] = -tilt * mRecoParam->getCorrYDy() * CAMath::Sqrt(sdy2 * c2); From 7dc1b63d0b1eb7077761332d8ffc4f2a3129fef3 Mon Sep 17 00:00:00 2001 From: David Rohr Date: Tue, 14 Jul 2026 10:42:20 +0200 Subject: [PATCH 07/47] GPU Standalone Display: Switch to Qt6 --- GPU/GPUTracking/Definitions/GPUSettingsList.h | 4 ++-- GPU/GPUTracking/Standalone/Benchmark/standalone.cxx | 1 - GPU/GPUTracking/Standalone/CMakeLists.txt | 2 +- GPU/GPUTracking/display/CMakeLists.txt | 8 ++++---- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/GPU/GPUTracking/Definitions/GPUSettingsList.h b/GPU/GPUTracking/Definitions/GPUSettingsList.h index cbd8f07752617..842576621c39c 100644 --- a/GPU/GPUTracking/Definitions/GPUSettingsList.h +++ b/GPU/GPUTracking/Definitions/GPUSettingsList.h @@ -119,7 +119,7 @@ AddOptionRTC(hipTailFilterAlpha, float, 0.5f, "", 0, "Smoothing factor for the e AddOptionRTC(occupancyMapTimeBins, uint16_t, 16, "", 0, "Number of timebins per histogram bin of occupancy map (0 = disable occupancy map)") AddOptionRTC(occupancyMapTimeBinsAverage, uint16_t, 0, "", 0, "Number of timebins +/- to use for the averaging") AddOptionRTC(trackFitCovLimit, uint16_t, 1000, "", 0, "Abort fit when y/z cov exceed the limit") -AddOptionRTC(addErrorsCECrossing, uint8_t, 0, "", 0, "Add additional custom track errors when crossing CE, 0 = no custom errors but att 0.5 to sigma_z^2, 1 = only to cov diagonal, 2 = preserve correlations") +AddOptionRTC(addErrorsCECrossing, uint8_t, 0, "", 0, "Add additional custom track errors when crossing CE, 0 = no custom errors but add 0.5 to sigma_z^2, 1 = only to cov diagonal, 2 = preserve correlations") AddOptionRTC(trackMergerMinPartHits, uint8_t, 10, "", 0, "Minimum hits of track part during track merging") AddOptionRTC(trackMergerMinTotalHits, uint8_t, 20, "", 0, "Minimum total of track part during track merging") AddOptionRTC(mergerCERowLimit, uint8_t, 5, "", 0, "Distance from first / last row in order to attempt merging accross CE") @@ -305,7 +305,7 @@ AddHelp("help", 'h') EndConfig() // Scaling factors for gpu buffer size estimation -BeginSubConfig(GPUSettingsProcessingScaling, scaling, configStandalone.proc, "SCALING", 0, "Processing settings for neural network clusterizer", proc_scaling) +BeginSubConfig(GPUSettingsProcessingScaling, scaling, configStandalone.proc, "SCALING", 0, "Memory scaling factor settings", proc_scaling) AddOption(offset, float, 1000., "", 0, "Scaling Factor: offset") AddOption(hitOffset, float, 20000, "", 0, "Scaling Factor: hitOffset") AddOption(tpcPeaksPerDigit, float, 0.2, "", 0, "Scaling Factor: tpcPeaksPerDigit") diff --git a/GPU/GPUTracking/Standalone/Benchmark/standalone.cxx b/GPU/GPUTracking/Standalone/Benchmark/standalone.cxx index a9fed2d4c9897..cfa3ad7c81b83 100644 --- a/GPU/GPUTracking/Standalone/Benchmark/standalone.cxx +++ b/GPU/GPUTracking/Standalone/Benchmark/standalone.cxx @@ -690,7 +690,6 @@ int32_t RunBenchmark(GPUReconstruction* recUse, GPUChainTracking* chainTrackingU } if (tmpRetVal == 0 && configStandalone.testSyncAsync) { - vecpod compressedTmpMem(chainTracking->mIOPtrs.tpcCompressedClusters->totalDataSize); memcpy(compressedTmpMem.data(), (const void*)chainTracking->mIOPtrs.tpcCompressedClusters, chainTracking->mIOPtrs.tpcCompressedClusters->totalDataSize); o2::tpc::CompressedClusters tmp(*chainTracking->mIOPtrs.tpcCompressedClusters); diff --git a/GPU/GPUTracking/Standalone/CMakeLists.txt b/GPU/GPUTracking/Standalone/CMakeLists.txt index 0c04f5e562fef..a4c5817d1f7f9 100644 --- a/GPU/GPUTracking/Standalone/CMakeLists.txt +++ b/GPU/GPUTracking/Standalone/CMakeLists.txt @@ -107,7 +107,7 @@ if(GPUCA_BUILD_EVENT_DISPLAY) set(Vulkan_FOUND OFF) endif() if(GPUCA_BUILD_EVENT_DISPLAY_QT) - find_package(Qt5 COMPONENTS Widgets REQUIRED) + find_package(Qt6 COMPONENTS Widgets REQUIRED) endif() else() set(OpenGL_FOUND OFF) diff --git a/GPU/GPUTracking/display/CMakeLists.txt b/GPU/GPUTracking/display/CMakeLists.txt index 82ce0d4a9b190..eb7a0dc6d6400 100644 --- a/GPU/GPUTracking/display/CMakeLists.txt +++ b/GPU/GPUTracking/display/CMakeLists.txt @@ -26,8 +26,8 @@ if(ALIGPU_BUILD_TYPE STREQUAL "O2") set_package_properties(Fontconfig PROPERTIES TYPE OPTIONAL) find_package(O2GPUWayland) set_package_properties(O2GPUWayland PROPERTIES TYPE OPTIONAL) - find_package(Qt5 COMPONENTS Widgets) - set_package_properties(Qt5 PROPERTIES TYPE OPTIONAL) + find_package(Qt6 COMPONENTS Widgets) + set_package_properties(Qt6 PROPERTIES TYPE OPTIONAL) endif() if(Vulkan_FOUND) @@ -46,7 +46,7 @@ endif() if(Freetype_FOUND) set(GPUCA_EVENT_DISPLAY_FREETYPE ON) endif() -if(Qt5_FOUND) +if(Qt6_FOUND) set(GPUCA_EVENT_DISPLAY_QT ON) endif() @@ -222,7 +222,7 @@ endif() if(GPUCA_EVENT_DISPLAY_QT) target_compile_definitions(${targetName} PRIVATE GPUCA_BUILD_EVENT_DISPLAY_QT) - target_link_libraries(${targetName} PRIVATE Qt5::Widgets) + target_link_libraries(${targetName} PRIVATE Qt6::Widgets) endif() target_link_libraries(${targetName} PRIVATE TBB::tbb) From 79391f410b33fa15b77e6f74c4897986817e3d25 Mon Sep 17 00:00:00 2001 From: shahoian Date: Fri, 17 Jul 2026 15:48:15 +0200 Subject: [PATCH 08/47] Propagate PV MCLabel to extended PV in the trackingStudy --- .../PrimaryVertexExt.h | 4 +++- .../study/src/TrackingStudy.cxx | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/DataFormats/Reconstruction/include/ReconstructionDataFormats/PrimaryVertexExt.h b/DataFormats/Reconstruction/include/ReconstructionDataFormats/PrimaryVertexExt.h index a228984f2ae5d..4fd36ed248677 100644 --- a/DataFormats/Reconstruction/include/ReconstructionDataFormats/PrimaryVertexExt.h +++ b/DataFormats/Reconstruction/include/ReconstructionDataFormats/PrimaryVertexExt.h @@ -14,6 +14,7 @@ #include "ReconstructionDataFormats/PrimaryVertex.h" #include "ReconstructionDataFormats/GlobalTrackID.h" +#include "SimulationDataFormat/MCEventLabel.h" namespace o2 { @@ -27,6 +28,7 @@ struct PrimaryVertexExt : public PrimaryVertex { std::array nSrc{}; // N contributors for each source type std::array nSrcA{}; // N associated and passing cuts for each source type std::array nSrcAU{}; // N ambgous associated and passing cuts for each source type + o2::MCEventLabel mcLb{}; double FT0Time = -1.; // time of closest FT0 trigger float FT0A = -1; // amplitude of closest FT0 A side float FT0C = -1; // amplitude of closest FT0 C side @@ -41,7 +43,7 @@ struct PrimaryVertexExt : public PrimaryVertex { std::string asString() const; #endif - ClassDefNV(PrimaryVertexExt, 6); + ClassDefNV(PrimaryVertexExt, 7); }; #ifndef GPUCA_GPUCODE_DEVICE diff --git a/Detectors/GlobalTrackingWorkflow/study/src/TrackingStudy.cxx b/Detectors/GlobalTrackingWorkflow/study/src/TrackingStudy.cxx index 881ce9041ae04..38079d36a2e84 100644 --- a/Detectors/GlobalTrackingWorkflow/study/src/TrackingStudy.cxx +++ b/Detectors/GlobalTrackingWorkflow/study/src/TrackingStudy.cxx @@ -369,6 +369,9 @@ void TrackingStudySpec::process(o2::globaltracking::RecoContainer& recoData) if (iv != nv - 1) { auto& pve = pveVec[iv]; static_cast(pve) = pvvec[iv]; + if (mUseMC) { + pve.mcLb = recoData.getPrimaryVertexMCLabel(iv); + } // find best matching FT0 signal float bestTimeDiff = 1000, bestTime = -999; int bestFTID = -1; @@ -611,13 +614,16 @@ void TrackingStudySpec::process(o2::globaltracking::RecoContainer& recoData) vid[slot] = id; }; + std::vector pveT; // neighbours in time + std::vector pveZ; // neighbours in Z + std::vector idT(mMaxNeighbours), idZ(mMaxNeighbours); + std::vector dT(mMaxNeighbours), dZ(mMaxNeighbours); for (int cnt = 0; cnt < nvtot; cnt++) { + pveT.clear(); // neighbours in time + pveZ.clear(); // neighbours in Z + const auto& pve = pveVec[cnt]; float tv = pve.getTimeStamp().getTimeStamp(); - std::vector pveT(mMaxNeighbours); // neighbours in time - std::vector pveZ(mMaxNeighbours); // neighbours in Z - std::vector idT(mMaxNeighbours), idZ(mMaxNeighbours); - std::vector dT(mMaxNeighbours), dZ(mMaxNeighbours); for (int i = 0; i < mMaxNeighbours; i++) { idT[i] = idZ[i] = -1; dT[i] = mMaxVTTimeDiff; @@ -666,14 +672,14 @@ void TrackingStudySpec::process(o2::globaltracking::RecoContainer& recoData) } for (int i = 0; i < mMaxNeighbours; i++) { if (idT[i] != -1) { - pveT[i] = pveVec[idT[i]]; + pveT.push_back(pveVec[idT[i]]); } else { break; } } for (int i = 0; i < mMaxNeighbours; i++) { if (idZ[i] != -1) { - pveZ[i] = pveVec[idZ[i]]; + pveZ.push_back(pveVec[idZ[i]]); } else { break; } From 17653800da5de12cc475a749d659853f9ccff50b Mon Sep 17 00:00:00 2001 From: Marian Ivanov Date: Sat, 18 Jul 2026 12:39:03 +0200 Subject: [PATCH 09/47] TPC: add ITS-TPC combined-momentum sampling trigger (#15599) Add a third sampling trigger bit for tracks with matched ITS-TPC combined momentum. The new trigger uses the Tsallis sampling function on the combined-track pT instead of the TPC-only pT and stores the corresponding weight as weight_ITSTPC. This extends the existing trigger mask scheme: 0x1: minimum-bias sampling 0x2: Tsallis sampling using TPC-only track pT 0x4: Tsallis sampling using ITS-TPC combined-track pT The output condition is updated so tracks selected by the new ITS-TPC sampling path are written even when they are not selected by the existing TPC-only or minimum-bias triggers. Motivation: sampling only on TPC-only momentum can bias correlated observables such as qpt_TPC - qpt_Comb. The new trigger provides a dedicated sampled stream for combined-track performance studies while preserving the existing TPC-only and minimum-bias sampling streams. Co-authored-by: miranov25 --- Detectors/TPC/workflow/src/TPCTimeSeriesSpec.cxx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Detectors/TPC/workflow/src/TPCTimeSeriesSpec.cxx b/Detectors/TPC/workflow/src/TPCTimeSeriesSpec.cxx index dce9703a6a365..dcb53c9f80ff8 100644 --- a/Detectors/TPC/workflow/src/TPCTimeSeriesSpec.cxx +++ b/Detectors/TPC/workflow/src/TPCTimeSeriesSpec.cxx @@ -1335,12 +1335,17 @@ class TPCTimeSeries : public Task if (mUnbinnedWriter && mStreamer[iThread]) { const float factorPt = mSamplingFactor; bool writeData = true; + bool writeDataITSTPC = false; float weight = 0; + float weightITSTPC = 0; if (mSampleTsallis) { std::uniform_real_distribution<> distr(0., 1.); writeData = o2::math_utils::Tsallis::downsampleTsallisCharged(tracksTPC[iTrk].getPt(), factorPt, mSqrt, weight, distr(mGenerator[iThread])); + if (hasITSTPC) { + writeDataITSTPC = o2::math_utils::Tsallis::downsampleTsallisCharged(tracksITSTPC[idxITSTPC.front()].getPt(), factorPt, mSqrt, weightITSTPC, distr(mGenerator[iThread])); + } } - if (writeData || minBiasOk) { + if (writeData || writeDataITSTPC || minBiasOk) { auto clusterMask = makeClusterBitMask(trackFull); const auto& trkOrig = tracksTPC[iTrk]; const bool isNearestVtx = (idxITSTPC.back() == -1); // is nearest vertex in case no vertex was found @@ -1395,7 +1400,11 @@ class TPCTimeSeries : public Task } } } - const int triggerMask = 0x1 * minBiasOk + 0x2 * writeData; + // triggerMask bits: + // 0x1: flat minimum-bias stream + // 0x2: Tsallis stream sampled with TPC-only pT + // 0x4: Tsallis stream sampled with ITS-TPC combined pT + const int triggerMask = 0x1 * minBiasOk + 0x2 * writeData + 0x4 * writeDataITSTPC; float deltaP2ConstrVtx = -999; float deltaP3ConstrVtx = -999; @@ -1446,6 +1455,7 @@ class TPCTimeSeries : public Task << "factorMinBias=" << factorMinBias << "factorPt=" << factorPt << "weight=" << weight + << "weight_ITSTPC=" << weightITSTPC << "dcar_tpc_vertex=" << dcaTPCAtVertex << "dcar_tpc=" << dca[0] << "dcaz_tpc=" << dca[1] From 9e7582fc04abd719e93baea0d2b23bbe63d14fac Mon Sep 17 00:00:00 2001 From: shahor02 Date: Mon, 20 Jul 2026 15:25:21 +0400 Subject: [PATCH 10/47] Improve TrackParCov covmat conversion to&from Lab Covariance (#15612) --- .../src/TrackParametrizationWithError.cxx | 211 ++++++++---------- 1 file changed, 93 insertions(+), 118 deletions(-) diff --git a/DataFormats/Reconstruction/src/TrackParametrizationWithError.cxx b/DataFormats/Reconstruction/src/TrackParametrizationWithError.cxx index ee2e96736aa6d..eb8071ec0073d 100644 --- a/DataFormats/Reconstruction/src/TrackParametrizationWithError.cxx +++ b/DataFormats/Reconstruction/src/TrackParametrizationWithError.cxx @@ -527,8 +527,9 @@ GPUd() void TrackParametrizationWithError::set(const dim3_t& xyz, const math_utils::detail::rotateZ(ver, -alp); math_utils::detail::rotateZ(mom, -alp); // - value_t pt = gpu::CAMath::Sqrt(mom[0] * mom[0] + mom[1] * mom[1]); - value_t ptI = 1.f / pt; + const value_t pt2 = mom[0] * mom[0] + mom[1] * mom[1]; + const value_t pt = gpu::CAMath::Sqrt(pt2); + const value_t ptI = 1.f / pt; this->setX(ver[0]); this->setAlpha(alp); this->setY(ver[1]); @@ -545,89 +546,53 @@ GPUd() void TrackParametrizationWithError::set(const dim3_t& xyz, const this->setSnp(-1.f + kSafe); // Protection } // - // Covariance matrix (formulas to be simplified) - value_t r = mom[0] * ptI; // cos(phi) - value_t cv34 = gpu::CAMath::Sqrt(cv[3] * cv[3] + cv[4] * cv[4]); - // - int special = 0; - value_t sgcheck = r * sn + this->getSnp() * cs; - if (gpu::CAMath::Abs(sgcheck) > 1 - kSafe) { // special case: lab phi is +-pi/2 - special = 1; - sgcheck = sgcheck < 0 ? -1.f : 1.f; - } else if (gpu::CAMath::Abs(sgcheck) < kSafe) { - sgcheck = cs < 0 ? -1.0f : 1.0f; - special = 2; // special case: lab phi is 0 + // Covariance matrix from the fixed-alpha Jacobian + // d(Y,Z,snp,tgl,q/pt) / d(X,Y,Z,Px,Py,Pz). + const value_t pt3I = ptI / pt2; + const value_t qeff = charge ? static_cast(charge) : 1.f; + + value_t cLab[6][6] = {}; + int idx = 0; + for (int i = 0; i < 6; ++i) { + for (int j = 0; j <= i; ++j) { + cLab[i][j] = cLab[j][i] = cv[idx++]; + } } - // - mC[kSigY2] = cv[0] + cv[2]; - mC[kSigZY] = (-cv[3] * sn) < 0 ? -cv34 : cv34; - mC[kSigZ2] = cv[5]; - // - value_t ptI2 = ptI * ptI; - value_t tgl2 = this->getTgl() * this->getTgl(); - if (special == 1) { - mC[kSigSnpY] = cv[6] * ptI; - mC[kSigSnpZ] = -sgcheck * cv[8] * r * ptI; - mC[kSigSnp2] = gpu::CAMath::Abs(cv[9] * r * r * ptI2); - mC[kSigTglY] = (cv[10] * this->getTgl() - sgcheck * cv[15]) * ptI / r; - mC[kSigTglZ] = (cv[17] - sgcheck * cv[12] * this->getTgl()) * ptI; - mC[kSigTglSnp] = (-sgcheck * cv[18] + cv[13] * this->getTgl()) * r * ptI2; - mC[kSigTgl2] = gpu::CAMath::Abs(cv[20] - 2 * sgcheck * cv[19] * mC[4] + cv[14] * tgl2) * ptI2; - mC[kSigQ2PtY] = cv[10] * ptI2 / r * charge; - mC[kSigQ2PtZ] = -sgcheck * cv[12] * ptI2 * charge; - mC[kSigQ2PtSnp] = cv[13] * r * ptI * ptI2 * charge; - mC[kSigQ2PtTgl] = (-sgcheck * cv[19] + cv[14] * this->getTgl()) * r * ptI2 * ptI; - mC[kSigQ2Pt2] = gpu::CAMath::Abs(cv[14] * ptI2 * ptI2); - } else if (special == 2) { - mC[kSigSnpY] = -cv[10] * ptI * cs / sn; - mC[kSigSnpZ] = cv[12] * cs * ptI; - mC[kSigSnp2] = gpu::CAMath::Abs(cv[14] * cs * cs * ptI2); - mC[kSigTglY] = (sgcheck * cv[6] * this->getTgl() - cv[15]) * ptI / sn; - mC[kSigTglZ] = (cv[17] - sgcheck * cv[8] * this->getTgl()) * ptI; - mC[kSigTglSnp] = (cv[19] - sgcheck * cv[13] * this->getTgl()) * cs * ptI2; - mC[kSigTgl2] = gpu::CAMath::Abs(cv[20] - 2 * sgcheck * cv[18] * this->getTgl() + cv[9] * tgl2) * ptI2; - mC[kSigQ2PtY] = sgcheck * cv[6] * ptI2 / sn * charge; - mC[kSigQ2PtZ] = -sgcheck * cv[8] * ptI2 * charge; - mC[kSigQ2PtSnp] = -sgcheck * cv[13] * cs * ptI * ptI2 * charge; - mC[kSigQ2PtTgl] = (-sgcheck * cv[18] + cv[9] * this->getTgl()) * ptI2 * ptI * charge; - mC[kSigQ2Pt2] = gpu::CAMath::Abs(cv[9] * ptI2 * ptI2); - } else { - double m00 = -sn; // m10=cs; - double m23 = -pt * (sn + this->getSnp() * cs / r), m43 = -pt * pt * (r * cs - this->getSnp() * sn); - double m24 = pt * (cs - this->getSnp() * sn / r), m44 = -pt * pt * (r * sn + this->getSnp() * cs); - double m35 = pt, m45 = -pt * pt * this->getTgl(); - // - if (charge) { // RS: this is a hack, proper treatment to be implemented - m43 *= charge; - m44 *= charge; - m45 *= charge; + + value_t jac[5][6] = {}; + jac[kY][0] = -sn; + jac[kY][1] = cs; + jac[kZ][2] = 1.; + + const value_t u = mom[0]; + const value_t v = mom[1]; + const value_t w = mom[2]; + const value_t dSnpDu = -u * v * pt3I; + const value_t dSnpDv = u * u * pt3I; + const value_t dTglDu = -w * u * pt3I; + const value_t dTglDv = -w * v * pt3I; + const value_t dTglDw = ptI; + const value_t dQ2PtDu = -qeff * u * pt3I; + const value_t dQ2PtDv = -qeff * v * pt3I; + + jac[kSnp][3] = dSnpDu * cs - dSnpDv * sn; + jac[kSnp][4] = dSnpDu * sn + dSnpDv * cs; + jac[kTgl][3] = dTglDu * cs - dTglDv * sn; + jac[kTgl][4] = dTglDu * sn + dTglDv * cs; + jac[kTgl][5] = dTglDw; + jac[kQ2Pt][3] = dQ2PtDu * cs - dQ2PtDv * sn; + jac[kQ2Pt][4] = dQ2PtDu * sn + dQ2PtDv * cs; + + for (int i = 0; i < kNParams; ++i) { + for (int j = 0; j <= i; ++j) { + value_t cij = 0.; + for (int k = 0; k < 6; ++k) { + for (int l = 0; l < 6; ++l) { + cij += jac[i][k] * cLab[k][l] * jac[j][l]; + } + } + mC[CovarMap[i][j]] = cij; } - // - double a1 = cv[13] - cv[9] * (m23 * m44 + m43 * m24) / m23 / m43; - double a2 = m23 * m24 - m23 * (m23 * m44 + m43 * m24) / m43; - double a3 = m43 * m44 - m43 * (m23 * m44 + m43 * m24) / m23; - double a4 = cv[14] + 2. * cv[9]; - double a5 = m24 * m24 - 2. * m24 * m44 * m23 / m43; - double a6 = m44 * m44 - 2. * m24 * m44 * m43 / m23; - // - mC[kSigSnpY] = (cv[10] * m43 - cv[6] * m44) / (m24 * m43 - m23 * m44) / m00; - mC[kSigQ2PtY] = (cv[6] / m00 - mC[kSigSnpY] * m23) / m43; - mC[kSigTglY] = (cv[15] / m00 - mC[kSigQ2PtY] * m45) / m35; - mC[kSigSnpZ] = (cv[12] * m43 - cv[8] * m44) / (m24 * m43 - m23 * m44); - mC[kSigQ2PtZ] = (cv[8] - mC[kSigSnpZ] * m23) / m43; - mC[kSigTglZ] = cv[17] / m35 - mC[kSigQ2PtZ] * m45 / m35; - mC[kSigSnp2] = gpu::CAMath::Abs((a4 * a3 - a6 * a1) / (a5 * a3 - a6 * a2)); - mC[kSigQ2Pt2] = gpu::CAMath::Abs((a1 - a2 * mC[kSigSnp2]) / a3); - mC[kSigQ2PtSnp] = (cv[9] - mC[kSigSnp2] * m23 * m23 - mC[kSigQ2Pt2] * m43 * m43) / m23 / m43; - double b1 = cv[18] - mC[kSigQ2PtSnp] * m23 * m45 - mC[kSigQ2Pt2] * m43 * m45; - double b2 = m23 * m35; - double b3 = m43 * m35; - double b4 = cv[19] - mC[kSigQ2PtSnp] * m24 * m45 - mC[kSigQ2Pt2] * m44 * m45; - double b5 = m24 * m35; - double b6 = m44 * m35; - mC[kSigTglSnp] = (b4 - b6 * b1 / b3) / (b5 - b6 * b2 / b3); - mC[kSigQ2PtTgl] = b1 / b3 - b2 * mC[kSigTglSnp] / b3; - mC[kSigTgl2] = gpu::CAMath::Abs((cv[20] - mC[kSigQ2Pt2] * (m45 * m45) - mC[kSigQ2PtTgl] * 2.f * m35 * m45) / (m35 * m35)); } checkCovariance(); } @@ -1668,42 +1633,52 @@ GPUd() bool TrackParametrizationWithError::getCovXYZPxPyPzGlo(std::arra return false; } - auto pt = this->getPt(); - value_t sn, cs; + const value_t pt = this->getPt(); + const value_t q2pt = this->getQ2Pt(); + value_t sn = 0.f, cs = 0.f; o2::math_utils::detail::sincos(this->getAlpha(), sn, cs); - auto r = gpu::CAMath::Sqrt((1. - this->getSnp()) * (1. + this->getSnp())); - auto m00 = -sn, m10 = cs; - auto m23 = -pt * (sn + this->getSnp() * cs / r), m43 = -pt * pt * (r * cs - this->getSnp() * sn); - auto m24 = pt * (cs - this->getSnp() * sn / r), m44 = -pt * pt * (r * sn + this->getSnp() * cs); - auto m35 = pt, m45 = -pt * pt * this->getTgl(); - - if (this->getSign() < 0) { - m43 = -m43; - m44 = -m44; - m45 = -m45; - } - - cv[0] = mC[0] * m00 * m00; - cv[1] = mC[0] * m00 * m10; - cv[2] = mC[0] * m10 * m10; - cv[3] = mC[1] * m00; - cv[4] = mC[1] * m10; - cv[5] = mC[2]; - cv[6] = m00 * (mC[3] * m23 + mC[10] * m43); - cv[7] = m10 * (mC[3] * m23 + mC[10] * m43); - cv[8] = mC[4] * m23 + mC[11] * m43; - cv[9] = m23 * (mC[5] * m23 + mC[12] * m43) + m43 * (mC[12] * m23 + mC[14] * m43); - cv[10] = m00 * (mC[3] * m24 + mC[10] * m44); - cv[11] = m10 * (mC[3] * m24 + mC[10] * m44); - cv[12] = mC[4] * m24 + mC[11] * m44; - cv[13] = m23 * (mC[5] * m24 + mC[12] * m44) + m43 * (mC[12] * m24 + mC[14] * m44); - cv[14] = m24 * (mC[5] * m24 + mC[12] * m44) + m44 * (mC[12] * m24 + mC[14] * m44); - cv[15] = m00 * (mC[6] * m35 + mC[10] * m45); - cv[16] = m10 * (mC[6] * m35 + mC[10] * m45); - cv[17] = mC[7] * m35 + mC[11] * m45; - cv[18] = m23 * (mC[8] * m35 + mC[12] * m45) + m43 * (mC[13] * m35 + mC[14] * m45); - cv[19] = m24 * (mC[8] * m35 + mC[12] * m45) + m44 * (mC[13] * m35 + mC[14] * m45); - cv[20] = m35 * (mC[9] * m35 + mC[13] * m45) + m45 * (mC[13] * m35 + mC[14] * m45); + const value_t snp = this->getSnp(); + const value_t csp = gpu::CAMath::Sqrt((1.f - snp) * (1.f + snp)); + const value_t pXLoc = pt * csp; + const value_t pYLoc = pt * snp; + const value_t pZ = pt * this->getTgl(); + const value_t pX = cs * pXLoc - sn * pYLoc; + const value_t pY = sn * pXLoc + cs * pYLoc; + + value_t cTr[5][5] = {}; + for (int i = 0; i < kNParams; ++i) { + for (int j = 0; j <= i; ++j) { + cTr[i][j] = cTr[j][i] = mC[CovarMap[i][j]]; + } + } + + double jac[6][5] = {}; + jac[0][kY] = -sn; + jac[1][kY] = cs; + jac[2][kZ] = 1.f; + + const value_t dPxDSnp = -pt * (cs * snp / csp + sn); + const value_t dPyDSnp = pt * (cs - sn * snp / csp); + jac[3][kSnp] = dPxDSnp; + jac[4][kSnp] = dPyDSnp; + jac[5][kTgl] = pt; + + jac[3][kQ2Pt] = -pX / q2pt; + jac[4][kQ2Pt] = -pY / q2pt; + jac[5][kQ2Pt] = -pZ / q2pt; + + int idx = 0; + for (int i = 0; i < 6; ++i) { + for (int j = 0; j <= i; ++j) { + double cij = 0.f; + for (int k = 0; k < kNParams; ++k) { + for (int l = 0; l < kNParams; ++l) { + cij += jac[i][k] * cTr[k][l] * jac[j][l]; + } + } + cv[idx++] = cij; + } + } return true; } From 775528b421ce6b9ec381a759c664cd5a2ab76fe6 Mon Sep 17 00:00:00 2001 From: shahor02 Date: Mon, 20 Jul 2026 15:25:49 +0400 Subject: [PATCH 11/47] Fix x-axis related error treatments (#15610) * Fix x-axis related error treatments Inspired by the refining (@galocco) of similar errors in the na60+ adaptation of DCAFitter (@fprino). List of changes: 1. TrackCovI now stores a full symmetric 3D information matrix. The old dummy X variance is removed. The X information is derived from the track slopes dY/dX and dZ/dX via H^T C_YZ^{-1} H. 2. XerrFactor was removed. The fit no longer creates an artificial longitudinal uncertainty from sigma_Y. 3. getTrackCovMatrix() was removed. PCA covariance is now computed from summed track information matrices, not by rotating an artificial 3D covariance with dummy X error. 4. calcPCACoefs() now multiplies the full local information matrix. Terms involving local XY and XZ correlations are included in the Ti coefficient matrices. 5. calcInverseWeight() now rotates and sums the full information matrix. All six local information elements contribute to the global 3x3 weight. 6. calcChi2Derivatives() now contracts residual derivatives with the full information matrix. The Hessian curvature term is added only to diagonal Hessian elements, matching the fact that each trajectory has second derivative only with respect to its own running parameter. 7. calcChi2DerivativesNoErr() was corrected analogously. The Gauss-Newton term contributes to all Hessian elements, while the residual-times-second-derivative term contributes only to diagonal elements. 8. calcPCACovMatrix() now returns the inverse of summed track information. The longitudinal vertex uncertainty comes from track slopes rather than from a dummy X covariance. 9. calcChi2() now uses all six information-matrix elements. Residual correlations involving X are included in the chi2 value. 10. correctTracks() now propagates mCandTr analytically to the corrected X. This keeps the candidate track state, slopes, covariance, and mTrPos synchronized between Newton iterations while avoiding full-field Propagator calls for the small Newton corrections. 11. createParentTrackParCov() now builds the parent momentum covariance from the native O2 momentum parameters (snp,tgl,q/pt). The Jacobian includes the track-frame alpha rotation, and daughter momentum covariances are summed in lab px,py,pz before the existing O2 parent TrackParCov constructor converts to the parent frame. * Add regularization for degenerate matrices * Add pulls and gen.parent TrackParCov to test output --- Common/DCAFitter/DCAFitterN_derivation.md | 552 ++++++++++++++++++ .../DCAFitter/include/DCAFitter/DCAFitterN.h | 303 +++++++--- Common/DCAFitter/test/testDCAFitterN.cxx | 12 +- 3 files changed, 777 insertions(+), 90 deletions(-) create mode 100644 Common/DCAFitter/DCAFitterN_derivation.md diff --git a/Common/DCAFitter/DCAFitterN_derivation.md b/Common/DCAFitter/DCAFitterN_derivation.md new file mode 100644 index 0000000000000..eb1c7e36c1fe2 --- /dev/null +++ b/Common/DCAFitter/DCAFitterN_derivation.md @@ -0,0 +1,552 @@ +# DCAFitterN derivation notes + +This file combines the extracted content of +`~/Downloads/DCAFitterN_derivation_for_codex.md` with the O2-specific covariance +updates made in `include/DCAFitter/DCAFitterN.h`. + +## 1. Convention check from the extracted scan + +The extracted scan starts with a matrix named `M_i` and states that it maps local +track coordinates to global coordinates: + +```text +p_i^g = M_i p_i +``` + +but the printed 2D block + +```text +[ cos(alpha_i) sin(alpha_i) ] +[ -sin(alpha_i) cos(alpha_i) ] +``` + +is the inverse of the O2 local-to-global rotation. O2 uses + +```text +R_i = +[ cos(alpha_i) -sin(alpha_i) 0 ] +[ sin(alpha_i) cos(alpha_i) 0 ] +[ 0 0 1 ] +``` + +with + +```text +p_i^g = R_i p_i, p_i = R_i^T p_i^g . +``` + +The later extracted formulas note this possible sign reversal. The code in +`DCAFitterN.h` matches the O2 convention above. In the rest of this document +`R_i` is always the O2 local-to-global rotation. + +## 2. Weighted N-prong vertex fit + +For track `i`, let the current point in its local frame be + +```text +p_i = (x_i, y_i, z_i)^T . +``` + +Let `B_i` be the inverse covariance, or information matrix, for this local +point. The fitted vertex `V` is in the global frame. Its representation in +track `i`'s local frame is `R_i^T V`, so the residual is + +```text +Delta_i = p_i - R_i^T V . +``` + +The weighted objective is + +```text +chi2 = 1/2 sum_i Delta_i^T B_i Delta_i . +``` + +For fixed track points, differentiating with respect to `V` gives + +```text +A V = sum_i R_i B_i p_i +``` + +where + +```text +A = sum_i R_i B_i R_i^T . +``` + +Therefore + +```text +V = A^{-1} sum_i R_i B_i p_i . +``` + +Define + +```text +T_i = A^{-1} R_i B_i . +``` + +Then + +```text +V = sum_i T_i p_i . +``` + +This is the `calcInverseWeight`, `calcPCACoefs`, and `calcPCA` structure in the +code. The useful identity is + +```text +sum_i T_i R_i^T = I . +``` + +## 3. Residuals after eliminating the vertex + +Substituting `V = sum_k T_k p_k` into the residual gives + +```text +Delta_i = sum_k D_ik p_k +``` + +with + +```text +D_ik = delta_ik I - R_i^T T_k . +``` + +The fit parameters are the local running coordinates `x_k`. During one Newton +linearization, `R_i`, `B_i`, `A`, and `T_i` are treated as fixed. Each track +point depends only on its own parameter: + +```text +d p_i / d x_k = delta_ik p_i' +``` + +so + +```text +d Delta_i / d x_k = D_ik p_k' +``` + +and + +```text +d^2 Delta_i / d x_k d x_l = delta_kl D_ik p_k'' . +``` + +## 4. Track derivatives in the O2 local frame + +O2 central-barrel parameters are + +```text +(Y, Z, snp, tgl, q/pt) +``` + +where + +```text +snp = sin(phi_local), csp = sqrt(1 - snp^2), +tgl = tan(lambda), kappa = curvature = (q/pt) Bz B2C . +``` + +For the fast constant-`Bz` helix model: + +```text +d snp / dX = kappa +dY / dX = snp / csp +dZ / dX = tgl / csp +``` + +and + +```text +d2Y / dX2 = kappa / csp^3 +d2Z / dX2 = kappa tgl snp / csp^3 . +``` + +Thus + +```text +p_i' = (1, dY/dX, dZ/dX)^T +p_i'' = (0, d2Y/dX2, d2Z/dX2)^T . +``` + +This matches `TrackDeriv::set`. + +## 5. Gradient and Hessian + +With symmetric `B_i`, + +```text +g_k = d chi2 / d x_k + = sum_i Delta_i^T B_i D_ik p_k' . +``` + +The exact Hessian is + +```text +H_kl = + sum_i (D_il p_l')^T B_i (D_ik p_k') + + delta_kl sum_i Delta_i^T B_i D_ik p_k'' . +``` + +The first term is the Gauss-Newton term. It contributes to diagonal and mixed +Hessian elements. The second term is the residual-curvature term. Since each +trajectory has an intrinsic second derivative only with respect to its own +running coordinate, this term contributes only when `k == l`. This is the +reason for the code condition + +```cpp +if (i == j) { + ... +} +``` + +when computing the Hessian element `H_ij`. + +The implementation solves + +```text +H dX = g +``` + +and then applies + +```text +X_new = X_old - dX . +``` + +This is equivalent to the more usual Newton notation `deltaX = -H^{-1} g`. + +## 6. No-error special case + +For the absolute-distance fit, take all information matrices as identity: + +```text +B_i = I . +``` + +Then + +```text +A = N I, A^{-1} = (1/N) I, +T_i = (1/N) R_i . +``` + +The vertex is the average of global track points: + +```text +V = (1/N) sum_i R_i p_i . +``` + +The residual is + +```text +Delta_i = p_i - (1/N) sum_j R_i^T R_j p_j . +``` + +Define + +```text +R_ij = (1/N) R_i^T R_j . +``` + +With the O2 convention, + +```text +R_i^T R_j = +[ cos(ai-aj) sin(ai-aj) 0 ] +[ -sin(ai-aj) cos(ai-aj) 0 ] +[ 0 0 1 ] . +``` + +This matches the code definitions + +```cpp +mCosDif[i][j] = (ci*cj + si*sj) / N; +mSinDif[i][j] = (si*cj - ci*sj) / N; +``` + +and the residual derivative components in `calcResidDerivativesNoErr`. + +## 7. Track information matrix involving the local X axis + +The old DCAFitterN code assigned a dummy uncertainty to local `X`, derived from +the local `Y` uncertainty. The corrected treatment derives longitudinal vertex +information from the track geometry. + +At fixed local `X`, the track measures `(Y,Z)` with covariance + +```text +C_YZ = +[ C_YY C_YZ ] +[ C_YZ C_ZZ ] . +``` + +Let + +```text +D = C_YY C_ZZ - C_YZ^2 +``` + +and + +```text +W = C_YZ^{-1} + = 1/D [ C_ZZ -C_YZ ] + [ -C_YZ C_YY ] . +``` + +Writing + +```text +wYY = C_ZZ / D +wYZ = -C_YZ / D +wZZ = C_YY / D +``` + +and + +```text +y' = dY/dX = snp/csp +z' = dZ/dX = tgl/csp +``` + +the vertex measurement matrix in local `(X,Y,Z)` coordinates is + +```text +H = +[ -y' 1 0 ] +[ -z' 0 1 ] . +``` + +The local 3D information matrix is + +```text +I = H^T W H . +``` + +Its independent elements are + +```text +I_YY = wYY +I_YZ = wYZ +I_ZZ = wZZ +I_XY = -(wYY y' + wYZ z') +I_XZ = -(wYZ y' + wZZ z') +I_XX = y'^2 wYY + 2 y' z' wYZ + z'^2 wZZ . +``` + +These are the six members of `TrackCovI`: + +```text +sxx, sxy, sxz, syy, syz, szz . +``` + +To combine tracks in the global frame, rotate the local information: + +```text +I_i^g = R_i I_i R_i^T . +``` + +The vertex covariance is then + +```text +C_V = (sum_i I_i^g)^{-1} . +``` + +## 8. Parent momentum covariance + +The parent momentum is the sum of independent daughter momenta: + +```text +P = sum_i p_i, C_P = sum_i C_{p_i} . +``` + +For one O2 daughter track, + +```text +px = pt (csp cos(alpha) - snp sin(alpha)) +py = pt (snp cos(alpha) + csp sin(alpha)) +pz = pt tgl +``` + +with `pt = |q|/|q/pt|`. The charge is treated as exact and discrete. The +derivatives with respect to native momentum parameters `(snp, tgl, q/pt)` are + +```text +dpx/dsnp = -pt (snp cos(alpha)/csp + sin(alpha)) +dpy/dsnp = pt (cos(alpha) - snp sin(alpha)/csp) +dpz/dtgl = pt + +dpx/d(q/pt) = -px / (q/pt) +dpy/d(q/pt) = -py / (q/pt) +dpz/d(q/pt) = -pz / (q/pt) +``` + +and the omitted mixed derivatives in this Jacobian are zero: + +```text +dpx/dtgl = 0, dpy/dtgl = 0, +dpz/dsnp = 0 . +``` + +Let `A` be this 3 by 3 Jacobian and let `C_a` be the daughter covariance +submatrix in the native momentum-parameter order `(snp,tgl,q/pt)`. Then + +```text +C_p = A C_a A^T . +``` + +The six independent elements of `C_p` are accumulated into the O2 lab covariance +slots + +```text +cov[9], cov[13], cov[14], cov[18], cov[19], cov[20] +``` + +which correspond to + +```text +Cov(px,px), Cov(py,px), Cov(py,py), +Cov(pz,px), Cov(pz,py), Cov(pz,pz). +``` + +## 9. Parent track covariance + +The final lab covariance passed to the O2 parent constructor contains the fitted +vertex covariance and the summed parent momentum covariance: + +```text +C_lab = +[ C_V 0 ] +[ 0 C_P ] . +``` + +Position-momentum cross-covariances are not included in this approximation. +The existing O2 constructor + +```cpp +TrackParCov(xyz, pxpypz, cov, charge, sectorAlpha) +``` + +then transforms this lab covariance to the selected parent track frame, +including the parent `alpha` convention. + +## 10. Regularization of singular or weakly constrained covariance matrices + +### Single-track `TrackCovI` + +The matrix + +```text +I = H^T W H +``` + +has rank at most two for one track, because one track measures only the two +coordinates transverse to its own trajectory. The null direction is the local +track tangent + +```text +t = (1, y_prime, z_prime)^T . +``` + +Indeed + +```text +H t = 0, I t = 0 . +``` + +Therefore a single-track `TrackCovI` is positive semidefinite, not positive +definite. Inverting this 3D matrix by itself is not meaningful; the apparent +negative diagonal elements seen in such an inverse are numerical symptoms of +trying to invert a rank-deficient information matrix. The physically meaningful +inverse at single-track level is only the original 2 by 2 `(Y,Z)` covariance. + +For the multi-track vertex fit, different track directions usually make + +```text +A = sum_i R_i I_i R_i^T +``` + +full rank. However, nearly parallel or otherwise weak geometries can still make +the longitudinal eigenvalue very small. To avoid an exactly singular +single-track contribution while preserving the measured `(Y,Z)` block, the code +may add a weak positive longitudinal prior only to `I_XX`: + +```text +I_XX -> I_XX + 1/sigma_X,prior^2 . +``` + +In code this is implemented as + +```cpp +constexpr float XRegErrFactor = 10.f; +const float sigmaX2 = C_YY * XRegErrFactor; +sxx += 1.f / sigmaX2; +``` + +This is different from multiplying `I_XX` by a number below one. A reduction of +`I_XX` can make the matrix indefinite, while adding a positive diagonal term +keeps the information matrix positive definite in the tangent direction and does +not alter `I_YY`, `I_YZ`, or `I_ZZ`. + +The regularization should remain weak. It is a numerical stabilizer for badly +conditioned geometries, not an additional detector measurement of the local +track `X` coordinate. + +### `calcPCACovMatrix()` + +The vertex covariance is + +```text +C_V = A^-1, A = sum_i R_i I_i R_i^T . +``` + +If `A` is singular or ill-conditioned, returning a small fallback covariance +would incorrectly shrink the uncertainty along the weakly constrained direction. +The conservative behavior is: + +1. Check that `A` is compatible with a positive-definite information matrix. +2. Reject cases where the determinant is too small compared with the diagonal + scale. +3. Invert only when these checks pass. +4. If inversion fails, or if the inverted covariance has non-positive diagonal + elements, return a deliberately loose fallback covariance. + +The code uses the leading-minor checks + +```text +A_XX > 0, +det(A_XY block) > 0, +det(A) > epsilon max(diag(A))^3 +``` + +with + +```text +epsilon = 1e-12 . +``` + +On failure, it returns a diagonal covariance with + +```text +sigma^2 = 1e6 cm^2 . +``` + +This corresponds to a 10 m uncertainty in each coordinate. It is intentionally +loose: the fallback marks the parent vertex as poorly constrained rather than +creating an artificially precise parent covariance. + +## 11. Code changes summarized + +1. `TrackCovI` now stores a full symmetric local 3D information matrix. +2. The dummy `XerrFactor` approximation was removed. +3. PCA weights and chi2 derivatives now use `sxx,sxy,sxz,syy,syz,szz`. +4. PCA covariance is the inverse of the summed global information matrix. +5. The Hessian residual-curvature term is restricted to diagonal Hessian + elements. +6. `correctTracks()` now propagates the actual candidate track state with O2's + analytic constant-`Bz` transport, keeping `mCandTr` and `mTrPos` + synchronized. +7. `createParentTrackParCov()` now propagates daughter momentum covariance from + native O2 `(snp,tgl,q/pt)` parameters to lab `(px,py,pz)` before constructing + the parent `TrackParCov`. diff --git a/Common/DCAFitter/include/DCAFitter/DCAFitterN.h b/Common/DCAFitter/include/DCAFitter/DCAFitterN.h index 2641dec84aed9..d02efa68526db 100644 --- a/Common/DCAFitter/include/DCAFitter/DCAFitterN.h +++ b/Common/DCAFitter/include/DCAFitter/DCAFitterN.h @@ -12,7 +12,8 @@ /// \file DCAFitterN.h /// \brief Defintions for N-prongs secondary vertex fit /// \author ruben.shahoyan@cern.ch -/// For the formulae derivation see /afs/cern.ch/user/s/shahoian/public/O2/DCAFitter/DCAFitterN.pdf +/// For the original derivation see /afs/cern.ch/user/s/shahoian/public/O2/DCAFitter/DCAFitterN.pdf +/// The AI-assisted readme is in DCAFitterN_derivation.md #ifndef _ALICEO2_DCA_FITTERN_ #define _ALICEO2_DCA_FITTERN_ @@ -28,17 +29,19 @@ namespace vertexing { ///__________________________________________________________________________________ -///< Inverse cov matrix (augmented by a dummy X error) of the point defined by the track +///< Inverse covariance matrix of the point defined by the track struct TrackCovI { - float sxx, syy, syz, szz; + // Independent elements of the symmetric 3D information matrix + // H^T Cyz^{-1} H. A track constrains Y and Z at a given X through + // H = {{-dY/dX, 1, 0}, {-dZ/dX, 0, 1}}. + float sxx, sxy, sxz, syy, syz, szz; GPUdDefault() TrackCovI() = default; - GPUd() bool set(const o2::track::TrackParCov& trc, float xerrFactor = 1.f) + GPUd() bool set(const o2::track::TrackParCov& trc) { - // we assign Y error to X for DCA calculation - // (otherwise for quazi-collinear tracks the X will not be constrained) - float cyy = trc.getSigmaY2(), czz = trc.getSigmaZ2(), cyz = trc.getSigmaZY(), cxx = cyy * xerrFactor; + // Invert the 2D covariance of the measured track position (Y,Z). + float cyy = trc.getSigmaY2(), czz = trc.getSigmaZ2(), cyz = trc.getSigmaZY(); float detYZ = cyy * czz - cyz * cyz; bool res = true; if (detYZ <= 0.) { @@ -47,10 +50,19 @@ struct TrackCovI { res = false; } auto detYZI = 1. / detYZ; - sxx = 1. / cxx; syy = czz * detYZI; syz = -cyz * detYZI; szz = cyy * detYZI; + const float cspI = 1.f / trc.getCsp(); + const float dydx = trc.getSnp() * cspI; + const float dzdx = trc.getTgl() * cspI; + sxy = -(syy * dydx + syz * dzdx); + sxz = -(syz * dydx + szz * dzdx); + sxx = dydx * dydx * syy + 2.f * dydx * dzdx * syz + dzdx * dzdx * szz; + // The matrix is degenerate by construction, regularize sxx term to preserve the original YZ block exactly + constexpr float XRegErrFactor = 10.f; + const float sigmaX2 = cyy * XRegErrFactor; + sxx += 1.f / sigmaX2; return res; } }; @@ -98,7 +110,6 @@ class DCAFitterN static constexpr double NMax = 4; static constexpr double NInv = 1. / N; static constexpr int MAXHYP = 2; - static constexpr float XerrFactor = 5.; // factor for conversion of track covYY to dummy covXX using Track = o2::track::TrackParCov; using TrackAuxPar = o2::track::TrackAuxPar; using CrossInfo = o2::track::CrossInfo; @@ -213,6 +224,7 @@ class DCAFitterN ///< recalculate PCA as a cov-matrix weighted mean, even if absDCA method was used GPUd() bool recalculatePCAWithErrors(int cand = 0); + GPUd() double calcCollinearInflation(int cand) const; GPUd() MatSym3D calcPCACovMatrix(int cand = 0) const; std::array calcPCACovMatrixFlat(int cand = 0) const @@ -313,17 +325,6 @@ class DCAFitterN return mat; } - GPUd() MatSym3D getTrackCovMatrix(int i, int cand = 0) const // generate covariance matrix of track position, adding fake X error - { - const auto& trc = mCandTr[mOrder[cand]][i]; - MatSym3D mat; - mat(0, 0) = trc.getSigmaY2() * XerrFactor; - mat(1, 1) = trc.getSigmaY2(); - mat(2, 2) = trc.getSigmaZ2(); - mat(2, 1) = trc.getSigmaZY(); - return mat; - } - GPUd() void assign(int) {} template GPUd() void assign(int i, const T& t, const Tr&... args) @@ -389,9 +390,10 @@ class DCAFitterN std::array mNIters; // number of iterations for each seed std::array mTrPropDone{}; // Flag that the tracks are fully propagated to PCA std::array mPropFailed{}; // Flag that some propagation failed for this PCA candidate - LogLogThrottler mLoggerBadCov{}; - LogLogThrottler mLoggerBadInv{}; - LogLogThrottler mLoggerBadProp{}; + mutable LogLogThrottler mLoggerBadCov{}; + mutable LogLogThrottler mLoggerBadInv{}; + mutable LogLogThrottler mLoggerBadProp{}; + mutable LogLogThrottler mLoggerBadPCACov{}; MatSym3D mWeightInv; // inverse weight of single track, [sum{M^T E M}]^-1 in EQ.T std::array mOrder{0}; int mCurHyp = 0; @@ -502,13 +504,13 @@ GPUd() bool DCAFitterN::calcPCACoefs() const auto& taux = mTrAux[i]; const auto& tcov = mTrcEInv[mCurHyp][i]; MatStd3D miei; - miei[0][0] = taux.c * tcov.sxx; - miei[0][1] = -taux.s * tcov.syy; - miei[0][2] = -taux.s * tcov.syz; - miei[1][0] = taux.s * tcov.sxx; - miei[1][1] = taux.c * tcov.syy; - miei[1][2] = taux.c * tcov.syz; - miei[2][0] = 0; + miei[0][0] = taux.c * tcov.sxx - taux.s * tcov.sxy; + miei[0][1] = taux.c * tcov.sxy - taux.s * tcov.syy; + miei[0][2] = taux.c * tcov.sxz - taux.s * tcov.syz; + miei[1][0] = taux.s * tcov.sxx + taux.c * tcov.sxy; + miei[1][1] = taux.s * tcov.sxy + taux.c * tcov.syy; + miei[1][2] = taux.s * tcov.sxz + taux.c * tcov.syz; + miei[2][0] = tcov.sxz; miei[2][1] = tcov.syz; miei[2][2] = tcov.szz; mTrCFVT[mCurHyp][i] = mWeightInv * miei; @@ -532,11 +534,11 @@ GPUd() bool DCAFitterN::calcInverseWeight() for (int i = N; i--;) { const auto& taux = mTrAux[i]; const auto& tcov = mTrcEInv[mCurHyp][i]; - arrmat[XX] += taux.cc * tcov.sxx + taux.ss * tcov.syy; - arrmat[XY] += taux.cs * (tcov.sxx - tcov.syy); - arrmat[XZ] += -taux.s * tcov.syz; - arrmat[YY] += taux.cc * tcov.syy + taux.ss * tcov.sxx; - arrmat[YZ] += taux.c * tcov.syz; + arrmat[XX] += taux.cc * tcov.sxx - 2. * taux.cs * tcov.sxy + taux.ss * tcov.syy; + arrmat[XY] += taux.cs * (tcov.sxx - tcov.syy) + (taux.cc - taux.ss) * tcov.sxy; + arrmat[XZ] += taux.c * tcov.sxz - taux.s * tcov.syz; + arrmat[YY] += taux.ss * tcov.sxx + 2. * taux.cs * tcov.sxy + taux.cc * tcov.syy; + arrmat[YZ] += taux.s * tcov.sxz + taux.c * tcov.syz; arrmat[ZZ] += tcov.szz; } // invert 3x3 symmetrix matrix @@ -670,9 +672,9 @@ GPUd() void DCAFitterN::calcChi2Derivatives() const auto& covI = mTrcEInv[mCurHyp][j]; // inverse cov matrix of track j const auto& dr1 = mDResidDx[j][i]; // vector of j-th residuals 1st derivative over X param of track i auto& cidr = covIDrDx[i][j]; // vector covI_j * dres_j/dx_i, save for 2nd derivative calculation - cidr[0] = covI.sxx * dr1[0]; - cidr[1] = covI.syy * dr1[1] + covI.syz * dr1[2]; - cidr[2] = covI.syz * dr1[1] + covI.szz * dr1[2]; + cidr[0] = covI.sxx * dr1[0] + covI.sxy * dr1[1] + covI.sxz * dr1[2]; + cidr[1] = covI.sxy * dr1[0] + covI.syy * dr1[1] + covI.syz * dr1[2]; + cidr[2] = covI.sxz * dr1[0] + covI.syz * dr1[1] + covI.szz * dr1[2]; // calculate res_i * covI_j * dres_j/dx_i dchi1 += o2::math_utils::Dot(res, cidr); } @@ -686,11 +688,13 @@ GPUd() void DCAFitterN::calcChi2Derivatives() const auto& dr1j = mDResidDx[k][j]; // vector of k-th residuals 1st derivative over X param of track j const auto& cidrkj = covIDrDx[i][k]; // vector covI_k * dres_k/dx_i dchi2 += o2::math_utils::Dot(dr1j, cidrkj); - if (k == j) { + if (i == j) { const auto& res = mTrRes[mCurHyp][k]; // vector of residuals of track k const auto& covI = mTrcEInv[mCurHyp][k]; // inverse cov matrix of track k - const auto& dr2ij = mD2ResidDx2[k][j]; // vector of k-th residuals 2nd derivative over X params of track j - dchi2 += res[0] * covI.sxx * dr2ij[0] + res[1] * (covI.syy * dr2ij[1] + covI.syz * dr2ij[2]) + res[2] * (covI.syz * dr2ij[1] + covI.szz * dr2ij[2]); + const auto& dr2ij = mD2ResidDx2[k][i]; // vector of k-th residuals 2nd derivative over X param i + dchi2 += res[0] * (covI.sxx * dr2ij[0] + covI.sxy * dr2ij[1] + covI.sxz * dr2ij[2]) + + res[1] * (covI.sxy * dr2ij[0] + covI.syy * dr2ij[1] + covI.syz * dr2ij[2]) + + res[2] * (covI.sxz * dr2ij[0] + covI.syz * dr2ij[1] + covI.szz * dr2ij[2]); } } } @@ -705,16 +709,23 @@ GPUd() void DCAFitterN::calcChi2DerivativesNoErr() for (int i = N; i--;) { auto& dchi1 = mDChi2Dx[i]; // DChi2/Dx_i = sum_j { res_j * Dres_j/Dx_i } dchi1 = 0; // chi2 1st derivative - for (int j = N; j--;) { - const auto& res = mTrRes[mCurHyp][j]; // vector of residuals of track j - const auto& dr1 = mDResidDx[j][i]; // vector of j-th residuals 1st derivative over X param of track i + for (int k = N; k--;) { + const auto& res = mTrRes[mCurHyp][k]; // vector of residuals of track k + const auto& dr1 = mDResidDx[k][i]; // vector of k-th residuals 1st derivative over X param of track i dchi1 += o2::math_utils::Dot(res, dr1); - if (i >= j) { // symmetrix matrix - // chi2 2nd derivative - auto& dchi2 = mD2Chi2Dx2[i][j]; // D2Chi2/Dx_i/Dx_j = sum_k { Dres_k/Dx_j * covI_k * Dres_k/Dx_i + res_k * covI_k * D2res_k/Dx_i/Dx_j } - dchi2 = o2::math_utils::Dot(mTrRes[mCurHyp][i], mD2ResidDx2[i][j]); - for (int k = N; k--;) { - dchi2 += o2::math_utils::Dot(mDResidDx[k][i], mDResidDx[k][j]); + } + } + for (int i = N; i--;) { + for (int j = i + 1; j--;) { + auto& dchi2 = mD2Chi2Dx2[i][j]; + dchi2 = 0.; + for (int k = N; k--;) { + // Gauss-Newton term, present for diagonal and mixed elements. + dchi2 += o2::math_utils::Dot(mDResidDx[k][i], mDResidDx[k][j]); + // A trajectory has a second derivative only with respect to its own + // X parameter, hence the curvature term contributes only to H_ii. + if (i == j) { + dchi2 += o2::math_utils::Dot(mTrRes[mCurHyp][k], mD2ResidDx2[k][i]); } } } @@ -744,7 +755,7 @@ GPUd() bool DCAFitterN::recalculatePCAWithErrors(int cand) mCurHyp = mOrder[cand]; if (mUseAbsDCA) { for (int i = N; i--;) { - if (!mTrcEInv[mCurHyp][i].set(mCandTr[mCurHyp][i], XerrFactor)) { // prepare inverse cov.matrices at starting point + if (!mTrcEInv[mCurHyp][i].set(mCandTr[mCurHyp][i])) { // prepare inverse cov.matrices at starting point if (mLoggerBadCov.needToLog()) { #ifndef GPUCA_GPUCODE printf("fitter %d: error (%ld muted): overrode invalid track covariance from %s\n", @@ -797,30 +808,106 @@ GPUd() void DCAFitterN::calcPCANoErr() //___________________________________________________________________ template -GPUd() o2::math_utils::SMatrix> DCAFitterN::calcPCACovMatrix(int cand) const +GPUd() double DCAFitterN::calcCollinearInflation(int cand) const { - // calculate covariance matrix for the point of closest approach - MatSym3D covm; - int nAdded = 0; - for (int i = N; i--;) { // calculate sum of inverses - // MatSym3D covTr = o2::math_utils::Similarity(mUseAbsDCA ? getTrackRotMatrix(i) : mTrCFVT[mOrder[cand]][i], getTrackCovMatrix(i, cand)); - // RS by using Similarity(mTrCFVT[mOrder[cand]][i], getTrackCovMatrix(i, cand)) we underestimate the error, use simple rotation - MatSym3D covTr = o2::math_utils::Similarity(getTrackRotMatrix(i), getTrackCovMatrix(i, cand)); - if (covTr.Invert()) { - covm += covTr; - nAdded++; + std::array, N> u{}; + int nu = 0; + + for (int i = 0; i < N; ++i) { + std::array p{}; + if (!getTrack(i, cand).getPxPyPzGlo(p)) { + continue; + } + const double p2 = p[0] * p[0] + p[1] * p[1] + p[2] * p[2]; + if (p2 <= 0.) { + continue; + } + const double pI = 1. / std::sqrt(p2); + u[nu++] = {p[0] * pI, p[1] * pI, p[2] * pI}; + } + + if (nu < 2) { + return 1.; + } + + double sin2Mean = 0.; + int npairs = 0; + for (int i = 0; i < nu; ++i) { + for (int j = i + 1; j < nu; ++j) { + double cij = u[i][0] * u[j][0] + u[i][1] * u[j][1] + u[i][2] * u[j][2]; + cij = std::clamp(cij, -1., 1.); + sin2Mean += std::max(0., 1. - cij * cij); + ++npairs; } } - if (nAdded && covm.Invert()) { - return covm; + sin2Mean /= npairs; + + constexpr double Sin2Ref = 1.e-5; + constexpr double MaxInflation = 1.e4; + if (sin2Mean <= 0.) { + return MaxInflation; } - // correct way has failed, use simple sum - MatSym3D covmSum; + return sin2Mean < Sin2Ref ? std::min(MaxInflation, Sin2Ref / sin2Mean) : 1.; +} + +//___________________________________________________________________ +template +GPUd() o2::math_utils::SMatrix> DCAFitterN::calcPCACovMatrix(int cand) const +{ + // Each track measures Y and Z at the vertex X. With the local slopes + // sy = dY/dX and sz = dZ/dX, its vertex measurement matrix is + // H = {{-sy, 1, 0}, {-sz, 0, 1}}. The longitudinal information must come + // from the track geometry, not from a dummy X variance. + MatSym3D info; + auto* arrmat = info.Array(); + memset(arrmat, 0, sizeof(info)); + enum { XX, + XY, + YY, + XZ, + YZ, + ZZ }; + const int ord = mOrder[cand]; for (int i = N; i--;) { - MatSym3D covTr = o2::math_utils::Similarity(getTrackRotMatrix(i), getTrackCovMatrix(i, cand)); - covmSum += covTr; + const auto& taux = mTrAux[i]; + TrackCovI tcov; + tcov.set(mCandTr[ord][i]); + arrmat[XX] += taux.cc * tcov.sxx - 2. * taux.cs * tcov.sxy + taux.ss * tcov.syy; + arrmat[XY] += taux.cs * (tcov.sxx - tcov.syy) + (taux.cc - taux.ss) * tcov.sxy; + arrmat[XZ] += taux.c * tcov.sxz - taux.s * tcov.syz; + arrmat[YY] += taux.ss * tcov.sxx + 2. * taux.cs * tcov.sxy + taux.cc * tcov.syy; + arrmat[YZ] += taux.s * tcov.sxz + taux.c * tcov.syz; + arrmat[ZZ] += tcov.szz; } - return covmSum; + const double maxDiag = o2::gpu::GPUCommonMath::Max(o2::gpu::GPUCommonMath::Max(info(0, 0), info(1, 1)), info(2, 2)); + const double det2 = info(0, 0) * info(1, 1) - info(1, 0) * info(1, 0); + const double det3 = info(0, 0) * (info(1, 1) * info(2, 2) - info(2, 1) * info(2, 1)) - + info(1, 0) * (info(1, 0) * info(2, 2) - info(2, 1) * info(2, 0)) + + info(2, 0) * (info(1, 0) * info(2, 1) - info(1, 1) * info(2, 0)); + constexpr double MinRelDet = 1.e-12; + constexpr double InflateRelDet = 1.e-6; + constexpr double MaxInflation = 1.e4; + const bool isWellConditionedInfo = maxDiag > 0. && info(0, 0) > 0. && det2 > 0. && det3 > MinRelDet * maxDiag * maxDiag * maxDiag; + if (isWellConditionedInfo) { + auto cov = info; + if (cov.Invert() && cov(0, 0) > 0. && cov(1, 1) > 0. && cov(2, 2) > 0.) { + // if (mIsCollinear) { + // cov *= calcCollinearInflation(cand); + // } + return cov; + } + } + if (mLoggerBadPCACov.needToLog()) { + printf("fitter %d: error (%ld muted): override ill-conditioned PCACovMatrix by dummy matrix", mFitterID, mLoggerBadPCACov.evCount); + } + // Fall back on a deliberately loose vertex covariance. Returning a tight + // identity covariance for a singular or ill-conditioned information matrix + // would shrink the uncertainty in the weakly constrained direction. + memset(arrmat, 0, sizeof(info)); + info(0, 0) = 4.; + info(1, 1) = 4.; + info(2, 2) = 4.; + return info; } //___________________________________________________________________ @@ -856,7 +943,8 @@ GPUdi() double DCAFitterN::calcChi2() const for (int i = N; i--;) { const auto& res = mTrRes[mCurHyp][i]; const auto& covI = mTrcEInv[mCurHyp][i]; - chi2 += res[0] * res[0] * covI.sxx + res[1] * res[1] * covI.syy + res[2] * res[2] * covI.szz + 2. * res[1] * res[2] * covI.syz; + chi2 += res[0] * res[0] * covI.sxx + res[1] * res[1] * covI.syy + res[2] * res[2] * covI.szz + + 2. * (res[0] * res[1] * covI.sxy + res[0] * res[2] * covI.sxz + res[1] * res[2] * covI.syz); } return chi2; } @@ -878,13 +966,26 @@ GPUdi() double DCAFitterN::calcChi2NoErr() const template GPUd() bool DCAFitterN::correctTracks(const VecND& corrX) { - // propagate tracks to updated X + // Propagate the actual candidate tracks to the updated X. Use the analytic + // constant-Bz transport here: Newton corrections are small, but the track + // state must stay synchronized with mTrPos for the next derivative update. for (int i = N; i--;) { - const auto& trDer = mTrDer[mCurHyp][i]; - auto dx2h = 0.5 * corrX[i] * corrX[i]; - mTrPos[mCurHyp][i][0] -= corrX[i]; - mTrPos[mCurHyp][i][1] -= trDer.dydx * corrX[i] - dx2h * trDer.d2ydx2; - mTrPos[mCurHyp][i][2] -= trDer.dzdx * corrX[i] - dx2h * trDer.d2zdx2; + /* + // Updating only mTrPos by Taylor expansion leaves mCandTr at the previous X, + // leaving calcTrackDerivatives() insensitive to the update. Use full fast propagation instead. + const auto& trDer = mTrDer[mCurHyp][i]; + auto dx2h = 0.5 * corrX[i] * corrX[i]; + mTrPos[mCurHyp][i][0] -= corrX[i]; + mTrPos[mCurHyp][i][1] -= trDer.dydx * corrX[i] - dx2h * trDer.d2ydx2; + mTrPos[mCurHyp][i][2] -= trDer.dzdx * corrX[i] - dx2h * trDer.d2zdx2; + */ + auto& trc = mCandTr[mCurHyp][i]; + const float x = static_cast(mTrPos[mCurHyp][i][0] - corrX[i]); + const bool propagated = mUseAbsDCA ? trc.propagateParamTo(x, mBz) : trc.propagateTo(x, mBz); + if (!propagated) { + return false; + } + setTrackPos(mTrPos[mCurHyp][i], trc); } return true; } @@ -972,7 +1073,7 @@ GPUd() bool DCAFitterN::minimizeChi2() return false; } setTrackPos(mTrPos[mCurHyp][i], mCandTr[mCurHyp][i]); // prepare positions - if (!mTrcEInv[mCurHyp][i].set(mCandTr[mCurHyp][i], XerrFactor)) { // prepare inverse cov.matrices at starting point + if (!mTrcEInv[mCurHyp][i].set(mCandTr[mCurHyp][i])) { // prepare inverse cov.matrices at starting point if (mLoggerBadCov.needToLog()) { #ifndef GPUCA_GPUCODE printf("fitter %d: error (%ld muted): overrode invalid track covariance from %s\n", @@ -1176,21 +1277,45 @@ GPUd() void DCAFitterN::print() const template GPUd() o2::track::TrackParCov DCAFitterN::createParentTrackParCov(int cand, bool sectorAlpha) const { - const auto& trP = getTrack(0, cand); - const auto& trN = getTrack(1, cand); std::array covV = {0.}; std::array pvecV = {0.}; int q = 0; for (int it = 0; it < N; it++) { const auto& trc = getTrack(it, cand); std::array pvecT = {0.}; - std::array covT = {0.}; - trc.getPxPyPzGlo(pvecT); - trc.getCovXYZPxPyPzGlo(covT); - constexpr int MomInd[6] = {9, 13, 14, 18, 19, 20}; // cov matrix elements for momentum component - for (int i = 0; i < 6; i++) { - covV[MomInd[i]] += covT[MomInd[i]]; + const bool hasMomentum = trc.getPxPyPzGlo(pvecT); + + // Propagate only the native momentum-parameter covariance + // (snp,tgl,q/pt) to the lab momentum covariance. The final constructor + // below rotates the summed lab covariance to the parent alpha frame. + if (hasMomentum) { + const double snp = trc.getSnp(); + const double csp = trc.getCsp(); + const double pt = trc.getPt(); + const double alpha = trc.getAlpha(); + double sna = 0., csa = 0.; + o2::math_utils::detail::sincos(alpha, sna, csa); + const double dPxdSnp = -pt * (snp * csa / csp + sna); + const double dPydSnp = pt * (csa - snp * sna / csp); + const double dPzdTgl = pt; + const double q2ptI = 1. / trc.getQ2Pt(); + const double dPxdQ = -pvecT[0] * q2ptI; + const double dPydQ = -pvecT[1] * q2ptI; + const double dPzdQ = -pvecT[2] * q2ptI; + const double cSnpSnp = trc.getSigmaSnp2(); + const double cTglSnp = trc.getSigmaTglSnp(); + const double cTglTgl = trc.getSigmaTgl2(); + const double cQSnp = trc.getSigma1PtSnp(); + const double cQTgl = trc.getSigma1PtTgl(); + const double cQQ = trc.getSigma1Pt2(); + covV[9] += dPxdSnp * dPxdSnp * cSnpSnp + 2. * dPxdSnp * dPxdQ * cQSnp + dPxdQ * dPxdQ * cQQ; + covV[13] += dPydSnp * (dPxdSnp * cSnpSnp + dPxdQ * cQSnp) + dPydQ * (dPxdSnp * cQSnp + dPxdQ * cQQ); + covV[14] += dPydSnp * dPydSnp * cSnpSnp + 2. * dPydSnp * dPydQ * cQSnp + dPydQ * dPydQ * cQQ; + covV[18] += dPzdTgl * (dPxdSnp * cTglSnp + dPxdQ * cQTgl) + dPzdQ * (dPxdSnp * cQSnp + dPxdQ * cQQ); + covV[19] += dPzdTgl * (dPydSnp * cTglSnp + dPydQ * cQTgl) + dPzdQ * (dPydSnp * cQSnp + dPydQ * cQQ); + covV[20] += dPzdTgl * dPzdTgl * cTglTgl + 2. * dPzdTgl * dPzdQ * cQTgl + dPzdQ * dPzdQ * cQQ; } + for (int i = 0; i < 3; i++) { pvecV[i] += pvecT[i]; } @@ -1245,9 +1370,9 @@ GPUdi() bool DCAFitterN::propagateParamToX(o2::track::TrackPar& t, f mPropFailed[mCurHyp] = true; if (mLoggerBadProp.needToLog()) { #ifndef GPUCA_GPUCODE - printf("fitter %d: error (%ld muted): propagation failed for %s\n", mFitterID, mLoggerBadProp.evCount, t.asString().c_str()); + printf("fitter %d: error (%ld muted): propagation to %.4f failed for %s\n", mFitterID, mLoggerBadProp.evCount, x, t.asString().c_str()); #else - printf("fitter %d: error (%ld muted): propagation failed\n", mFitterID, mLoggerBadProp.evCount); + printf("fitter %d: error (%ld muted): propagation to %.4f failed\n", mFitterID, mLoggerBadProp.evCount, x); #endif } } @@ -1271,9 +1396,9 @@ GPUdi() bool DCAFitterN::propagateToX(o2::track::TrackParCov& t, flo mPropFailed[mCurHyp] = true; if (mLoggerBadProp.needToLog()) { #ifndef GPUCA_GPUCODE - printf("fitter %d: error (%ld muted): propagation failed for %s\n", mFitterID, mLoggerBadProp.evCount, t.asString().c_str()); + printf("fitter %d: error (%ld muted): propagation to %.4f failed for %s\n", mFitterID, mLoggerBadProp.evCount, x, t.asString().c_str()); #else - printf("fitter %d: error (%ld muted): propagation failed\n", mFitterID, mLoggerBadProp.evCount); + printf("fitter %d: error (%ld muted): propagation to %.4f failed\n", mFitterID, mLoggerBadProp.evCount, x); #endif } } diff --git a/Common/DCAFitter/test/testDCAFitterN.cxx b/Common/DCAFitter/test/testDCAFitterN.cxx index bd00b5bed841e..8ab2c7f4ca323 100644 --- a/Common/DCAFitter/test/testDCAFitterN.cxx +++ b/Common/DCAFitter/test/testDCAFitterN.cxx @@ -56,11 +56,21 @@ float checkResults(o2::utils::TreeStreamRedirector& outs, std::string& treeName, double dst = TMath::Sqrt(df[0] * df[0] + df[1] * df[1] + df[2] * df[2]); distMin = dst < distMin ? dst : distMin; auto parentTrack = fitter.createParentTrackParCov(ic); - // float genX + const std::array genPos{static_cast(vgen[0]), static_cast(vgen[1]), static_cast(vgen[2])}; + const std::array genMom{static_cast(genPar.Px()), static_cast(genPar.Py()), static_cast(genPar.Pz())}; + o2::track::TrackPar genParentTrack(genPos, genMom, parentTrack.getCharge(), false); + genParentTrack.rotateParam(parentTrack.getAlpha()); + std::array parentCovGlo{}; + const bool hasParentCovGlo = parentTrack.getCovXYZPxPyPzGlo(parentCovGlo); + const double pullX = hasParentCovGlo && parentCovGlo[0] > 0.f ? df[0] / TMath::Sqrt(parentCovGlo[0]) : 0.; + const double pullY = hasParentCovGlo && parentCovGlo[2] > 0.f ? df[1] / TMath::Sqrt(parentCovGlo[2]) : 0.; + const double pullZ = hasParentCovGlo && parentCovGlo[5] > 0.f ? df[2] / TMath::Sqrt(parentCovGlo[5]) : 0.; outs << treeName.c_str() << "cand=" << ic << "ncand=" << nCand << "nIter=" << nIter << "chi2=" << chi2 << "genPart=" << genPar << "recPart=" << moth << "genX=" << vgen[0] << "genY=" << vgen[1] << "genZ=" << vgen[2] << "dx=" << df[0] << "dy=" << df[1] << "dz=" << df[2] << "dst=" << dst + << "pullX=" << pullX << "pullY=" << pullY << "pullZ=" << pullZ + << "genParentTrack=" << genParentTrack << "useAbsDCA=" << absDCA << "useWghDCA=" << useWghDCA << "parent=" << parentTrack; for (int i = 0; i < fitter.getNProngs(); i++) { outs << treeName.c_str() << fmt::format("prong{}=", i).c_str() << fitter.getTrack(i, ic); From 656fd896030d2d4f385fec81826aae363fe2b6e1 Mon Sep 17 00:00:00 2001 From: shahor02 Date: Mon, 20 Jul 2026 16:45:30 +0400 Subject: [PATCH 12/47] Properly discard Alice3 TRK hits preceding RO start (#15606) --- .../TRK/simulation/include/TRKSimulation/Digitizer.h | 4 ++-- Detectors/Upgrades/ALICE3/TRK/simulation/src/Digitizer.cxx | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Detectors/Upgrades/ALICE3/TRK/simulation/include/TRKSimulation/Digitizer.h b/Detectors/Upgrades/ALICE3/TRK/simulation/include/TRKSimulation/Digitizer.h index 5910fc98134aa..0f857c6a0cfe2 100644 --- a/Detectors/Upgrades/ALICE3/TRK/simulation/include/TRKSimulation/Digitizer.h +++ b/Detectors/Upgrades/ALICE3/TRK/simulation/include/TRKSimulation/Digitizer.h @@ -65,7 +65,7 @@ class Digitizer mROFrameMin = 0; mROFrameMax = 0; mNewROFrame = 0; - mIsBeforeFirstRO = false; + mROFsWrtFirstRO = 0; mExtraBuff.clear(); } @@ -139,7 +139,7 @@ class Digitizer uint32_t mROFrameMax = 0; ///< highest RO frame of current digits uint32_t mNewROFrame = 0; ///< ROFrame corresponding to provided time - bool mIsBeforeFirstRO = false; + int mROFsWrtFirstRO = 0; uint32_t mEventROFrameMin = 0xffffffff; ///< lowest RO frame for processed events (w/o automatic noise ROFs) uint32_t mEventROFrameMax = 0; ///< highest RO frame forfor processed events (w/o automatic noise ROFs) diff --git a/Detectors/Upgrades/ALICE3/TRK/simulation/src/Digitizer.cxx b/Detectors/Upgrades/ALICE3/TRK/simulation/src/Digitizer.cxx index 890c272fefbc2..4b85238dc8e24 100644 --- a/Detectors/Upgrades/ALICE3/TRK/simulation/src/Digitizer.cxx +++ b/Detectors/Upgrades/ALICE3/TRK/simulation/src/Digitizer.cxx @@ -164,15 +164,14 @@ void Digitizer::setEventTime(const o2::InteractionTimeRecord& irt, int layer) nbc--; } + mROFsWrtFirstRO = std::floor(float(nbc) / mParams.getROFrameLengthInBC(layer)); if (nbc < 0) { mNewROFrame = 0; - mIsBeforeFirstRO = true; } else { mNewROFrame = nbc / mParams.getROFrameLengthInBC(layer); - mIsBeforeFirstRO = false; } - LOG(debug) << " NewROFrame " << mNewROFrame << " = " << nbc << "/" << mParams.getROFrameLengthInBC(layer) << " (nbc/mParams.getROFrameLengthInBC()"; + LOG(debug) << " NewROFrame " << mNewROFrame << " nbc " << nbc << " ROFsWrtFirstRO " << mROFsWrtFirstRO; // in continuous mode depends on starts of periodic readout frame mCollisionTimeWrtROF += (nbc % mParams.getROFrameLengthInBC(layer)) * o2::constants::lhc::LHCBunchSpacingNS; @@ -283,7 +282,7 @@ void Digitizer::processHit(const o2::trk::Hit& hit, uint32_t& maxFr, int evID, i return; } timeInROF += mCollisionTimeWrtROF; - if (mIsBeforeFirstRO && timeInROF < 0) { + if (mROFsWrtFirstRO < -1 || (mROFsWrtFirstRO == -1 && timeInROF < 0)) { // disregard this hit because it comes from an event byefore readout starts and it does not effect this RO LOG(debug) << "Ignoring hit with timeInROF = " << timeInROF; return; From fce05cf673e2bdc55e787082879d4bf41441756b Mon Sep 17 00:00:00 2001 From: Giorgio Alberto Lucia <87222843+GiorgioAlbertoLucia@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:04:11 +0200 Subject: [PATCH 13/47] [ALICE3] added segmentation to the particle propagation in TOF3 (#15591) * adding stepping in digitization * added segmentation to the digitization * added non-segmented version, with configurable choice * clang format * removed redundant hit response in the middle, nsteps=1 is now default * Charge threshold 0 by defult, using efficiency set by the digitizer Changed chargeThreshold from 75 to 0. * Please consider the following formatting changes --------- Co-authored-by: ALICE Action Bot --- .../base/include/IOTOFBase/GeometryTGeo.h | 8 +- .../IOTOFSimulation/DPLDigitizerParam.h | 5 +- .../include/IOTOFSimulation/Digitizer.h | 2 + .../ALICE3/IOTOF/simulation/src/Digitizer.cxx | 138 +++++++++++++++--- 4 files changed, 129 insertions(+), 24 deletions(-) diff --git a/Detectors/Upgrades/ALICE3/IOTOF/base/include/IOTOFBase/GeometryTGeo.h b/Detectors/Upgrades/ALICE3/IOTOF/base/include/IOTOFBase/GeometryTGeo.h index d466cc2987f23..616c6409f4577 100644 --- a/Detectors/Upgrades/ALICE3/IOTOF/base/include/IOTOFBase/GeometryTGeo.h +++ b/Detectors/Upgrades/ALICE3/IOTOF/base/include/IOTOFBase/GeometryTGeo.h @@ -39,7 +39,7 @@ class GeometryTGeo : public o2::detectors::DetMatrixCache static const char* getIOTOFVolPattern() { return sIOTOFVolumeName.c_str(); } // Inner TOF - const int getITOFNumberOfChips() { return mNumberOfChipsIOTOF[0]; } + const int getITOFNumberOfChips() const { return mNumberOfChipsIOTOF[0]; } static const char* getITOFLayerPattern() { return sITOFLayerName.c_str(); } static const char* getITOFStavePattern() { return sITOFStaveName.c_str(); } static const char* getITOFModulePattern() { return sITOFModuleName.c_str(); } @@ -47,7 +47,7 @@ class GeometryTGeo : public o2::detectors::DetMatrixCache static const char* getITOFSensorPattern() { return sITOFSensorName.c_str(); } // Outer TOF - const int getOTOFNumberOfChips() { return mNumberOfChipsIOTOF[1]; } + const int getOTOFNumberOfChips() const { return mNumberOfChipsIOTOF[1]; } static const char* getOTOFLayerPattern() { return sOTOFLayerName.c_str(); } static const char* getOTOFStavePattern() { return sOTOFStaveName.c_str(); } static const char* getOTOFModulePattern() { return sOTOFModuleName.c_str(); } @@ -55,13 +55,13 @@ class GeometryTGeo : public o2::detectors::DetMatrixCache static const char* getOTOFSensorPattern() { return sOTOFSensorName.c_str(); } // Forward TOF - const int getFTOFNumberOfChips() { return mNumberOfChipsFTOF; } + const int getFTOFNumberOfChips() const { return mNumberOfChipsFTOF; } static const char* getFTOFLayerPattern() { return sFTOFLayerName.c_str(); } static const char* getFTOFChipPattern() { return sFTOFChipName.c_str(); } static const char* getFTOFSensorPattern() { return sFTOFSensorName.c_str(); } // Backward TOF - const int getBTOFNumberOfChips() { return mNumberOfChipsBTOF; } + const int getBTOFNumberOfChips() const { return mNumberOfChipsBTOF; } static const char* getBTOFLayerPattern() { return sBTOFLayerName.c_str(); } static const char* getBTOFChipPattern() { return sBTOFChipName.c_str(); } static const char* getBTOFSensorPattern() { return sBTOFSensorName.c_str(); } diff --git a/Detectors/Upgrades/ALICE3/IOTOF/simulation/include/IOTOFSimulation/DPLDigitizerParam.h b/Detectors/Upgrades/ALICE3/IOTOF/simulation/include/IOTOFSimulation/DPLDigitizerParam.h index 66014e1c7f06e..fbff3330865db 100644 --- a/Detectors/Upgrades/ALICE3/IOTOF/simulation/include/IOTOFSimulation/DPLDigitizerParam.h +++ b/Detectors/Upgrades/ALICE3/IOTOF/simulation/include/IOTOFSimulation/DPLDigitizerParam.h @@ -27,10 +27,11 @@ struct DPLDigitizerParam : public o2::conf::ConfigurableParamHelper @@ -30,7 +31,6 @@ namespace o2::iotof { o2::iotof::Segmentation* Digitizer::sSegmentation = nullptr; - //_______________________________________________________________________ void Digitizer::init() { @@ -100,7 +100,7 @@ void Digitizer::processHit(const o2::itsmft::Hit& hit, int evID, int srcID) } // Get detector element ID - int chipID = hit.GetDetectorID(); + const int chipID = hit.GetDetectorID(); auto& chip = mChips[chipID]; if (chip.isDisabled()) { LOG(debug) << "Hit rejected because chip " << chipID << " is disabled"; @@ -110,6 +110,8 @@ void Digitizer::processHit(const o2::itsmft::Hit& hit, int evID, int srcID) // Convert energy loss to charge (number of electrons) float energyLoss = hit.GetEnergyLoss(); // in GeV int charge = energyToCharge(energyLoss); + const auto& digitizerParams = o2::iotof::DPLDigitizerParam::Instance(); + int electronsPerStep = static_cast(charge / digitizerParams.nSimSteps); // Apply charge threshold if (charge < mChargeThreshold) { @@ -128,25 +130,126 @@ void Digitizer::processHit(const o2::itsmft::Hit& hit, int evID, int srcID) LOG(debug) << "Invalid detector ID: " << chipID << ", geometry size: " << mGeometry->getSize(); return; // invalid detector ID } + + // Create the digit with time information + o2::MCCompLabel label(hit.GetTrackID(), evID, srcID, false); + const int roFrameAbs = 0; // For now, we can set this to 0 or calculate based on time if needed + const int nROF = 1; // For now, we can assume the signal is contained in one ROF, this can be extended to multiple ROFs based on the time + + float** respMatrix = nullptr; + int rowStart = 0, colStart = 0, rowSpan = 0, colSpan = 0; + stepping(hit, respMatrix, rowStart, colStart, rowSpan, colSpan); + + for (int irow = rowSpan; irow--;) { + uint16_t rowIS = irow + rowStart; + for (int icol = colSpan; icol--;) { + uint16_t colIS = icol + colStart; + float nEleResp = respMatrix[irow][icol]; + if (!nEleResp) { + continue; + } + const int nElectronsSampled = gRandom->Poisson(electronsPerStep * nEleResp); + // Noise can be added here if needed + + registerDigits(chip, roFrameAbs, smearedTime, nROF, + static_cast(rowIS), static_cast(colIS), nElectronsSampled, label); + } + } + + for (int irow = 0; irow < rowSpan; ++irow) { + delete[] respMatrix[irow]; + } + delete[] respMatrix; +} + +void Digitizer::stepping(const o2::itsmft::Hit& hit, float**& respMatrix, int& rowStart, int& colStart, int& rowSpan, int& colSpan) +{ const auto& matrix = mGeometry->getMatrixL2G(hit.GetDetectorID()); + const int chipID = hit.GetDetectorID(); + const int subdetectorID = mGeometry->getIOTOFLayer(chipID); + + auto xyzPositionStart(matrix ^ (hit.GetPosStart())); // start position in sensor frame + auto xyzPositionEnd(matrix ^ (hit.GetPos())); // end position in sensor frame + + const auto& digitizerParams = o2::iotof::DPLDigitizerParam::Instance(); + const auto stepVector = (xyzPositionEnd - xyzPositionStart) / digitizerParams.nSimSteps; + xyzPositionStart = xyzPositionStart + stepVector * 0.5f; // center the start position in the middle of the step + xyzPositionEnd = xyzPositionEnd - stepVector * 0.5f; // center the end position in the middle of the step + + rowStart = -1; + colStart = -1; + int rowEnd = -1, colEnd = -1, nSkip = 0, nSteps = digitizerParams.nSimSteps; + while (!sSegmentation->localToDetector(xyzPositionStart.X(), xyzPositionStart.Z(), rowStart, colStart, mGeometry->getIOTOFLayer(chipID))) { + if (++nSkip > digitizerParams.nSimSteps) { // additional check to add: should we exclude something? + LOG(debug) << "Hit position out of bounds for detector ID " << chipID; + return; // hit is outside the active area + } + xyzPositionStart += stepVector; + } + + while (!sSegmentation->localToDetector(xyzPositionEnd.X(), xyzPositionEnd.Z(), rowEnd, colEnd, mGeometry->getIOTOFLayer(chipID))) { + if (++nSkip > digitizerParams.nSimSteps) { // additional check to add: should we exclude something? + LOG(debug) << "Hit position out of bounds for detector ID " << chipID; + return; // hit is outside the active area + } + xyzPositionEnd += stepVector; + } - math_utils::Vector3D xyzPositionStart(matrix ^ (hit.GetPosStart())); // start position in sensor frame - // math_utils::Vector3D xyzPositionEnd(matrix ^ (hit.GetPos())); // end position in sensor frame + if (rowStart > rowEnd) { + std::swap(rowStart, rowEnd); + } + if (colStart > colEnd) { + std::swap(colStart, colEnd); + } - int row = 0; // Will be determined from start hit position - int col = 0; // Will be determined from start hit position + // Expand the range to take into account the effects of charge sharing + rowStart -= digitizerParams.responseMatrixSize / 2; + rowEnd += digitizerParams.responseMatrixSize / 2; + rowStart = std::max(rowStart, 0); + colStart = std::max(colStart, 0); - if (!sSegmentation->localToDetector(xyzPositionStart.X(), xyzPositionStart.Z(), row, col, mGeometry->getIOTOFLayer(chipID))) { - LOG(debug) << "Hit position out of bounds for detector ID " << chipID; - return; // hit is outside the active area + rowEnd = std::min(rowEnd, (subdetectorID == 0 ? sSegmentation->mITofSpecsConfig.NRows : sSegmentation->mOTofSpecsConfig.NRows) - 1); + colEnd = std::min(colEnd, (subdetectorID == 0 ? sSegmentation->mITofSpecsConfig.NCols : sSegmentation->mOTofSpecsConfig.NCols) - 1); + rowSpan = rowEnd - rowStart + 1; + colSpan = colEnd - colStart + 1; + + respMatrix = new float*[rowSpan]; + for (int i = 0; i < rowSpan; ++i) { + respMatrix[i] = new float[colSpan](); } - // Create the digit with time information - o2::MCCompLabel label(hit.GetTrackID(), evID, srcID, false); - const int roFrameAbs = 0; // For now, we can set this to 0 or calculate based on time if needed - const int nROF = 1; // For now, we can assume the signal is contained in one ROF, this can be extended to multiple ROFs based on the time + int rowPrev = -1, colPrev = -1, row = 0, col = 0; + if (!respMatrix || rowSpan <= 0 || colSpan <= 0) { + return; + } + if (nSkip) { + nSteps -= nSkip; + } - registerDigits(chip, roFrameAbs, smearedTime, nROF, static_cast(row), static_cast(col), charge, label); + auto& currentPosLocal = xyzPositionStart; + for (int iStep = nSteps; iStep--;) { + sSegmentation->localToDetector(currentPosLocal.X(), currentPosLocal.Z(), row, col, subdetectorID); + if (row != rowPrev || col != colPrev) { + rowPrev = row; + colPrev = col; + } + + currentPosLocal += stepVector; // Move to the next step position + + for (int irow = digitizerParams.responseMatrixSize; irow--;) { + int rowDest = row + irow - (digitizerParams.responseMatrixSize / 2) - rowStart; // destination row in the respMatrix + if (rowDest < 0 || rowDest >= rowSpan) { + continue; + } + for (int icol = digitizerParams.responseMatrixSize; icol--;) { + int colDest = col + icol - (digitizerParams.responseMatrixSize / 2) - colStart; // destination column in the respMatrix + if (colDest < 0 || colDest >= colSpan) { + continue; + } + respMatrix[rowDest][colDest] += 1.; + } + } + } } //_______________________________________________________________________ @@ -200,10 +303,9 @@ void Digitizer::fillOutputContainer() auto& chipDigits = chip.getDigits(); for (const auto& [key, digit] : chipDigits) { - /// Charge threshold not implemented yet - /// if (digit.getCharge() < mChargeThreshold) { - /// continue; // skip digits below threshold - /// } + if (digit.getCharge() < mChargeThreshold) { + continue; // skip digits below threshold + } int digitID = mDigits->size(); mDigits->emplace_back(digit.getChipIndex(), digit.getRow(), digit.getColumn(), digit.getCharge(), digit.getTime()); From 335e4b554a3a85d74fa56b5bac4e0d3cafe52e9e Mon Sep 17 00:00:00 2001 From: Anton Alkin Date: Tue, 21 Jul 2026 07:53:42 +0200 Subject: [PATCH 14/47] DPL Analysis: extract concepts definitions from headers (#15590) --- Framework/Core/include/Framework/ASoA.h | 205 ++---------- .../Core/include/Framework/AnalysisHelpers.h | 71 +--- Framework/Core/include/Framework/Concepts.h | 313 ++++++++++++++++++ Framework/Core/test/test_Concepts.cxx | 4 + 4 files changed, 354 insertions(+), 239 deletions(-) create mode 100644 Framework/Core/include/Framework/Concepts.h diff --git a/Framework/Core/include/Framework/ASoA.h b/Framework/Core/include/Framework/ASoA.h index a21532e42e692..ba6d58df9a639 100644 --- a/Framework/Core/include/Framework/ASoA.h +++ b/Framework/Core/include/Framework/ASoA.h @@ -15,7 +15,7 @@ #if defined(__CLING__) #error "Please do not include this file in ROOT dictionary generation" #endif - +#include "Framework/Concepts.h" #include "Framework/ConcreteDataMatcher.h" #include "Framework/Pack.h" // IWYU pragma: export #include "Framework/FunctionalHelpers.h" // IWYU pragma: export @@ -189,30 +189,13 @@ consteval auto intersectOriginals() namespace o2::soa { -struct Binding; - -template -concept not_void = requires { !std::same_as; }; - -/// column identification concepts -template -concept is_persistent_column = requires(C c) { c.mColumnIterator; }; - +/// column identification template constexpr bool is_persistent_v = is_persistent_column; template using is_persistent_column_t = std::conditional_t, std::true_type, std::false_type>; -template -concept is_self_index_column = not_void && std::same_as; - -template -concept is_index_column = !is_self_index_column && requires(C c, o2::soa::Binding b) { - { c.setCurrentRaw(b) } -> std::same_as; - requires std::same_as; -}; - template using is_external_index_t = typename std::conditional_t, std::true_type, std::false_type>; @@ -239,6 +222,7 @@ static consteval int getIndexPosToKey_impl() /// Base type for table metadata template struct TableMetadata { + static constexpr void isTableMetadata() {}; using columns = framework::pack; using persistent_columns_t = framework::selected_pack; using external_index_columns_t = framework::selected_pack; @@ -270,6 +254,7 @@ struct TableMetadata { template struct MetadataTrait { + static constexpr void isMetadataTrait() {}; using metadata = void; }; @@ -277,6 +262,7 @@ struct MetadataTrait { /// type signature template struct Hash { + static constexpr void isHash() {}; static constexpr uint32_t hash = H; static constexpr char const* const str{""}; }; @@ -298,6 +284,7 @@ consteval auto filterForKey() #define O2HASH(_Str_) \ template <> \ struct Hash<_Str_ ""_h> { \ + static constexpr void isHash() {}; \ static constexpr uint32_t hash = _Str_ ""_h; \ static constexpr char const* const str{_Str_}; \ }; @@ -306,6 +293,8 @@ consteval auto filterForKey() #define O2ORIGIN(_Str_) \ template <> \ struct Hash<_Str_ ""_h> { \ + static constexpr void isHash() {}; \ + static constexpr void isOriginHash() {}; \ static constexpr header::DataOrigin origin{_Str_}; \ static constexpr uint32_t hash = _Str_ ""_h; \ static constexpr char const* const str{_Str_}; \ @@ -386,13 +375,6 @@ constexpr framework::ConcreteDataMatcher matcher() return {origin(), description(signature()), R.version}; } -/// hash identification concepts -template -concept is_aod_hash = requires(T t) { t.hash; t.str; }; - -template -concept is_origin_hash = is_aod_hash && requires(T t) { t.origin; }; - /// convert TableRef to a DPL source specification template static constexpr auto sourceSpec() @@ -446,27 +428,6 @@ struct Binding { using SelectionVector = std::vector; -template -concept has_parent_t = not_void; - -template -concept is_metadata = framework::base_of_template; - -template -concept is_metadata_trait = framework::specialization_of_template; - -template -concept has_metadata = is_metadata_trait && not_void; - -template -concept has_extension = is_metadata && not_void; - -template -concept has_configurable_extension = has_extension && requires(T t) { typename T::configurable_t; requires std::same_as; }; - -template -concept is_spawnable_column = std::same_as; - template struct EquivalentIndex { constexpr static bool value = false; @@ -696,6 +657,8 @@ class ColumnIterator : ChunkingPolicy template struct Column { + static constexpr void isIteratableColumn() {}; + using inherited_t = INHERIT; Column(ColumnIterator const& it) : mColumnIterator{it} @@ -730,6 +693,7 @@ struct Column { /// method call. template struct DynamicColumn { + static constexpr void isDynamicColumn() {}; using inherited_t = INHERIT; static constexpr const char* const& columnLabel() { return INHERIT::mLabel; } @@ -737,6 +701,7 @@ struct DynamicColumn { template struct IndexColumn { + static constexpr void isEnumeratingColumn() {}; using inherited_t = INHERIT; static constexpr const uint32_t hash = 0; @@ -745,6 +710,7 @@ struct IndexColumn { template struct MarkerColumn { + static constexpr void isMarkingColumn() {}; using inherited_t = INHERIT; static constexpr const uint32_t hash = 0; @@ -846,29 +812,6 @@ struct Index : o2::soa::IndexColumn> { std::tuple rowOffsets; }; -template -concept is_indexing_column = requires(C& c) { - c.rowIndices; - c.rowOffsets; -}; - -template -concept is_dynamic_column = requires(C& c) { - c.boundIterators; -}; - -template -concept is_marker_column = requires { &C::mark; }; - -template -using is_dynamic_t = std::conditional_t, std::true_type, std::false_type>; - -template -concept is_column = is_persistent_column || is_dynamic_column || is_indexing_column || is_marker_column; - -template -using is_indexing_t = std::conditional_t, std::true_type, std::false_type>; - struct IndexPolicyBase { /// Position inside the current table int64_t mRowIndex = 0; @@ -877,10 +820,12 @@ struct IndexPolicyBase { }; struct RowViewSentinel { + static constexpr void isRowViewSentinel() {}; int64_t const index; }; struct FilteredIndexPolicy : IndexPolicyBase { + static constexpr void isFilteredIndexPolicy(); // We use -1 in the IndexPolicyBase to indicate that the index is // invalid. What will validate the index is the this->setCursor() // which happens below which will properly setup the first index @@ -986,6 +931,7 @@ struct FilteredIndexPolicy : IndexPolicyBase { }; struct DefaultIndexPolicy : IndexPolicyBase { + static constexpr void isDefaultIndexPolicy() {}; /// Needed to be able to copy the policy DefaultIndexPolicy() = default; DefaultIndexPolicy(DefaultIndexPolicy&&) = default; @@ -1060,15 +1006,6 @@ struct DefaultIndexPolicy : IndexPolicyBase { int64_t mMaxRow = 0; }; -// template -// class Table; - -template -class Table; - -template -concept is_table = framework::specialization_of_template || framework::base_of_template; - /// Similar to a pair but not a pair, to avoid /// exposing the second type everywhere. template @@ -1077,17 +1014,10 @@ struct ColumnDataHolder { arrow::ChunkedArray* second; }; -template -concept can_bind = requires(T&& t) { - { t.B::mColumnIterator }; -}; - -template -concept has_index = (is_indexing_column || ...); - template struct TableIterator : IP, C... { public: + static constexpr void isTableIterator() {}; using self_t = TableIterator; using policy_t = IP; using all_columns = framework::pack; @@ -1312,48 +1242,6 @@ struct ArrowHelpers { static std::shared_ptr concatTables(std::vector>&& tables); }; -//! Helper to check if a type T is an iterator -template -concept is_iterator = framework::base_of_template || framework::specialization_of_template; - -template -concept is_table_or_iterator = is_table || is_iterator; - -template -concept with_originals = requires { - T::originals.size(); -}; - -template -concept with_sources = requires { - T::sources.size(); -}; - -template -concept with_sources_generator = requires(T t) { - t.template generateSources>(); -}; - -template -concept with_ccdb_urls = requires { - T::ccdb_urls.size(); -}; - -template -concept with_base_table = requires { - typename aod::MetadataTrait>::metadata::base_table_t; -}; - -template -concept with_expression_pack = requires { - typename T::expression_pack_t{}; -}; - -template -concept with_index_pack = requires { - typename T::index_pack_t{}; -}; - template os1, size_t N2, std::array os2> consteval bool is_compatible() { @@ -1376,12 +1264,6 @@ consteval bool is_binding_compatible_v() template using is_binding_compatible = std::conditional_t(), std::true_type, std::false_type>; -template -struct IndexTable; - -template -concept is_index_table = framework::specialization_of_template; - template static constexpr std::string getLabelForTable() { @@ -1509,6 +1391,7 @@ namespace o2::framework { /// tracks origin in bindingKey matcher to handle the correct arguments struct PreslicePolicyBase { + static constexpr void isPreslicePolicy() {}; const std::string binding; Entry bindingKey; @@ -1535,11 +1418,9 @@ struct PreslicePolicyGeneral : public PreslicePolicyBase { std::span getSliceFor(int value) const; }; -template -concept is_preslice_policy = std::derived_from; - template struct PresliceBase : public Policy { + static constexpr void isPresliceContainer() {}; constexpr static bool optional = OPT; using target_t = T; using policy_t = Policy; @@ -1580,13 +1461,6 @@ using Preslice = PresliceBase; template using PresliceOptional = PresliceBase; -template -concept is_preslice = std::derived_from&& - requires(T) -{ - T::optional; -}; - /// Can be user to group together a number of Preslice declaration /// to avoid the limit of 100 data members per task /// @@ -1600,11 +1474,8 @@ concept is_preslice = std::derived_from&& /// /// preslices.perCol; struct PresliceGroup { + static constexpr void isPresliceGroup() {}; }; - -template -concept is_preslice_group = std::derived_from; - } // namespace o2::framework namespace o2::soa @@ -1614,25 +1485,10 @@ class FilteredBase; template class Filtered; -template -concept has_filtered_policy = not_void && std::same_as; - -template -concept is_filtered_iterator = is_iterator && has_filtered_policy; - -template -concept is_filtered_table = framework::base_of_template; - // FIXME: compatbility declaration to be removed template constexpr bool is_soa_filtered_v = is_filtered_table; -template -concept is_filtered = is_filtered_table || is_filtered_iterator; - -template -concept is_not_filtered_table = is_table && !is_filtered_table; - /// Helper function to extract bound indices template static consteval auto extractBindings(framework::pack) @@ -1844,6 +1700,7 @@ template ; using table_t = self_t; @@ -2337,7 +2194,7 @@ concept dynamic_with_common_getter = is_dynamic_column && }; template -concept persistent_with_common_getter = is_persistent_v && requires(T t) { +concept persistent_with_common_getter = is_persistent_column && requires(T t) { { t.get() } -> std::convertible_to; }; @@ -3241,6 +3098,7 @@ consteval auto getIndexTargets() #define DECLARE_SOA_TABLE_METADATA_TRAIT(_Name_, _Desc_, _Version_) \ template <> \ struct MetadataTrait> { \ + static constexpr void isMetadataTrait() {}; \ using metadata = _Name_##Metadata; \ }; @@ -3251,6 +3109,7 @@ consteval auto getIndexTargets() using _Name_ = _Name_##From>; \ template <> \ struct MetadataTrait> { \ + static constexpr void isMetadataTrait() {}; \ using metadata = _Name_##Metadata; \ }; @@ -3310,6 +3169,7 @@ consteval auto getIndexTargets() }; \ template <> \ struct MetadataTrait> { \ + static constexpr void isMetadataTrait() {}; \ using metadata = _Name_##ExtensionMetadata; \ }; \ template \ @@ -3344,6 +3204,7 @@ consteval auto getIndexTargets() }; \ template <> \ struct MetadataTrait> { \ + static constexpr void isMetadataTrait() {}; \ using metadata = _Name_##CfgExtensionMetadata; \ }; \ template \ @@ -3379,6 +3240,7 @@ consteval auto getIndexTargets() }; \ template <> \ struct MetadataTrait> { \ + static constexpr void isMetadataTrait() {}; \ using metadata = _Name_##Metadata; \ }; \ template \ @@ -3433,6 +3295,7 @@ consteval auto getIndexTargets() }; \ template <> \ struct MetadataTrait> { \ + static constexpr void isMetadataTrait() {}; \ using metadata = _Name_##TimestampMetadata; \ }; \ template \ @@ -3449,6 +3312,7 @@ namespace o2::soa { template struct Join : Table, o2::aod::Hash<"JOIN/0"_h>, o2::aod::Hash<"JOIN"_h>, Ts...> { + static constexpr void isJoin() {}; using base = Table, o2::aod::Hash<"JOIN/0"_h>, o2::aod::Hash<"JOIN"_h>, Ts...>; Join(std::shared_ptr&& table, uint64_t offset = 0) @@ -3557,9 +3421,6 @@ constexpr auto join(Ts const&... t) return Join(ArrowHelpers::joinTables({t.asArrowTable()...}, std::span{Join::base::originalLabels})); } -template -concept is_join = framework::specialization_of_template; - template constexpr bool is_soa_join_v = is_join; @@ -3605,6 +3466,7 @@ template class FilteredBase : public T { public: + static constexpr void isFilteredBase() {}; using self_t = FilteredBase; using table_t = typename T::table_t; using T::originals; @@ -4216,6 +4078,7 @@ class Filtered> : public FilteredBase /// First index will be used by process() as the grouping template struct IndexTable : Table { + static constexpr void isIndexTable() {}; using self_t = IndexTable; using base_t = Table; using table_t = base_t; @@ -4261,6 +4124,7 @@ struct IndexTable : Table { template struct SmallGroupsBase : public Filtered { + static constexpr void isSmallGroups() {}; static constexpr bool applyFilters = APPLY; SmallGroupsBase(std::vector>&& tables, gandiva::Selection const& selection, uint64_t offset = 0) : Filtered(std::move(tables), selection, offset) {} @@ -4277,11 +4141,6 @@ using SmallGroups = SmallGroupsBase; template using SmallGroupsUnfiltered = SmallGroupsBase; - -template -concept is_smallgroups = requires { - [](SmallGroupsBase*) {}(std::declval*>()); -}; } // namespace o2::soa #endif // O2_FRAMEWORK_ASOA_H_ diff --git a/Framework/Core/include/Framework/AnalysisHelpers.h b/Framework/Core/include/Framework/AnalysisHelpers.h index c7d567c9eb810..96d89b722780b 100644 --- a/Framework/Core/include/Framework/AnalysisHelpers.h +++ b/Framework/Core/include/Framework/AnalysisHelpers.h @@ -492,12 +492,6 @@ class TableConsumer; /// Helper class actually implementing the cursor which can write to /// a table. The provided template arguments are if type Column and /// therefore refer only to the persisted columns. -template -concept is_producable = soa::has_metadata> || soa::has_metadata>; - -template -concept is_enumerated_iterator = requires(T t) { t.globalIndex(); }; - template struct WritingCursor { public: @@ -579,13 +573,13 @@ struct WritingCursor { decltype(FFL(std::declval())) cursor; private: - static decltype(auto) extract(is_enumerated_iterator auto const& arg) + static decltype(auto) extract(soa::is_enumerated_iterator auto const& arg) { return arg.globalIndex(); } template - requires(!is_enumerated_iterator) + requires(!soa::is_enumerated_iterator) static decltype(auto) extract(A&& arg) { return arg; @@ -642,9 +636,6 @@ template struct Produces : WritingCursor { }; -template -concept is_produces = requires(T t) { typename T::cursor_t; typename T::persistent_table_t; &T::cursor; }; - /// Use this to group together produces. Useful to separate them logically /// or simply to stay within the 100 elements per Task limit. /// Use as: @@ -654,11 +645,9 @@ concept is_produces = requires(T t) { typename T::cursor_t; typename T::persiste /// /// Notice the label MySetOfProduces is just a mnemonic and can be omitted. struct ProducesGroup { + static constexpr void isProducesGroup() {}; }; -template -concept is_produces_group = std::derived_from; - /// Helper template for table transformations template struct TableTransform { @@ -682,12 +671,6 @@ struct TableTransform { /// This helper struct allows you to declare extended tables which should be /// created by the task (as opposed to those pre-defined by data model) -template -concept is_spawnable = soa::has_metadata>> && soa::has_extension>::metadata>; - -template -concept is_dynamically_spawnable = soa::has_metadata>> && soa::has_configurable_extension>::metadata>; - template consteval auto transformBase() { @@ -736,13 +719,6 @@ struct Spawns : decltype(transformBase()) { }(); }; -template -concept is_spawns = requires(T t) { - typename T::metadata; - typename T::expression_pack_t; - requires std::same_as>; -}; - /// This helper struct allows you to declare extended tables with dynamically-supplied /// expressions to be created by the task /// The actual expressions have to be set in init() for the configurable expression @@ -792,15 +768,6 @@ struct Defines : decltype(transformBase()) { template using DefinesDelayed = Defines; -template -concept is_defines = requires(T t) { - typename T::metadata; - typename T::placeholders_pack_t; - requires std::same_as>; - requires std::same_as; - &T::recompile; -}; - /// Policy to control index building /// Exclusive index: each entry in a row has a valid index /// Sparse index: values in a row can be (-1), index table is isomorphic (joinable) to T1 @@ -859,13 +826,6 @@ struct Builds : decltype(transformBase()) { } }; -template -concept is_builds = requires(T t) { - typename T::metadata; - typename T::Key; - requires std::same_as>; -}; - /// a task with rewritten origin, if running together with a task with the default, will /// have a different name and thus its output would be routed separately @@ -877,6 +837,7 @@ concept is_builds = requires(T t) { /// to determine the target file, e.g. analysis result, QA or control histogram, /// etc. template + requires(std::derived_from) struct OutputObj { using obj_t = T; @@ -964,15 +925,6 @@ struct OutputObj { uint32_t mTaskHash; }; -template -concept is_outputobj = requires(T t) { - &T::setHash; - &T::spec; - &T::ref; - requires std::same_as()), typename T::obj_t*>; - requires std::same_as>; -}; - /// This helper allows you to fetch a Sevice from the context or /// by using some singleton. This hopefully will hide the Singleton and /// We will be able to retrieve it in a more thread safe manner later on. @@ -991,12 +943,6 @@ struct Service { } }; -template -concept is_service = requires(T t) { - requires std::same_as; - &T::operator->; -}; - auto getTableFromFilter(soa::is_filtered_table auto const& table, soa::SelectionVector&& selection) { return std::make_unique>>(std::vector{table}, std::forward(selection)); @@ -1016,7 +962,7 @@ void initializePartitionCaches(std::set const& hashes, std::shared_ptr /// the real reason is to provide grouped parts for the process functions that request it /// better solution would be to "slice" the selection, as is already done in GroupSlicer /// for the same purpose, instead of reapplying the filtering -template +template struct Partition { using content_t = T; Partition(expressions::Node&& filter_) : filter{std::forward(filter_)} @@ -1128,13 +1074,6 @@ struct Partition { return mFiltered->size(); } }; - -template -concept is_partition = requires(T t) { - &T::updatePlaceholders; - requires std::same_as; - requires std::same_as>>; -}; } // namespace o2::framework namespace o2::soa diff --git a/Framework/Core/include/Framework/Concepts.h b/Framework/Core/include/Framework/Concepts.h new file mode 100644 index 0000000000000..fea40b25ff1ff --- /dev/null +++ b/Framework/Core/include/Framework/Concepts.h @@ -0,0 +1,313 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef O2_FRAMEWORK_CONCEPTS_H +#define O2_FRAMEWORK_CONCEPTS_H + +#include +#include +#include + +namespace o2::aod +{ +template +struct Hash; +/// hash +/// 1. require aod::Hash +template +concept is_aod_hash = requires(T t) { t.isHash(); }; +/// 2. requires aod::Hash with header::DataOrigin +template +concept is_origin_hash = requires(T t) { t.isOriginHash(); }; + +template +struct MetadataTrait; +} // namespace o2::aod + +namespace o2::soa +{ +/// general +/// require a type to be not void +template +concept not_void = requires { requires !std::same_as; }; + +/// columns +/// 1. require a storage-backed column +template +concept is_persistent_column = requires(C c) { c.isIteratableColumn(); }; + +/// 2. require self-index column +template +concept is_self_index_column = requires(C c) { + typename C::compatible_signature; + // requires aod::is_aod_hash; + typename C::self_index_t; + requires std::same_as; +}; + +/// 3. require bindable index column +template +concept is_index_column = requires(C c) { + typename C::binding_t; + requires not_void; +}; + +/// 4. require a column that can be created from an expression +template +concept is_spawnable_column = std::same_as::spawnable_t, std::true_type>; + +/// 5. require an enumerating column, like soa::Index +template +concept is_indexing_column = requires(C c) { c.isEnumeratingColumn(); }; + +/// 6. require a dynamic column +template +concept is_dynamic_column = requires(C c) { c.isDynamicColumn(); }; + +/// 7. require a marking column +template +concept is_marker_column = requires(C c) { c.isMarkingColumn(); }; + +/// 8. require any supported column +template +concept is_column = is_persistent_column || is_dynamic_column || is_indexing_column || is_marker_column; + +/// 9. require a type that can be bound as a column of type B +template +concept can_bind = requires(T&& t) { + { t.B::mColumnIterator }; +}; + +/// 10. require at least one column in an exploded pack to be an indexing column +template +concept has_index = (is_indexing_column || ...); + +/// pack filtering helpers +template +using is_dynamic_t = std::conditional_t, std::true_type, std::false_type>; + +template +using is_indexing_t = std::conditional_t, std::true_type, std::false_type>; + +/// tables, iterators and metadata +/// 1. require a type with parent_t dependent type +template +concept has_parent_t = not_void; + +/// 2. require a MetadataTrait specialization/descendant +template +concept is_metadata_trait = requires(T t) { t.isMetadataTrait(); }; + +/// 3. require a TableMetadata depcialization/descendant +template +concept is_metadata = requires(T t) { t.isTableMetadata(); }; + +/// 4. require a type with non-void metadata dependent type +template +concept has_metadata = is_metadata::metadata>; + +/// 5. require a type with non-void extension_table_t dependent type +template +concept has_extension = is_metadata && not_void::extension_table_t>; + +/// 6. require a type with non-void configurable_t dependent type, that is same as true_type +template +concept has_configurable_extension = has_extension && requires(T t) { typename std::decay_t::configurable_t; requires std::same_as::configurable_t>; }; + +/// 7. require an soa::Table +template +concept is_table = requires(T t) { t.isSOATable(); }; + +/// 8. require a specialization/descendant of a TableIterator +template +concept is_iterator = requires(T t) { t.isTableIterator(); }; + +/// 9. require a table or iterator +template +concept is_table_or_iterator = is_table || is_iterator; + +/// 10. require soa::IndexTable +template +concept is_index_table = requires(T t) { + t.isIndexTable(); +}; + +/// 11. require a type with a filtered policy +template +concept has_filtered_policy = not_void::policy_t> && requires { std::decay_t::policy_t::isFilteredIndexPolicy(); }; + +/// 12. require a filtered table iterator +template +concept is_filtered_iterator = is_iterator && has_filtered_policy; + +/// 13. require a filtered table +template +concept is_filtered_table = requires(T t) { t.isFilteredBase(); }; + +/// 14. require a filtered table or iterator +template +concept is_filtered = is_filtered_table || is_filtered_iterator; + +/// 15. require not filtered table +template +concept is_not_filtered_table = is_table && !is_filtered_table; + +/// 16. require a join +template +concept is_join = requires(T t) { t.isJoin(); }; + +/// 17. require an enumerated iterator +template +concept is_enumerated_iterator = requires(T t) { t.globalIndex(); }; + +/// misc +/// 1. require a type with originals container +template +concept with_originals = requires { + T::originals.size(); +}; + +/// 2. require a type with sources container +template +concept with_sources = requires { + T::sources.size(); +}; + +/// 3. require a type with sources generator method +template +concept with_sources_generator = requires(T t) { + t.generateSources(); +}; + +/// 4. require a type with ccd_urls container +template +concept with_ccdb_urls = requires(T t) { + t.ccdb_urls.size(); +}; + +/// 5. require a type, whos metadata has base_table_t dependant type +template +concept with_base_table = with_originals && has_metadata>> && requires { + typename aod::MetadataTrait>::metadata::base_table_t; +}; + +template +concept with_base_table_ng = not_void; // redicrection should be done at the check site + +/// 6. require a type with expression_pack_t dependant type +template +concept with_expression_pack = requires { + typename T::expression_pack_t{}; +}; + +/// 7. require a type with index_pack_t dependant type +template +concept with_index_pack = requires { + typename T::index_pack_t{}; +}; + +/// 8. require SmallGroups +template +concept is_smallgroups = requires(T t) { t.isSmallGroups(); }; +} // namespace o2::soa + +namespace o2::framework +{ +/// preslice +/// 1. require a preslice policy +template +concept is_preslice_policy = requires(T t) { t.isPreslicePolicy(); }; + +/// 2. require a preslice container +template +concept is_preslice = requires(T t) { t.isPresliceContainer(); }; + +/// 3. reqiure a preslice group +template +concept is_preslice_group = requires(T t) { t.isPresliceGroup(); }; + +/// 4. require a producable entity +template +concept is_producable = soa::has_metadata>> || soa::has_metadata>>; + +/// 5. require produces declaration +template +concept is_produces = requires(T t) { typename T::cursor_t; typename T::persistent_table_t; &T::cursor; }; + +/// 6. require produces group +template +concept is_produces_group = requires(T t) { t.isProducesGroup(); }; + +/// 7. require spawnable entity +template +concept is_spawnable = soa::has_metadata>> && soa::has_extension>::metadata>; + +/// 8. require dynamically spawnable entity +template +concept is_dynamically_spawnable = soa::has_metadata>> && soa::has_configurable_extension>::metadata>; + +/// 9. require spawns declaration +template +concept is_spawns = requires(T t) { + typename T::metadata; + typename T::expression_pack_t; + t.projector.get(); +}; + +/// 10. require defines declaration +template +concept is_defines = requires(T t) { + typename T::metadata; + typename T::placeholders_pack_t; + t.projector.get(); + requires std::same_as; + t.recompile(); +}; + +/// 11. require builds declaration +template +concept is_builds = requires(T t) { + typename T::metadata; + typename T::Key; + t.map.size(); +}; + +/// 12. require outputobj declaration +template +concept is_outputobj = requires(T t) { + &T::setHash; + &T::spec; + &T::ref; + requires std::same_as()), typename T::obj_t*>; + requires std::same_as; +}; + +/// 13. require service declaration +template +concept is_service = requires(T t) { + requires std::same_as; + &T::operator->; +}; + +/// 14. require partition declaration +template +concept is_partition = requires(T t) { + &T::updatePlaceholders; + t.mFiltered.get(); + &T::operator->; + requires std::same_as; + t.begin(); + t.end(); + t.size(); +}; +} // namespace o2::framework + +#endif // O2_FRAMEWORK_CONCEPTS_H diff --git a/Framework/Core/test/test_Concepts.cxx b/Framework/Core/test/test_Concepts.cxx index 375e537cfaec0..ff5e0fa6200db 100644 --- a/Framework/Core/test/test_Concepts.cxx +++ b/Framework/Core/test/test_Concepts.cxx @@ -9,6 +9,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. +#include "Framework/Concepts.h" #include #include "Framework/ASoA.h" #include "Framework/AnalysisDataModel.h" @@ -87,6 +88,9 @@ TEST_CASE("IdentificationConcepts") REQUIRE(with_originals); + REQUIRE(o2::soa::is_metadata_trait>>); + REQUIRE(o2::soa::has_metadata>>); + REQUIRE(o2::soa::is_metadata>::metadata>); REQUIRE(with_sources_generator>::metadata>); REQUIRE(with_base_table); From 3ce995c84a4208353642eb3b831064d6f429c231 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:41:19 +0200 Subject: [PATCH 15/47] Bump actions/setup-python from 6 to 7 (#15614) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/datamodel-doc.yml | 2 +- .github/workflows/reports.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/datamodel-doc.yml b/.github/workflows/datamodel-doc.yml index 3ba015631aec6..1dd54790bff76 100644 --- a/.github/workflows/datamodel-doc.yml +++ b/.github/workflows/datamodel-doc.yml @@ -40,7 +40,7 @@ jobs: git checkout -B auto-datamodel-doc - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: 3.x diff --git a/.github/workflows/reports.yml b/.github/workflows/reports.yml index 5a04e56382fb3..444ca1002f6ff 100644 --- a/.github/workflows/reports.yml +++ b/.github/workflows/reports.yml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python 3.10 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.10' - uses: actions/cache@v5 From e92d8809e2279d90cc58690d3564a534ee80c48d Mon Sep 17 00:00:00 2001 From: David Rohr Date: Tue, 21 Jul 2026 10:41:55 +0200 Subject: [PATCH 16/47] GPU: Improve some debug messages (#15611) --- GPU/GPUTracking/Base/GPUReconstruction.cxx | 2 ++ GPU/GPUTracking/Base/cuda/GPUReconstructionCUDA.cu | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/GPU/GPUTracking/Base/GPUReconstruction.cxx b/GPU/GPUTracking/Base/GPUReconstruction.cxx index 9fd7d16c99f11..ac67c617872bd 100644 --- a/GPU/GPUTracking/Base/GPUReconstruction.cxx +++ b/GPU/GPUTracking/Base/GPUReconstruction.cxx @@ -275,6 +275,8 @@ int32_t GPUReconstruction::InitPhaseBeforeDevice() if (GetProcessingSettings().deterministicGPUReconstruction) { if (!detMode) { GPUError("WARNING, deterministicGPUReconstruction needs GPUCA_DETERMINISTIC_MODE for being fully deterministic, without only most indeterminism by concurrency is removed, but floating point effects remain!"); + } else { + GPUInfo("GPU Deterministic Reconstruction is enabled"); } if (mProcessingSettings->debugLevel >= 6 && ((mProcessingSettings->debugMask + 1) & mProcessingSettings->debugMask)) { GPUError("WARNING: debugMask %d - debug output might not be deterministic with intermediate steps missing", mProcessingSettings->debugMask); diff --git a/GPU/GPUTracking/Base/cuda/GPUReconstructionCUDA.cu b/GPU/GPUTracking/Base/cuda/GPUReconstructionCUDA.cu index 8628abb1c0374..cd9bab59e397b 100644 --- a/GPU/GPUTracking/Base/cuda/GPUReconstructionCUDA.cu +++ b/GPU/GPUTracking/Base/cuda/GPUReconstructionCUDA.cu @@ -194,7 +194,7 @@ int32_t GPUReconstructionCUDA::InitDevice_Runtime() bool noDevice = false; if (bestDevice == -1) { - GPUWarning("No %sCUDA Device available, aborting CUDA Initialisation (Required mem: %ld)", count ? "appropriate " : "", (int64_t)mDeviceMemorySize); + GPUWarning("No %sCUDA Device available, aborting CUDA Initialisation (Required mem: %ld, scanned %d devices)", count ? "appropriate " : "", (int64_t)mDeviceMemorySize, count); #ifndef __HIPCC__ GPUImportant("Requiring Revision %d.%d, Mem: %lu", reqVerMaj, reqVerMin, std::max(mDeviceMemorySize, REQUIRE_MIN_MEMORY)); #endif @@ -228,7 +228,7 @@ int32_t GPUReconstructionCUDA::InitDevice_Runtime() GPUChkErrI(cudaGetDeviceProperties(&deviceProp, mDeviceId)); if (GetProcessingSettings().debugLevel >= 2) { - GPUInfo("Using CUDA Device %s with Properties:", deviceProp.name); + GPUInfo("Using CUDA Device %d: %s with Properties:", bestDevice, deviceProp.name); GPUInfo("\ttotalGlobalMem = %ld", (uint64_t)deviceProp.totalGlobalMem); GPUInfo("\tsharedMemPerBlock = %ld", (uint64_t)deviceProp.sharedMemPerBlock); GPUInfo("\tregsPerBlock = %d", deviceProp.regsPerBlock); From 8148d9c7e317b23633b0da1fbdf27fd099a9d4b5 Mon Sep 17 00:00:00 2001 From: Gabriele Cimador Date: Tue, 16 Jun 2026 11:04:23 +0200 Subject: [PATCH 17/47] Update and add optimal parameters for several GPU architectures --- .../Definitions/Parameters/GPUParameters.csv | 232 +++++++++--------- dependencies/FindO2GPU.cmake | 8 +- 2 files changed, 123 insertions(+), 117 deletions(-) diff --git a/GPU/GPUTracking/Definitions/Parameters/GPUParameters.csv b/GPU/GPUTracking/Definitions/Parameters/GPUParameters.csv index 5a8c7cf4ddaec..12c87afceb86f 100644 --- a/GPU/GPUTracking/Definitions/Parameters/GPUParameters.csv +++ b/GPU/GPUTracking/Definitions/Parameters/GPUParameters.csv @@ -1,115 +1,117 @@ -Architecture,default,default_cpu,MI100,VEGA,TAHITI,TESLA,FERMI,PASCAL,KEPLER,AMPERE,TURING,ADA,OPENCL,RDNA,MI210,BLACKWELL -,,,,,,,,,,,,,,,, -CORE:,,,,,,,,,,,,,,,, -WARP_SIZE,0,,64,64,32,32,32,32,32,32,32,32,32,32,64,32 -THREAD_COUNT_DEFAULT,256,,256,256,,,,,,512,512,512,256,512,512,512 -,,,,,,,,,,,,,,,, -LB:,,,,,,,,,,,,,,,, -GPUTPCCreateTrackingData,256,,"[256, 7]","[192, 2]",,,,,,384,256,256,,,,384 -GPUTPCTrackletConstructor,256,,"[768, 8]","[512, 10]","[256, 2]","[256, 1]","[256, 2]","[1024, 2]","[512, 4]","[256, 2]","[256, 2]","[256, 2]",,,,768 -GPUTPCTrackletSelector,256,,"[384, 5]","[192, 10]","[256, 3]","[256, 1]","[256, 3]","[512, 4]","[256, 3]","[192, 3]","[192, 3]","[192, 3]",,,,992 -GPUTPCNeighboursFinder,256,,"[192, 8]","[960, 8]",256,256,256,512,256,"[640, 1]","[640, 1]","[640, 1]",,,,992 -GPUTPCNeighboursCleaner,256,,"[128, 5]","[384, 9]",256,256,256,256,256,512,512,512,,,,672 -GPUTPCExtrapolationTracking,256,,"[256, 7]","[256, 2]",,,,,,"[128, 4]","[192, 2]","[192, 2]",,,,896 -GPUTRDTrackerKernels_gpuVersion,512,,,,,,,,,,,,,,, -GPUTPCCreateOccupancyMap_fill,256,,,,,,,,,,,,,,, -GPUTPCCreateOccupancyMap_fold,256,,,,,,,,,,,,,,, -GPUTRDTrackerKernels_o2Version,512,,,,,,,,,,,,,,, -GPUTPCCompressionKernels_step0attached,256,,"[128, 1]","[64, 2]",,,,,,"[64, 2]",128,128,,,,"[96, 3]" -GPUTPCCompressionKernels_step1unattached,256,,"[512, 2]","[512, 2]",,,,,,"[512, 3]","[512, 2]","[512, 2]",,,,"[512, 2]" -GPUTPCDecompressionKernels_step0attached,256,,"[128, 2]","[128, 2]",,,,,,"[32, 1]","[32, 1]","[32, 1]",,,,"[32, 1]" -GPUTPCDecompressionKernels_step1unattached,256,,"[64, 2]","[64, 2]",,,,,,"[32, 1]","[32, 1]","[32, 1]",,,,"[32, 1]" -GPUTPCDecompressionUtilKernels_sortPerSectorRow,256,,,,,,,,,,,,,,, -GPUTPCDecompressionUtilKernels_countFilteredClusters,256,,,,,,,,,,,,,,, -GPUTPCDecompressionUtilKernels_storeFilteredClusters,256,,,,,,,,,,,,,,, -GPUTPCCFDecodeZS,"[128, 4]",,"[64, 4]","[64, 1]",,,,,,"[64, 10]","[64, 8]","[64, 8]",,,,"[64, 10]" -GPUTPCCFDecodeZSLink,"""GPUCA_WARP_SIZE""",,"""GPUCA_WARP_SIZE""","""GPUCA_WARP_SIZE""",,,,,,"""GPUCA_WARP_SIZE""","""GPUCA_WARP_SIZE""","""GPUCA_WARP_SIZE""",,,,"""GPUCA_WARP_SIZE""" -GPUTPCCFDecodeZSDenseLink,"""GPUCA_WARP_SIZE""",,"[""GPUCA_WARP_SIZE"", 4]","[""GPUCA_WARP_SIZE"", 14]",,,,,,"""GPUCA_WARP_SIZE""","""GPUCA_WARP_SIZE""","""GPUCA_WARP_SIZE""",,,,"[""GPUCA_WARP_SIZE"", 8]" -GPUTPCCFGather,"[1024, 1]",,"[1024, 5]","[1024, 1]",,,,,,"[1024, 1]","[1024, 1]","[1024, 1]",,,,"[1024, 1]" -COMPRESSION_GATHER,1024,,1024,1024,,,,,,1024,1024,1024,,,, -GPUTPCGMMergerTrackFit,256,,"[192, 2]","[64, 7]",,,,,,"[64, 4]","[32, 8]","[32, 8]",,,,"[64, 8]" -GPUTPCGMMergerFollowLoopers,256,,"[256, 5]","[256, 4]",,,,,,"[64, 12]","[128, 4]","[128, 4]",,,,"[224, 3]" -GPUTPCGMMergerSectorRefit,256,,"[64, 4]","[256, 2]",,,,,,"[32, 6]","[64, 5]","[64, 5]",,,,"[32, 10]" -GPUTPCGMMergerUnpackResetIds,256,,256,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerUnpackGlobal,256,,256,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerResolve_step0,256,,512,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerResolve_step1,256,,512,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerResolve_step2,256,,512,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerResolve_step3,256,,512,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerResolve_step4,256,,512,256,,,,,,"[256, 4]","[256, 4]","[256, 4]",,,,"[256, 4]" -GPUTPCGMMergerClearLinks,256,,256,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerMergeWithinPrepare,256,,256,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerMergeSectorsPrepare,256,,256,256,,,,,,"[256, 2]","[256, 2]","[256, 2]",,,,"[256, 2]" -GPUTPCGMMergerMergeBorders_step0,256,,512,256,,,,,,192,192,192,,,,192 -GPUTPCGMMergerMergeBorders_step2,256,,512,256,,,,,,"[64, 2]",256,256,,,,"[64, 2]" -GPUTPCGMMergerMergeCE,256,,512,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerLinkExtrapolatedTracks,256,,256,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerCollect,256,,"[768, 1]","[1024, 1]",,,,,,"[256, 2]","[128, 2]","[128, 2]",,,,"[288, 1]" -GPUTPCGMMergerSortTracksPrepare,256,,256,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerPrepareForFit_step0,256,,256,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerPrepareForFit_step1,256,,256,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerPrepareForFit_step2,256,,256,256,,,,,,256,256,256,,,,256 -GPUTPCGMMergerFinalize_step0,256,,,256,,,,,,,,,,,,256 -GPUTPCGMMergerFinalize_step1,256,,,256,,,,,,,,,,,,256 -GPUTPCGMMergerFinalize_step2,256,,,256,,,,,,,,,,,,256 -GPUTPCGMMergerMergeLoopers_step0,256,,,,,,,,,,,,,,,256 -GPUTPCGMMergerMergeLoopers_step1,256,,,,,,,,,,,,,,,256 -GPUTPCGMMergerMergeLoopers_step2,256,,,,,,,,,,,,,,,256 -GPUTPCGMO2Output_prepare,256,,,,,,,,,,,,,,,256 -GPUTPCGMO2Output_output,256,,,,,,,,,,,,,,,256 -GPUTPCStartHitsFinder,256,,"[1024, 2]","[1024, 7]",256,256,256,256,256,512,512,512,,,,608 -GPUTPCStartHitsSorter,256,,"[1024, 5]","[512, 7]",256,256,256,256,256,"[512, 1]","[512, 1]","[512, 1]",,,,608 -GPUTPCCFCheckPadBaseline,576,,"[576, 2]","[576, 2]",,,,,,"[576, 2]",,,,,,"[576, 2]" -GPUTPCCFHIPTailConnector,256,,256,256,,,,,,256,,,,,, -GPUTPCCFHIPClusterizer,256,,256,256,,,,,,256,,,,,, -GPUTPCCFChargeMapFiller_fillIndexMap,512,,512,512,,,,,,448,,,,,,448 -GPUTPCCFChargeMapFiller_fillFromDigits,512,,512,512,,,,,,448,,,,,,448 -GPUTPCCFChargeMapFiller_findFragmentStart,512,,512,512,,,,,,448,,,,,,448 -GPUTPCCFPeakFinder,512,,"[512, 9]","[512, 4]",,,,,,128,,,,,,"[128, 5]" -GPUTPCCFNoiseSuppression,512,,512,512,,,,,,448,,,,,, -GPUTPCCFDeconvolution,512,,"[512, 5]","[512, 5]",,,,,,384,,,,,,384 -GPUTPCCFClusterizer,512,,"[448, 3]","[512, 2]",,,,,,448,,,,,,"[160, 5]" -GPUTPCNNClusterizerKernels,512,,,,,,,,,,,,,,, -GPUTrackingRefitKernel_mode0asGPU,256,,,,,,,,,,,,,,,256 -GPUTrackingRefitKernel_mode1asTrackParCov,256,,,,,,,,,,,,,,,256 -GPUMemClean16,"[""GPUCA_THREAD_COUNT_DEFAULT"", 1]",,,,,,,,,,,,,,, -GPUitoa,"[""GPUCA_THREAD_COUNT_DEFAULT"", 1]",,,,,,,,,,,,,,, -GPUTPCCFNoiseSuppression_noiseSuppression,"""GPUCA_LB_GPUTPCCFNoiseSuppression""",,,,,,,,,,,,,,,448 -GPUTPCCFNoiseSuppression_updatePeaks,"""GPUCA_LB_GPUTPCCFNoiseSuppression""",,,,,,,,,,,,,,,448 -GPUTPCNNClusterizerKernels_runCfClusterizer,"""GPUCA_LB_GPUTPCCFClusterizer""",,,,,,,,,,,,,,, -GPUTPCNNClusterizerKernels_fillInputNNCPU,"""GPUCA_LB_GPUTPCNNClusterizerKernels""",,,,,,,,,,,,,,, -GPUTPCNNClusterizerKernels_fillInputNNGPU,1024,,,,,,,,,,,,,,, -GPUTPCNNClusterizerKernels_determineClass1Labels,"""GPUCA_LB_GPUTPCNNClusterizerKernels""",,,,,,,,,,,,,,, -GPUTPCNNClusterizerKernels_determineClass2Labels,"""GPUCA_LB_GPUTPCNNClusterizerKernels""",,,,,,,,,,,,,,, -GPUTPCNNClusterizerKernels_publishClass1Regression,"""GPUCA_LB_GPUTPCNNClusterizerKernels""",,,,,,,,,,,,,,, -GPUTPCNNClusterizerKernels_publishClass2Regression,"""GPUCA_LB_GPUTPCNNClusterizerKernels""",,,,,,,,,,,,,,, -GPUTPCNNClusterizerKernels_publishDeconvolutionFlags,"""GPUCA_LB_GPUTPCNNClusterizerKernels""",,,,,,,,,,,,,,, -GPUTPCCFStreamCompaction_scanStart,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""",,,,,,,,,,,,,,,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""" -GPUTPCCFStreamCompaction_scanUp,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""",,,,,,,,,,,,,,,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""" -GPUTPCCFStreamCompaction_scanTop,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""",,,,,,,,,,,,,,,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""" -GPUTPCCFStreamCompaction_scanDown,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""",,,,,,,,,,,,,,,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""" -GPUTPCCFStreamCompaction_compactDigits,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""",,,,,,,,,,,,,,,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""" -GPUTPCCompressionGatherKernels_unbuffered,"""GPUCA_LB_COMPRESSION_GATHER""",,,,,,,,,,,,,,, -GPUTPCCompressionGatherKernels_buffered32,"""GPUCA_LB_COMPRESSION_GATHER""",,,,,,,,,,,,,,, -GPUTPCCompressionGatherKernels_buffered64,"""GPUCA_LB_COMPRESSION_GATHER""",,,,,,,,,,,,,,, -GPUTPCCompressionGatherKernels_buffered128,"""GPUCA_LB_COMPRESSION_GATHER""",,,,,,,,,,,,,,, -GPUTPCCompressionGatherKernels_multiBlock,"""GPUCA_LB_COMPRESSION_GATHER""",,,,,,,,,,,,,,, -GPUTPCGMMergerFinalize_0,256,,256,,,,,,,256,256,256,,,,256 -GPUTPCGMMergerFinalize_1,256,,256,,,,,,,256,256,256,,,,256 -GPUTPCGMMergerFinalize_2,256,,256,,,,,,,256,256,256,,,,256 -,,,,,,,,,,,,,,,, -PAR:,,,,,,,,,,,,,,,, -AMD_EUS_PER_CU,0,0,4,4,,,,,,,,,,,,0 -SORT_STARTHITS,1,0,,,,,,,,,,,,,,1 -NEIGHBOURS_FINDER_MAX_NNEIGHUP,6,0,10,4,,,,,,4,4,4,,,,2 -NEIGHBOURS_FINDER_UNROLL_GLOBAL,4,0,4,2,,,,,,,,,,,,2 -NEIGHBOURS_FINDER_UNROLL_SHARED,1,0,0,0,,,,,,,,,,,,1 -TRACKLET_SELECTOR_HITS_REG_SIZE,12,0,9,27,,,,,,20,20,20,,,,2 -ALTERNATE_BORDER_SORT,1,0,1,1,,,,,,1,1,1,,,,1 -SORT_BEFORE_FIT,1,0,1,1,,,,,,1,1,1,,,,1 -NO_ATOMIC_PRECHECK,0,0,1,1,,,,,,1,1,1,,,,1 -DEDX_STORAGE_TYPE,"""half""","""float""",,,,,,,,,,,,,, -MERGER_INTERPOLATION_ERROR_TYPE,"""half""","""float""",,,,,,,,,,,,,, -COMP_GATHER_KERNEL,4,0,4,4,,,,,,4,4,4,,,,4 -COMP_GATHER_MODE,3,0,3,3,,,,,,3,3,3,,,,3 -CF_SCAN_WORKGROUP_SIZE,512,0,,,,,,,,,,,,,, +Architecture,default,default_cpu,MI100,VEGA,TAHITI,TESLA,FERMI,PASCAL,KEPLER,AMPERE,TURING,HOPPER,ADA,OPENCL,RDNA,MI210,BLACKWELL,MI300 +,,,,,,,,,,,,,,,,,, +CORE:,,,,,,,,,,,,,,,,,, +WARP_SIZE,0,,64,64,32,32,32,32,32,32,32,32,32,32,32,64,32,64 +THREAD_COUNT_DEFAULT,256,,256,256,,,,,,512,512,,512,256,512,512,512, +,,,,,,,,,,,,,,,,,, +LB:,,,,,,,,,,,,,,,,,, +GPUTPCCreateTrackingData,256,,"[256, 7]","[192, 2]",,,,,,"[224, 7]",256,"[128, 14]",416,,"[64, 21]",,384,"[320, 2]" +GPUTPCTrackletConstructor,256,,"[768, 8]","[512, 10]","[256, 2]","[256, 1]","[256, 2]","[1024, 2]","[512, 4]",1024,"[256, 2]",1024,"[1024, 1]",,"[768, 2]",,768,512 +GPUTPCTrackletSelector,256,,"[384, 5]","[192, 10]","[256, 3]","[256, 1]","[256, 3]","[512, 4]","[256, 3]","[288, 3]","[192, 3]","[544, 1]","[32, 2]",,"[384, 3]",,992,"[256, 6]" +GPUTPCNeighboursFinder,256,,"[192, 8]","[960, 8]",256,256,256,512,256,864,"[640, 1]","[512, 2]","[736, 1]",,"[480, 3]",,992,"[704, 1]" +GPUTPCNeighboursCleaner,256,,"[128, 5]","[384, 9]",256,256,256,256,256,544,512,"[192, 9]","[512, 1]",,"[384, 5]",,672,"[640, 1]" +GPUTPCExtrapolationTracking,256,,"[256, 7]","[256, 2]",,,,,,"[352, 4]","[192, 2]","[896, 1]","[352, 1]",,"[1024, 1]",,896,1024 +GPUTRDTrackerKernels_gpuVersion,512,,,,,,,,,512,,512,512,,512,,,512 +GPUTPCCreateOccupancyMap_fill,256,,,,,,,,,256,,256,256,,256,,,256 +GPUTPCCreateOccupancyMap_fold,256,,,,,,,,,256,,256,256,,256,,,256 +GPUTRDTrackerKernels_o2Version,512,,,,,,,,,512,,512,512,,512,,,512 +GPUTPCCompressionKernels_step0attached,256,,"[128, 1]","[64, 2]",,,,,,"[160, 2]",128,"[448, 1]",352,,"[1024, 1]",,"[96, 3]","[128, 4]" +GPUTPCCompressionKernels_step1unattached,256,,"[512, 2]","[512, 2]",,,,,,"[288, 4]","[512, 2]","[256, 4]","[512, 2]",,"[512, 3]",,"[512, 2]","[512, 3]" +GPUTPCDecompressionKernels_step0attached,256,,"[128, 2]","[128, 2]",,,,,,"[32, 1]","[32, 1]","[32, 1]","[32, 1]",,"[128, 1]",,"[32, 1]","[128, 1]" +GPUTPCDecompressionKernels_step1unattached,256,,"[64, 2]","[64, 2]",,,,,,"[32, 1]","[32, 1]","[32, 1]","[32, 1]",,"[64, 1]",,"[32, 1]","[64, 1]" +GPUTPCDecompressionUtilKernels_sortPerSectorRow,256,,,,,,,,,256,,256,256,,256,,,256 +GPUTPCDecompressionUtilKernels_countFilteredClusters,256,,,,,,,,,256,,256,256,,256,,,256 +GPUTPCDecompressionUtilKernels_storeFilteredClusters,256,,,,,,,,,256,,256,256,,256,,,256 +GPUTPCCFDecodeZS,"[128, 4]",,"[64, 4]","[64, 1]",,,,,,"[32, 10]","[64, 8]","[32, 10]","[32, 10]",,"[64, 1]",,"[64, 10]","[64, 1]" +GPUTPCCFDecodeZSLink,"""GPUCA_WARP_SIZE""",,"""GPUCA_WARP_SIZE""","""GPUCA_WARP_SIZE""",,,,,,"""GPUCA_WARP_SIZE""","""GPUCA_WARP_SIZE""","""GPUCA_WARP_SIZE""","""GPUCA_WARP_SIZE""",,64,,"""GPUCA_WARP_SIZE""","""GPUCA_WARP_SIZE""" +GPUTPCCFDecodeZSDenseLink,"""GPUCA_WARP_SIZE""",,"[""GPUCA_WARP_SIZE"", 4]","[""GPUCA_WARP_SIZE"", 14]",,,,,,"[""GPUCA_WARP_SIZE"", 14]","""GPUCA_WARP_SIZE""","[""GPUCA_WARP_SIZE"", 22]","[""GPUCA_WARP_SIZE"", 22]",,"[64, 17]",,"[""GPUCA_WARP_SIZE"", 8]","[""GPUCA_WARP_SIZE"", 5]" +GPUTPCCFGather,"[1024, 1]",,"[1024, 5]","[1024, 1]",,,,,,"[160, 11]","[1024, 1]",736,896,,"[928, 1]",,"[1024, 1]","[320, 2]" +COMPRESSION_GATHER,1024,,1024,1024,,,,,,1024,1024,,1024,,,,, +GPUTPCGMMergerTrackFit,256,,"[192, 2]","[64, 7]",,,,,,"[32, 16]","[32, 8]","[32, 14]","[160, 2]",,"[32, 24]",,"[64, 8]","[64, 6]" +GPUTPCGMMergerFollowLoopers,256,,"[256, 5]","[256, 4]",,,,,,"[256, 4]","[128, 4]","[1024, 1]",640,,"[128, 16]",,"[224, 3]","[256, 7]" +GPUTPCGMMergerSectorRefit,256,,"[64, 4]","[256, 2]",,,,,,"[32, 8]","[64, 5]","[32, 7]","[32, 7]",,"[32, 20]",,"[32, 10]","[64, 4]" +GPUTPCGMMergerUnpackResetIds,256,,256,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerUnpackGlobal,256,,256,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerResolve_step0,256,,512,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerResolve_step1,256,,512,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerResolve_step2,256,,512,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerResolve_step3,256,,512,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerResolve_step4,256,,512,256,,,,,,"[256, 4]","[256, 4]","[256, 4]","[256, 4]",,256,,"[256, 4]",256 +GPUTPCGMMergerClearLinks,256,,256,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerMergeWithinPrepare,256,,256,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerMergeSectorsPrepare,256,,256,256,,,,,,"[256, 2]","[256, 2]","[256, 2]","[256, 2]",,256,,"[256, 2]",256 +GPUTPCGMMergerMergeBorders_step0,256,,512,256,,,,,,192,192,192,192,,256,,192,256 +GPUTPCGMMergerMergeBorders_step2,256,,512,256,,,,,,"[64, 2]",256,"[64, 2]","[64, 2]",,256,,"[64, 2]",256 +GPUTPCGMMergerMergeCE,256,,512,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerLinkExtrapolatedTracks,256,,256,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerCollect,256,,"[768, 1]","[1024, 1]",,,,,,"[864, 1]","[128, 2]","[896, 1]",128,,1024,,"[288, 1]","[384, 4]" +GPUTPCGMMergerSortTracksPrepare,256,,256,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerPrepareForFit_step0,256,,256,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerPrepareForFit_step1,256,,256,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerPrepareForFit_step2,256,,256,256,,,,,,256,256,256,256,,256,,256,256 +GPUTPCGMMergerFinalize_step0,256,,,256,,,,,,256,,256,256,,256,,256,256 +GPUTPCGMMergerFinalize_step1,256,,,256,,,,,,256,,256,256,,256,,256,256 +GPUTPCGMMergerFinalize_step2,256,,,256,,,,,,256,,256,256,,256,,256,256 +GPUTPCGMMergerMergeLoopers_step0,256,,,,,,,,,256,,256,256,,256,,256,256 +GPUTPCGMMergerMergeLoopers_step1,256,,,,,,,,,256,,256,256,,256,,256,256 +GPUTPCGMMergerMergeLoopers_step2,256,,,,,,,,,256,,256,256,,256,,256,256 +GPUTPCGMO2Output_prepare,256,,,,,,,,,256,,256,256,,256,,256,256 +GPUTPCGMO2Output_output,256,,,,,,,,,256,,256,256,,256,,256,256 +GPUTPCStartHitsFinder,256,,"[1024, 2]","[1024, 7]",256,256,256,256,256,"[224, 1]",512,"[416, 4]",928,,"[320, 5]",,608,"[448, 3]" +GPUTPCStartHitsSorter,256,,"[1024, 5]","[512, 7]",256,256,256,256,256,"[320, 2]","[512, 1]","[864, 1]","[96, 2]",,"[192, 5]",,608,"[448, 1]" +GPUTPCCFCheckPadBaseline,576,,"[576, 2]","[576, 2]",,,,,,"[576, 3]",,"[576, 1]","[576, 1]",,"[576, 2]",,"[576, 2]",576 +GPUTPCCFHIPTailConnector,256,,256,256,,,,,,"[224, 2]",,"[320, 5]","[704, 1]",,"[128, 7]",,,"[448, 4]" +GPUTPCCFHIPClusterizer,256,,256,256,,,,,,"[288, 5]",,"[480, 3]","[448, 3]",,352,,,"[512, 3]" +GPUTPCCFChargeMapFiller_fillIndexMap,512,,512,512,,,,,,448,,448,448,,512,,448,512 +GPUTPCCFChargeMapFiller_fillFromDigits,512,,512,512,,,,,,448,,448,448,,512,,448,512 +GPUTPCCFChargeMapFiller_findFragmentStart,512,,512,512,,,,,,448,,448,448,,512,,448,512 +GPUTPCCFPeakFinder,512,,"[512, 9]","[512, 4]",,,,,,416,,992,"[672, 1]",,"[384, 2]",,"[128, 5]","[192, 10]" +GPUTPCCFNoiseSuppression,512,,512,512,,,,,,608,,896,480,,160,,,448 +GPUTPCCFDeconvolution,512,,"[512, 5]","[512, 5]",,,,,,"[480, 4]",,224,512,,480,,384,"[448, 3]" +GPUTPCCFClusterizer,512,,"[448, 3]","[512, 2]",,,,,,"[608, 3]",,736,"[192, 3]",,576,,"[160, 5]","[832, 2]" +GPUTPCNNClusterizerKernels,512,,,,,,,,,,,,,,,,, +GPUTrackingRefitKernel_mode0asGPU,256,,,,,,,,,256,,256,256,,256,,256,256 +GPUTrackingRefitKernel_mode1asTrackParCov,256,,,,,,,,,256,,256,256,,256,,256,256 +GPUMemClean16,"[""GPUCA_THREAD_COUNT_DEFAULT"", 1]",,,,,,,,,"[512, 1]",,"[512, 1]","[512, 1]",,"[256, 1]",,,"[256, 1]" +GPUitoa,"[""GPUCA_THREAD_COUNT_DEFAULT"", 1]",,,,,,,,,"[512, 1]",,"[512, 1]","[512, 1]",,"[256, 1]",,,"[256, 1]" +GPUTPCCFNoiseSuppression_noiseSuppression,"""GPUCA_LB_GPUTPCCFNoiseSuppression""",,,,,,,,,,,,,,,,448, +GPUTPCCFNoiseSuppression_updatePeaks,"""GPUCA_LB_GPUTPCCFNoiseSuppression""",,,,,,,,,,,,,,,,448, +GPUTPCNNClusterizerKernels_runCfClusterizer,"""GPUCA_LB_GPUTPCCFClusterizer""",,,,,,,,,,,,,,,,, +GPUTPCNNClusterizerKernels_fillInputNNCPU,"""GPUCA_LB_GPUTPCNNClusterizerKernels""",,,,,,,,,,,,,,,,, +GPUTPCNNClusterizerKernels_fillInputNNGPU,1024,,,,,,,,,,,,,,,,, +GPUTPCNNClusterizerKernels_determineClass1Labels,"""GPUCA_LB_GPUTPCNNClusterizerKernels""",,,,,,,,,,,,,,,,, +GPUTPCNNClusterizerKernels_determineClass2Labels,"""GPUCA_LB_GPUTPCNNClusterizerKernels""",,,,,,,,,,,,,,,,, +GPUTPCNNClusterizerKernels_publishClass1Regression,"""GPUCA_LB_GPUTPCNNClusterizerKernels""",,,,,,,,,,,,,,,,, +GPUTPCNNClusterizerKernels_publishClass2Regression,"""GPUCA_LB_GPUTPCNNClusterizerKernels""",,,,,,,,,,,,,,,,, +GPUTPCNNClusterizerKernels_publishDeconvolutionFlags,"""GPUCA_LB_GPUTPCNNClusterizerKernels""",,,,,,,,,,,,,,,,, +GPUTPCCFStreamCompaction_scanStart,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""",,,,,,,,,,,,,,,,, +GPUTPCCFStreamCompaction_scanUp,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""",,,,,,,,,,,,,,,,, +GPUTPCCFStreamCompaction_scanTop,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""",,,,,,,,,,,,,,,,, +GPUTPCCFStreamCompaction_scanDown,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""",,,,,,,,,,,,,,,,, +GPUTPCCFStreamCompaction_compactDigits,"""GPUCA_PAR_CF_SCAN_WORKGROUP_SIZE""",,,,,,,,,,,,,,,,, +GPUTPCCompressionGatherKernels_unbuffered,"""GPUCA_LB_COMPRESSION_GATHER""",,,,,,,,,,,,,,,,, +GPUTPCCompressionGatherKernels_buffered32,"""GPUCA_LB_COMPRESSION_GATHER""",,,,,,,,,,,,,,,,, +GPUTPCCompressionGatherKernels_buffered64,"""GPUCA_LB_COMPRESSION_GATHER""",,,,,,,,,,,,,,,,, +GPUTPCCompressionGatherKernels_buffered128,"""GPUCA_LB_COMPRESSION_GATHER""",,,,,,,,,,,,,,,,, +GPUTPCCompressionGatherKernels_multiBlock,"""GPUCA_LB_COMPRESSION_GATHER""",,,,,,,,,,,,,,,,, +GPUTPCGMMergerFinalize_0,256,,256,,,,,,,256,256,,256,,,,256, +GPUTPCGMMergerFinalize_1,256,,256,,,,,,,256,256,,256,,,,256, +GPUTPCGMMergerFinalize_2,256,,256,,,,,,,256,256,,256,,,,256, +GPUTPCConvertKernel,,,,,,,,,,,,,,,256,,,256 +,,,,,,,,,,,,,,,,,, +PAR:,,,,,,,,,,,,,,,,,, +AMD_EUS_PER_CU,0,0,4,4,,,,,,,,,,,4,,0,4 +SORT_STARTHITS,1,0,,,,,,,,1,,1,1,,1,,1,1 +NEIGHBOURS_FINDER_MAX_NNEIGHUP,6,0,10,4,,,,,,4,4,4,4,,5,,2,5 +NEIGHBOURS_FINDER_UNROLL_GLOBAL,4,0,4,2,,,,,,2,,8,8,,4,,2,2 +NEIGHBOURS_FINDER_UNROLL_SHARED,1,0,0,0,,,,,,1,,1,0,,1,,1,1 +TRACKLET_SELECTOR_HITS_REG_SIZE,12,0,9,27,,,,,,20,20,20,20,,20,,2,20 +ALTERNATE_BORDER_SORT,1,0,1,1,,,,,,1,1,1,1,,1,,1,1 +SORT_BEFORE_FIT,1,0,1,1,,,,,,1,1,1,1,,1,,1,1 +NO_ATOMIC_PRECHECK,0,0,1,1,,,,,,1,1,1,1,,1,,1,1 +DEDX_STORAGE_TYPE,"""half""","""float""",,,,,,,,,,,,,,,, +MERGER_INTERPOLATION_ERROR_TYPE,"""half""","""float""",,,,,,,,,,,,,,,, +COMP_GATHER_KERNEL,4,0,4,4,,,,,,4,4,4,4,,4,,4,4 +COMP_GATHER_MODE,3,0,3,3,,,,,,3,3,3,3,,3,,3,3 +CF_SCAN_WORKGROUP_SIZE,512,0,,,,,,,,224,,992,448,,1024,,,448 +MERGER_SPLIT_LOOP_INTERPOLATION,,,,,,,,,,,,,,,1,,,1 diff --git a/dependencies/FindO2GPU.cmake b/dependencies/FindO2GPU.cmake index b229f46422eb8..1c3b997f64272 100644 --- a/dependencies/FindO2GPU.cmake +++ b/dependencies/FindO2GPU.cmake @@ -10,7 +10,7 @@ # or submit itself to any jurisdiction. # NOTE!!!! - Whenever this file is changed, move it over to alidist/resources -# FindO2GPU.cmake Version 16 +# FindO2GPU.cmake Version 17 set(CUDA_COMPUTETARGET_DEFAULT_FULL 80-real 86-real 89-real 120-real 75-virtual) set(HIP_AMDGPUTARGET_DEFAULT_FULL gfx906;gfx908) @@ -54,9 +54,11 @@ function(detect_gpu_arch backend) # Detect GPU architecture, optionally filterri endif() if(CUDA_FIRST_TARGET GREATER_EQUAL 120) set(CUDA_TARGET BLACKWELL) + elseif(CUDA_FIRST_TARGET GREATER_EQUAL 90) + set(CUDA_TARGET HOPPER) elseif(CUDA_FIRST_TARGET GREATER_EQUAL 89) set(CUDA_TARGET ADA) - elseif(CUDA_FIRST_TARGET GREATER_EQUAL 86) + elseif(CUDA_FIRST_TARGET GREATER_EQUAL 80) set(CUDA_TARGET AMPERE) elseif(CUDA_FIRST_TARGET GREATER_EQUAL 75) set(CUDA_TARGET TURING) @@ -81,6 +83,8 @@ function(detect_gpu_arch backend) # Detect GPU architecture, optionally filterri string(REGEX MATCH "....$" HIP_FIRST_TARGET_PADDED "0000${HIP_FIRST_TARGET}") if(HIP_FIRST_TARGET_PADDED STRGREATER_EQUAL "1000") set(HIP_TARGET RDNA) + elseif(HIP_FIRST_TARGET_PADDED STRGREATER_EQUAL "0940") + set(HIP_TARGET MI300) elseif(HIP_FIRST_TARGET_PADDED STRGREATER_EQUAL "090a") set(HIP_TARGET MI210) elseif(HIP_FIRST_TARGET_PADDED STRGREATER_EQUAL "0908") From 91307a4b466bdc4d8c2e60caec2fde7ef07942dd Mon Sep 17 00:00:00 2001 From: NNicassio99 <140729099+NNicassio99@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:25:46 +0200 Subject: [PATCH 18/47] Latest v3b.1 RICH geometry with quadrants, modules and shielding (#15608) * Latest v3b.1 RICH geometry with quadrants, modules and shielding * Latest v3b.1 RICH geometry with quadrants, modules and shielding * Latest v3b.1 RICH geometry with quadrants, modules and shielding (clang) * Latest v3b.1 RICH geometry with quadrants, modules and shielding (clang fixed) --------- Co-authored-by: Nicola Nicassio Co-authored-by: Nicola Nicassio --- .../base/include/RICHBase/RICHBaseParam.h | 103 ++- .../include/RICHSimulation/RICHRing.h | 100 +-- .../ALICE3/RICH/simulation/src/Detector.cxx | 708 +++++++++++++++++- .../ALICE3/RICH/simulation/src/RICHRing.cxx | 653 +++++++++++++--- 4 files changed, 1336 insertions(+), 228 deletions(-) diff --git a/Detectors/Upgrades/ALICE3/RICH/base/include/RICHBase/RICHBaseParam.h b/Detectors/Upgrades/ALICE3/RICH/base/include/RICHBase/RICHBaseParam.h index a9f2f7fbba5d1..2d1d83d376ea3 100644 --- a/Detectors/Upgrades/ALICE3/RICH/base/include/RICHBase/RICHBaseParam.h +++ b/Detectors/Upgrades/ALICE3/RICH/base/include/RICHBase/RICHBaseParam.h @@ -20,34 +20,89 @@ namespace o2 namespace rich { struct RICHBaseParam : public o2::conf::ConfigurableParamHelper { - float zBaseSize = 18.6; // cm (18.4 in v3) - float rMax = 131.0; // cm (117.0 in v3) - float rMin = 104.0; // cm (90.0 in v3) - float radiatorThickness = 2.0; // cm - float detectorThickness = 0.2; // cm - float zRichLength = 700.0; // cm - int nRings = 11; // (25 in v3) - int nTiles = 44; // (36 in v3) - bool oddGeom = true; // (false in v3) - - // FWD and BWD RICH - bool enableFWDRich = false; - bool enableBWDRich = false; + double zBaseSize = 18.6; // cm (18.4 in v3) + double rMax = 131.0; // cm (117.0 in v3) + double rMin = 104.0; // cm (90.0 in v3) + double radiatorThickness = 2.0; // cm + double zRichLength = 700.0; // cm + int nRings = 11; // (25 in v3) + int nTiles = 44; // (36 in v3) + bool oddGeom = true; // (false in v3) - float rFWDMin = 13.7413f; - float rFWDMax = 103.947f; + // The active and passive silicon thicknesses must sum to detectorThickness. + double siliconeLayerThickness = 0.010; // cm: 0.1 mm resin layer in front + double detectorThickness = 0.1; // cm + double activeSiliconThickness = 0.01; // cm: 0.1 mm sensitive silicon + // double passiveSiliconThickness = 0.09f; // cm: (detectorThickness - activeSiliconThickness) - // Aerogel: - float zAerogelMin = 375.f; - float zAerogelMax = 377.f; + // cylindrical aerogel layout + bool useCylindricalAerogel = true; + double cylindricalAerogelEtaRef = 0.85; - // Argon: - float zArgonMin = 377.f; - float zArgonMax = 407.f; + // Enable geometry with rectangular modules + bool useRectangularModules = true; + + // Barrel photosensor active area. + double sipmActiveSizeZ = 18.0; // cm + double sipmActiveSizeRPhi = 17.0; // cm + + // Gas refractive index (then scaled with chromaticity) + double nGasEffective = 1.0006; + + // Aerogel refractive index (then scaled with chromaticity) + double nAerogelEffective = 1.03; + + // Parameters for geometry with quadrants + bool flagUseQuadrants = false; + // Opening between adjacent vessel quadrants, measured as a chord at shieldRMin. + double vesselPhiGap = 1.0; // cm + // Thickness of each lateral insulating wall at a quadrant boundary. + double vesselThicknessShieldingLateral = 1.0; // cm + // Rectangular size could be smaller with quadrants (< 17 cm depending on wall thickness) + double quadrantModuleSizeRPhi = 16.5; // cm + // Readout stack behind each SiPM plane, thicknesses along the local outward normal. + double pcb1Thickness = 0.4; // cm + double coolingPlateThickness = 0.4; // cm + double pcb2Thickness = 0.4; // cm + double pcb3Thickness = 0.4; // cm + // Surface-to-surface gaps between consecutive layers. + double gapSiPMToPCB1 = 0.10; // cm + double gapPCB1ToCoolingPlate = 0.10; // cm + double gapCoolingPlateToPCB2 = 0.10; // cm + double gapPCB2ToPCB3 = 0.10; // cm + + // Minimum edge-to-edge clearances used to avoid exact contacts between adjacent modules. + double moduleClearanceZ = 0.02; // cm + double moduleClearanceRPhi = 0.02; // cm + + // Shielding: + // Radial boundaries of the complete cylindrical enclosure. + double shieldRMin = 100.0; + double shieldRMax = 136.0; + // Radial thickness of the inner insulating wall. + double innerWallThickness = 2.0; + // Radial thickness of the outer insulating wall. + double outerWallThickness = 2.0; + // Full longitudinal length of the cylindrical side walls. + double shieldLengthZ = 220.0; + // Thickness of each insulating end cap along Z. + double endCapThicknessZ = 2.0; + + // FWD and BWD RICH (legacy) + bool enableFWDRich = false; + bool enableBWDRich = false; + double rFWDMin = 13.7413; + double rFWDMax = 103.947; + // Aerogel: + double zAerogelMin = 375.; + double zAerogelMax = 377.; + // Argon: + double zArgonMin = 377.; + double zArgonMax = 407.; // Detector: - float zSiliconMin = 407.f; - float zSiliconMax = 407.2f; + double zSiliconMin = 407.; + double zSiliconMax = 407.2; O2ParamDef(RICHBaseParam, "RICHBase"); }; @@ -55,4 +110,4 @@ struct RICHBaseParam : public o2::conf::ConfigurableParamHelper { } // namespace rich } // end namespace o2 -#endif \ No newline at end of file +#endif diff --git a/Detectors/Upgrades/ALICE3/RICH/simulation/include/RICHSimulation/RICHRing.h b/Detectors/Upgrades/ALICE3/RICH/simulation/include/RICHSimulation/RICHRing.h index 296e24cbd8f06..a1b8974c0b748 100644 --- a/Detectors/Upgrades/ALICE3/RICH/simulation/include/RICHSimulation/RICHRing.h +++ b/Detectors/Upgrades/ALICE3/RICH/simulation/include/RICHSimulation/RICHRing.h @@ -35,20 +35,20 @@ class Ring // z_ph: z position of the photosensitive surface (from the center) Ring(int rPosId, int nTilesPhi, - float rMin, - float rMax, - float radThick, - float radYmin, - float radYmax, - float radZ, - float photThick, - float photYmin, - float photYmax, - float photZ, - float radRad0, - float photRad0, - float aerDetDistance, - float thetaB, + double rMin, + double rMax, + double radThick, + double radYmin, + double radYmax, + double radZ, + double photThick, + double photYmin, + double photYmax, + double photZ, + double radRad0, + double photRad0, + double aerDetDistance, + double thetaB, const std::string motherName = "RICHV"); ~Ring() = default; @@ -60,10 +60,10 @@ class Ring private: int mPosId; // id of the ring int mNTiles; // number of modules - float mRRad; // max distance for radiators - float mRPhot; // max distance for photosensitive surfaces - float mRadThickness; // thickness of the radiator - float mPhotThickness; // thickness of the photosensitive surface + double mRRad; // max distance for radiators + double mRPhot; // max distance for photosensitive surfaces + double mRadThickness; // thickness of the radiator + double mPhotThickness; // thickness of the photosensitive surface ClassDef(Ring, 0); }; @@ -74,32 +74,32 @@ class FWDRich public: FWDRich() = default; FWDRich(std::string name, - float rMin, - float rMax, - float zAerogelMin, - float dZAerogel, - float zArgonMin, - float dZArgon, - float zSiliconMin, - float dZSilicon); + double rMin, + double rMax, + double zAerogelMin, + double dZAerogel, + double zArgonMin, + double dZArgon, + double zSiliconMin, + double dZSilicon); void createFWDRich(TGeoVolume* motherVolume); protected: std::string mName; - float mRmin; - float mRmax; + double mRmin; + double mRmax; // Aerogel: - float mZAerogelMin; - float mDZAerogel; + double mZAerogelMin; + double mDZAerogel; // Argon: - float mZArgonMin; - float mDZArgon; + double mZArgonMin; + double mDZArgon; // Silicon: - float mZSiliconMin; - float mDZSilicon; + double mZSiliconMin; + double mDZSilicon; ClassDef(FWDRich, 0); }; @@ -109,32 +109,32 @@ class BWDRich public: BWDRich() = default; BWDRich(std::string name, - float rMin, - float rMax, - float zAerogelMin, - float dZAerogel, - float zArgonMin, - float dZArgon, - float zSiliconMin, - float dZSilicon); + double rMin, + double rMax, + double zAerogelMin, + double dZAerogel, + double zArgonMin, + double dZArgon, + double zSiliconMin, + double dZSilicon); void createBWDRich(TGeoVolume* motherVolume); protected: std::string mName; - float mRmin; - float mRmax; + double mRmin; + double mRmax; // Aerogel: - float mZAerogelMin; - float mDZAerogel; + double mZAerogelMin; + double mDZAerogel; // Argon: - float mZArgonMin; - float mDZArgon; + double mZArgonMin; + double mDZArgon; // Silicon: - float mZSiliconMin; - float mDZSilicon; + double mZSiliconMin; + double mDZSilicon; ClassDef(BWDRich, 0); }; diff --git a/Detectors/Upgrades/ALICE3/RICH/simulation/src/Detector.cxx b/Detectors/Upgrades/ALICE3/RICH/simulation/src/Detector.cxx index 02719d6f93a00..0dd938b931665 100644 --- a/Detectors/Upgrades/ALICE3/RICH/simulation/src/Detector.cxx +++ b/Detectors/Upgrades/ALICE3/RICH/simulation/src/Detector.cxx @@ -15,6 +15,9 @@ #include #include #include +#include +#include +#include #include "DetectorsBase/Stack.h" #include "ITSMFTSimulation/Hit.h" @@ -27,6 +30,49 @@ namespace o2 { namespace rich { +namespace // quadrant equation solver +{ +double quadrantDeltaPhiEquation(double x, int nTilesPhi, double rMin, double totalBoundaryWidth) +{ + const double argument = totalBoundaryWidth * TMath::Cos(x / 2.0) / (2.0 * rMin); + if (TMath::Abs(argument) >= 1.0) { + return std::numeric_limits::quiet_NaN(); + } + const double rhs = 2.0 * TMath::Pi() / static_cast(nTilesPhi) - (8.0 / static_cast(nTilesPhi)) * TMath::ASin(argument); + return rhs - x; +} + +double solveQuadrantDeltaPhi(int nTilesPhi, double rMin, double totalBoundaryWidth) +{ + double lower = 0.0; + double upper = 1.1 * 2.0 * TMath::Pi() / static_cast(nTilesPhi); + double fLower = quadrantDeltaPhiEquation(lower, nTilesPhi, rMin, totalBoundaryWidth); + double fUpper = quadrantDeltaPhiEquation(upper, nTilesPhi, rMin, totalBoundaryWidth); + if (!std::isfinite(fLower) || !std::isfinite(fUpper) || fLower * fUpper > 0.0) { + return -1.0; + } + constexpr double tolerance = 1.0e-12; + constexpr int maxIterations = 200; + for (int iteration = 0; iteration < maxIterations; iteration++) { + const double middle = 0.5 * (lower + upper); + const double fMiddle = quadrantDeltaPhiEquation(middle, nTilesPhi, rMin, totalBoundaryWidth); + if (!std::isfinite(fMiddle)) { + return -1.0; + } + if (TMath::Abs(fMiddle) < tolerance || 0.5 * (upper - lower) < tolerance) { + return middle; + } + if (fLower * fMiddle < 0.0) { + upper = middle; + fUpper = fMiddle; + } else { + lower = middle; + fLower = fMiddle; + } + } + return 0.5 * (lower + upper); +} +} // namespace Detector::Detector() : o2::base::DetImpl("RCH", true), @@ -61,6 +107,10 @@ void Detector::ConstructGeometry() void Detector::createMaterials() { + auto& richPars = RICHBaseParam::Instance(); + const double nGasEffective = richPars.nGasEffective; + const double nAerogelEffective = richPars.nAerogelEffective; + int ifield = 2; // ? float fieldm = 10.0; // ? o2::base::Detector::initFieldTrackingParams(ifield, fieldm); @@ -89,12 +139,75 @@ void Detector::createMaterials() float epsilAerogel = 1.0E-4; // .10000E+01; float stminAerogel = 0.0; // cm "Default value used" + float tmaxfdCO2 = 0.1; // .10000E+01; // Degree + float stemaxCO2 = .10000E+01; // cm + float deemaxCO2 = 0.1; // 0.30000E-02; // Fraction of particle's energy 0SetCerenkov(globalMediumID(2, "AEROGEL"), nAerogelRindex, aerogelRindexEnergyGeV, aerogelAbsorptionOnRindexGrid, aerogelDetectionEfficiency, aerogelRindex); + // + // constexpr int nAerogelAbsorption = 2; + // double aerogelAbsorptionEnergyGeV[nAerogelAbsorption] = {1.0 * eVInGeV, 8.26561 * eVInGeV}; + // double aerogelAbsorptionLengthCm[nAerogelAbsorption] = {aerogelAbsorptionLengthCm, aerogelAbsorptionLengthCm}; + // mc->SetMaterialProperty(globalMediumID(2, "AEROGEL"), "ABSLENGTH", nAerogelAbsorption, aerogelAbsorptionEnergyGeV, aerogelAbsorptionLengthCm); + // + constexpr int nAerogelRayleigh = 22; + double aerogelRayleighEnergyGeV[nAerogelRayleigh] = {1.00 * eVInGeV, 1.06 * eVInGeV, 1.12 * eVInGeV, 1.18 * eVInGeV, 1.23984 * eVInGeV, 1.3051 * eVInGeV, 1.3776 * eVInGeV, 1.45864 * eVInGeV, 1.5498 * eVInGeV, 1.65312 * eVInGeV, 1.7712 * eVInGeV, 1.90745 * eVInGeV, 2.0664 * eVInGeV, 2.25426 * eVInGeV, 2.47968 * eVInGeV, 2.7552 * eVInGeV, 3.0996 * eVInGeV, 3.54241 * eVInGeV, 4.13281 * eVInGeV, 4.95937 * eVInGeV, 6.19921 * eVInGeV, 8.26561 * eVInGeV}; + double aerogelRayleighLengthCm[nAerogelRayleigh] = {543.253684, 430.307801, 345.247537, 280.204207, 229.885, 187.243, 150.828, 120.001, 94.1609, 72.7371, 55.1954, 41.0359, 29.7931, 21.0359, 14.3678, 9.42672, 5.88506, 3.44971, 1.86207, 0.897989, 0.367816, 0.116379}; + mc->SetMaterialProperty(globalMediumID(2, "AEROGEL"), "RAYLEIGH", nAerogelRayleigh, aerogelRayleighEnergyGeV, aerogelRayleighLengthCm); + + /// GAS + constexpr int nCO2Optical = 2; + double co2EnergyGeV[nCO2Optical] = {1.0 * eVInGeV, 8.26561 * eVInGeV}; + double co2Rindex[nCO2Optical] = {nGasEffective, nGasEffective}; // <- Target gas index for dielectrons + double co2AbsorptionLengthCm[nCO2Optical] = {1.0e5, 1.0e5}; + double co2DetectionEfficiency[nCO2Optical] = {0.0, 0.0}; + mc->SetCerenkov(globalMediumID(5, "CO2"), nCO2Optical, co2EnergyGeV, co2AbsorptionLengthCm, co2DetectionEfficiency, co2Rindex); + + /// SiO2 + constexpr int nSiO2Optical = 2; + double sio2EnergyGeV[nSiO2Optical] = {1.0 * eVInGeV, 8.26561 * eVInGeV}; + double sio2Rindex[nSiO2Optical] = {1.47, 1.47}; + double sio2AbsorptionLengthCm[nSiO2Optical] = {1.0e5, 1.0e5}; + double sio2DetectionEfficiency[nSiO2Optical] = {0.0, 0.0}; + mc->SetCerenkov(globalMediumID(9, "SIO2"), nSiO2Optical, sio2EnergyGeV, sio2AbsorptionLengthCm, sio2DetectionEfficiency, sio2Rindex); + + /// Silicone resin + constexpr int nSiliconeOptical = 2; + double siliconeEnergyGeV[nSiliconeOptical] = {1.0 * eVInGeV, 8.26561 * eVInGeV}; + double siliconeRindex[nSiliconeOptical] = {1.41, 1.41}; + double siliconeAbsorptionLengthCm[nSiliconeOptical] = {1.0e5, 1.0e5}; + double siliconeDetectionEfficiency[nSiliconeOptical] = {0.0, 0.0}; + mc->SetCerenkov(globalMediumID(10, "SILICONE"), nSiliconeOptical, siliconeEnergyGeV, siliconeAbsorptionLengthCm, siliconeDetectionEfficiency, siliconeRindex); + + /// Si (assuming same index as silicone resin as reflection losses are already included in PDE) + constexpr int nSiliconOptical = 2; + double siliconEnergyGeV[nSiliconOptical] = {1.0 * eVInGeV, 8.26561 * eVInGeV}; + double siliconRindex[nSiliconOptical] = {1.41, 1.41}; + double siliconAbsorptionLengthCm[nSiliconOptical] = {1.0e5, 1.0e5}; + double siliconDetectionEfficiency[nSiliconOptical] = {0.0, 0.0}; + mc->SetCerenkov(globalMediumID(3, "SILICON"), nSiliconOptical, siliconEnergyGeV, siliconAbsorptionLengthCm, siliconDetectionEfficiency, siliconRindex); + + // Si: outer layer just for photon absorption + constexpr int nSiliconAbsorberOptical = 2; + double siliconAbsorberEnergyGeV[nSiliconAbsorberOptical] = {1.0 * eVInGeV, 8.26561 * eVInGeV}; + double siliconAbsorberAbsorptionLengthCm[nSiliconAbsorberOptical] = {1.0e-7, 1.0e-7}; // 1 nm + mc->SetMaterialProperty(globalMediumID(11, "SILICON_ABSORBER"), "ABSLENGTH", nSiliconAbsorberOptical, siliconAbsorberEnergyGeV, siliconAbsorberAbsorptionLengthCm); } void Detector::createGeometry() @@ -145,8 +429,237 @@ void Detector::createGeometry() vRICH->SetTitle(vstrng); auto& richPars = RICHBaseParam::Instance(); + // Quadrant parameters + const bool flagUseQuadrants = richPars.flagUseQuadrants; + const double vesselPhiGap = richPars.vesselPhiGap; + const double vesselThicknessShieldingLateral = richPars.vesselThicknessShieldingLateral; + + // shielding parameters + double shieldRMin = richPars.shieldRMin; + double shieldRMax = richPars.shieldRMax; + double innerWallThickness = richPars.innerWallThickness; + double outerWallThickness = richPars.outerWallThickness; + double shieldLengthZ = richPars.shieldLengthZ; + double endCapThicknessZ = richPars.endCapThicknessZ; + + if (innerWallThickness <= 0.0 || outerWallThickness <= 0.0 || endCapThicknessZ <= 0.0 || shieldLengthZ <= 0.0) { + LOGP(fatal, "RICH shielding dimensions must be positive"); + } + + if (shieldRMin + innerWallThickness >= shieldRMax - outerWallThickness) { + LOGP(fatal, + "RICH shielding walls overlap: inner outer radius = {}, outer inner radius = {}", + shieldRMin + innerWallThickness, + shieldRMax - outerWallThickness); + } + + if (flagUseQuadrants) { + if (richPars.nTiles <= 0 || richPars.nTiles % 4 != 0) { + LOGP(fatal, "RICH quadrant geometry requires nTiles to be positive and divisible by four; received {}", richPars.nTiles); + } + if (vesselPhiGap < 0.0 || vesselThicknessShieldingLateral <= 0.0) { + LOGP(fatal, "RICH quadrant gap must be non-negative and lateral shielding thickness must be positive"); + } + const double totalBoundaryWidth = 2.0 * vesselThicknessShieldingLateral + vesselPhiGap; + if (totalBoundaryWidth >= 2.0 * richPars.rMin || vesselPhiGap >= 2.0 * shieldRMin) { + LOGP(fatal, "RICH quadrant boundary dimensions are incompatible with rMin={} cm and shieldRMin={} cm", richPars.rMin, shieldRMin); + } + const double quadrantDeltaPhi = solveQuadrantDeltaPhi(richPars.nTiles, richPars.rMin, totalBoundaryWidth); + if (!(quadrantDeltaPhi > 0.0)) { + LOGP(fatal, "RICH could not solve the quadrant module angular pitch"); + } + } + + // Name of the gas mother volume. This name will also be passed + // to each Ring so that the ring components become its daughters. + const char* richGasMotherName = "RICH_GAS_MOTHER"; + + TGeoMedium* medCO2 = gGeoManager->GetMedium("RCH_CO2$"); + if (!medCO2) { + LOGP(fatal, "RICH: CO2 medium not found"); + } + + TGeoMedium* medPeek = gGeoManager->GetMedium("RCH_PEEK$"); + if (!medPeek) { + LOGP(fatal, "RICH: PEEK medium not found"); + } + + TGeoMedium* medArmaFlex = gGeoManager->GetMedium("RCH_ARMAFLEX$"); + if (!medArmaFlex) { + LOGP(fatal, "RICH: ArmaFlex medium not found"); + } + + TGeoMedium* medArmaGel = gGeoManager->GetMedium("RCH_ARMAGEL$"); + if (!medArmaGel) { + LOGP(fatal, "RICH: ArmaGel medium not found"); + } + prepareLayout(); // Preparing the positions of the rings and tiles + // The gas mother includes the side-wall region and both end caps. ( as vessel ) + const double gasEnvelopeLengthZ = shieldLengthZ + 2.0 * endCapThicknessZ; + auto* gasEnvelopeShape = new TGeoTube(shieldRMin, shieldRMax, gasEnvelopeLengthZ / 2.0); + auto* gasEnvelopeVolume = new TGeoVolume(richGasMotherName, gasEnvelopeShape, medCO2); + + gasEnvelopeVolume->SetLineColor(kBlue - 9); + gasEnvelopeVolume->SetTransparency(90); + + // The gas envelope is a daughter of the general RICH volume. + vRICH->AddNode(gasEnvelopeVolume, 1, new TGeoTranslation(0.0, 0.0, 0.0)); + + if (!flagUseQuadrants) { + // ============================================================ + // Inner cylindrical insulating wall + // + // Radial interval: + // shieldRMin --> shieldRMin + innerWallThickness + // + // Longitudinal interval: + // -shieldLengthZ/2 --> +shieldLengthZ/2 + // ============================================================ + auto* innerWallShape = new TGeoTube(shieldRMin, shieldRMin + innerWallThickness, shieldLengthZ / 2.0); + auto* innerWallVolume = new TGeoVolume("RICH_SHIELD_INNER_WALL", innerWallShape, medArmaGel); + + innerWallVolume->SetLineColor(kOrange - 8); // kGray + innerWallVolume->SetTransparency(0); // 80 + gasEnvelopeVolume->AddNode(innerWallVolume, 1, new TGeoTranslation(0.0, 0.0, 0.0)); + + // ============================================================ + // Outer cylindrical insulating wall + // + // Radial interval: + // shieldRMax - outerWallThickness --> shieldRMax + // + // Longitudinal interval: + // -shieldLengthZ/2 --> +shieldLengthZ/2 + // ============================================================ + + auto* outerWallShape = new TGeoTube(shieldRMax - outerWallThickness, shieldRMax, shieldLengthZ / 2.0); + auto* outerWallVolume = new TGeoVolume("RICH_SHIELD_OUTER_WALL", outerWallShape, medArmaGel); + + outerWallVolume->SetLineColor(kOrange - 8); // kGray + outerWallVolume->SetTransparency(0); // 80 + gasEnvelopeVolume->AddNode(outerWallVolume, 1, new TGeoTranslation(0.0, 0.0, 0.0)); + + // ============================================================ + // Insulating end caps + // + // Each end cap covers: + // shieldRMin --> shieldRMax + // + // Each has full thickness: + // endCapThicknessZ + // ============================================================ + + auto* endCapShape = new TGeoTube(shieldRMin, shieldRMax, endCapThicknessZ / 2.0); + auto* endCapPlusVolume = new TGeoVolume("RICH_SHIELD_ENDCAP_PLUS", endCapShape, medArmaGel); + auto* endCapMinusVolume = new TGeoVolume("RICH_SHIELD_ENDCAP_MINUS", endCapShape, medArmaGel); + + endCapPlusVolume->SetLineColor(kOrange - 8); // kGray + endCapPlusVolume->SetTransparency(0); // 80 + + endCapMinusVolume->SetLineColor(kOrange - 8); // kGray + endCapMinusVolume->SetTransparency(0); // 80 + + const double endCapCenterZ = shieldLengthZ / 2.0 + endCapThicknessZ / 2.0; + + gasEnvelopeVolume->AddNode(endCapPlusVolume, 1, new TGeoTranslation(0.0, 0.0, endCapCenterZ)); + gasEnvelopeVolume->AddNode(endCapMinusVolume, 1, new TGeoTranslation(0.0, 0.0, -endCapCenterZ)); + } else { + // ============================================================ + // Four independent insulating vessel quadrants + // ============================================================ + const double totalBoundaryWidth = 2.0 * vesselThicknessShieldingLateral + vesselPhiGap; + const double moduleDeltaPhi = solveQuadrantDeltaPhi(richPars.nTiles, richPars.rMin, totalBoundaryWidth); + const double moduleExtraPhi = TMath::ASin(totalBoundaryWidth / (2.0 * richPars.rMin)); + const double vesselGapHalfPhi = TMath::ASin(vesselPhiGap / (2.0 * shieldRMin)); + const double quadrantSpanPhi = TMath::Pi() / 2.0 - 2.0 * vesselGapHalfPhi; + const int modulesPerQuadrant = richPars.nTiles / 4; + + // Remaining angular space between the last module of a quadrant and the following vessel gap. + const double endModuleExtraPhi = TMath::Pi() / 2.0 - moduleExtraPhi - static_cast(modulesPerQuadrant) * moduleDeltaPhi; + const double lateralStartWallSpanPhi = moduleExtraPhi - vesselGapHalfPhi; + const double lateralEndWallSpanPhi = endModuleExtraPhi - vesselGapHalfPhi; + + if (quadrantSpanPhi <= 0.0 || lateralStartWallSpanPhi <= 0.0 || lateralEndWallSpanPhi <= 0.0 || lateralStartWallSpanPhi + lateralEndWallSpanPhi >= quadrantSpanPhi) { + LOGP(fatal, "RICH invalid quadrant angular dimensions: vessel span={}, start wall span={}, end wall span={}", quadrantSpanPhi, lateralStartWallSpanPhi, lateralEndWallSpanPhi); + } + + const double radToDeg = 180.0 / TMath::Pi(); + const double quadrantSpanDeg = quadrantSpanPhi * radToDeg; + const double lateralStartWallSpanDeg = lateralStartWallSpanPhi * radToDeg; + const double lateralEndWallSpanDeg = lateralEndWallSpanPhi * radToDeg; + const double vesselGapHalfDeg = vesselGapHalfPhi * radToDeg; + const double innerGasRadius = shieldRMin + innerWallThickness; + const double outerGasRadius = shieldRMax - outerWallThickness; + + // Inner cylindrical shielding, divided into four sectors. + auto* innerWallQuadrantShape = new TGeoTubeSeg("RICH_SHIELD_INNER_WALL_QUADRANT_SHAPE", shieldRMin, innerGasRadius, shieldLengthZ / 2.0, 0.0, quadrantSpanDeg); + + // Outer cylindrical shielding, divided into four sectors. + auto* outerWallQuadrantShape = new TGeoTubeSeg("RICH_SHIELD_OUTER_WALL_QUADRANT_SHAPE", outerGasRadius, shieldRMax, shieldLengthZ / 2.0, 0.0, quadrantSpanDeg); + + // End caps divided into four sectors. + auto* endCapQuadrantShape = new TGeoTubeSeg("RICH_SHIELD_ENDCAP_QUADRANT_SHAPE", shieldRMin, shieldRMax, endCapThicknessZ / 2.0, 0.0, quadrantSpanDeg); + + // Lateral wall at the beginning of each quadrant. + auto* lateralStartWallShape = new TGeoTubeSeg("RICH_SHIELD_LATERAL_START_WALL_SHAPE", innerGasRadius, outerGasRadius, shieldLengthZ / 2.0, 0.0, lateralStartWallSpanDeg); + + // Lateral wall at the end of each quadrant. + auto* lateralEndWallShape = new TGeoTubeSeg("RICH_SHIELD_LATERAL_END_WALL_SHAPE", innerGasRadius, outerGasRadius, shieldLengthZ / 2.0, 0.0, lateralEndWallSpanDeg); + + auto* innerWallQuadrantVolume = new TGeoVolume("RICH_SHIELD_INNER_WALL_QUADRANT", innerWallQuadrantShape, medArmaGel); + auto* outerWallQuadrantVolume = new TGeoVolume("RICH_SHIELD_OUTER_WALL_QUADRANT", outerWallQuadrantShape, medArmaGel); + auto* endCapPlusQuadrantVolume = new TGeoVolume("RICH_SHIELD_ENDCAP_PLUS_QUADRANT", endCapQuadrantShape, medArmaGel); + auto* endCapMinusQuadrantVolume = new TGeoVolume("RICH_SHIELD_ENDCAP_MINUS_QUADRANT", endCapQuadrantShape, medArmaGel); + auto* lateralStartWallVolume = new TGeoVolume("RICH_SHIELD_LATERAL_START_WALL", lateralStartWallShape, medArmaGel); + auto* lateralEndWallVolume = new TGeoVolume("RICH_SHIELD_LATERAL_END_WALL", lateralEndWallShape, medArmaGel); + + innerWallQuadrantVolume->SetLineColor(kOrange - 8); // kGray + outerWallQuadrantVolume->SetLineColor(kOrange - 8); // kGray + endCapPlusQuadrantVolume->SetLineColor(kOrange - 8); // kGray + endCapMinusQuadrantVolume->SetLineColor(kOrange - 8); // kGray + lateralStartWallVolume->SetLineColor(kOrange - 8); // kGray + lateralEndWallVolume->SetLineColor(kOrange - 8); // kGray + + innerWallQuadrantVolume->SetTransparency(0); // 80 + outerWallQuadrantVolume->SetTransparency(0); // 80 + endCapPlusQuadrantVolume->SetTransparency(0); // 80 + endCapMinusQuadrantVolume->SetTransparency(0); // 80 + lateralStartWallVolume->SetTransparency(0); // 80 + lateralEndWallVolume->SetTransparency(0); // 80 + + const double endCapCenterZ = shieldLengthZ / 2.0 + endCapThicknessZ / 2.0; + + for (int quadrant = 0; quadrant < 4; quadrant++) { + + const double quadrantStartDeg = -45.0 + static_cast(quadrant) * 90.0 + vesselGapHalfDeg; + const double quadrantEndDeg = quadrantStartDeg + quadrantSpanDeg; + + auto makeRotation = [&](const char* prefix, double angleDeg) { + auto* rotation = new TGeoRotation(Form("%s_%d", prefix, quadrant)); + rotation->RotateZ(angleDeg); + return rotation; + }; + + // Inner cylindrical wall sector. + gasEnvelopeVolume->AddNode(innerWallQuadrantVolume, quadrant + 1, new TGeoCombiTrans(0.0, 0.0, 0.0, makeRotation("RICHInnerQuadrantRotation", quadrantStartDeg))); + // Outer cylindrical wall sector. + gasEnvelopeVolume->AddNode(outerWallQuadrantVolume, quadrant + 1, new TGeoCombiTrans(0.0, 0.0, 0.0, makeRotation("RICHOuterQuadrantRotation", quadrantStartDeg))); + // Positive-z end cap sector. + gasEnvelopeVolume->AddNode(endCapPlusQuadrantVolume, quadrant + 1, new TGeoCombiTrans(0.0, 0.0, endCapCenterZ, makeRotation("RICHEndCapPlusQuadrantRotation", quadrantStartDeg))); + // Negative-z end cap sector. + gasEnvelopeVolume->AddNode(endCapMinusQuadrantVolume, quadrant + 1, new TGeoCombiTrans(0.0, 0.0, -endCapCenterZ, makeRotation("RICHEndCapMinusQuadrantRotation", quadrantStartDeg))); + // Start-side lateral wall. + gasEnvelopeVolume->AddNode(lateralStartWallVolume, quadrant + 1, new TGeoCombiTrans(0.0, 0.0, 0.0, makeRotation("RICHLateralStartRotation", quadrantStartDeg))); + // End-side lateral wall. + gasEnvelopeVolume->AddNode(lateralEndWallVolume, quadrant + 1, new TGeoCombiTrans(0.0, 0.0, 0.0, makeRotation("RICHLateralEndRotation", quadrantEndDeg - lateralEndWallSpanDeg))); + } + + LOGP(info, "RICH quadrant geometry: module pitch={} deg, module boundary half-gap={} deg, vessel half-gap={} deg", moduleDeltaPhi * radToDeg, moduleExtraPhi * radToDeg, vesselGapHalfDeg); + } + + // ============================================================ modules for (int iRing{0}; iRing < richPars.nRings; ++iRing) { if (!richPars.oddGeom && iRing == (richPars.nRings / 2)) { continue; @@ -156,18 +669,18 @@ void Detector::createGeometry() richPars.rMin, richPars.rMax, richPars.radiatorThickness, - (float)mVTile1[iRing], - (float)mVTile2[iRing], - (float)mLAerogelZ[iRing], + (double)mVTile1[iRing], + (double)mVTile2[iRing], + (double)mLAerogelZ[iRing], richPars.detectorThickness, - (float)mVMirror1[iRing], - (float)mVMirror2[iRing], + (double)mVMirror1[iRing], + (double)mVMirror2[iRing], richPars.zBaseSize, - (float)mR0Radiator[iRing], - (float)mR0PhotoDet[iRing], - (float)mTRplusG[iRing], - (float)mThetaBi[iRing], - GeometryTGeo::getRICHVolPattern()}; + (double)mR0Radiator[iRing], + (double)mR0PhotoDet[iRing], + (double)mTRplusG[iRing], + (double)mThetaBi[iRing], + richGasMotherName}; // GeometryTGeo::getRICHVolPattern() } if (richPars.enableFWDRich) { @@ -233,7 +746,12 @@ void Detector::Reset() bool Detector::ProcessHits(FairVolume* vol) { // This method is called from the MC stepping - if (!(fMC->TrackCharge())) { + + constexpr int kOpticalPhotonPDG = 50000050; + const bool isOpticalPhoton = (fMC->TrackPid() == kOpticalPhotonPDG); + const bool isChargedParticle = (TMath::Abs(fMC->TrackCharge()) > 0.0); + // Reject neutral particles other than optical photons. + if (!isChargedParticle && !isOpticalPhoton) { return false; } @@ -242,6 +760,39 @@ bool Detector::ProcessHits(FairVolume* vol) // Is it needed to keep a track reference when the outer ITS volume is encountered? auto stack = (o2::data::Stack*)fMC->GetStack(); + + // Only the active silicon volumes are registered as sensitive in + // defineSensitiveVolumes(). The explicit volume-name check is kept + // as a safety guard in case additional sensitive volumes are added. + if (isOpticalPhoton) { + const char* currentVolumeName = fMC->CurrentVolName(); + const bool isActiveSiliconVolume = currentVolumeName && TString(currentVolumeName).BeginsWith(GeometryTGeo::getRICHSensorPattern()); + // Create only one hit when entering the active silicon. + if (!isActiveSiliconVolume || !fMC->IsTrackEntering()) { + return false; + } + TLorentzVector photonPosition; + TLorentzVector photonMomentum; + fMC->TrackPosition(photonPosition); + fMC->TrackMomentum(photonMomentum); + constexpr unsigned char photonStatus = Hit::kTrackEntering; + addHit( + stack->GetCurrentTrackNumber(), + lay, + photonPosition.Vect(), + photonPosition.Vect(), + photonMomentum.Vect(), + photonMomentum.E(), + photonPosition.T(), + 0.0, + photonStatus, + photonStatus); + + stack->addHit(GetDetId()); + + return true; + } + if (fMC->IsTrackExiting() && (lay == 0 || lay == mRings.size() - 1)) { // Keep the track refs for the innermost and outermost rings only o2::TrackReference tr(*fMC, GetDetId()); @@ -380,30 +931,121 @@ void Detector::prepareLayout() } // Dimensioning tiles - double percentage = 0.999; - for (int iRing = 0; iRing < richPars.nRings; iRing++) { - if (iRing == richPars.nRings / 2) { - mVMirror1[iRing] = percentage * 2.0 * richPars.rMax * TMath::Sin(TMath::Pi() / double(richPars.nTiles)); - mVMirror2[iRing] = percentage * 2.0 * richPars.rMax * TMath::Sin(TMath::Pi() / double(richPars.nTiles)); - mVTile1[iRing] = percentage * 2.0 * richPars.rMin * TMath::Sin(TMath::Pi() / double(richPars.nTiles)); - mVTile2[iRing] = percentage * 2.0 * richPars.rMin * TMath::Sin(TMath::Pi() / double(richPars.nTiles)); - } else if (iRing > richPars.nRings / 2) { - mVMirror1[iRing] = percentage * 2.0 * richPars.rMax * TMath::Sin(TMath::Pi() / double(richPars.nTiles)); - mVMirror2[iRing] = percentage * 2.0 * mMinRadialMirror[iRing] * TMath::Sin(TMath::Pi() / double(richPars.nTiles)); - mVTile1[iRing] = percentage * 2.0 * mMaxRadialRadiator[iRing] * TMath::Sin(TMath::Pi() / double(richPars.nTiles)); - mVTile2[iRing] = percentage * 2.0 * richPars.rMin * TMath::Sin(TMath::Pi() / double(richPars.nTiles)); - } else if (iRing < richPars.nRings / 2) { - mVMirror2[iRing] = percentage * 2.0 * richPars.rMax * TMath::Sin(TMath::Pi() / double(richPars.nTiles)); - mVMirror1[iRing] = percentage * 2.0 * mMinRadialMirror[iRing] * TMath::Sin(TMath::Pi() / double(richPars.nTiles)); - mVTile2[iRing] = percentage * 2.0 * mMaxRadialRadiator[iRing] * TMath::Sin(TMath::Pi() / double(richPars.nTiles)); - mVTile1[iRing] = percentage * 2.0 * richPars.rMin * TMath::Sin(TMath::Pi() / double(richPars.nTiles)); + if (!richPars.flagUseQuadrants) { + double percentage = 0.999; + for (int iRing = 0; iRing < richPars.nRings; iRing++) { + if (iRing == richPars.nRings / 2) { + mVMirror1[iRing] = percentage * 2.0 * richPars.rMax * TMath::Tan(TMath::Pi() / double(richPars.nTiles)); + mVMirror2[iRing] = percentage * 2.0 * richPars.rMax * TMath::Tan(TMath::Pi() / double(richPars.nTiles)); + mVTile1[iRing] = percentage * 2.0 * richPars.rMin * TMath::Tan(TMath::Pi() / double(richPars.nTiles)); + mVTile2[iRing] = percentage * 2.0 * richPars.rMin * TMath::Tan(TMath::Pi() / double(richPars.nTiles)); + } else if (iRing > richPars.nRings / 2) { + mVMirror1[iRing] = percentage * 2.0 * richPars.rMax * TMath::Tan(TMath::Pi() / double(richPars.nTiles)); + mVMirror2[iRing] = percentage * 2.0 * mMinRadialMirror[iRing] * TMath::Tan(TMath::Pi() / double(richPars.nTiles)); + mVTile1[iRing] = percentage * 2.0 * mMaxRadialRadiator[iRing] * TMath::Tan(TMath::Pi() / double(richPars.nTiles)); + mVTile2[iRing] = percentage * 2.0 * richPars.rMin * TMath::Tan(TMath::Pi() / double(richPars.nTiles)); + } else { + mVMirror2[iRing] = percentage * 2.0 * richPars.rMax * TMath::Tan(TMath::Pi() / double(richPars.nTiles)); + mVMirror1[iRing] = percentage * 2.0 * mMinRadialMirror[iRing] * TMath::Tan(TMath::Pi() / double(richPars.nTiles)); + mVTile2[iRing] = percentage * 2.0 * mMaxRadialRadiator[iRing] * TMath::Tan(TMath::Pi() / double(richPars.nTiles)); + mVTile1[iRing] = percentage * 2.0 * richPars.rMin * TMath::Tan(TMath::Pi() / double(richPars.nTiles)); + } + } + + } else { + + const double totalBoundaryWidth = 2.0 * richPars.vesselThicknessShieldingLateral + richPars.vesselPhiGap; + const double quadrantDeltaPhi = solveQuadrantDeltaPhi(richPars.nTiles, richPars.rMin, totalBoundaryWidth); + if (!(quadrantDeltaPhi > 0.0)) { + LOGP(fatal, "RICH could not solve the quadrant module angular pitch"); + } + const double halfWidthFactor = TMath::Tan(quadrantDeltaPhi / 2.0); + double percentage = 0.999; + for (int iRing = 0; iRing < richPars.nRings; iRing++) { + if (iRing == richPars.nRings / 2) { + mVMirror1[iRing] = percentage * 2.0 * richPars.rMax * halfWidthFactor; + mVMirror2[iRing] = percentage * 2.0 * richPars.rMax * halfWidthFactor; + mVTile1[iRing] = percentage * 2.0 * richPars.rMin * halfWidthFactor; + mVTile2[iRing] = percentage * 2.0 * richPars.rMin * halfWidthFactor; + } else if (iRing > richPars.nRings / 2) { + mVMirror1[iRing] = percentage * 2.0 * richPars.rMax * halfWidthFactor; + mVMirror2[iRing] = percentage * 2.0 * mMinRadialMirror[iRing] * halfWidthFactor; + mVTile1[iRing] = percentage * 2.0 * mMaxRadialRadiator[iRing] * halfWidthFactor; + mVTile2[iRing] = percentage * 2.0 * richPars.rMin * halfWidthFactor; + + } else { + mVMirror2[iRing] = percentage * 2.0 * richPars.rMax * halfWidthFactor; + mVMirror1[iRing] = percentage * 2.0 * mMinRadialMirror[iRing] * halfWidthFactor; + mVTile2[iRing] = percentage * 2.0 * mMaxRadialRadiator[iRing] * halfWidthFactor; + mVTile1[iRing] = percentage * 2.0 * richPars.rMin * halfWidthFactor; + } + } + } + + // ============================================================ + // Cylindrical aerogel geometry + // ============================================================ + // + // In this mode the photosensors remain projective, but all + // aerogel tiles: + // + // - have identical dimensions; + // - are parallel to the beam axis; + // - lie at the same cylindrical radius; + // - are uniformly distributed along Z. + // + if (richPars.useCylindricalAerogel) { + + // In the even geometry the central projective ring is skipped + // in createGeometry(), so the number of actual aerogel rows is + // nRings - 1. + const int nAerogelRows = richPars.oddGeom ? richPars.nRings : richPars.nRings - 1; + + if (nAerogelRows <= 0) { + LOGP(fatal, "Invalid number of cylindrical aerogel rows: {}", nAerogelRows); + } + + if (richPars.nTiles <= 0) { + LOGP(fatal, "Invalid number of aerogel tiles in phi: {}", richPars.nTiles); + } + + const double thetaRef = 2.0 * TMath::ATan(TMath::Exp(-richPars.cylindricalAerogelEtaRef)); + const double cylindricalAerogelTileSizeZ = (2.0 * richPars.rMin / TMath::Tan(thetaRef)) / static_cast(nAerogelRows); + // const double cylindricalAerogelTileSizeRPhi = 2.0 * richPars.rMin * TMath::Tan(TMath::Pi() / static_cast(richPars.nTiles)); + double cylindricalAerogelTileSizeRPhi = 0.0; + + if (!richPars.flagUseQuadrants) { + // Original uniform-phi geometry. + cylindricalAerogelTileSizeRPhi = 2.0 * richPars.rMin * TMath::Tan(TMath::Pi() / static_cast(richPars.nTiles)); + } else { + const double totalBoundaryWidth = 2.0 * richPars.vesselThicknessShieldingLateral + richPars.vesselPhiGap; + const double quadrantDeltaPhi = solveQuadrantDeltaPhi(richPars.nTiles, richPars.rMin, totalBoundaryWidth); + cylindricalAerogelTileSizeRPhi = 2.0 * richPars.rMin * TMath::Tan(quadrantDeltaPhi / 2.0); + } + + LOGP(info, "Cylindrical aerogel: rows={}, etaRef={}, tileSizeZ={} cm, tileSizeRPhi={} cm", nAerogelRows, richPars.cylindricalAerogelEtaRef, cylindricalAerogelTileSizeZ, cylindricalAerogelTileSizeRPhi); + + for (int iRing = 0; iRing < richPars.nRings; iRing++) { + mLAerogelZ[iRing] = cylindricalAerogelTileSizeZ; + + // Equal values make the TGeoArb8 a rectangle instead of the projective trapezoid. + mVTile1[iRing] = cylindricalAerogelTileSizeRPhi; + mVTile2[iRing] = cylindricalAerogelTileSizeRPhi; } } // Translation parameters for (size_t iRing{0}; iRing < richPars.nRings; ++iRing) { - mR0Radiator[iRing] = mR0Tilt[iRing] - (mTRplusG[iRing] - richPars.radiatorThickness / 2) * TMath::Cos(mThetaBi[iRing]); - mR0PhotoDet[iRing] = mR0Tilt[iRing] - (richPars.detectorThickness / 2) * TMath::Cos(mThetaBi[iRing]); + + if (richPars.useCylindricalAerogel) { + mR0Radiator[iRing] = richPars.rMin + richPars.radiatorThickness / 2.0; + } else { + // Original projective aerogel position. + mR0Radiator[iRing] = mR0Tilt[iRing] - (mTRplusG[iRing] - richPars.radiatorThickness / 2.0) * TMath::Cos(mThetaBi[iRing]); + } + + // Photosensors remain projective for both configurations. + mR0PhotoDet[iRing] = mR0Tilt[iRing] - richPars.detectorThickness / 2.0 * TMath::Cos(mThetaBi[iRing]); } // FWD and BWD RICH diff --git a/Detectors/Upgrades/ALICE3/RICH/simulation/src/RICHRing.cxx b/Detectors/Upgrades/ALICE3/RICH/simulation/src/RICHRing.cxx index 46e6ae515b390..45c5c52b27043 100644 --- a/Detectors/Upgrades/ALICE3/RICH/simulation/src/RICHRing.cxx +++ b/Detectors/Upgrades/ALICE3/RICH/simulation/src/RICHRing.cxx @@ -19,183 +19,594 @@ #include #include +#include +#include + namespace o2 { namespace rich { +namespace // quadrant operations +{ + +double quadrantDeltaPhiEquation(double x, int nTilesPhi, double rMin, double totalBoundaryWidth) +{ + const double argument = totalBoundaryWidth * TMath::Cos(x / 2.0) / (2.0 * rMin); + if (TMath::Abs(argument) >= 1.0) { + return std::numeric_limits::quiet_NaN(); + } + const double rhs = 2.0 * TMath::Pi() / static_cast(nTilesPhi) - (8.0 / static_cast(nTilesPhi)) * TMath::ASin(argument); + return rhs - x; +} + +double solveQuadrantDeltaPhi(int nTilesPhi, double rMin, double totalBoundaryWidth) +{ + double lower = 0.0; + double upper = 1.1 * 2.0 * TMath::Pi() / static_cast(nTilesPhi); + double fLower = quadrantDeltaPhiEquation(lower, nTilesPhi, rMin, totalBoundaryWidth); + double fUpper = quadrantDeltaPhiEquation(upper, nTilesPhi, rMin, totalBoundaryWidth); + if (!std::isfinite(fLower) || !std::isfinite(fUpper) || fLower * fUpper > 0.0) { + return -1.0; + } + constexpr double tolerance = 1.0e-12; + constexpr int maxIterations = 200; + for (int iteration = 0; iteration < maxIterations; iteration++) { + const double middle = 0.5 * (lower + upper); + const double fMiddle = quadrantDeltaPhiEquation(middle, nTilesPhi, rMin, totalBoundaryWidth); + if (!std::isfinite(fMiddle)) { + return -1.0; + } + if (TMath::Abs(fMiddle) < tolerance || 0.5 * (upper - lower) < tolerance) { + return middle; + } + if (fLower * fMiddle < 0.0) { + upper = middle; + fUpper = fMiddle; + } else { + lower = middle; + fLower = fMiddle; + } + } + return 0.5 * (lower + upper); +} + +double quadrantModulePhi(int moduleIndex, int nTilesPhi, double deltaPhi, double extraPhi) +{ + const int modulesPerQuadrant = nTilesPhi / 4; + const int quadrant = moduleIndex / modulesPerQuadrant; + return extraPhi + static_cast(moduleIndex) * deltaPhi - TMath::Pi() / 4.0 + deltaPhi / 2.0 + 2.0 * static_cast(quadrant) * extraPhi; +} + +} // namespace + Ring::Ring(int rPosId, int nTilesPhi, - float rMin, - float rMax, - float radThick, - float radYmin, - float radYmax, - float radZ, - float photThick, - float photYmin, - float photYmax, - float photZ, - float radRad0, - float photR0, - float aerDetDistance, - float thetaB, + double rMin, + double rMax, + double radThick, + double radYmin, + double radYmax, + double radZ, + double photThick, + double photYmin, + double photYmax, + double photZ, + double radRad0, + double photR0, + double aerDetDistance, + double thetaB, const std::string motherName) : mNTiles{nTilesPhi}, mPosId{rPosId}, mRadThickness{radThick} { TGeoManager* geoManager = gGeoManager; TGeoVolume* motherVolume = geoManager->GetVolume(motherName.c_str()); + + if (!motherVolume) { + LOGP(fatal, + "RICH: mother volume {} not found while creating ring {}", + motherName, + rPosId); + } + + const auto& richPars = RICHBaseParam::Instance(); + + const bool useCylindricalAerogel = richPars.useCylindricalAerogel; + TGeoMedium* medAerogel = gGeoManager->GetMedium("RCH_AEROGEL$"); if (!medAerogel) { LOGP(fatal, "RICH: Aerogel medium not found"); } + TGeoMedium* medSi = gGeoManager->GetMedium("RCH_SILICON$"); if (!medSi) { LOGP(fatal, "RICH: Silicon medium not found"); } + + TGeoMedium* medCO2 = gGeoManager->GetMedium("RCH_CO2$"); + if (!medCO2) { + LOGP(fatal, "RICH: CO2 medium not found"); + } + + TGeoMedium* medFR4 = gGeoManager->GetMedium("RCH_FR4$"); + if (!medFR4) { + LOGP(fatal, "RICH: FR4 medium not found"); + } + TGeoMedium* medAr = gGeoManager->GetMedium("RCH_ARGON$"); if (!medAr) { LOGP(fatal, "RICH: Argon medium not found"); } - std::vector radiatorTiles(nTilesPhi), photoTiles(nTilesPhi), argonSectors(nTilesPhi); + + TGeoMedium* medAl = gGeoManager->GetMedium("RCH_ALUMINUM$"); + if (!medAl) { + LOGP(fatal, "RICH: Aluminum medium not found"); + } + + TGeoMedium* medSiAbsorber = gGeoManager->GetMedium("RCH_SILICON_ABSORBER$"); + if (!medSiAbsorber) { + LOGP(fatal, "RICH: Passive silicon absorber medium not found"); + } + + TGeoMedium* medSilicone = gGeoManager->GetMedium("RCH_SILICONE$"); + if (!medSilicone) { + LOGP(fatal, "RICH: Silicone medium not found"); + } + + TGeoMedium* medHTCC = gGeoManager->GetMedium("RCH_HTCC$"); + if (!medHTCC) { + LOGP(fatal, "RICH: HTCC medium not found"); + } + + std::vector radiatorTiles(nTilesPhi), photoFrames(nTilesPhi), photoTiles(nTilesPhi), gasSectors(nTilesPhi); LOGP(info, "Creating ring: id: {} with {} tiles. ", rPosId, nTilesPhi); LOGP(info, "Rmin: {} Rmax: {} RadThick: {} RadYmin: {} RadYmax: {} RadZ: {} PhotThick: {} PhotYmin: {} PhotYmax: {} PhotZ: {}, zTransRad: {}, zTransPhot: {}, ThetaB: {}", rMin, rMax, radThick, radYmin, radYmax, radZ, photThick, photYmin, photYmax, photZ, radRad0, photR0, thetaB); - float deltaPhiDeg = 360.0 / nTilesPhi; // Transformation are constructed in degrees... - float thetaBDeg = thetaB * 180.0 / TMath::Pi(); - int radTileCount{0}, photTileCount{0}, argSectorsCount{0}; + // Use different phi depending on use of quadrants or not + const bool flagUseQuadrants = richPars.flagUseQuadrants; + if (flagUseQuadrants && (nTilesPhi <= 0 || nTilesPhi % 4 != 0)) { + LOGP(fatal, "RICH quadrant geometry requires nTilesPhi to be positive and divisible by four; received {}", nTilesPhi); + } + const double regularDeltaPhi = 2.0 * TMath::Pi() / static_cast(nTilesPhi); + double moduleDeltaPhi = regularDeltaPhi; + double quadrantExtraPhi = 0.0; + + if (flagUseQuadrants) { + const double totalBoundaryWidth = 2.0 * richPars.vesselThicknessShieldingLateral + richPars.vesselPhiGap; + if (totalBoundaryWidth >= 2.0 * rMin) { + LOGP(fatal, "RICH quadrant boundary width {} cm is incompatible with rMin={} cm", totalBoundaryWidth, rMin); + } + moduleDeltaPhi = solveQuadrantDeltaPhi(nTilesPhi, rMin, totalBoundaryWidth); + + quadrantExtraPhi = TMath::ASin(totalBoundaryWidth / (2.0 * rMin)); + + if (!(moduleDeltaPhi > 0.0)) { + LOGP(fatal, "RICH ring {} could not solve the quadrant angular pitch", rPosId); + } + } + + auto modulePhiRad = [&](int moduleIndex) { + if (!flagUseQuadrants) { + // Original placement exactly. + return static_cast(moduleIndex) * regularDeltaPhi; + } + return quadrantModulePhi(moduleIndex, nTilesPhi, moduleDeltaPhi, quadrantExtraPhi); + }; + + const double thetaBDeg = thetaB * 180.0 / TMath::Pi(); + + const double sipmActiveSizeZ = richPars.sipmActiveSizeZ; + // const double sipmActiveSizeRPhi = richPars.sipmActiveSizeRPhi; + // Select width depending on having quadrants or not (and wall thickness) + const double sipmActiveSizeRPhi = flagUseQuadrants ? richPars.quadrantModuleSizeRPhi : richPars.sipmActiveSizeRPhi; + + const double pcb1Thickness = richPars.pcb1Thickness; + const double coolingPlateThickness = richPars.coolingPlateThickness; + const double pcb2Thickness = richPars.pcb2Thickness; + const double pcb3Thickness = richPars.pcb3Thickness; + + const double gapSiPMToPCB1 = richPars.gapSiPMToPCB1; + const double gapPCB1ToCoolingPlate = richPars.gapPCB1ToCoolingPlate; + const double gapCoolingPlateToPCB2 = richPars.gapCoolingPlateToPCB2; + const double gapPCB2ToPCB3 = richPars.gapPCB2ToPCB3; + + const bool oddGeom = richPars.oddGeom; + const bool useRectangularModules = richPars.useRectangularModules; + + const int nRings = richPars.nRings; + + const double moduleClearanceZ = richPars.moduleClearanceZ; + const double moduleClearanceRPhi = richPars.moduleClearanceRPhi; + + const double siliconeLayerThickness = richPars.siliconeLayerThickness; + const double activeSiliconThickness = richPars.activeSiliconThickness; + const double passiveSiliconThickness = photThick - activeSiliconThickness; + + const double siliconFrontSurfaceOffset = -photThick / 2.0; + const double siliconeCenterOffset = siliconFrontSurfaceOffset - siliconeLayerThickness / 2.0; + const double activeSiliconCenterOffset = siliconFrontSurfaceOffset + activeSiliconThickness / 2.0; + const double passiveSiliconCenterOffset = siliconFrontSurfaceOffset + activeSiliconThickness + passiveSiliconThickness / 2.0; + + if (siliconeLayerThickness <= 0.0) { + LOGP(fatal, "RICH: siliconeLayerThickness must be positive"); + } + + if (activeSiliconThickness <= 0.0 || activeSiliconThickness >= photThick) { + LOGP(fatal, "RICH: activeSiliconThickness={} cm must be larger than zero and smaller than detectorThickness={} cm", activeSiliconThickness, photThick); + } + + if (passiveSiliconThickness <= 0.0) { + LOGP(fatal, "RICH: passive silicon thickness must be positive"); + } + + if (moduleClearanceZ < 0.0 || moduleClearanceRPhi < 0.0) { + LOGP(fatal, "RICH: module clearances cannot be negative"); + } + + if (photThick <= 0.0 || sipmActiveSizeZ <= 0.0 || sipmActiveSizeRPhi <= 0.0 || pcb1Thickness <= 0.0 || coolingPlateThickness <= 0.0 || pcb2Thickness <= 0.0 || pcb3Thickness <= 0.0) { + LOGP(fatal, "RICH: SiPM and readout-stack dimensions must be positive"); + } + + if (gapSiPMToPCB1 < 0.0 || gapPCB1ToCoolingPlate < 0.0 || gapCoolingPlateToPCB2 < 0.0 || gapPCB2ToPCB3 < 0.0) { + LOGP(fatal, "RICH: readout-stack gaps cannot be negative"); + } + + const double minimumFrameSizeRPhi = photYmin < photYmax ? photYmin : photYmax; + + if (sipmActiveSizeZ > photZ || sipmActiveSizeRPhi > minimumFrameSizeRPhi) { + LOGP(fatal, + "RICH: rectangular module {} x {} cm2 does not fit inside the trapezoidal sector {} x [{}, {}] cm2 for ring {}. " + "For quadrant mode reduce: quadrantModuleSizeRPhi.", + sipmActiveSizeZ, sipmActiveSizeRPhi, photZ, photYmin, photYmax, rPosId); + } + + // Number of actual aerogel rows. + const int nAerogelRows = oddGeom ? nRings : nRings - 1; + + // Convert the projective-ring ID into a contiguous aerogel-row index. + // Example for nRings=11 and even geometry: + // projective IDs: 0 1 2 3 4 [5 skipped] 6 7 8 9 10 + // aerogel index: 0 1 2 3 4 5 6 7 8 9 + int aerogelRowIndex = rPosId; + if (!oddGeom && rPosId > nRings / 2) { + --aerogelRowIndex; + } + + const double cylindricalAerogelCenterZ = -0.5 * static_cast(nAerogelRows) * radZ + 0.5 * radZ + static_cast(aerogelRowIndex) * radZ; + + int radTileCount{0}, photTileCount{0}; // argSectorsCount{0}; + + if (flagUseQuadrants) { + LOGP(info, "RICH ring {} quadrant placement: deltaPhi={} deg, boundary half-gap={} deg", rPosId, moduleDeltaPhi * 180.0 / TMath::Pi(), quadrantExtraPhi * 180.0 / TMath::Pi()); + } + // Radiator tiles for (auto& radiatorTile : radiatorTiles) { // Local Z is the thin (radial) dimension, looking outward from the IP // (previously this was local X, while for running with ACTS we need local Z). // The placement rotation below is adjusted by +90 deg about Y // to keep the tile in the same physical position. - radiatorTile = new TGeoArb8(radThick / 2); - radiatorTile->SetVertex(0, radZ / 2, -radYmin / 2); - radiatorTile->SetVertex(1, -radZ / 2, -radYmax / 2); - radiatorTile->SetVertex(2, -radZ / 2, radYmax / 2); - radiatorTile->SetVertex(3, radZ / 2, radYmin / 2); - radiatorTile->SetVertex(4, radZ / 2, -radYmin / 2); - radiatorTile->SetVertex(5, -radZ / 2, -radYmax / 2); - radiatorTile->SetVertex(6, -radZ / 2, radYmax / 2); - radiatorTile->SetVertex(7, radZ / 2, radYmin / 2); + if (useCylindricalAerogel) { + // Including gab between adjacent aerogel tiles + const double cylindricalTileSizeZ = radZ - moduleClearanceZ; + const double cylindricalTileYmin = radYmin - moduleClearanceRPhi; + const double cylindricalTileYmax = radYmax - moduleClearanceRPhi; + if (cylindricalTileSizeZ <= 0.0 || cylindricalTileYmin <= 0.0 || cylindricalTileYmax <= 0.0) { + LOGP(fatal, "RICH: cylindrical-aerogel clearances are larger than the tile dimensions for ring {}", rPosId); + } + radiatorTile = new TGeoArb8(radThick / 2); + radiatorTile->SetVertex(0, cylindricalTileSizeZ / 2, -cylindricalTileYmin / 2); + radiatorTile->SetVertex(1, -cylindricalTileSizeZ / 2, -cylindricalTileYmax / 2); + radiatorTile->SetVertex(2, -cylindricalTileSizeZ / 2, cylindricalTileYmax / 2); + radiatorTile->SetVertex(3, cylindricalTileSizeZ / 2, cylindricalTileYmin / 2); + radiatorTile->SetVertex(4, cylindricalTileSizeZ / 2, -cylindricalTileYmin / 2); + radiatorTile->SetVertex(5, -cylindricalTileSizeZ / 2, -cylindricalTileYmax / 2); + radiatorTile->SetVertex(6, -cylindricalTileSizeZ / 2, cylindricalTileYmax / 2); + radiatorTile->SetVertex(7, cylindricalTileSizeZ / 2, cylindricalTileYmin / 2); + } else { + // Original non-cylindrical tile definition. + radiatorTile = new TGeoArb8(radThick / 2); + radiatorTile->SetVertex(0, radZ / 2, -radYmin / 2); + radiatorTile->SetVertex(1, -radZ / 2, -radYmax / 2); + radiatorTile->SetVertex(2, -radZ / 2, radYmax / 2); + radiatorTile->SetVertex(3, radZ / 2, radYmin / 2); + radiatorTile->SetVertex(4, radZ / 2, -radYmin / 2); + radiatorTile->SetVertex(5, -radZ / 2, -radYmax / 2); + radiatorTile->SetVertex(6, -radZ / 2, radYmax / 2); + radiatorTile->SetVertex(7, radZ / 2, radYmin / 2); + } TGeoVolume* radiatorTileVol = new TGeoVolume(Form("radTile_%d_%d", rPosId, radTileCount), radiatorTile, medAerogel); - radiatorTileVol->SetLineColor(kOrange - 8); + radiatorTileVol->SetLineColor(kBlue - 9); radiatorTileVol->SetLineWidth(1); + // const double phiDeg = static_cast(radTileCount) * deltaPhiDeg; + // const double phiRad = static_cast(radTileCount) * 2.0 * TMath::Pi() / static_cast(nTilesPhi); + + const double phiRad = modulePhiRad(radTileCount); + const double phiDeg = phiRad * 180.0 / TMath::Pi(); + auto* rotRadiator = new TGeoRotation(Form("radTileRotation_%d_%d", radTileCount, rPosId)); - rotRadiator->RotateY(90.0 - thetaBDeg); // +90 compensates the X->Z swap of the tile's local axes - rotRadiator->RotateZ(radTileCount * deltaPhiDeg); - auto* rotTransRadiator = new TGeoCombiTrans(radRad0 * TMath::Cos(radTileCount * TMath::Pi() / (nTilesPhi / 2)), - radRad0 * TMath::Sin(radTileCount * TMath::Pi() / (nTilesPhi / 2)), - radRad0 * TMath::Tan(thetaB), - rotRadiator); + if (useCylindricalAerogel) { + // The TGeoArb8 local Z axis is the thin direction. + // RotateY(90 degrees) maps that thin local Z direction onto the global radial direction at phi=0. + // There is no thetaB tilt because the cylindrical aerogel tiles are parallel to the beam axis. + rotRadiator->RotateY(90.0); + } else { + // Original projective rotation. + rotRadiator->RotateY(90.0 - thetaBDeg); + } + + // Rotate the radial tile around the beam axis to its phi sector. + rotRadiator->RotateZ(phiDeg); + + const double radiatorCenterZ = useCylindricalAerogel ? cylindricalAerogelCenterZ : radRad0 * TMath::Tan(thetaB); + + auto* rotTransRadiator = new TGeoCombiTrans(radRad0 * TMath::Cos(phiRad), radRad0 * TMath::Sin(phiRad), radiatorCenterZ, rotRadiator); motherVolume->AddNode(radiatorTileVol, 1, rotTransRadiator); radTileCount++; } - // Photosensor tiles - for (auto& photoTile : photoTiles) { - // Local Z is the thin (radial) dimension, looking outward from the IP - photoTile = new TGeoArb8(photThick / 2); - photoTile->SetVertex(0, photZ / 2, -photYmin / 2); - photoTile->SetVertex(1, -photZ / 2, -photYmax / 2); - photoTile->SetVertex(2, -photZ / 2, photYmax / 2); - photoTile->SetVertex(3, photZ / 2, photYmin / 2); - photoTile->SetVertex(4, photZ / 2, -photYmin / 2); - photoTile->SetVertex(5, -photZ / 2, -photYmax / 2); - photoTile->SetVertex(6, -photZ / 2, photYmax / 2); - photoTile->SetVertex(7, photZ / 2, photYmin / 2); - - TGeoVolume* photoTileVol = new TGeoVolume(Form("%s_%d_%d", GeometryTGeo::getRICHSensorPattern(), rPosId, photTileCount), photoTile, medSi); - photoTileVol->SetLineColor(kOrange - 8); - photoTileVol->SetLineWidth(1); - - auto* rotPhoto = new TGeoRotation(Form("photoTileRotation_%d_%d", photTileCount, rPosId)); - rotPhoto->RotateY(90.0 - thetaBDeg); // +90 compensates the X->Z swap of the tile's local axes - rotPhoto->RotateZ(photTileCount * deltaPhiDeg); - auto* rotTransPhoto = new TGeoCombiTrans(photR0 * TMath::Cos(photTileCount * TMath::Pi() / (nTilesPhi / 2)), - photR0 * TMath::Sin(photTileCount * TMath::Pi() / (nTilesPhi / 2)), - photR0 * TMath::Tan(thetaB), - rotPhoto); - - motherVolume->AddNode(photoTileVol, 1, rotTransPhoto); - photTileCount++; - } - - // Argon sectors "connect" radiator and photosensor tiles, they are not really physical - for (auto& argonSector : argonSectors) { - float separation{(aerDetDistance - radThick - photThick)}; + // Photosensor tiles: legacy trapezoidal modules and rectangular modules + if (!useRectangularModules) { + for (auto& photoTile : photoTiles) { + const double phiRad = modulePhiRad(photTileCount); + const double phiDeg = phiRad * 180.0 / TMath::Pi(); + // Local Z is the thin (radial) dimension, looking outward from the IP + photoTile = new TGeoArb8(photThick / 2); + photoTile->SetVertex(0, photZ / 2, -photYmin / 2); + photoTile->SetVertex(1, -photZ / 2, -photYmax / 2); + photoTile->SetVertex(2, -photZ / 2, photYmax / 2); + photoTile->SetVertex(3, photZ / 2, photYmin / 2); + photoTile->SetVertex(4, photZ / 2, -photYmin / 2); + photoTile->SetVertex(5, -photZ / 2, -photYmax / 2); + photoTile->SetVertex(6, -photZ / 2, photYmax / 2); + photoTile->SetVertex(7, photZ / 2, photYmin / 2); + + TGeoVolume* photoTileVol = new TGeoVolume(Form("%s_%d_%d", GeometryTGeo::getRICHSensorPattern(), rPosId, photTileCount), photoTile, medSi); + photoTileVol->SetLineColor(kOrange + 2); + photoTileVol->SetLineWidth(1); + + auto* rotPhoto = new TGeoRotation(Form("photoTileRotation_%d_%d", photTileCount, rPosId)); + rotPhoto->RotateY(90.0 - thetaBDeg); // +90 compensates the X->Z swap of the tile's local axes + // rotPhoto->RotateZ(photTileCount * deltaPhiDeg); + rotPhoto->RotateZ(phiDeg); + // auto* rotTransPhoto = new TGeoCombiTrans(photR0 * TMath::Cos(photTileCount * TMath::Pi() / (nTilesPhi / 2)), + // photR0 * TMath::Sin(photTileCount * TMath::Pi() / (nTilesPhi / 2)), + // photR0 * TMath::Tan(thetaB), + // rotPhoto); + auto* rotTransPhoto = new TGeoCombiTrans(photR0 * TMath::Cos(phiRad), photR0 * TMath::Sin(phiRad), photR0 * TMath::Tan(thetaB), rotPhoto); + + motherVolume->AddNode(photoTileVol, 1, rotTransPhoto); + photTileCount++; + } + } else // <-- New gemetry with rectangular modules + { + // Photosensor tiles and readout stack + for (auto& photoTile : photoTiles) { + // const double phiDeg = static_cast(photTileCount) * deltaPhiDeg; + // const double phiRad = static_cast(photTileCount) * 2.0 * TMath::Pi() / static_cast(nTilesPhi); + const double phiRad = modulePhiRad(photTileCount); + const double phiDeg = phiRad * 180.0 / TMath::Pi(); + + const double photoCenterR = photR0; + const double photoCenterZ = photR0 * TMath::Tan(thetaB); + + // Unit vector normal to the projective plane, pointing away from the IP. Positive offset places layer behind the SiPM. + const double normalRadial = TMath::Cos(thetaB); + const double normalZ = TMath::Sin(thetaB); + + auto makeProjectiveRotation = [&](const char* prefix) { + auto* rotation = new TGeoRotation(Form("%sRotation_%d_%d", prefix, photTileCount, rPosId)); + rotation->RotateY(90.0 - thetaBDeg); // same orientation as the original photosensor + rotation->RotateZ(phiDeg); + return rotation; + }; + + const double frameSizeZ = photZ - moduleClearanceZ; + const double frameYmin = photYmin - moduleClearanceRPhi; + const double frameYmax = photYmax - moduleClearanceRPhi; + // Footprint of the frames with the configured clearances for overlaps + auto makeFrameFootprint = [&](double thickness) { + auto* shape = new TGeoArb8(thickness / 2.0); + shape->SetVertex(0, frameSizeZ / 2.0, -frameYmin / 2.0); + shape->SetVertex(1, -frameSizeZ / 2.0, -frameYmax / 2.0); + shape->SetVertex(2, -frameSizeZ / 2.0, frameYmax / 2.0); + shape->SetVertex(3, frameSizeZ / 2.0, frameYmin / 2.0); + shape->SetVertex(4, frameSizeZ / 2.0, -frameYmin / 2.0); + shape->SetVertex(5, -frameSizeZ / 2.0, -frameYmax / 2.0); + shape->SetVertex(6, -frameSizeZ / 2.0, frameYmax / 2.0); + shape->SetVertex(7, frameSizeZ / 2.0, frameYmin / 2.0); + return shape; + }; + + auto makeRectangularFootprint = [&](double thickness) { + auto* shape = new TGeoArb8(thickness / 2.0); + shape->SetVertex(0, sipmActiveSizeZ / 2.0, -sipmActiveSizeRPhi / 2.0); + shape->SetVertex(1, -sipmActiveSizeZ / 2.0, -sipmActiveSizeRPhi / 2.0); + shape->SetVertex(2, -sipmActiveSizeZ / 2.0, sipmActiveSizeRPhi / 2.0); + shape->SetVertex(3, sipmActiveSizeZ / 2.0, sipmActiveSizeRPhi / 2.0); + shape->SetVertex(4, sipmActiveSizeZ / 2.0, -sipmActiveSizeRPhi / 2.0); + shape->SetVertex(5, -sipmActiveSizeZ / 2.0, -sipmActiveSizeRPhi / 2.0); + shape->SetVertex(6, -sipmActiveSizeZ / 2.0, sipmActiveSizeRPhi / 2.0); + shape->SetVertex(7, sipmActiveSizeZ / 2.0, sipmActiveSizeRPhi / 2.0); + return shape; + }; + + auto addReadoutLayer = [&](const char* prefix, + double thickness, + double centerOffset, + TGeoMedium* medium, + Color_t lineColor, + bool useRectangularFootprint) { + auto* shape = useRectangularFootprint ? makeRectangularFootprint(thickness) : makeFrameFootprint(thickness); + auto* volume = new TGeoVolume(Form("%s_%d_%d", prefix, rPosId, photTileCount), shape, medium); + volume->SetLineColor(lineColor); + volume->SetLineWidth(1); + const double layerCenterR = photoCenterR + centerOffset * normalRadial; + const double layerCenterZ = photoCenterZ + centerOffset * normalZ; + auto* transform = new TGeoCombiTrans(layerCenterR * TMath::Cos(phiRad), layerCenterR * TMath::Sin(phiRad), layerCenterZ, makeProjectiveRotation(prefix)); + motherVolume->AddNode(volume, 1, transform); + }; + + // ------------------------------------------------------------ + // Optional trapezoidal frame + // ------------------------------------------------------------ + // This is exactly the old photosensor envelope. It is created for + // reference, but deliberately not added to the geometry. + photoFrames[photTileCount] = makeFrameFootprint(photThick); + auto* photoFrameVol = new TGeoVolume(Form("photoFrame_%d_%d", rPosId, photTileCount), photoFrames[photTileCount], medSi); + photoFrameVol->SetLineColor(kGray + 2); + photoFrameVol->SetLineWidth(1); + // Uncomment only when the mechanical frame material/solid geometry should be included. + // This would be a solid trapezoid and would overlap the sensitive silicon: need for opening + // motherVolume->AddNode(photoFrameVol, 1, new TGeoCombiTrans(photoCenterR * TMath::Cos(phiRad), photoCenterR * TMath::Sin(phiRad), photoCenterZ, makeProjectiveRotation("photoFrame"))); + + // ------------------------------------------------------------ + // True sensitive silicon: centered 17 x 18 cm2 rectangle + // ------------------------------------------------------------ + // Local X corresponds to the in-plane Z direction after placement. + // Local Y corresponds to the in-plane r-phi direction. + // Local Z is the 1 mm thickness direction. + /*photoTile = new TGeoArb8(photThick / 2.0); + photoTile->SetVertex(0, sipmActiveSizeZ / 2.0, -sipmActiveSizeRPhi / 2.0); + photoTile->SetVertex(1, -sipmActiveSizeZ / 2.0, -sipmActiveSizeRPhi / 2.0); + photoTile->SetVertex(2, -sipmActiveSizeZ / 2.0, sipmActiveSizeRPhi / 2.0); + photoTile->SetVertex(3, sipmActiveSizeZ / 2.0, sipmActiveSizeRPhi / 2.0); + photoTile->SetVertex(4, sipmActiveSizeZ / 2.0, -sipmActiveSizeRPhi / 2.0); + photoTile->SetVertex(5, -sipmActiveSizeZ / 2.0, -sipmActiveSizeRPhi / 2.0); + photoTile->SetVertex(6, -sipmActiveSizeZ / 2.0, sipmActiveSizeRPhi / 2.0); + photoTile->SetVertex(7, sipmActiveSizeZ / 2.0, sipmActiveSizeRPhi / 2.0); + auto* photoTileVol = new TGeoVolume(Form("%s_%d_%d", GeometryTGeo::getRICHSensorPattern(), rPosId, photTileCount), photoTile, medSi); + photoTileVol->SetLineColor(kRed); + photoTileVol->SetLineWidth(1); + auto* rotTransPhoto = new TGeoCombiTrans(photoCenterR * TMath::Cos(phiRad), photoCenterR * TMath::Sin(phiRad), photoCenterZ, makeProjectiveRotation("photoTile")); + motherVolume->AddNode(photoTileVol, 1, rotTransPhoto);*/ + + // Silicone resin in front of SiPMs + addReadoutLayer("siliconeLayer", siliconeLayerThickness, siliconeCenterOffset, medSilicone, kOrange + 2, useRectangularModules); + + // Active sensitive silicon. + photoTile = makeRectangularFootprint(activeSiliconThickness); + auto* photoTileVol = new TGeoVolume(Form("%s_%d_%d", GeometryTGeo::getRICHSensorPattern(), rPosId, photTileCount), photoTile, medSi); + const double activeSiliconCenterR = photoCenterR + activeSiliconCenterOffset * normalRadial; + const double activeSiliconCenterZ = photoCenterZ + activeSiliconCenterOffset * normalZ; + auto* rotTransPhoto = new TGeoCombiTrans(activeSiliconCenterR * TMath::Cos(phiRad), activeSiliconCenterR * TMath::Sin(phiRad), activeSiliconCenterZ, makeProjectiveRotation("photoTile")); + motherVolume->AddNode(photoTileVol, 1, rotTransPhoto); + + // Passive silicon absorber. + addReadoutLayer("siliconAbsorber", passiveSiliconThickness, passiveSiliconCenterOffset, medSiAbsorber, kBlue + 1, true); + + // ------------------------------------------------------------ + // Stack behind the SiPM + // ------------------------------------------------------------ + // Every gap is surface-to-surface. centerOffset is measured from + // the SiPM center along the outward local normal. + double outerSurfaceOffset = photThick / 2.0; + + outerSurfaceOffset += gapSiPMToPCB1; + const double pcb1CenterOffset = outerSurfaceOffset + pcb1Thickness / 2.0; + addReadoutLayer("pcb1", pcb1Thickness, pcb1CenterOffset, medFR4, kGreen + 1, useRectangularModules); + outerSurfaceOffset += pcb1Thickness; + + outerSurfaceOffset += gapPCB1ToCoolingPlate; + const double coolingPlateCenterOffset = outerSurfaceOffset + coolingPlateThickness / 2.0; + addReadoutLayer("coolingPlate", coolingPlateThickness, coolingPlateCenterOffset, medHTCC, kRed, useRectangularModules); + outerSurfaceOffset += coolingPlateThickness; + + outerSurfaceOffset += gapCoolingPlateToPCB2; + const double pcb2CenterOffset = outerSurfaceOffset + pcb2Thickness / 2.0; + addReadoutLayer("pcb2", pcb2Thickness, pcb2CenterOffset, medFR4, kGreen + 2, useRectangularModules); + outerSurfaceOffset += pcb2Thickness; + + outerSurfaceOffset += gapPCB2ToPCB3; + const double pcb3CenterOffset = outerSurfaceOffset + pcb3Thickness / 2.0; + addReadoutLayer("pcb3", pcb3Thickness, pcb3CenterOffset, medFR4, kGreen + 3, useRectangularModules); + + photTileCount++; + } + } + + // Gas sectors (argon) - legacy code, not used in the current geometry, but kept for reference + /* + for (auto& gasSector : gasSectors) { + double separation{(aerDetDistance - radThick - photThick)}; auto* radiator = radiatorTiles[argSectorsCount]; auto* photosensor = photoTiles[argSectorsCount]; - argonSector = new TGeoArb8(separation / 2); - - argonSector->SetVertex(0, -photZ / 2, -photYmin / 2); - argonSector->SetVertex(1, -photZ / 2, photYmin / 2); - argonSector->SetVertex(2, photZ / 2, photYmax / 2); - argonSector->SetVertex(3, photZ / 2, -photYmax / 2); - argonSector->SetVertex(4, -radZ / 2, -radYmin / 2); - argonSector->SetVertex(5, -radZ / 2, radYmin / 2); - argonSector->SetVertex(6, radZ / 2, radYmax / 2); - argonSector->SetVertex(7, radZ / 2, -radYmax / 2); - - TGeoVolume* argonSectorVol = new TGeoVolume(Form("argonSector_%d_%d", rPosId, argSectorsCount), argonSector, medAr); - argonSectorVol->SetVisibility(kTRUE); - argonSectorVol->SetLineColor(kOrange - 8); - argonSectorVol->SetLineWidth(1); - auto* rotArgon = new TGeoRotation(Form("argonSectorRotation_%d_%d", argSectorsCount, rPosId)); - rotArgon->RotateY(-90 - thetaBDeg); - rotArgon->RotateZ(argSectorsCount * deltaPhiDeg); - auto* rotTransArgon = new TGeoCombiTrans((radRad0 + TMath::Cos(thetaB) * (separation + radThick) / 2) * TMath::Cos(argSectorsCount * TMath::Pi() / (nTilesPhi / 2)), - (radRad0 + TMath::Cos(thetaB) * (separation + radThick) / 2) * TMath::Sin(argSectorsCount * TMath::Pi() / (nTilesPhi / 2)), - radRad0 * TMath::Tan(thetaB) + TMath::Sin(thetaB) * (separation + radThick) / 2, - rotArgon); - motherVolume->AddNode(argonSectorVol, 1, rotTransArgon); + gasSector = new TGeoArb8(separation / 2); + + gasSector->SetVertex(0, -photZ / 2, -photYmin / 2); + gasSector->SetVertex(1, -photZ / 2, photYmin / 2); + gasSector->SetVertex(2, photZ / 2, photYmax / 2); + gasSector->SetVertex(3, photZ / 2, -photYmax / 2); + gasSector->SetVertex(4, -radZ / 2, -radYmin / 2); + gasSector->SetVertex(5, -radZ / 2, radYmin / 2); + gasSector->SetVertex(6, radZ / 2, radYmax / 2); + gasSector->SetVertex(7, radZ / 2, -radYmax / 2); + + TGeoVolume* gasSectorVol = new TGeoVolume(Form("gasSector_%d_%d", rPosId, argSectorsCount), gasSector, medCO2); + gasSectorVol->SetVisibility(kTRUE); + gasSectorVol->SetLineColor(kOrange - 8); + gasSectorVol->SetLineWidth(1); + auto* rotGas = new TGeoRotation(Form("gasSectorRotation_%d_%d", argSectorsCount, rPosId)); + rotGas->RotateY(-90 - thetaBDeg); + //rotGas->RotateZ(argSectorsCount * deltaPhiDeg); + //auto* rotTransGas = new TGeoCombiTrans((radRad0 + TMath::Cos(thetaB) * (separation + radThick) / 2) * TMath::Cos(argSectorsCount * TMath::Pi() / (nTilesPhi / 2)), + // (radRad0 + TMath::Cos(thetaB) * (separation + radThick) / 2) * TMath::Sin(argSectorsCount * TMath::Pi() / (nTilesPhi / 2)), + // radRad0 * TMath::Tan(thetaB) + TMath::Sin(thetaB) * (separation + radThick) / 2, + // rotGas); + const double gasPhiRad = modulePhiRad(argSectorsCount); + rotGas->RotateZ(gasPhiRad * 180.0 / TMath::Pi()); + auto* rotTransGas = new TGeoCombiTrans((radRad0 + TMath::Cos(thetaB) * (separation + radThick) / 2.0) * TMath::Cos(gasPhiRad), + (radRad0 + TMath::Cos(thetaB) * (separation + radThick) / 2.0) * TMath::Sin(gasPhiRad), + radRad0 * TMath::Tan(thetaB) + TMath::Sin(thetaB) * (separation + radThick) / 2.0, + rotGas); + motherVolume->AddNode(gasSectorVol, 1, rotTransGas); argSectorsCount++; } + */ } FWDRich::FWDRich(std::string name, - float rMin, - float rMax, - float zAerogelMin, - float dZAerogel, - float zArgonMin, - float dZArgon, - float zSiliconMin, - float dZSilicon) : mName{name}, - mRmin{rMin}, - mRmax{rMax}, - mZAerogelMin{zAerogelMin}, - mDZAerogel{dZAerogel}, - mZArgonMin{zArgonMin}, - mDZArgon{dZArgon}, - mZSiliconMin{zSiliconMin}, - mDZSilicon{dZSilicon} + double rMin, + double rMax, + double zAerogelMin, + double dZAerogel, + double zArgonMin, + double dZArgon, + double zSiliconMin, + double dZSilicon) : mName{name}, + mRmin{rMin}, + mRmax{rMax}, + mZAerogelMin{zAerogelMin}, + mDZAerogel{dZAerogel}, + mZArgonMin{zArgonMin}, + mDZArgon{dZArgon}, + mZSiliconMin{zSiliconMin}, + mDZSilicon{dZSilicon} { } BWDRich::BWDRich(std::string name, - float rMin, - float rMax, - float zAerogelMin, - float dZAerogel, - float zArgonMin, - float dZArgon, - float zSiliconMin, - float dZSilicon) : mName{name}, - mRmin{rMin}, - mRmax{rMax}, - mZAerogelMin{zAerogelMin}, - mDZAerogel{dZAerogel}, - mZArgonMin{zArgonMin}, - mDZArgon{dZArgon}, - mZSiliconMin{zSiliconMin}, - mDZSilicon{dZSilicon} + double rMin, + double rMax, + double zAerogelMin, + double dZAerogel, + double zArgonMin, + double dZArgon, + double zSiliconMin, + double dZSilicon) : mName{name}, + mRmin{rMin}, + mRmax{rMax}, + mZAerogelMin{zAerogelMin}, + mDZAerogel{dZAerogel}, + mZArgonMin{zArgonMin}, + mDZArgon{dZArgon}, + mZSiliconMin{zSiliconMin}, + mDZSilicon{dZSilicon} { } From 7e06656205a59c7efc2d8dfbcfb7d11448afefa3 Mon Sep 17 00:00:00 2001 From: Giulio Eulisse <10544+ktf@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:31:17 +0200 Subject: [PATCH 19/47] Drop Current Refactoring github workflow (#15617) Given how easy it is nowadays to have LLMs do full refactorings, I am not sure this makes sense anymore. --- .github/workflows/code-transformations.yml | 84 ---------------------- 1 file changed, 84 deletions(-) delete mode 100644 .github/workflows/code-transformations.yml diff --git a/.github/workflows/code-transformations.yml b/.github/workflows/code-transformations.yml deleted file mode 100644 index 35493afda94f5..0000000000000 --- a/.github/workflows/code-transformations.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Current refactorings - -on: [pull_request_target] -env: - RATIONALE: "Adapt to new FairLogger API" - REFACTORING: "s|LOGP[(]ERROR|LOGP(error|g;s|LOGP[(]INFO|LOGP(info|g;s|LOGP[(]WARNING|LOGP(warning|g;s|LOGP[(]WARN|LOGP(warn|g;s|LOGP[(]DEBUG|LOGP(debug|;s|LOG[(]ERROR|LOG(error|g;s|LOG[(]INFO|LOG(info|g;s|LOG[(]WARNING|LOG(warning|g;s|LOG[(]WARN|LOG(warn|g;s|LOG[(]DEBUG|LOG(debug|g" - -jobs: - build: - # We need at least 20.04 to install clang-format-11. - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v5 - with: - ref: ${{ github.event.pull_request.head.sha }} - persist-credentials: false - # We need the history of the dev branch all the way back to where the PR - # diverged. We're fetching everything here, as we don't know how many - # commits back that point is. - fetch-depth: 0 - - - name: Run refactoring - id: run_refactoring - env: - ALIBUILD_GITHUB_TOKEN: ${{secrets.ALIBUILD_GITHUB_TOKEN}} - run: | - set -x - # We need to fetch the other commit. - git fetch origin ${{ github.event.pull_request.base.ref }} \ - pull/${{ github.event.pull_request.number }}/head:${{ github.event.pull_request.head.ref }} - - # We create a new branch which we will use for the eventual PR. - git config --global user.email "alibuild@cern.ch" - git config --global user.name "ALICE Action Bot" - git checkout -b alibot-refactor-${{ github.event.pull_request.number }} ${{ github.event.pull_request.head.sha }} - - # github.event.pull_request.base.sha is the latest commit on the branch - # the PR will be merged into, NOT the commit this PR derives from! For - # that, we need to find the latest common ancestor between the PR and - # the branch we are merging into. - BASE_COMMIT=$(git merge-base HEAD ${{ github.event.pull_request.base.sha }}) - echo "Running refactoring against branch ${{ github.event.pull_request.base.ref }}, with hash ${{ github.event.pull_request.base.sha }}" - COMMIT_FILES=$(git diff --diff-filter d --name-only $BASE_COMMIT) - if [ -z "$COMMIT_FILES" ]; then - echo "No files to check" >&2 - echo clean=true >> "$GITHUB_OUTPUT" - exit 0 - fi - perl -p -i -e "${{ env.REFACTORING }}" $COMMIT_FILES - - if git diff --exit-code; then - echo "Refactoring not needed." - git push --set-upstream https://alibuild:$ALIBUILD_GITHUB_TOKEN@github.com/alibuild/AliceO2.git :alibot-refactoring-${{ github.event.pull_request.number }} -f || true - echo clean=true >> "$GITHUB_OUTPUT" - else - git commit -m "${{ env.RATIONALE }}" -a - git show | cat - git fetch https://github.com/AliceO2Group/AliceO2.git pull/${{ github.event.pull_request.number }}/head - git push --set-upstream https://alibuild:$ALIBUILD_GITHUB_TOKEN@github.com/alibuild/AliceO2.git HEAD:refs/heads/alibot-refactoring-${{ github.event.pull_request.number }} -f - echo clean=false >> "$GITHUB_OUTPUT" - fi - - - name: pull-request - uses: alisw/pull-request@master - with: - source_branch: 'alibuild:alibot-refactoring-${{ github.event.pull_request.number }}' - destination_branch: '${{ github.event.pull_request.head.label }}' - github_token: ${{ secrets.ALIBUILD_GITHUB_TOKEN }} - pr_title: "Please consider the refactoring changes to AliceO2Group/AliceO2#${{ github.event.pull_request.number }}" - pr_body: | - AliceO2Group/AliceO2#${{ github.event.pull_request.number }}" cannot be merged as is. - You should either modify your code according to what is done in this PR, or directly merge this PR in yours. - The rationale for this change is: - ${{ env.RATIONALE }} - - continue-on-error: true # We do not create PRs if the branch is not there. - - - name: Exit with error if the PR is not clean - run: | - case ${{ steps.run_refactoring.outputs.clean }} in - true) echo "PR clean" ; exit 0 ;; - false) echo "PR not clean" ; exit 1 ;; - esac From 7fb5f593d1404a364d32b4c96d75aca1a02a4671 Mon Sep 17 00:00:00 2001 From: ehellbar Date: Wed, 22 Jul 2026 09:59:15 +0200 Subject: [PATCH 20/47] DPL: add allocator to forward a fair::mq::Message payload by shallow copy (#15577) --- .../Core/include/Framework/DataAllocator.h | 5 +++++ .../Core/include/Framework/InputRecord.h | 4 ++++ .../include/Framework/InputRecordWalker.h | 5 +++++ Framework/Core/include/Framework/InputSpan.h | 17 ++++++++++++++++ Framework/Core/src/DataAllocator.cxx | 20 +++++++++++++++++++ Framework/Core/src/DataProcessingDevice.cxx | 9 ++++++++- Framework/Core/src/DataRelayer.cxx | 18 +++++++++++++++-- Framework/Core/src/InputRecord.cxx | 5 +++++ Framework/Core/src/InputSpan.cxx | 4 +++- Framework/Core/test/benchmark_InputRecord.cxx | 2 ++ Framework/Core/test/test_CompletionPolicy.cxx | 1 + Framework/Core/test/test_InputRecord.cxx | 2 ++ .../Core/test/test_InputRecordWalker.cxx | 2 +- Framework/Core/test/test_InputSpan.cxx | 2 +- Framework/Utils/test/RawPageTestData.h | 1 + Framework/Utils/test/test_RootTreeWriter.cxx | 1 + 16 files changed, 92 insertions(+), 6 deletions(-) diff --git a/Framework/Core/include/Framework/DataAllocator.h b/Framework/Core/include/Framework/DataAllocator.h index 104e418cbcd19..89572fc86ca2c 100644 --- a/Framework/Core/include/Framework/DataAllocator.h +++ b/Framework/Core/include/Framework/DataAllocator.h @@ -455,6 +455,11 @@ class DataAllocator void snapshot(const Output& spec, const char* payload, size_t payloadSize, o2::header::SerializationMethod serializationMethod = o2::header::gSerializationMethodNone); + /// create a shallow copy of the @a inputPayload and forward it to the output route of @a spec + /// if the transport types of the input and output routes are different, a real copy is created as fallback + void forwardPayload(const Output& spec, fair::mq::Message& inputPayload, + o2::header::SerializationMethod serializationMethod = o2::header::gSerializationMethodNone); + /// make an object of type T and route to output specified by OutputRef /// The object is owned by the framework, returned reference can be used to fill the object. /// diff --git a/Framework/Core/include/Framework/InputRecord.h b/Framework/Core/include/Framework/InputRecord.h index ff5c902ec9e40..dc8d8455bb418 100644 --- a/Framework/Core/include/Framework/InputRecord.h +++ b/Framework/Core/include/Framework/InputRecord.h @@ -213,6 +213,9 @@ class InputRecord /// O(1) access to the part described by @a indices in slot @a pos. [[nodiscard]] DataRef getAtIndices(int pos, DataRefIndices indices) const; + /// Return the payload as fair::mq::Message* for the part described by @a indices in slot @a slotIdx + fair::mq::Message* getPayloadAtIndices(size_t slotIdx, DataRefIndices indices) const; + /// O(1) advance from @a current to the next part's indices in slot @a pos. [[nodiscard]] DataRefIndices nextIndices(int pos, DataRefIndices current) const { @@ -745,6 +748,7 @@ class InputRecord [[nodiscard]] DataRefIndices initialIndices() const { return {0, 1}; } [[nodiscard]] DataRefIndices endIndices() const { return {size_t(-1), size_t(-1)}; } [[nodiscard]] DataRef getAtIndices(DataRefIndices idx) const { return record->getAtIndices((int)slot, idx); } + [[nodiscard]] fair::mq::Message* getPayloadAtIndices(DataRefIndices idx) const { return record->getPayloadAtIndices((int)slot, idx); } [[nodiscard]] DataRefIndices nextIndices(DataRefIndices idx) const { return record->nextIndices((int)slot, idx); } [[nodiscard]] size_t size() const { return record->getNofParts((int)slot); } diff --git a/Framework/Core/include/Framework/InputRecordWalker.h b/Framework/Core/include/Framework/InputRecordWalker.h index 528d5ad0c327c..88403cabc1190 100644 --- a/Framework/Core/include/Framework/InputRecordWalker.h +++ b/Framework/Core/include/Framework/InputRecordWalker.h @@ -115,6 +115,11 @@ class InputRecordWalker return not operator==(rh); } + fair::mq::Message* getPayload() const + { + return mCurrentRange.getPayloadAtIndices(mCurrent.indices()); + } + private: bool next(bool isInitialPart = false) { diff --git a/Framework/Core/include/Framework/InputSpan.h b/Framework/Core/include/Framework/InputSpan.h index d708d2e2f5dde..424ea5ad9e139 100644 --- a/Framework/Core/include/Framework/InputSpan.h +++ b/Framework/Core/include/Framework/InputSpan.h @@ -13,9 +13,11 @@ #include "Framework/DataRef.h" #include +#include extern template class std::function; extern template class std::function; +extern template class std::function; namespace o2::framework { @@ -38,6 +40,7 @@ class InputSpan std::function refCountGetter, std::function indicesGetter, std::function nextIndicesGetter, + std::function payloadGetter, size_t size); /// @a i-th element of the InputSpan (O(partidx) sequential scan via indices protocol) @@ -56,6 +59,12 @@ class InputSpan return mIndicesGetter(slotIdx, indices); } + /// Return the payload as fair::mq::Message* for the part described by @a indices in slot @a slotIdx + [[nodiscard]] fair::mq::Message* getPayloadAtIndices(size_t slotIdx, DataRefIndices indices) const + { + return mPayloadGetter(slotIdx, indices); + } + /// Advance from @a current to the indices of the next part in slot @a slotIdx in O(1). [[nodiscard]] DataRefIndices nextIndices(size_t slotIdx, DataRefIndices current) const { @@ -179,6 +188,12 @@ class InputSpan return mCurrentIndices.headerIdx; } + // return current indices + [[nodiscard]] DataRefIndices indices() const + { + return mCurrentIndices; + } + // return an iterable range over all parts in the current slot // only available for slot-level iterators whose parent has parts(size_t) [[nodiscard]] auto parts() const @@ -201,6 +216,7 @@ class InputSpan [[nodiscard]] DataRefIndices initialIndices() const { return {0, 1}; } [[nodiscard]] DataRefIndices endIndices() const { return {size_t(-1), size_t(-1)}; } [[nodiscard]] DataRef getAtIndices(DataRefIndices idx) const { return span->getAtIndices(slot, idx); } + [[nodiscard]] fair::mq::Message* getPayloadAtIndices(DataRefIndices idx) const { return span->getPayloadAtIndices(slot, idx); } [[nodiscard]] DataRefIndices nextIndices(DataRefIndices idx) const { return span->nextIndices(slot, idx); } [[nodiscard]] size_t size() const { return span->getNofParts(slot); } @@ -230,6 +246,7 @@ class InputSpan std::function mRefCountGetter; std::function mIndicesGetter; std::function mNextIndicesGetter; + std::function mPayloadGetter; size_t mSize; }; diff --git a/Framework/Core/src/DataAllocator.cxx b/Framework/Core/src/DataAllocator.cxx index 0802bb8300ae7..92f7a7f8a6e8f 100644 --- a/Framework/Core/src/DataAllocator.cxx +++ b/Framework/Core/src/DataAllocator.cxx @@ -342,6 +342,26 @@ void DataAllocator::snapshot(const Output& spec, const char* payload, size_t pay addPartToContext(routeIndex, std::move(payloadMessage), spec, serializationMethod); } +void DataAllocator::forwardPayload(const Output& spec, fair::mq::Message& inputPayload, + o2::header::SerializationMethod serializationMethod) +{ + auto& proxy = mRegistry.get(); + auto& timingInfo = mRegistry.get(); + + RouteIndex routeIndex = matchDataHeader(spec, timingInfo.timeslice); + auto* transport = proxy.getOutputTransport(routeIndex); + + if (inputPayload.GetTransport() == transport) { + auto payloadMessage = transport->CreateMessage(); + payloadMessage->Copy(inputPayload); + addPartToContext(routeIndex, std::move(payloadMessage), spec, serializationMethod); + } else { + auto payloadMessage = transport->CreateMessage(inputPayload.GetSize(), fair::mq::Alignment{64}); + memcpy(payloadMessage->GetData(), inputPayload.GetData(), inputPayload.GetSize()); + addPartToContext(routeIndex, std::move(payloadMessage), spec, serializationMethod); + } +} + Output DataAllocator::getOutputByBind(OutputRef&& ref) { if (ref.label.empty()) { diff --git a/Framework/Core/src/DataProcessingDevice.cxx b/Framework/Core/src/DataProcessingDevice.cxx index 4121d333f6b56..ab9a66f4e45d3 100644 --- a/Framework/Core/src/DataProcessingDevice.cxx +++ b/Framework/Core/src/DataProcessingDevice.cxx @@ -2203,7 +2203,14 @@ bool DataProcessingDevice::tryDispatchComputation(ServiceRegistryRef ref, std::v auto next = currentSetOfInputs[i] | get_next_pair{current}; return next.headerIdx < currentSetOfInputs[i].size() ? next : DataRefIndices{size_t(-1), size_t(-1)}; }; - return InputSpan{nofPartsGetter, refCountGetter, indicesGetter, nextIndicesGetter, currentSetOfInputs.size()}; + auto payloadGetter = [¤tSetOfInputs](size_t i, DataRefIndices current) -> fair::mq::Message* { + auto const& msgs = currentSetOfInputs[i]; + if (msgs.size() <= current.payloadIdx || !msgs[current.payloadIdx]) { + return nullptr; + } + return msgs[current.payloadIdx].get(); + }; + return InputSpan{nofPartsGetter, refCountGetter, indicesGetter, nextIndicesGetter, payloadGetter, currentSetOfInputs.size()}; }; auto markInputsAsDone = [ref](TimesliceSlot slot) -> void { diff --git a/Framework/Core/src/DataRelayer.cxx b/Framework/Core/src/DataRelayer.cxx index 7adf5b5c97fbb..67c382d413783 100644 --- a/Framework/Core/src/DataRelayer.cxx +++ b/Framework/Core/src/DataRelayer.cxx @@ -236,7 +236,14 @@ DataRelayer::ActivityStats DataRelayer::processDanglingInputs(std::vector(partial.size())}; + auto payloadGetter = [&partial](size_t idx, DataRefIndices current) -> fair::mq::Message* { + auto const& msgs = partial[idx]; + if (msgs.size() <= current.payloadIdx || !msgs[current.payloadIdx]) { + return nullptr; + } + return msgs[current.payloadIdx].get(); + }; + InputSpan span{nPartsGetter, refCountGetter, indicesGetter, nextIndicesGetter, payloadGetter, static_cast(partial.size())}; // Setup the input span if (expirator.checker(services, timestamp.value, span) == false) { @@ -818,7 +825,14 @@ void DataRelayer::getReadyToProcess(std::vector& comp auto next = partial[idx] | get_next_pair{current}; return next.headerIdx < partial[idx].size() ? next : DataRefIndices{size_t(-1), size_t(-1)}; }; - InputSpan span{nPartsGetter, refCountGetter, indicesGetter, nextIndicesGetter, static_cast(partial.size())}; + auto payloadGetter = [&partial](size_t idx, DataRefIndices current) -> fair::mq::Message* { + auto const& msgs = partial[idx]; + if (msgs.size() <= current.payloadIdx || !msgs[current.payloadIdx]) { + return nullptr; + } + return msgs[current.payloadIdx].get(); + }; + InputSpan span{nPartsGetter, refCountGetter, indicesGetter, nextIndicesGetter, payloadGetter, static_cast(partial.size())}; CompletionPolicy::CompletionOp action = mCompletionPolicy.callbackFull(span, mInputs, mContext); auto& variables = mTimesliceIndex.getVariablesForSlot(slot); diff --git a/Framework/Core/src/InputRecord.cxx b/Framework/Core/src/InputRecord.cxx index 7bc9907b13ba4..514f4a33b337a 100644 --- a/Framework/Core/src/InputRecord.cxx +++ b/Framework/Core/src/InputRecord.cxx @@ -149,6 +149,11 @@ DataRef InputRecord::getAtIndices(int pos, DataRefIndices indices) const return ref; } +fair::mq::Message* InputRecord::getPayloadAtIndices(size_t slotIdx, DataRefIndices indices) const +{ + return mSpan.getPayloadAtIndices(slotIdx, indices); +} + size_t InputRecord::size() const { return mSpan.size(); diff --git a/Framework/Core/src/InputSpan.cxx b/Framework/Core/src/InputSpan.cxx index ccea2d1dd66ed..e6a81d3088d72 100644 --- a/Framework/Core/src/InputSpan.cxx +++ b/Framework/Core/src/InputSpan.cxx @@ -13,6 +13,7 @@ template class std::function; template class std::function; +template class std::function; namespace o2::framework { @@ -20,8 +21,9 @@ InputSpan::InputSpan(std::function nofPartsGetter, std::function refCountGetter, std::function indicesGetter, std::function nextIndicesGetter, + std::function payloadGetter, size_t size) - : mNofPartsGetter{nofPartsGetter}, mRefCountGetter(refCountGetter), mIndicesGetter{std::move(indicesGetter)}, mNextIndicesGetter{std::move(nextIndicesGetter)}, mSize{size} + : mNofPartsGetter{nofPartsGetter}, mRefCountGetter(refCountGetter), mIndicesGetter{std::move(indicesGetter)}, mNextIndicesGetter{std::move(nextIndicesGetter)}, mPayloadGetter{std::move(payloadGetter)}, mSize{size} { } diff --git a/Framework/Core/test/benchmark_InputRecord.cxx b/Framework/Core/test/benchmark_InputRecord.cxx index e3ec00ac815ed..224dafd53d13b 100644 --- a/Framework/Core/test/benchmark_InputRecord.cxx +++ b/Framework/Core/test/benchmark_InputRecord.cxx @@ -52,6 +52,7 @@ static void BM_InputRecordGenericGetters(benchmark::State& state) nullptr, [](size_t, DataRefIndices) { return DataRef{nullptr, nullptr, nullptr}; }, [](size_t, DataRefIndices) -> DataRefIndices { return {size_t(-1), size_t(-1)}; }, + nullptr, 0}; ServiceRegistry registry; InputRecord emptyRecord(schema, span, registry); @@ -92,6 +93,7 @@ static void BM_InputRecordGenericGetters(benchmark::State& state) nullptr, [&inputs](size_t i, DataRefIndices idx) { return DataRef{nullptr, static_cast(inputs[2 * i + idx.headerIdx]), static_cast(inputs[2 * i + idx.payloadIdx])}; }, [](size_t, DataRefIndices) -> DataRefIndices { return {size_t(-1), size_t(-1)}; }, + nullptr, inputs.size() / 2}; InputRecord record{schema, span2, registry}; diff --git a/Framework/Core/test/test_CompletionPolicy.cxx b/Framework/Core/test/test_CompletionPolicy.cxx index cc16ba95ba8f2..ee7dc7e91df24 100644 --- a/Framework/Core/test/test_CompletionPolicy.cxx +++ b/Framework/Core/test/test_CompletionPolicy.cxx @@ -60,6 +60,7 @@ TEST_CASE("TestCompletionPolicy_callback") nullptr, [&ref](size_t, DataRefIndices) -> DataRef { return ref; }, [](size_t, DataRefIndices) -> DataRefIndices { return {size_t(-1), size_t(-1)}; }, + nullptr, 1}; std::vector specs; ServiceRegistryRef servicesRef{services}; diff --git a/Framework/Core/test/test_InputRecord.cxx b/Framework/Core/test/test_InputRecord.cxx index 5dff09409325f..6633bd40a30f8 100644 --- a/Framework/Core/test/test_InputRecord.cxx +++ b/Framework/Core/test/test_InputRecord.cxx @@ -51,6 +51,7 @@ TEST_CASE("TestInputRecord") nullptr, [](size_t, DataRefIndices) { return DataRef{nullptr, nullptr, nullptr}; }, [](size_t, DataRefIndices) -> DataRefIndices { return {size_t(-1), size_t(-1)}; }, + nullptr, 0}; ServiceRegistry registry; InputRecord emptyRecord(schema, span, registry); @@ -99,6 +100,7 @@ TEST_CASE("TestInputRecord") nullptr, [&inputs](size_t i, DataRefIndices idx) { return DataRef{nullptr, static_cast(inputs[2 * i + idx.headerIdx]), static_cast(inputs[2 * i + idx.payloadIdx])}; }, [](size_t, DataRefIndices) -> DataRefIndices { return {size_t(-1), size_t(-1)}; }, + nullptr, inputs.size() / 2}; InputRecord record{schema, span2, registry}; diff --git a/Framework/Core/test/test_InputRecordWalker.cxx b/Framework/Core/test/test_InputRecordWalker.cxx index 1fcfea1ba1587..bfbbd3651b5d3 100644 --- a/Framework/Core/test/test_InputRecordWalker.cxx +++ b/Framework/Core/test/test_InputRecordWalker.cxx @@ -40,7 +40,7 @@ struct DataSet { auto payload = static_cast(this->messages[i].second.at(idx.payloadIdx)->data()); return DataRef{nullptr, header, payload}; }, [this](size_t i, DataRefIndices current) -> DataRefIndices { size_t next = current.headerIdx + 2; - return next < this->messages[i].second.size() ? DataRefIndices{next, next + 1} : DataRefIndices{size_t(-1), size_t(-1)}; }, this->messages.size()}, record{schema, span, registry}, values{std::move(v)} + return next < this->messages[i].second.size() ? DataRefIndices{next, next + 1} : DataRefIndices{size_t(-1), size_t(-1)}; }, nullptr, this->messages.size()}, record{schema, span, registry}, values{std::move(v)} { REQUIRE(messages.size() == schema.size()); } diff --git a/Framework/Core/test/test_InputSpan.cxx b/Framework/Core/test/test_InputSpan.cxx index f8d043a2a48ba..9c3ae67e0e063 100644 --- a/Framework/Core/test/test_InputSpan.cxx +++ b/Framework/Core/test/test_InputSpan.cxx @@ -41,7 +41,7 @@ TEST_CASE("TestInputSpan") return next < inputs[i].size() ? DataRefIndices{next, next + 1} : DataRefIndices{size_t(-1), size_t(-1)}; }; - InputSpan span{nPartsGetter, nullptr, indicesGetter, nextIndicesGetter, inputs.size()}; + InputSpan span{nPartsGetter, nullptr, indicesGetter, nextIndicesGetter, nullptr, inputs.size()}; REQUIRE(span.size() == inputs.size()); routeNo = 0; for (; routeNo < span.size(); ++routeNo) { diff --git a/Framework/Utils/test/RawPageTestData.h b/Framework/Utils/test/RawPageTestData.h index 29ac4eeba6b5b..e219f8ba84637 100644 --- a/Framework/Utils/test/RawPageTestData.h +++ b/Framework/Utils/test/RawPageTestData.h @@ -53,6 +53,7 @@ struct DataSet { size_t next = current.headerIdx + 2; return next < this->messages[i].size() ? DataRefIndices{next, next + 1} : DataRefIndices{size_t(-1), size_t(-1)}; }, + nullptr, this->messages.size()}, record{schema, span, registry}, values{std::move(v)} diff --git a/Framework/Utils/test/test_RootTreeWriter.cxx b/Framework/Utils/test/test_RootTreeWriter.cxx index e372fb4e1302e..ebacd0e71e5c3 100644 --- a/Framework/Utils/test/test_RootTreeWriter.cxx +++ b/Framework/Utils/test/test_RootTreeWriter.cxx @@ -231,6 +231,7 @@ TEST_CASE("test_RootTreeWriter") return DataRef{nullptr, static_cast(store[2 * i + idx.headerIdx]->GetData()), static_cast(store[2 * i + idx.payloadIdx]->GetData())}; }, [](size_t, DataRefIndices) -> DataRefIndices { return {size_t(-1), size_t(-1)}; }, + nullptr, store.size() / 2}; ServiceRegistry registry; InputRecord inputs{ From 31f5f1092a4144f1118c76183b49d84ca2ee6070 Mon Sep 17 00:00:00 2001 From: Autumn McKee Date: Wed, 22 Jul 2026 10:39:14 +0200 Subject: [PATCH 21/47] DPL: add discard support for LifetimeHolder (#15592) --- .../Core/include/Framework/DataAllocator.h | 27 ++++++++++++++++--- Framework/Core/test/test_ASoA.cxx | 2 +- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Framework/Core/include/Framework/DataAllocator.h b/Framework/Core/include/Framework/DataAllocator.h index 89572fc86ca2c..8d0660dfbbaec 100644 --- a/Framework/Core/include/Framework/DataAllocator.h +++ b/Framework/Core/include/Framework/DataAllocator.h @@ -115,11 +115,30 @@ struct LifetimeHolder { // invoke the callback early (e.g. for the Product<> case) void release() { - if (ptr && callback) { - callback(*ptr); - delete ptr; - ptr = nullptr; + if (!ptr) { + return; } + + std::unique_ptr released{ptr}; + ptr = nullptr; + auto releaseCallback = std::move(callback); + if (!releaseCallback) { + return; + } + releaseCallback(*released); + } + + // Delete the owned object without invoking the release callback. This is used + // when a partially filled object must be abandoned. + void discard() + { + if (!ptr) { + return; + } + + callback = nullptr; + delete ptr; + ptr = nullptr; } }; diff --git a/Framework/Core/test/test_ASoA.cxx b/Framework/Core/test/test_ASoA.cxx index 48cfb277acc5c..d96304bd3bce2 100644 --- a/Framework/Core/test/test_ASoA.cxx +++ b/Framework/Core/test/test_ASoA.cxx @@ -1420,5 +1420,5 @@ TEST_CASE("TestWritingCursorLastIndexAndReserve") auto table = builder->finalize(); REQUIRE(table->num_rows() == 5); REQUIRE(table->num_columns() == 2); - delete builder; + cursor.release(); } From 9caae96a3713027b594ffcf65690d8319dc97ebc Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Tue, 21 Jul 2026 16:41:34 +0200 Subject: [PATCH 22/47] Fix wrong parameters injected in SimFieldUtils Third patameter of createFieldMap is the convention and the 4th is the uniformity. This was fixed everywhere, but here. --- Detectors/Base/src/SimFieldUtils.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Detectors/Base/src/SimFieldUtils.cxx b/Detectors/Base/src/SimFieldUtils.cxx index 9673e39bf1b07..ba828c0c2ac25 100644 --- a/Detectors/Base/src/SimFieldUtils.cxx +++ b/Detectors/Base/src/SimFieldUtils.cxx @@ -33,7 +33,7 @@ FairField* const SimFieldUtils::createMagField() auto& ccdb = o2::ccdb::BasicCCDBManager::instance(); auto grpmagfield = ccdb.get("GLO/Config/GRPMagField"); // TODO: clarify if we need to pass other params such as beam energy/type etc. - field = o2::field::MagneticField::createFieldMap(grpmagfield->getL3Current(), grpmagfield->getDipoleCurrent(), grpmagfield->getFieldUniformity()); + field = o2::field::MagneticField::createFieldMap(grpmagfield->getL3Current(), grpmagfield->getDipoleCurrent(), o2::field::MagneticField::kConvLHC, grpmagfield->getFieldUniformity()); } // b) using the given values on the command line else { From e6152d702a8fa3162793fb6295e712cd859f92b3 Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Tue, 14 Jul 2026 17:50:46 +0200 Subject: [PATCH 23/47] Created group labels for randomisation + added protection on non-integer fractions --- Generators/src/GeneratorHybrid.cxx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Generators/src/GeneratorHybrid.cxx b/Generators/src/GeneratorHybrid.cxx index fdca64beeb701..ed831aef8b16a 100644 --- a/Generators/src/GeneratorHybrid.cxx +++ b/Generators/src/GeneratorHybrid.cxx @@ -245,6 +245,18 @@ Bool_t GeneratorHybrid::Init() } count++; } + // Label for groups: concatenation of the names of the generators in the group, separated by '+'. + // Currently used only if randomisation is enabled + auto groupLabel = [this](int k) { + std::string label; + for (auto subIndex : mGroups[k]) { + if (!label.empty()) { + label += "+"; + } + label += (mConfigs[subIndex] == "" ? mGens[subIndex] : mConfigs[subIndex]); + } + return label; + }; if (mRandomize) { if (std::all_of(mFractions.begin(), mFractions.end(), [](int i) { return i == 1; })) { LOG(info) << "Full randomisation of generators order"; @@ -261,12 +273,12 @@ Bool_t GeneratorHybrid::Init() if (mFractions[k] == 0) { // Generator will not be used if fraction is 0 mRngFractions.push_back(-1); - LOG(info) << "Generator " << mGens[k] << " will not be used"; + LOG(info) << "Generator " << groupLabel(k) << " will not be used"; } else { chance = static_cast(mFractions[k]) / allfracs; sum += chance; mRngFractions.push_back(sum); - LOG(info) << "Generator " << (mConfigs[k] == "" ? mGens[k] : mConfigs[k]) << " has a " << chance * 100 << "% chance of being used"; + LOG(info) << "Generator " << groupLabel(k) << " has a " << chance * 100 << "% chance of being used"; } } } @@ -707,6 +719,10 @@ Bool_t GeneratorHybrid::parseJSON(const std::string& path) if (doc.HasMember("fractions")) { const auto& fractions = doc["fractions"]; for (const auto& frac : fractions.GetArray()) { + if (!frac.IsInt()) { + LOG(fatal) << "Fractions must be integers. Wrong type found in JSON"; + return false; + } mFractions.push_back(frac.GetInt()); } } else { From 94e1eec7066f18d98b0e2a61b48a8807c3ecdf1e Mon Sep 17 00:00:00 2001 From: Anton Alkin Date: Wed, 22 Jul 2026 14:41:56 +0200 Subject: [PATCH 24/47] DPL Analysis: avoid Arrow's Slice API (#15613) --- Framework/Core/include/Framework/ASoA.h | 480 ++++++++---------- .../Core/include/Framework/AnalysisHelpers.h | 10 +- .../Core/include/Framework/AnalysisManagers.h | 12 +- .../Core/include/Framework/AnalysisTask.h | 6 +- Framework/Core/include/Framework/ArrowTypes.h | 43 ++ .../Core/include/Framework/GroupSlicer.h | 18 +- .../include/Framework/GroupedCombinations.h | 10 +- Framework/Core/src/ASoA.cxx | 135 +++-- Framework/Core/src/AnalysisHelpers.cxx | 2 +- Framework/Core/test/test_ASoA.cxx | 87 +++- Framework/Core/test/test_ASoAHelpers.cxx | 4 +- .../Core/test/test_AnalysisDataModel.cxx | 6 +- Framework/Core/test/test_AnalysisTask.cxx | 2 +- Framework/Core/test/test_Concepts.cxx | 2 +- Framework/Core/test/test_GroupSlicer.cxx | 38 +- Framework/Core/test/test_TableSpawner.cxx | 4 +- 16 files changed, 446 insertions(+), 413 deletions(-) diff --git a/Framework/Core/include/Framework/ASoA.h b/Framework/Core/include/Framework/ASoA.h index ba6d58df9a639..9e9b1d40f4008 100644 --- a/Framework/Core/include/Framework/ASoA.h +++ b/Framework/Core/include/Framework/ASoA.h @@ -58,6 +58,14 @@ void missingFilterDeclaration(int hash, int ai); void notBoundTable(const char* tableName); void* extractCCDBPayload(char* payload, size_t size, TClass const* cl, const char* what); +// ASCII-only lowercase. Column labels are plain identifiers, so we deliberately +// avoid the locale-aware std::tolower: it goes through the C locale facet on +// every character and dominated getIndexFromLabel in profiles. +constexpr char asciiToLower(char c) +{ + return (c >= 'A' && c <= 'Z') ? static_cast(c + 32) : c; +} + template auto createFieldsFromColumns(framework::pack) { @@ -494,13 +502,13 @@ class ColumnIterator : ChunkingPolicy : mColumn{column}, mCurrent{nullptr}, mCurrentPos{nullptr}, + mGlobalOffset{nullptr}, mLast{nullptr}, mFirstIndex{0}, - mCurrentChunk{0}, - mOffset{0} + mCurrentChunk{0} { auto array = getCurrentArray(); - mCurrent = reinterpret_cast const*>(array->values()->data()) + (mOffset >> SCALE_FACTOR); + mCurrent = reinterpret_cast const*>(array->values()->data()); mLast = mCurrent + array->length(); } @@ -516,10 +524,9 @@ class ColumnIterator : ChunkingPolicy { auto previousArray = getCurrentArray(); mFirstIndex += previousArray->length(); - mCurrentChunk++; auto array = getCurrentArray(); - mCurrent = reinterpret_cast const*>(array->values()->data()) + (mOffset >> SCALE_FACTOR) - (mFirstIndex >> SCALE_FACTOR); + mCurrent = reinterpret_cast const*>(array->values()->data()) - (mFirstIndex >> SCALE_FACTOR); mLast = mCurrent + array->length() + (mFirstIndex >> SCALE_FACTOR); } @@ -527,10 +534,9 @@ class ColumnIterator : ChunkingPolicy { auto previousArray = getCurrentArray(); mFirstIndex -= previousArray->length(); - mCurrentChunk--; auto array = getCurrentArray(); - mCurrent = reinterpret_cast const*>(array->values()->data()) + (mOffset >> SCALE_FACTOR) - (mFirstIndex >> SCALE_FACTOR); + mCurrent = reinterpret_cast const*>(array->values()->data()) - (mFirstIndex >> SCALE_FACTOR); mLast = mCurrent + array->length() + (mFirstIndex >> SCALE_FACTOR); } @@ -553,7 +559,7 @@ class ColumnIterator : ChunkingPolicy mCurrentChunk = mColumn->num_chunks() - 1; auto array = getCurrentArray(); mFirstIndex = mColumn->length() - array->length(); - mCurrent = reinterpret_cast const*>(array->values()->data()) + (mOffset >> SCALE_FACTOR) - (mFirstIndex >> SCALE_FACTOR); + mCurrent = reinterpret_cast const*>(array->values()->data()) - (mFirstIndex >> SCALE_FACTOR); mLast = mCurrent + array->length() + (mFirstIndex >> SCALE_FACTOR); } @@ -561,7 +567,7 @@ class ColumnIterator : ChunkingPolicy requires std::same_as> { checkSkipChunk(); - return (*(mCurrent - (mOffset >> SCALE_FACTOR) + ((*mCurrentPos + mOffset) >> SCALE_FACTOR)) & (1 << ((*mCurrentPos + mOffset) & 0x7))) != 0; + return (*(mCurrent + ((*mCurrentPos + *mGlobalOffset) >> SCALE_FACTOR)) & (1 << ((*mCurrentPos + *mGlobalOffset) & ((1 << SCALE_FACTOR) - 1)))) != 0; } auto operator*() const @@ -569,8 +575,8 @@ class ColumnIterator : ChunkingPolicy { checkSkipChunk(); auto list = std::static_pointer_cast(mColumn->chunk(mCurrentChunk)); - auto offset = list->value_offset(*mCurrentPos - mFirstIndex); - auto length = list->value_length(*mCurrentPos - mFirstIndex); + auto offset = list->value_offset(*mCurrentPos + *mGlobalOffset - mFirstIndex); + auto length = list->value_length(*mCurrentPos + *mGlobalOffset - mFirstIndex); return gsl::span const>{mCurrent + mFirstIndex + offset, mCurrent + mFirstIndex + (offset + length)}; } @@ -579,14 +585,14 @@ class ColumnIterator : ChunkingPolicy { checkSkipChunk(); auto array = std::static_pointer_cast(mColumn->chunk(mCurrentChunk)); - return array->GetView(*mCurrentPos - mFirstIndex); + return array->GetView(*mCurrentPos + *mGlobalOffset - mFirstIndex); } decltype(auto) operator*() const requires((!std::same_as>) && !std::same_as, arrow::ListArray> && !std::same_as, arrow::BinaryViewArray>) { checkSkipChunk(); - return *(mCurrent + (*mCurrentPos >> SCALE_FACTOR)); + return *(mCurrent + ((*mCurrentPos + *mGlobalOffset) >> SCALE_FACTOR)); } // Move to the chunk which containts element pos @@ -598,18 +604,18 @@ class ColumnIterator : ChunkingPolicy mutable unwrap_t const* mCurrent; int64_t const* mCurrentPos; + uint64_t const* mGlobalOffset; mutable unwrap_t const* mLast; arrow::ChunkedArray const* mColumn; mutable int mFirstIndex; mutable int mCurrentChunk; - mutable int mOffset; private: void checkSkipChunk() const requires((ChunkingPolicy::chunked == true) && std::same_as, arrow::ListArray>) { auto list = std::static_pointer_cast(mColumn->chunk(mCurrentChunk)); - if (O2_BUILTIN_UNLIKELY(*mCurrentPos - mFirstIndex >= list->length())) { + if (O2_BUILTIN_UNLIKELY(*mCurrentPos + *mGlobalOffset - mFirstIndex >= list->length())) { nextChunk(); } } @@ -617,7 +623,7 @@ class ColumnIterator : ChunkingPolicy void checkSkipChunk() const requires((ChunkingPolicy::chunked == true) && !std::same_as, arrow::ListArray>) { - if (O2_BUILTIN_UNLIKELY(((mCurrent + (*mCurrentPos >> SCALE_FACTOR)) >= mLast))) { + if (O2_BUILTIN_UNLIKELY(((mCurrent + ((*mCurrentPos + *mGlobalOffset) >> SCALE_FACTOR)) >= mLast))) { nextChunk(); } } @@ -631,7 +637,6 @@ class ColumnIterator : ChunkingPolicy requires(std::same_as, arrow::FixedSizeListArray>) { std::shared_ptr chunkToUse = mColumn->chunk(mCurrentChunk); - mOffset = chunkToUse->offset(); chunkToUse = std::dynamic_pointer_cast(chunkToUse)->values(); return std::static_pointer_cast>>(chunkToUse); } @@ -640,9 +645,7 @@ class ColumnIterator : ChunkingPolicy requires(std::same_as, arrow::ListArray>) { std::shared_ptr chunkToUse = mColumn->chunk(mCurrentChunk); - mOffset = chunkToUse->offset(); chunkToUse = std::dynamic_pointer_cast(chunkToUse)->values(); - mOffset = chunkToUse->offset(); return std::static_pointer_cast>>(chunkToUse); } @@ -650,7 +653,6 @@ class ColumnIterator : ChunkingPolicy requires(!std::same_as, arrow::FixedSizeListArray> && !std::same_as, arrow::ListArray>) { std::shared_ptr chunkToUse = mColumn->chunk(mCurrentChunk); - mOffset = chunkToUse->offset(); return std::static_pointer_cast>(chunkToUse); } }; @@ -1197,7 +1199,7 @@ struct TableIterator : IP, C... { { using namespace o2::soa; auto f = framework::overloaded{ - [this](T*) -> void { T::mColumnIterator.mCurrentPos = &this->mRowIndex; }, + [this](T*) -> void { T::mColumnIterator.mCurrentPos = &this->mRowIndex; T::mColumnIterator.mGlobalOffset = &this->mOffset; }, [this](T*) -> void { bindDynamicColumn(typename T::bindings_t{}); }, [this](T*) -> void {}, }; @@ -1225,7 +1227,6 @@ struct TableIterator : IP, C... { { static_assert(std::same_as(this)->mColumnIterator)), std::decay_t*>, "foo"); return &(static_cast(this)->mColumnIterator); - // return static_cast*>(nullptr); } template @@ -1236,10 +1237,14 @@ struct TableIterator : IP, C... { }; struct ArrowHelpers { - static std::shared_ptr joinTables(std::vector>&& tables); - static std::shared_ptr joinTables(std::vector>&& tables, std::span labels); - static std::shared_ptr joinTables(std::vector>&& tables, std::span labels); - static std::shared_ptr concatTables(std::vector>&& tables); + static o2::soa::ArrowTableRef joinTables(std::vector>&& tables); + static o2::soa::ArrowTableRef joinTables(std::vector&& tables); + static o2::soa::ArrowTableRef joinTables(std::vector&& tables, std::span labels); + static o2::soa::ArrowTableRef joinTables(std::vector&& tables, std::span labels); + static o2::soa::ArrowTableRef joinTables(std::vector>&& tables, std::span labels); + static o2::soa::ArrowTableRef joinTables(std::vector>&& tables, std::span labels); + static o2::soa::ArrowTableRef concatTables(std::vector&& tables); + static o2::soa::ArrowTableRef concatTables(std::vector>&& tables); }; template os1, size_t N2, std::array os2> @@ -1302,8 +1307,8 @@ static constexpr auto hasColumnForKey(framework::pack, std::string_view ke return std::ranges::equal( str1, str2, [](char c1, char c2) { - return std::tolower(static_cast(c1)) == - std::tolower(static_cast(c2)); + return asciiToLower(static_cast(c1)) == + asciiToLower(static_cast(c2)); }); }; return (caseInsensitiveCompare(C::inherited_t::mLabel, key) || ...); @@ -1403,12 +1408,7 @@ struct PreslicePolicySorted : public PreslicePolicyBase { void updateSliceInfo(SliceInfoPtr&& si); SliceInfoPtr sliceInfo; - std::shared_ptr getSliceFor(int value, std::shared_ptr const& input, uint64_t& offset) const; - // One-slot cache for the empty (0-row) slice, so that empty groups do not - // slice every column only to produce 0 rows (the common case for sparse - // grouping, e.g. candidates per collision). Keyed by the input table, which - // changes with every dataframe. - mutable std::pair> emptySlice{nullptr, nullptr}; + o2::soa::ArrowTableRef getSliceFor(int value, o2::soa::ArrowTableRef const& input) const; }; struct PreslicePolicyGeneral : public PreslicePolicyBase { @@ -1431,14 +1431,14 @@ struct PresliceBase : public Policy { { } - std::shared_ptr getSliceFor(int value, std::shared_ptr const& input, uint64_t& offset) const + o2::soa::ArrowTableRef getSliceFor(int value, o2::soa::ArrowTableRef const& input) const { if constexpr (OPT) { if (Policy::isMissing()) { - return nullptr; + return {nullptr, {0, 0}}; } } - return Policy::getSliceFor(value, input, offset); + return Policy::getSliceFor(value, input); } std::span getSliceFor(int value) const @@ -1507,9 +1507,8 @@ auto doSliceBy(T const* table, o2::framework::PresliceBase const missingOptionalPreslice(getLabelFromType>().data(), container.bindingKey.key.c_str()); } } - uint64_t offset = 0; - auto out = container.getSliceFor(value, table->asArrowTable(), offset); - auto t = typename T::self_t({out}, offset); + auto out = container.getSliceFor(value, table->asArrowTableRef()); + auto t = typename T::self_t({out}); if (t.tableSize() != 0) { table->copyIndexBindings(t); t.bindInternalIndicesTo(table); @@ -1520,7 +1519,7 @@ auto doSliceBy(T const* table, o2::framework::PresliceBase const template auto doSliceByHelper(T const* table, std::span const& selection) { - auto t = soa::Filtered({table->asArrowTable()}, selection); + auto t = soa::Filtered({table->asArrowTableRef()}, selection); if (t.tableSize() != 0) { table->copyIndexBindings(t); t.bindInternalIndicesTo(table); @@ -1533,7 +1532,7 @@ template requires(!soa::is_filtered_table) auto doSliceByHelper(T const* table, std::span const& selection) { - auto t = soa::Filtered({table->asArrowTable()}, selection); + auto t = soa::Filtered({table->asArrowTableRef()}, selection); if (t.tableSize() != 0) { table->copyIndexBindings(t); t.bindInternalIndicesTo(table); @@ -1557,17 +1556,17 @@ auto doSliceBy(T const* table, o2::framework::PresliceBase const SelectionVector sliceSelection(std::span const& mSelectedRows, int64_t nrows, uint64_t offset); template -auto prepareFilteredSlice(T const* table, std::shared_ptr slice, uint64_t offset) +auto prepareFilteredSlice(T const* table, o2::soa::ArrowTableRef slice) { - if (offset >= static_cast(table->tableSize())) { - Filtered fresult{{{slice}}, SelectionVector{}, 0}; + if (slice.range.offset >= static_cast(table->tableSize())) { + Filtered fresult{{slice}, SelectionVector{}}; if (fresult.tableSize() != 0) { table->copyIndexBindings(fresult); } return fresult; } - auto slicedSelection = sliceSelection(table->getSelectedRows(), slice->num_rows(), offset); - Filtered fresult{{{slice}}, std::move(slicedSelection), offset}; + auto slicedSelection = sliceSelection(table->getSelectedRows(), slice.range.size, slice.range.offset); + Filtered fresult{{slice}, std::move(slicedSelection)}; if (fresult.tableSize() != 0) { table->copyIndexBindings(fresult); } @@ -1583,9 +1582,8 @@ auto doFilteredSliceBy(T const* table, o2::framework::PresliceBase().data(), container.bindingKey.key.c_str()); } } - uint64_t offset = 0; - auto slice = container.getSliceFor(value, table->asArrowTable(), offset); - return prepareFilteredSlice(table, slice, offset); + auto slice = container.getSliceFor(value, table->asArrowTableRef()); + return prepareFilteredSlice(table, slice); } std::function originReplacement(header::DataOrigin newOrigin); @@ -1596,10 +1594,7 @@ auto doSliceByCached(T const* table, framework::expressions::BindingNode const& auto localCache = cache.ptr->getCacheFor({"", originReplacement(cache.ptr->newOrigin)(o2::soa::getMatcherFromTypeForKey(node.name)), node.name}); auto [offset, count] = localCache.getSliceFor(value); - // Empty group: reuse a cached empty (0-row) table instead of slicing every column. - auto slice = count == 0 ? cache.ptr->getEmptySliceFor(table->asArrowTable()) - : table->asArrowTable()->Slice(static_cast(offset), count); - auto t = typename T::self_t({slice}, static_cast(offset)); + auto t = typename T::self_t({table->asArrowTableRef().slice({static_cast(offset), count})}); if (t.tableSize() != 0) { table->copyIndexBindings(t); } @@ -1612,10 +1607,7 @@ auto doFilteredSliceByCached(T const* table, framework::expressions::BindingNode auto localCache = cache.ptr->getCacheFor({"", originReplacement(cache.ptr->newOrigin)(o2::soa::getMatcherFromTypeForKey(node.name)), node.name}); auto [offset, count] = localCache.getSliceFor(value); - // Empty group: reuse a cached empty (0-row) table instead of slicing every column. - auto slice = count == 0 ? cache.ptr->getEmptySliceFor(table->asArrowTable()) - : table->asArrowTable()->Slice(static_cast(offset), count); - return prepareFilteredSlice(table, slice, offset); + return prepareFilteredSlice(table, table->asArrowTableRef().slice({static_cast(offset), count})); } template @@ -1624,14 +1616,14 @@ auto doSliceByCachedUnsorted(T const* table, framework::expressions::BindingNode auto localCache = cache.ptr->getCacheUnsortedFor({"", originReplacement(cache.ptr->newOrigin)(o2::soa::getMatcherFromTypeForKey(node.name)), node.name}); if constexpr (soa::is_filtered_table) { - auto t = typename T::self_t({table->asArrowTable()}, localCache.getSliceFor(value)); + auto t = typename T::self_t({table->asArrowTableRef()}, localCache.getSliceFor(value)); if (t.tableSize() != 0) { t.intersectWithSelection(table->getSelectedRows()); table->copyIndexBindings(t); } return t; } else { - auto t = Filtered({table->asArrowTable()}, localCache.getSliceFor(value)); + auto t = Filtered({table->asArrowTableRef()}, localCache.getSliceFor(value)); if (t.tableSize() != 0) { table->copyIndexBindings(t); } @@ -1642,7 +1634,7 @@ auto doSliceByCachedUnsorted(T const* table, framework::expressions::BindingNode template auto select(T const& t, framework::expressions::Filter const& f) { - return Filtered({t.asArrowTable()}, selectionToVector(framework::expressions::createSelection(t.asArrowTable(), f))); + return Filtered({t.asArrowTableRef()}, selectionToVector(framework::expressions::createSelection(t.asArrowTable(), f))); } arrow::ChunkedArray* getIndexFromLabel(arrow::Table* table, std::string_view label); @@ -1651,7 +1643,6 @@ template consteval auto base_iter(framework::pack&&) -> TableIterator { } - template requires((sizeof...(Ts) > 0) && (soa::is_column && ...)) consteval auto getColumns() @@ -1709,7 +1700,7 @@ class Table static constexpr const auto originalLabels = [] refs, size_t... Is>(std::index_sequence) { return std::array{o2::aod::label()...}; }.template operator()(std::make_index_sequence()); - static constexpr const uint32_t binding_origin = originals[0].origin_hash; // commonOrigin(); + static constexpr const uint32_t binding_origin = originals[0].origin_hash; static constexpr header::DataOrigin binding_origin_ = o2::aod::Hash::origin; template bindings> @@ -1757,7 +1748,6 @@ class Table using columns_t = typename Parent::columns_t; using external_index_columns_t = typename Parent::external_index_columns_t; using bindings_pack_t = decltype([](framework::pack) -> framework::pack {}(external_index_columns_t{})); - // static constexpr const std::array originals{T::ref...}; static constexpr auto originals = Parent::originals; using policy_t = IP; using parent_t = Parent; @@ -1909,8 +1899,7 @@ class Table using iterator_template = TableIteratorBase; template - static consteval auto full_iter() - { + using iterator_template_o = decltype([]() { if constexpr (sizeof...(Ts) == 0) { return iterator_template{}; } else { @@ -1920,10 +1909,7 @@ class Table return iterator_template{}; } } - } - - template - using iterator_template_o = decltype(full_iter()); + }()); using iterator = iterator_template_o; using filtered_iterator = iterator_template_o; @@ -1937,12 +1923,11 @@ class Table return [](framework::pack) { return std::set{{C::hash...}}; }(columns_t{}); } - Table(std::shared_ptr table, uint64_t offset = 0) - : mTable(table), - mOffset(offset), - mEnd{table->num_rows()} + Table(o2::soa::ArrowTableRef tableRef) + : mArrowTableRef(tableRef), + mEnd{tableRef.range.size} { - if (mTable->num_rows() == 0) { + if (mArrowTableRef.tablePtr->num_rows() == 0) { for (size_t ci = 0; ci < framework::pack_size(columns_t{}); ++ci) { mColumnChunks[ci] = nullptr; } @@ -1952,20 +1937,37 @@ class Table for (size_t ci = 0; ci < framework::pack_size(columns_t{}); ++ci) { mColumnChunks[ci] = lookups[ci]; } - mBegin = unfiltered_iterator{mColumnChunks, {table->num_rows(), offset}}; + mBegin = unfiltered_iterator{mColumnChunks, {mEnd.index, mArrowTableRef.range.offset}}; mBegin.bindInternalIndices(this); } } - Table(std::vector>&& tables, uint64_t offset = 0) + Table(std::shared_ptr table) + : Table(o2::soa::ArrowTableRef{table}) + { + } + + Table(std::vector&& tables) + requires(ref.origin_hash != "CONC"_h) + : Table(ArrowHelpers::joinTables(std::forward>(tables), std::span{originalLabels})) + { + } + + Table(std::vector&& tables) + requires(ref.origin_hash == "CONC"_h) + : Table(ArrowHelpers::concatTables(std::forward>(tables))) + { + } + + Table(std::vector>&& tables) requires(ref.origin_hash != "CONC"_h) - : Table(ArrowHelpers::joinTables(std::move(tables), std::span{originalLabels}), offset) + : Table(ArrowHelpers::joinTables(std::forward>>(tables))) { } - Table(std::vector>&& tables, uint64_t offset = 0) + Table(std::vector>&& tables) requires(ref.origin_hash == "CONC"_h) - : Table(ArrowHelpers::concatTables(std::move(tables)), offset) + : Table(ArrowHelpers::concatTables(std::forward>>(tables))) { } @@ -2015,7 +2017,7 @@ class Table // is held by the table, so we are safe passing the bare pointer. If it does it // means that the iterator on a table is outliving the table itself, which is // a bad idea. - return filtered_iterator(mColumnChunks, {selection, mTable->num_rows(), mOffset}); + return filtered_iterator(mColumnChunks, {selection, mArrowTableRef.tablePtr->num_rows(), mArrowTableRef.range.offset}); } iterator iteratorAt(uint64_t i) const @@ -2043,17 +2045,27 @@ class Table /// Return a type erased arrow table backing store for / the type safe table. [[nodiscard]] std::shared_ptr asArrowTable() const { - return mTable; + return mArrowTableRef.tablePtr; + } + + [[nodiscard]] std::shared_ptr asArrowTableConstrained() const + { + return mArrowTableRef.tablePtr->Slice(mArrowTableRef.range.offset, mArrowTableRef.range.size); + } + + [[nodiscard]] ArrowTableRef asArrowTableRef() const + { + return mArrowTableRef; } /// Return offset auto offset() const { - return mOffset; + return mArrowTableRef.range.offset; } /// Size of the table, in rows. [[nodiscard]] int64_t size() const { - return mTable->num_rows(); + return mArrowTableRef.range.size; } [[nodiscard]] int64_t tableSize() const @@ -2139,27 +2151,30 @@ class Table auto rawSlice(uint64_t start, uint64_t end) const { - return self_t{mTable->Slice(start, end - start + 1), start}; + return self_t{mArrowTableRef.slice({start, static_cast(end - start + 1)})}; } auto emptySlice() const { - return self_t{mTable->Slice(0, 0), 0}; + return self_t{mArrowTableRef.makeEmpty()}; } private: template arrow::ChunkedArray* lookupColumn() { - if constexpr (soa::is_persistent_column) { - auto label = T::columnLabel(); - return getIndexFromLabel(mTable.get(), label); - } else { - return nullptr; - } + return nullptr; } - std::shared_ptr mTable = nullptr; - uint64_t mOffset = 0; + + template + arrow::ChunkedArray* lookupColumn() + { + return getIndexFromLabel(mArrowTableRef.tablePtr.get(), T::columnLabel()); + } + + ArrowTableRef mArrowTableRef; + // std::shared_ptr mTable = nullptr; + // uint64_t mOffset = 0; // Cached pointers to the ChunkedArray associated to a column arrow::ChunkedArray* mColumnChunks[framework::pack_size(columns_t{})]; RowViewSentinel mEnd; @@ -2262,13 +2277,13 @@ namespace o2::aod O2ORIGIN("AOD"); O2ORIGIN("AOD1"); O2ORIGIN("AOD2"); -// O2ORIGIN("DYN"); -// O2ORIGIN("IDX"); -// O2ORIGIN("ATIM"); + O2ORIGIN("JOIN"); O2HASH("JOIN/0"); + O2ORIGIN("CONC"); O2HASH("CONC/0"); + O2ORIGIN("TEST"); O2HASH("TEST/0"); } // namespace o2::aod @@ -3315,20 +3330,14 @@ struct Join : Table, o2::aod::Hash<"JOIN/0"_h>, o2::aod: static constexpr void isJoin() {}; using base = Table, o2::aod::Hash<"JOIN/0"_h>, o2::aod::Hash<"JOIN"_h>, Ts...>; - Join(std::shared_ptr&& table, uint64_t offset = 0) - : base{std::move(table), offset} - { - if (this->tableSize() != 0) { - bindInternalIndicesTo(this); - } - } - Join(std::vector>&& tables, uint64_t offset = 0) - : base{ArrowHelpers::joinTables(std::move(tables), std::span{base::originalLabels}), offset} + Join(std::vector&& tables) + : base{ArrowHelpers::joinTables(std::move(tables))} { if (this->tableSize() != 0) { bindInternalIndicesTo(this); } } + using base::bindExternalIndices; using base::bindInternalIndicesTo; static constexpr const uint32_t binding_origin = base::binding_origin; @@ -3398,12 +3407,12 @@ struct Join : Table, o2::aod::Hash<"JOIN/0"_h>, o2::aod: auto rawSlice(uint64_t start, uint64_t end) const { - return self_t{{this->asArrowTable()->Slice(start, end - start + 1)}, start}; + return self_t{{this->asArrowTableRef().slice({start, static_cast(end - start + 1)})}}; } auto emptySlice() const { - return self_t{{this->asArrowTable()->Slice(0, 0)}, 0}; + return self_t{{this->asArrowTableRef().slice({0, 0})}}; } template @@ -3418,7 +3427,7 @@ struct Join : Table, o2::aod::Hash<"JOIN/0"_h>, o2::aod: template constexpr auto join(Ts const&... t) { - return Join(ArrowHelpers::joinTables({t.asArrowTable()...}, std::span{Join::base::originalLabels})); + return Join({ArrowHelpers::joinTables({t.asArrowTableRef()...}, std::span{Join::base::originalLabels})}); } template @@ -3428,15 +3437,26 @@ template struct Concat : Table, o2::aod::Hash<"CONC/0"_h>, o2::aod::Hash<"CONC"_h>, Ts...> { using base = Table, o2::aod::Hash<"CONC/0"_h>, o2::aod::Hash<"CONC"_h>, Ts...>; using self_t = Concat; - Concat(std::vector>&& tables, uint64_t offset = 0) - : base{ArrowHelpers::concatTables(std::move(tables)), offset} + + Concat(ArrowTableRef table) + : base{table} { bindInternalIndicesTo(this); } - Concat(Ts const&... t, uint64_t offset = 0) - : base{ArrowHelpers::concatTables({t.asArrowTable()...}), offset} + + Concat(std::shared_ptr table) + : Concat{ArrowTableRef{table}} + { + } + + Concat(std::vector&& tables) + : Concat{ArrowHelpers::concatTables(std::move(tables))} + { + } + + Concat(Ts const&... t) + : Concat{ArrowHelpers::concatTables({t.asArrowTableRef()...})} { - bindInternalIndicesTo(this); } using base::originals; @@ -3462,6 +3482,9 @@ constexpr auto concat(Ts const&... t) return Concat{t...}; } +template +concept is_a_selection = std::same_as, gandiva::Selection> || std::same_as, SelectionVector> || std::same_as, std::span>; + template class FilteredBase : public T { @@ -3491,34 +3514,10 @@ class FilteredBase : public T using unfiltered_iterator = T::template iterator_template_o; using const_iterator = iterator; - FilteredBase(std::vector>&& tables, gandiva::Selection const& selection, uint64_t offset = 0) - : T{std::move(tables), offset}, - mSelectedRows{getSpan(selection)} - { - if (this->tableSize() != 0) { - mFilteredBegin = table_t::filtered_begin(mSelectedRows); - } - resetRanges(); - mFilteredBegin.bindInternalIndices(this); - } - - FilteredBase(std::vector>&& tables, SelectionVector&& selection, uint64_t offset = 0) - : T{std::move(tables), offset}, - mSelectedRowsCache{std::move(selection)}, - mCached{true} - { - mSelectedRows = std::span{mSelectedRowsCache}; - if (this->tableSize() != 0) { - mFilteredBegin = table_t::filtered_begin(mSelectedRows); - } - resetRanges(); - mFilteredBegin.bindInternalIndices(this); - } - - FilteredBase(std::vector>&& tables, std::span const& selection, uint64_t offset = 0) - : T{std::move(tables), offset}, - mSelectedRows{selection} + FilteredBase(std::vector&& tables, is_a_selection auto selection) + : T{std::move(tables)} { + adoptSelection(selection); if (this->tableSize() != 0) { mFilteredBegin = table_t::filtered_begin(mSelectedRows); } @@ -3570,7 +3569,7 @@ class FilteredBase : public T [[nodiscard]] int64_t tableSize() const { - return table_t::asArrowTable()->num_rows(); + return this->asArrowTableRef().range.size; } auto const& getSelectedRows() const @@ -3583,12 +3582,12 @@ class FilteredBase : public T SelectionVector newSelection; newSelection.resize(static_cast(end - start + 1)); std::iota(newSelection.begin(), newSelection.end(), start); - return self_t{{this->asArrowTable()}, std::move(newSelection), 0}; + return self_t{{this->asArrowTableRef()}, std::move(newSelection)}; } auto emptySlice() const { - return self_t{{this->asArrowTable()}, SelectionVector{}, 0}; + return self_t{{this->asArrowTableRef()}, SelectionVector{}}; } static inline auto getSpan(gandiva::Selection const& sel) @@ -3671,41 +3670,21 @@ class FilteredBase : public T return static_cast(std::distance(mSelectedRows.begin(), locate)); } - void sumWithSelection(SelectionVector const& selection) - { - mCached = true; - SelectionVector rowsUnion; - std::set_union(mSelectedRows.begin(), mSelectedRows.end(), selection.begin(), selection.end(), std::back_inserter(rowsUnion)); - mSelectedRowsCache.clear(); - mSelectedRowsCache = rowsUnion; - resetRanges(); - } - - void intersectWithSelection(SelectionVector const& selection) - { - mCached = true; - SelectionVector intersection; - std::set_intersection(mSelectedRows.begin(), mSelectedRows.end(), selection.begin(), selection.end(), std::back_inserter(intersection)); - mSelectedRowsCache.clear(); - mSelectedRowsCache = intersection; - resetRanges(); - } - - void sumWithSelection(std::span const& selection) + void sumWithSelection(is_a_selection auto selection) { mCached = true; SelectionVector rowsUnion; - std::set_union(mSelectedRows.begin(), mSelectedRows.end(), selection.begin(), selection.end(), std::back_inserter(rowsUnion)); + std::ranges::set_union(mSelectedRows, selection, std::back_inserter(rowsUnion)); mSelectedRowsCache.clear(); mSelectedRowsCache = rowsUnion; resetRanges(); } - void intersectWithSelection(std::span const& selection) + void intersectWithSelection(is_a_selection auto selection) { mCached = true; SelectionVector intersection; - std::set_intersection(mSelectedRows.begin(), mSelectedRows.end(), selection.begin(), selection.end(), std::back_inserter(intersection)); + std::ranges::set_intersection(mSelectedRows, selection, std::back_inserter(intersection)); mSelectedRowsCache.clear(); mSelectedRowsCache = intersection; resetRanges(); @@ -3730,6 +3709,36 @@ class FilteredBase : public T } } + template + inline void adoptSelection(S) + { + } + + template + requires(std::same_as, gandiva::Selection>) + inline void adoptSelection(S selection) + { + mSelectedRows = getSpan(selection); + mCached = false; + } + + template + requires(std::same_as, SelectionVector>) + inline void adoptSelection(S selection) + { + mSelectedRowsCache = std::move(selection); + mSelectedRows = std::span{mSelectedRowsCache}; + mCached = true; + } + + template + requires(std::same_as, std::span>) + inline void adoptSelection(S selection) + { + mSelectedRows = selection; + mCached = false; + } + std::span mSelectedRows; SelectionVector mSelectedRowsCache; bool mCached = false; @@ -3760,23 +3769,10 @@ class Filtered : public FilteredBase return const_iterator(this->cached_begin()); } - Filtered(std::vector>&& tables, gandiva::Selection const& selection, uint64_t offset = 0) - : FilteredBase(std::move(tables), selection, offset) {} - - Filtered(std::vector>&& tables, SelectionVector&& selection, uint64_t offset = 0) - : FilteredBase(std::move(tables), std::forward(selection), offset) {} + Filtered(std::vector&& tables, is_a_selection auto selection) + : FilteredBase{std::move(tables), std::forward(selection)} {} - Filtered(std::vector>&& tables, std::span const& selection, uint64_t offset = 0) - : FilteredBase(std::move(tables), selection, offset) {} - - Filtered operator+(SelectionVector const& selection) - { - Filtered copy(*this); - copy.sumWithSelection(selection); - return copy; - } - - Filtered operator+(std::span const& selection) + Filtered operator+(is_a_selection auto selection) { Filtered copy(*this); copy.sumWithSelection(selection); @@ -3788,13 +3784,7 @@ class Filtered : public FilteredBase return operator+(other.getSelectedRows()); } - Filtered operator+=(SelectionVector const& selection) - { - this->sumWithSelection(selection); - return *this; - } - - Filtered operator+=(std::span const& selection) + Filtered operator+=(is_a_selection auto selection) { this->sumWithSelection(selection); return *this; @@ -3805,14 +3795,7 @@ class Filtered : public FilteredBase return operator+=(other.getSelectedRows()); } - Filtered operator*(SelectionVector const& selection) - { - Filtered copy(*this); - copy.intersectWithSelection(selection); - return copy; - } - - Filtered operator*(std::span const& selection) + Filtered operator*(is_a_selection auto selection) { Filtered copy(*this); copy.intersectWithSelection(selection); @@ -3824,13 +3807,7 @@ class Filtered : public FilteredBase return operator*(other.getSelectedRows()); } - Filtered operator*=(SelectionVector const& selection) - { - this->intersectWithSelection(selection); - return *this; - } - - Filtered operator*=(std::span const& selection) + Filtered operator*=(is_a_selection auto selection) { this->intersectWithSelection(selection); return *this; @@ -3855,12 +3832,12 @@ class Filtered : public FilteredBase SelectionVector newSelection; newSelection.resize(static_cast(end - start + 1)); std::iota(newSelection.begin(), newSelection.end(), start); - return self_t{{this->asArrowTable()}, std::move(newSelection), 0}; + return self_t{{this->asArrowTableRef()}, std::move(newSelection)}; } auto emptySlice() const { - return self_t{{this->asArrowTable()}, SelectionVector{}, 0}; + return self_t{{this->asArrowTableRef()}, SelectionVector{}}; } template @@ -3922,38 +3899,15 @@ class Filtered> : public FilteredBase return const_iterator(this->cached_begin()); } - Filtered(std::vector>&& tables, gandiva::Selection const& selection, uint64_t offset = 0) - : FilteredBase(std::move(extractTablesFromFiltered(tables)), selection, offset) + Filtered(std::vector>&& tables, is_a_selection auto selection) + : FilteredBase(std::move(extractTablesFromFiltered(tables)), std::forward(selection)) { for (auto& table : tables) { *this *= table; } } - Filtered(std::vector>&& tables, SelectionVector&& selection, uint64_t offset = 0) - : FilteredBase(std::move(extractTablesFromFiltered(tables)), std::forward(selection), offset) - { - for (auto& table : tables) { - *this *= table; - } - } - - Filtered(std::vector>&& tables, std::span const& selection, uint64_t offset = 0) - : FilteredBase(std::move(extractTablesFromFiltered(tables)), selection, offset) - { - for (auto& table : tables) { - *this *= table; - } - } - - Filtered> operator+(SelectionVector const& selection) - { - Filtered> copy(*this); - copy.sumWithSelection(selection); - return copy; - } - - Filtered> operator+(std::span const& selection) + Filtered> operator+(is_a_selection auto selection) { Filtered> copy(*this); copy.sumWithSelection(selection); @@ -3965,13 +3919,7 @@ class Filtered> : public FilteredBase return operator+(other.getSelectedRows()); } - Filtered> operator+=(SelectionVector const& selection) - { - this->sumWithSelection(selection); - return *this; - } - - Filtered> operator+=(std::span const& selection) + Filtered> operator+=(is_a_selection auto selection) { this->sumWithSelection(selection); return *this; @@ -3982,14 +3930,7 @@ class Filtered> : public FilteredBase return operator+=(other.getSelectedRows()); } - Filtered> operator*(SelectionVector const& selection) - { - Filtered> copy(*this); - copy.intersectionWithSelection(selection); - return copy; - } - - Filtered> operator*(std::span const& selection) + Filtered> operator*(is_a_selection auto selection) { Filtered> copy(*this); copy.intersectionWithSelection(selection); @@ -4001,13 +3942,7 @@ class Filtered> : public FilteredBase return operator*(other.getSelectedRows()); } - Filtered> operator*=(SelectionVector const& selection) - { - this->intersectWithSelection(selection); - return *this; - } - - Filtered> operator*=(std::span const& selection) + Filtered> operator*=(is_a_selection auto selection) { this->intersectWithSelection(selection); return *this; @@ -4030,12 +3965,12 @@ class Filtered> : public FilteredBase SelectionVector newSelection; newSelection.resize(static_cast(end - start + 1)); std::iota(newSelection.begin(), newSelection.end(), start); - return self_t{{this->asArrowTable()}, std::move(newSelection), 0}; + return self_t{{this->asArrowTableRef()}, std::move(newSelection)}; } auto emptySlice() const { - return self_t{{this->asArrowTable()}, SelectionVector{}, 0}; + return self_t{{this->asArrowTableRef()}, SelectionVector{}}; } auto sliceByCached(framework::expressions::BindingNode const& node, int value, o2::framework::SliceCache& cache) const @@ -4061,11 +3996,11 @@ class Filtered> : public FilteredBase } private: - std::vector> extractTablesFromFiltered(std::vector>& tables) + std::vector extractTablesFromFiltered(std::vector>& tables) { - std::vector> outTables; + std::vector outTables; for (auto& table : tables) { - outTables.push_back(table.asArrowTable()); + outTables.push_back(table.asArrowTableRef()); } return outTables; } @@ -4101,15 +4036,13 @@ struct IndexTable : Table { ...); } - IndexTable(std::shared_ptr table, uint64_t offset = 0) - : base_t{table, offset} - { - } + IndexTable(ArrowTableRef table) + : base_t{table} {} - IndexTable(std::vector> tables, uint64_t offset = 0) - : base_t{tables[0], offset} - { - } + /// FIXME: this is a compatiblity for a generic constructor call with a vector + /// there has to be a safer way + IndexTable(std::vector&& tables) + : base_t{tables[0]} {} IndexTable(IndexTable const&) = default; IndexTable(IndexTable&&) = default; @@ -4126,14 +4059,9 @@ template struct SmallGroupsBase : public Filtered { static constexpr void isSmallGroups() {}; static constexpr bool applyFilters = APPLY; - SmallGroupsBase(std::vector>&& tables, gandiva::Selection const& selection, uint64_t offset = 0) - : Filtered(std::move(tables), selection, offset) {} - - SmallGroupsBase(std::vector>&& tables, SelectionVector&& selection, uint64_t offset = 0) - : Filtered(std::move(tables), std::forward(selection), offset) {} - SmallGroupsBase(std::vector>&& tables, std::span const& selection, uint64_t offset = 0) - : Filtered(std::move(tables), selection, offset) {} + SmallGroupsBase(std::vector&& tables, is_a_selection auto selection) + : Filtered(std::move(tables), selection) {} }; template diff --git a/Framework/Core/include/Framework/AnalysisHelpers.h b/Framework/Core/include/Framework/AnalysisHelpers.h index 96d89b722780b..e4d9682b35302 100644 --- a/Framework/Core/include/Framework/AnalysisHelpers.h +++ b/Framework/Core/include/Framework/AnalysisHelpers.h @@ -147,7 +147,7 @@ auto spawner(framework::pack, std::vector>&& if (fullTable->num_rows() == 0) { return makeEmptyTable(name, framework::pack{}); } - return spawnerHelper(fullTable, schema, sizeof...(C), projectors, name, projector); + return spawnerHelper(fullTable.tablePtr, schema, sizeof...(C), projectors, name, projector); } std::string serializeProjectors(std::vector& projectors); @@ -950,7 +950,7 @@ auto getTableFromFilter(soa::is_filtered_table auto const& table, soa::Selection auto getTableFromFilter(soa::is_not_filtered_table auto const& table, soa::SelectionVector&& selection) { - return std::make_unique>>(std::vector{table.asArrowTable()}, std::forward(selection)); + return std::make_unique>>(std::vector{table.asArrowTableRef()}, std::forward(selection)); } void initializePartitionCaches(std::set const& hashes, std::shared_ptr const& schema, expressions::Filter const& filter, gandiva::NodePtr& tree, gandiva::FilterPtr& gfilter); @@ -982,7 +982,7 @@ struct Partition { void bindTable(T const& table) { - intializeCaches(T::table_t::hashes(), table.asArrowTable()->schema()); + intializeCaches(T::table_t::hashes(), table.asArrowTableRef()->schema()); if (dataframeChanged) { mFiltered = getTableFromFilter(table, soa::selectionToVector(framework::expressions::createSelection(table.asArrowTable(), gfilter))); dataframeChanged = false; @@ -1086,7 +1086,7 @@ auto Extend(T const& table) static std::array projectors{{std::move(Cs::Projector())...}}; static std::shared_ptr projector = nullptr; static auto schema = std::make_shared(o2::soa::createFieldsFromColumns(framework::pack{})); - return output_t{{o2::framework::spawner(framework::pack{}, {table.asArrowTable()}, "dynamicExtension", projectors.data(), projector, schema), table.asArrowTable()}, 0}; + return output_t{{o2::framework::spawner(framework::pack{}, {table.asArrowTable()}, "dynamicExtension", projectors.data(), projector, schema), table.asArrowTable()}}; } /// Template function to attach dynamic columns on-the-fly (e.g. inside @@ -1095,7 +1095,7 @@ template auto Attach(T const& table) { using output_t = Join, o2::aod::Hash<"JOIN/0"_h>, o2::aod::Hash<"JOIN"_h>, Cs...>>; - return output_t{{table.asArrowTable()}, table.offset()}; + return output_t{{table.asArrowTableRef()}}; } } // namespace o2::soa diff --git a/Framework/Core/include/Framework/AnalysisManagers.h b/Framework/Core/include/Framework/AnalysisManagers.h index bb37fb9016c2f..8b426576ce556 100644 --- a/Framework/Core/include/Framework/AnalysisManagers.h +++ b/Framework/Core/include/Framework/AnalysisManagers.h @@ -319,12 +319,12 @@ bool prepareOutput(ProcessingContext& context, T& spawns) } using D = o2::aod::Hash; - spawns.extension = std::make_shared(o2::framework::spawner(originalTable, + spawns.extension = std::make_shared(o2::framework::spawner(originalTable.tablePtr, o2::aod::label(), spawns.projectors.data(), spawns.projector, spawns.schema)); - spawns.table = std::make_shared(soa::ArrowHelpers::joinTables({spawns.extension->asArrowTable(), originalTable}, std::span{T::spawnable_t::table_t::originalLabels})); + spawns.table = std::make_shared(soa::ArrowHelpers::joinTables({spawns.extension->asArrowTableRef(), originalTable}, std::span{T::spawnable_t::table_t::originalLabels})); return true; } @@ -348,12 +348,12 @@ bool prepareOutput(ProcessingContext& context, T& defines) } using D = o2::aod::Hash; - defines.extension = std::make_shared(o2::framework::spawner(originalTable, + defines.extension = std::make_shared(o2::framework::spawner(originalTable.tablePtr, o2::aod::label(), defines.projectors.data(), defines.projector, defines.schema)); - defines.table = std::make_shared(soa::ArrowHelpers::joinTables({defines.extension->asArrowTable(), originalTable}, std::span{T::spawnable_t::table_t::originalLabels})); + defines.table = std::make_shared(soa::ArrowHelpers::joinTables({defines.extension->asArrowTableRef(), originalTable}, std::span{T::spawnable_t::table_t::originalLabels})); return true; } @@ -380,12 +380,12 @@ bool prepareDelayedOutput(ProcessingContext& context, T& defines) } using D = o2::aod::Hash; - defines.extension = std::make_shared(o2::framework::spawner(originalTable, + defines.extension = std::make_shared(o2::framework::spawner(originalTable.tablePtr, o2::aod::label(), defines.projectors.data(), defines.projector, defines.schema)); - defines.table = std::make_shared(soa::ArrowHelpers::joinTables({defines.extension->asArrowTable(), originalTable}, std::span{T::spawnable_t::table_t::originalLabels})); + defines.table = std::make_shared(soa::ArrowHelpers::joinTables({defines.extension->asArrowTableRef(), originalTable}, std::span{T::spawnable_t::table_t::originalLabels})); return true; } diff --git a/Framework/Core/include/Framework/AnalysisTask.h b/Framework/Core/include/Framework/AnalysisTask.h index 3170236e18f09..e4e6c35d9a7e9 100644 --- a/Framework/Core/include/Framework/AnalysisTask.h +++ b/Framework/Core/include/Framework/AnalysisTask.h @@ -226,7 +226,7 @@ struct AnalysisDataProcessorBuilder { template static auto extractTablesFromRecord(InputRecord& record, R matchers) { - std::vector> tables; + std::vector tables; std::ranges::transform(matchers, std::back_inserter(tables), [&record](auto const& m) { return record.get(m.second)->asArrowTable(); }); @@ -248,8 +248,8 @@ struct AnalysisDataProcessorBuilder { template static auto extractFilteredFromRecord(InputRecord& record, R matchers, ExpressionInfo& info) { - std::shared_ptr table = soa::ArrowHelpers::joinTables(extractTablesFromRecord(record, matchers)); - expressions::updateFilterInfo(info, table); + auto table = soa::ArrowHelpers::joinTables(extractTablesFromRecord(record, matchers)); + expressions::updateFilterInfo(info, table.tablePtr); if constexpr (!o2::soa::is_smallgroups>) { if (info.selection == nullptr) { soa::missingFilterDeclaration(info.processHash, info.argumentIndex); diff --git a/Framework/Core/include/Framework/ArrowTypes.h b/Framework/Core/include/Framework/ArrowTypes.h index 2673472a81152..cd85db72315e2 100644 --- a/Framework/Core/include/Framework/ArrowTypes.h +++ b/Framework/Core/include/Framework/ArrowTypes.h @@ -11,12 +11,55 @@ #ifndef O2_FRAMEWORK_ARROWTYPES_H #define O2_FRAMEWORK_ARROWTYPES_H +#include #include "Framework/Traits.h" #include "arrow/type_fwd.h" #include namespace o2::soa { +struct ArrowRange { + uint64_t offset; + int64_t size; + + bool operator!=(ArrowRange const& other) const + { + return (offset != other.offset) && (size != other.size); + } +}; + +struct ArrowTableRef { + std::shared_ptr tablePtr = nullptr; + ArrowRange range{0, 0}; + + ArrowTableRef() = default; + ArrowTableRef(std::shared_ptr table) + : tablePtr{table}, + range{0, table->num_rows()} + { + } + ArrowTableRef(std::shared_ptr table, ArrowRange range_) + : tablePtr{table}, + range{range_} + { + } + + ArrowTableRef makeEmpty() const + { + return {tablePtr, {0, 0}}; + } + + ArrowTableRef slice(ArrowRange newRange) const + { + return {tablePtr, newRange}; + } + + std::shared_ptr const& operator->() const + { + return tablePtr; + } +}; + template struct arrow_array_for { }; diff --git a/Framework/Core/include/Framework/GroupSlicer.h b/Framework/Core/include/Framework/GroupSlicer.h index 74e5f16c1703f..f06cd6a0cd916 100644 --- a/Framework/Core/include/Framework/GroupSlicer.h +++ b/Framework/Core/include/Framework/GroupSlicer.h @@ -194,7 +194,7 @@ struct GroupSlicer { } } } - std::decay_t typedTable{{originalTable.asArrowTable()}, std::move(s)}; + std::decay_t typedTable{{originalTable.asArrowTableRef()}, std::move(s)}; typedTable.bindInternalIndicesTo(&originalTable); return typedTable; } @@ -218,16 +218,7 @@ struct GroupSlicer { auto oc = sliceInfos[index].getSliceFor(pos); uint64_t offset = oc.first; auto count = oc.second; - if (count == 0) { - // Empty group: avoid slicing every column only to discard it. Cache one - // empty (0-row) table per associated table and reuse it. This is the - // common case for sparse grouping (e.g. collisions with no candidates). - if (!emptyTables[index]) { - emptyTables[index] = originalTable.asArrowTable()->Slice(0, 0); - } - return std::decay_t{{emptyTables[index]}, soa::SelectionVector{}}; - } - auto groupedElementsTable = originalTable.asArrowTable()->Slice(offset, count); + auto groupedElementsTable = originalTable.asArrowTableRef().slice({offset, count}); // for each grouping element we need to slice the selection vector auto start_iterator = std::lower_bound(starts[index], selections[index]->end(), offset); @@ -239,7 +230,7 @@ struct GroupSlicer { return idx - static_cast(offset); }); - std::decay_t typedTable{{groupedElementsTable}, std::move(slicedSelection), offset}; + std::decay_t typedTable{{groupedElementsTable}, std::move(slicedSelection)}; typedTable.bindInternalIndicesTo(&originalTable); return typedTable; } @@ -281,9 +272,6 @@ struct GroupSlicer { std::span groupSelection; std::array const*, sizeof...(A)> selections; std::array::iterator, sizeof...(A)> starts; - // Cached empty (0-row) table per associated table, lazily built and reused - // for empty groups so we do not slice every column on each empty group. - std::array, sizeof...(A)> emptyTables{}; std::array sliceInfos; std::array sliceInfosUnsorted; diff --git a/Framework/Core/include/Framework/GroupedCombinations.h b/Framework/Core/include/Framework/GroupedCombinations.h index b0a6c9e658a10..d8c6aea44f31d 100644 --- a/Framework/Core/include/Framework/GroupedCombinations.h +++ b/Framework/Core/include/Framework/GroupedCombinations.h @@ -70,15 +70,15 @@ struct GroupedCombinationsGenerator { template GroupedIterator(const GroupingPolicy& groupingPolicy, const G& grouping, const std::tuple& associated, SliceCache* cache_) : GroupingPolicy(groupingPolicy), - mGrouping{std::make_shared(std::vector{grouping.asArrowTable()})}, + mGrouping{std::make_shared(std::vector{grouping.asArrowTableRef()})}, mAssociated{std::make_shared>(std::make_tuple(std::get(pack{})>(associated)...))}, mIndexColumns{getMatchingIndexNode()...}, cache{cache_} { if constexpr (soa::is_filtered_table>) { - mGrouping = std::make_shared(std::vector{grouping.asArrowTable()}, grouping.getSelectedRows()); + mGrouping = std::make_shared(std::vector{grouping.asArrowTableRef()}, grouping.getSelectedRows()); } else { - mGrouping = std::make_shared(std::vector{grouping.asArrowTable()}); + mGrouping = std::make_shared(std::vector{grouping.asArrowTableRef()}); } setMultipleGroupingTables(grouping); if (!this->mIsEnd) { @@ -94,9 +94,9 @@ struct GroupedCombinationsGenerator { void setTables(const G& grouping, const std::tuple& associated) { if constexpr (soa::is_filtered_table>) { - mGrouping = std::make_shared(std::vector{grouping.asArrowTable()}, grouping.getSelectedRows()); + mGrouping = std::make_shared(std::vector{grouping.asArrowTableRef()}, grouping.getSelectedRows()); } else { - mGrouping = std::make_shared(std::vector{grouping.asArrowTable()}); + mGrouping = std::make_shared(std::vector{grouping.asArrowTableRef()}); } mAssociated = std::make_shared>(std::make_tuple(std::get(pack{})>(associated)...)); setMultipleGroupingTables(grouping); diff --git a/Framework/Core/src/ASoA.cxx b/Framework/Core/src/ASoA.cxx index f565fa6e9ce47..486783c39d18a 100644 --- a/Framework/Core/src/ASoA.cxx +++ b/Framework/Core/src/ASoA.cxx @@ -69,21 +69,6 @@ SelectionVector sliceSelection(std::span const& mSelectedRows, in return slicedSelection; } -std::shared_ptr ArrowHelpers::joinTables(std::vector>&& tables) -{ - std::vector> fields; - std::vector> columns; - bool notEmpty = (tables[0]->num_rows() != 0); - std::ranges::for_each(tables, [&fields, &columns, notEmpty](auto const& t) { - std::ranges::copy(t->fields(), std::back_inserter(fields)); - if (notEmpty) { - std::ranges::copy(t->columns(), std::back_inserter(columns)); - } - }); - auto schema = std::make_shared(fields); - return arrow::Table::Make(schema, columns); -} - namespace { template @@ -109,53 +94,108 @@ void canNotJoin(std::vector> const& tables, std::s } } } -} // namespace -std::shared_ptr ArrowHelpers::joinTables(std::vector>&& tables, std::span labels) +template +void IncompatibleRanges(std::vector const& tables, std::span labels) +{ + auto loc = std::ranges::adjacent_find(tables, [](auto const& l, auto const& r) { return l.range != r.range; }); + if (loc != std::ranges::cend(tables)) { + auto pos = std::distance(tables.begin(), loc); + auto next = loc + 1; + if (labels.empty()) { + throw o2::framework::runtime_error_f("Incompatible ranges at %d: (%zu, %z) vs. (%zu, %z)", pos, loc->range.offset, loc->range.size, next->range.offset, next->range.size); + } else { + throw o2::framework::runtime_error_f("Incompatible ranges at %d between %s and %s: (%zu, %z) vs. (%zu, %z)", pos, makeString(labels[pos]), makeString(labels[pos + 1]), loc->range.offset, loc->range.size, next->range.offset, next->range.size); + } + } +} + +std::shared_ptr joinTablesImpl(std::ranges::input_range auto tables) +{ + std::vector> fields; + std::vector> columns; + bool notEmpty = (tables.front()->num_rows() != 0); + std::ranges::for_each(tables, [&fields, &columns, notEmpty](auto const& t) { + std::ranges::copy(t->fields(), std::back_inserter(fields)); + if (notEmpty) { + std::ranges::copy(t->columns(), std::back_inserter(columns)); + } + }); + auto schema = std::make_shared(fields); + return arrow::Table::Make(schema, columns); +} + +template +ArrowTableRef joinTablesImpl(std::ranges::input_range auto tables, std::span labels) { if (tables.size() == 1) { - return tables[0]; + return tables.front(); } + IncompatibleRanges(tables, labels); + ArrowRange commonRange{tables.front().range}; + return {joinTablesImpl(tables), commonRange}; +} +} // namespace + +o2::soa::ArrowTableRef ArrowHelpers::joinTables(std::vector>&& tables) +{ + std::vector refs; + std::ranges::transform(tables, std::back_inserter(refs), [](auto const& table) { return ArrowTableRef{table}; }); + return joinTablesImpl(refs, std::span()); +} + +o2::soa::ArrowTableRef ArrowHelpers::joinTables(std::vector&& tables) +{ + return joinTablesImpl(tables, std::span()); +} + +o2::soa::ArrowTableRef ArrowHelpers::joinTables(std::vector&& tables, std::span labels) +{ + return joinTablesImpl(tables, labels); +} + +o2::soa::ArrowTableRef ArrowHelpers::joinTables(std::vector&& tables, std::span labels) +{ + return joinTablesImpl(tables, labels); +} + +o2::soa::ArrowTableRef ArrowHelpers::joinTables(std::vector>&& tables, std::span labels) +{ canNotJoin(tables, labels); - return joinTables(std::forward>>(tables)); + return o2::soa::ArrowTableRef{joinTablesImpl(tables)}; } -std::shared_ptr ArrowHelpers::joinTables(std::vector>&& tables, std::span labels) +o2::soa::ArrowTableRef ArrowHelpers::joinTables(std::vector>&& tables, std::span labels) { - if (tables.size() == 1) { - return tables[0]; - } canNotJoin(tables, labels); - return joinTables(std::forward>>(tables)); + return o2::soa::ArrowTableRef{joinTablesImpl(tables)}; } -std::shared_ptr ArrowHelpers::concatTables(std::vector>&& tables) +o2::soa::ArrowTableRef ArrowHelpers::concatTables(std::vector&& tables) { if (tables.size() == 1) { - return tables[0]; + return tables.front(); } std::vector> columns; - std::vector> resultFields = tables[0]->schema()->fields(); + std::vector> resultFields = tables.front()->schema()->fields(); auto compareFields = [](std::shared_ptr const& f1, std::shared_ptr const& f2) { // Let's do this with stable sorting. return (!f1->Equals(f2)) && (f1->name() < f2->name()); }; - for (size_t i = 1; i < tables.size(); ++i) { - auto& fields = tables[i]->schema()->fields(); - std::vector> intersection; - std::set_intersection(resultFields.begin(), resultFields.end(), - fields.begin(), fields.end(), - std::back_inserter(intersection), compareFields); + for (auto i = 1; i < tables.size(); ++i) { + auto const& fields = tables[i]->fields(); + std::vector> intersection; + std::ranges::set_intersection(resultFields, fields, std::back_inserter(intersection), compareFields); resultFields.swap(intersection); } - for (auto& field : resultFields) { + for (auto const& field : resultFields) { arrow::ArrayVector chunks; - for (auto& table : tables) { + for (auto const& table : tables) { auto ci = table->schema()->GetFieldIndex(field->name()); if (ci == -1) { - throw std::runtime_error("Unable to find field " + field->name()); + throw framework::runtime_error_f("Unable to find field {}", field->name().c_str()); } auto column = table->column(ci); auto otherChunks = column->chunks(); @@ -164,15 +204,7 @@ std::shared_ptr ArrowHelpers::concatTables(std::vector(chunks)); } - return arrow::Table::Make(std::make_shared(resultFields), columns); -} - -// ASCII-only lowercase. Column labels are plain identifiers, so we deliberately -// avoid the locale-aware std::tolower: it goes through the C locale facet on -// every character and dominated getIndexFromLabel in profiles. -static constexpr char asciiToLower(char c) -{ - return (c >= 'A' && c <= 'Z') ? static_cast(c + 32) : c; + return {arrow::Table::Make(std::make_shared(resultFields), columns)}; } arrow::ChunkedArray* getIndexFromLabel(arrow::Table* table, std::string_view label) @@ -314,19 +346,10 @@ void PreslicePolicyGeneral::updateSliceInfo(SliceInfoUnsortedPtr&& si) sliceInfo = si; } -std::shared_ptr PreslicePolicySorted::getSliceFor(int value, std::shared_ptr const& input, uint64_t& offset) const +o2::soa::ArrowTableRef PreslicePolicySorted::getSliceFor(int value, o2::soa::ArrowTableRef const& input) const { auto [offset_, count] = this->sliceInfo.getSliceFor(value); - offset = static_cast(offset_); - if (count == 0) { - // Empty group: avoid slicing every column only to discard it. Cache one - // empty (0-row) table per input table and reuse it (see GroupSlicer). - if (emptySlice.first != input.get()) { - emptySlice = {input.get(), input->Slice(0, 0)}; - } - return emptySlice.second; - } - return input->Slice(offset_, count); + return input.slice({static_cast(offset_), count}); } std::span PreslicePolicyGeneral::getSliceFor(int value) const diff --git a/Framework/Core/src/AnalysisHelpers.cxx b/Framework/Core/src/AnalysisHelpers.cxx index 5e46ed86860e8..62229765ddbf8 100644 --- a/Framework/Core/src/AnalysisHelpers.cxx +++ b/Framework/Core/src/AnalysisHelpers.cxx @@ -207,7 +207,7 @@ std::shared_ptr Spawner::materialize(ProcessingContext& pc) const return arrow::Table::MakeEmpty(schema).ValueOrDie(); } - return spawnerHelper(fullTable, schema, binding.c_str(), schema->num_fields(), projector); + return spawnerHelper(fullTable.tablePtr, schema, binding.c_str(), schema->num_fields(), projector); } std::shared_ptr Builder::materialize(ProcessingContext& pc) diff --git a/Framework/Core/test/test_ASoA.cxx b/Framework/Core/test/test_ASoA.cxx index d96304bd3bce2..587348f8ffa12 100644 --- a/Framework/Core/test/test_ASoA.cxx +++ b/Framework/Core/test/test_ASoA.cxx @@ -106,7 +106,9 @@ TEST_CASE("TestTableIteration") auto i = ColumnIterator(table->column(0).get()); int64_t pos = 0; + uint64_t offset = 0; i.mCurrentPos = &pos; + i.mGlobalOffset = &offset; REQUIRE(*i == 0); pos++; REQUIRE(*i == 0); @@ -286,7 +288,7 @@ TEST_CASE("TestJoinedTables") REQUIRE(Test::contains()); REQUIRE(!Test::contains()); - Test tests{{tableX, tableY}, 0}; + Test tests{{tableX, tableY}}; REQUIRE(tests.contains()); REQUIRE(tests.contains()); @@ -308,7 +310,7 @@ TEST_CASE("TestJoinedTables") REQUIRE(15 == test.x() + test.y() + test.z()); } using TestMoreThanTwo = Join; - TestMoreThanTwo tests4{{tableX, tableY, tableZ}, 0}; + TestMoreThanTwo tests4{{tableX, tableY, tableZ}}; for (auto& test : tests4) { REQUIRE(15 == test.x() + test.y() + test.z()); } @@ -383,7 +385,7 @@ TEST_CASE("TestConcatTables") static_assert(std::same_as, o2::aod::test::Y, o2::aod::test::X, o2::aod::test::Z>>, "Bad nested join"); static_assert(std::same_as, o2::aod::test::X>>, "Bad intersection of columns"); - ConcatTest tests{tableA, tableB}; + ConcatTest tests{{tableA, tableB}}; REQUIRE(16 == tests.size()); for (auto& test : tests) { REQUIRE(test.index() == test.x()); @@ -428,7 +430,7 @@ TEST_CASE("TestConcatTables") gandiva::Selection selection_f = expressions::createSelection(tableA, testf); TestA testA{tableA}; - FilteredTest filtered{{testA.asArrowTable()}, selection_f}; + FilteredTest filtered{{testA.asArrowTableRef()}, selection_f}; REQUIRE(2 == filtered.size()); auto i = 0; @@ -451,7 +453,7 @@ TEST_CASE("TestConcatTables") selectionConcat->SetIndex(2, 10); selectionConcat->SetNumSlots(3); ConcatTest concatTest{tableA, tableB}; - FilteredConcatTest concatTestTable{{concatTest.asArrowTable()}, selectionConcat}; + FilteredConcatTest concatTestTable{{concatTest.asArrowTableRef()}, selectionConcat}; REQUIRE(3 == concatTestTable.size()); i = 0; @@ -480,8 +482,8 @@ TEST_CASE("TestConcatTables") selectionJoin->SetIndex(1, 2); selectionJoin->SetIndex(2, 4); selectionJoin->SetNumSlots(3); - JoinedTest testJoin{{tableA, tableC}, 0}; - FilteredJoinTest filteredJoin{{testJoin.asArrowTable()}, selectionJoin}; + JoinedTest testJoin{{tableA, tableC}}; + FilteredJoinTest filteredJoin{{testJoin.asArrowTableRef()}, selectionJoin}; i = 0; REQUIRE(filteredJoin.begin() != filteredJoin.end()); @@ -598,12 +600,12 @@ TEST_CASE("TestFilteredOperators") TestA testA{tableA}; auto s1 = expressions::createSelection(testA.asArrowTable(), f1); - FilteredTest filtered1{{testA.asArrowTable()}, s1}; + FilteredTest filtered1{{testA.asArrowTableRef()}, s1}; REQUIRE(4 == filtered1.size()); REQUIRE(filtered1.begin() != filtered1.end()); auto s2 = expressions::createSelection(testA.asArrowTable(), f2); - FilteredTest filtered2{{testA.asArrowTable()}, s2}; + FilteredTest filtered2{{testA.asArrowTableRef()}, s2}; REQUIRE(2 == filtered2.size()); REQUIRE(filtered2.begin() != filtered2.end()); @@ -631,7 +633,7 @@ TEST_CASE("TestFilteredOperators") expressions::Filter f3 = o2::aod::test::x < 3; auto s3 = expressions::createSelection(testA.asArrowTable(), f3); - FilteredTest filtered3{{testA.asArrowTable()}, s3}; + FilteredTest filtered3{{testA.asArrowTableRef()}, s3}; REQUIRE(3 == filtered3.size()); REQUIRE(filtered3.begin() != filtered3.end()); @@ -675,7 +677,7 @@ TEST_CASE("TestNestedFiltering") TestA testA{tableA}; auto s1 = expressions::createSelection(testA.asArrowTable(), f1); - FilteredTest filtered{{testA.asArrowTable()}, s1}; + FilteredTest filtered{{testA.asArrowTableRef()}, s1}; REQUIRE(4 == filtered.size()); REQUIRE(filtered.begin() != filtered.end()); @@ -718,7 +720,7 @@ TEST_CASE("TestEmptyTables") o2::aod::Infos i{iempty}; using PI = Join; - PI pi{{pempty, iempty}, 0}; + PI pi{{pempty, iempty}}; REQUIRE(pi.size() == 0); auto spawned = Extend(p); REQUIRE(spawned.size() == 0); @@ -772,7 +774,7 @@ TEST_CASE("TestIndexToFiltered") expressions::Filter flt = o2::aod::test::someBool == true; using Flt = o2::soa::Filtered; auto selection = expressions::createSelection(o.asArrowTable(), flt); - Flt f{{o.asArrowTable()}, selection}; + Flt f{{o.asArrowTableRef()}, selection}; r.bindExternalIndices(&f); auto it = r.begin(); it.moveByIndex(23); @@ -1084,7 +1086,7 @@ TEST_CASE("TestSelfIndexRecursion") } using FilteredPoints = o2::soa::Filtered; - FilteredPoints ffp({t1, t2}, {1, 2, 3}, 0); + FilteredPoints ffp({t1, t2}, SelectionVector{1, 2, 3}); ffp.bindInternalIndicesTo(&ffp); // Filter should not interfere with self-index and the binding should stay the same @@ -1252,6 +1254,63 @@ TEST_CASE("TestSliceByCachedMismatched") } } +TEST_CASE("TestSliceByCachedFiltered") +{ + TableBuilder b; + auto writer = b.cursor(); + for (auto i = 0; i < 20; ++i) { + writer(0, i, i % 3 == 0); + } + auto origins = b.finalize(); + o2::aod::Origints o{origins}; + + TableBuilder w; + auto writer_w = w.cursor(); + auto step = -1; + for (auto i = 0; i < 5 * 20; ++i) { + if (i % 5 == 0) { + ++step; + } + writer_w(0, step); + } + auto refs = w.finalize(); + o2::aod::References r{refs}; + + TableBuilder w2; + auto writer_w2 = w2.cursor(); + step = -1; + for (auto i = 0; i < 5 * 20; ++i) { + if (i % 3 == 0) { + ++step; + } + writer_w2(0, step); + } + auto refs2 = w2.finalize(); + o2::aod::OtherReferences r2{refs2}; + + using J = o2::soa::Join; + J rr{{refs, refs2}}; + + auto rrf = rr.select(o2::aod::test::altOrigintId > 2 && o2::aod::test::altOrigintId < 15); + + auto key = "fIndex" + o2::framework::cutString(o2::soa::getLabelFromType()) + "_alt"; + ArrowTableSlicingCache atscache({{o2::soa::getLabelFromTypeForKey(key), o2::soa::getMatcherFromTypeForKey(key), key}}); + auto s = atscache.updateCacheEntry(0, refs2); + SliceCache cache{&atscache}; + + for (auto& oi : o) { + auto cachedSlice = rrf.sliceByCached(o2::aod::test::altOrigintId, oi.globalIndex(), cache); + if (oi.globalIndex() <= 2 || oi.globalIndex() >= 15) { + CHECK(cachedSlice.size() == 0); + } else { + CHECK(cachedSlice.size() == 3); + } + for (auto& ri : cachedSlice) { + REQUIRE(ri.altOrigintId() == oi.globalIndex()); + } + } +} + TEST_CASE("TestIndexUnboundExceptions") { TableBuilder b; diff --git a/Framework/Core/test/test_ASoAHelpers.cxx b/Framework/Core/test/test_ASoAHelpers.cxx index c4d7f727aa295..701dc0bbced50 100644 --- a/Framework/Core/test/test_ASoAHelpers.cxx +++ b/Framework/Core/test/test_ASoAHelpers.cxx @@ -72,7 +72,7 @@ TEST_CASE("IteratorTuple") REQUIRE(*(static_cast(std::get<1>(maxOffset2)).getIterator().mCurrentPos) == 8); expressions::Filter filter = test::x > 3; - auto filtered = Filtered{{tests.asArrowTable()}, o2::framework::expressions::createSelection(tests.asArrowTable(), filter)}; + auto filtered = Filtered{{tests.asArrowTableRef()}, o2::framework::expressions::createSelection(tests.asArrowTable(), filter)}; std::tuple, Filtered> filteredTuple = std::make_tuple(filtered, filtered); auto it1 = std::get<0>(filteredTuple).begin(); @@ -164,7 +164,7 @@ TEST_CASE("CombinationsGeneratorConstruction") o2::framework::expressions::Filter filter = test::x > 3; auto s1 = o2::framework::expressions::createSelection(testsA.asArrowTable(), filter); - auto filtered = Filtered{{testsA.asArrowTable()}, s1}; + auto filtered = Filtered{{testsA.asArrowTableRef()}, s1}; CombinationsGenerator, Filtered>>::CombinationsIterator combItFiltered(CombinationsStrictlyUpperIndexPolicy(filtered, filtered)); REQUIRE(!(static_cast(std::get<0>(*(combItFiltered))).getIterator().mCurrentPos == nullptr)); diff --git a/Framework/Core/test/test_AnalysisDataModel.cxx b/Framework/Core/test/test_AnalysisDataModel.cxx index b8b9c161f0e07..ae0914a285110 100644 --- a/Framework/Core/test/test_AnalysisDataModel.cxx +++ b/Framework/Core/test/test_AnalysisDataModel.cxx @@ -49,9 +49,9 @@ TEST_CASE("TestJoinedTablesContains") using Test = o2::soa::Join; - Test tests{{tXY, tZD}, 0}; - REQUIRE(tests.asArrowTable()->num_columns() != 0); - REQUIRE(tests.asArrowTable()->num_columns() == + Test tests{{tXY, tZD}}; + REQUIRE(tests.asArrowTableRef()->num_columns() != 0); + REQUIRE(tests.asArrowTableRef()->num_columns() == tXY->num_columns() + tZD->num_columns()); auto tests2 = join(XY{tXY}, ZD{tZD}); static_assert(std::same_as, diff --git a/Framework/Core/test/test_AnalysisTask.cxx b/Framework/Core/test/test_AnalysisTask.cxx index f5d8c4c43bc38..cb710b9a3871c 100644 --- a/Framework/Core/test/test_AnalysisTask.cxx +++ b/Framework/Core/test/test_AnalysisTask.cxx @@ -314,7 +314,7 @@ TEST_CASE("TestPartitionIteration") expressions::Filter f1 = aod::test::x < 4.0f; auto selection = expressions::createSelection(testA.asArrowTable(), f1); - FilteredTest filtered{{testA.asArrowTable()}, o2::soa::selectionToVector(selection)}; + FilteredTest filtered{{testA.asArrowTableRef()}, o2::soa::selectionToVector(selection)}; PartitionFilteredTest p2 = aod::test::y > 9.0f; p2.bindTable(filtered); diff --git a/Framework/Core/test/test_Concepts.cxx b/Framework/Core/test/test_Concepts.cxx index ff5e0fa6200db..65703082519b6 100644 --- a/Framework/Core/test/test_Concepts.cxx +++ b/Framework/Core/test/test_Concepts.cxx @@ -121,7 +121,7 @@ TEST_CASE("IdentificationConcepts") REQUIRE(is_join); - auto tl = []() -> SmallGroups { return {std::vector>{}, SelectionVector{}, 0}; }; + auto tl = []() -> SmallGroups { return {{}, SelectionVector{}}; }; REQUIRE(is_smallgroups); // AnalysisHelpers diff --git a/Framework/Core/test/test_GroupSlicer.cxx b/Framework/Core/test/test_GroupSlicer.cxx index ee6878f23ff80..f282dcbc5c33b 100644 --- a/Framework/Core/test/test_GroupSlicer.cxx +++ b/Framework/Core/test/test_GroupSlicer.cxx @@ -195,8 +195,9 @@ TEST_CASE("GroupSlicerSeveralAssociated") {soa::getLabelFromType(), soa::getMatcherFromTypeForKey(key), key}, {soa::getLabelFromType(), soa::getMatcherFromTypeForKey(key), key}}); auto s = slices.updateCacheEntry(0, {trkTableX}); - s = slices.updateCacheEntry(1, {trkTableY}); - s = slices.updateCacheEntry(2, {trkTableZ}); + s &= slices.updateCacheEntry(1, {trkTableY}); + s &= slices.updateCacheEntry(2, {trkTableZ}); + REQUIRE(s.ok()); o2::framework::GroupSlicer g(e, tt, slices); auto count = 0; @@ -358,7 +359,7 @@ TEST_CASE("GroupSlicerMismatchedFilteredGroups") auto trkTable = builderT.finalize(); using FilteredEvents = soa::Filtered; soa::SelectionVector rows{2, 4, 10, 9, 15}; - FilteredEvents e{{evtTable}, {2, 4, 10, 9, 15}}; + FilteredEvents e{{{evtTable}}, soa::SelectionVector{2, 4, 10, 9, 15}}; aod::TrksX t{trkTable}; REQUIRE(e.size() == 5); REQUIRE(t.size() == 10 * (20 - 4)); @@ -419,7 +420,7 @@ TEST_CASE("GroupSlicerMismatchedUnsortedFilteredGroups") using FilteredEvents = soa::Filtered; soa::SelectionVector rows{2, 4, 10, 9, 15}; - FilteredEvents e{{evtTable}, {2, 4, 10, 9, 15}}; + FilteredEvents e{{evtTable}, soa::SelectionVector{2, 4, 10, 9, 15}}; soa::SmallGroups t{{trkTable}, std::move(sel)}; REQUIRE(e.size() == 5); @@ -631,9 +632,9 @@ TEST_CASE("EmptySliceables") TEST_CASE("ArrowDirectSlicing") { int counts[] = {5, 5, 5, 4, 1}; - int offsets[] = {0, 5, 10, 15, 19, 20}; + int const offsets[] = {0, 5, 10, 15, 19, 20}; int ids[] = {0, 1, 2, 3, 4}; - int sizes[] = {4, 1, 12, 5, 2}; + int const sizes[] = {4, 1, 12, 5, 2}; using BigE = soa::Join; @@ -683,34 +684,25 @@ TEST_CASE("ArrowDirectSlicing") REQUIRE(slices_vec[i]->length() == counts[i]); } - std::vector slices; - std::vector offsts; auto bk = Entry(soa::getLabelFromType(), soa::getMatcherFromTypeForKey("fID"), "fID"); ArrowTableSlicingCache cache({bk}); auto s = cache.updateCacheEntry(0, {evtTable}); + REQUIRE(s.ok()); auto lcache = cache.getCacheFor(bk); for (auto i = 0u; i < 5; ++i) { - auto [offset, count] = lcache.getSliceFor(i); - auto tbl = b_e.asArrowTable()->Slice(offset, count); - auto ca = tbl->GetColumnByName("fArr"); - auto cb = tbl->GetColumnByName("fBoo"); - auto cv = tbl->GetColumnByName("fLst"); - REQUIRE(ca->length() == counts[i]); - REQUIRE(cb->length() == counts[i]); - REQUIRE(cv->length() == counts[i]); - REQUIRE(ca->Equals(slices_array[i])); - REQUIRE(cb->Equals(slices_bool[i])); - REQUIRE(cv->Equals(slices_vec[i])); + auto [loffset, count] = lcache.getSliceFor(i); + auto tbl = b_e.asArrowTableRef().slice({static_cast(loffset), count}); + REQUIRE(tbl.range.size == counts[i]); } int j = 0u; for (auto i = 0u; i < 5; ++i) { - auto [offset, count] = lcache.getSliceFor(i); - auto tbl = BigE{{b_e.asArrowTable()->Slice(offset, count)}, static_cast(offset)}; + auto [loffset, count] = lcache.getSliceFor(i); + auto tbl = BigE{{b_e.asArrowTableRef().slice({static_cast(loffset), count})}}; REQUIRE(tbl.size() == counts[i]); for (auto& row : tbl) { REQUIRE(row.id() == ids[i]); - REQUIRE(row.boo() == (j % 2 == 0)); + CHECK(row.boo() == (j % 2 == 0)); auto rid = row.globalIndex(); auto arr = row.arr(); REQUIRE(arr[0] == 0.1f * (float)rid); @@ -729,7 +721,7 @@ TEST_CASE("ArrowDirectSlicing") TEST_CASE("TestSlicingException") { - int offsets[] = {0, 5, 10, 15, 19, 20}; + int const offsets[] = {0, 5, 10, 15, 19, 20}; int ids[] = {0, 1, 2, 4, 3}; TableBuilder builderE; diff --git a/Framework/Core/test/test_TableSpawner.cxx b/Framework/Core/test/test_TableSpawner.cxx index e200adf37ccb4..d5bd4c83068ac 100644 --- a/Framework/Core/test/test_TableSpawner.cxx +++ b/Framework/Core/test/test_TableSpawner.cxx @@ -53,7 +53,7 @@ TEST_CASE("TestTableSpawner") auto expoints_a = o2::soa::Extend(st1); Spawns s; auto extension = ExPointsExtension{o2::framework::spawner>(t1, o2::aod::Hash<"ExPoints"_h>::str, s.projectors.data(), s.projector, s.schema)}; - auto expoints = ExPoints{{t1, extension.asArrowTable()}, 0}; + auto expoints = ExPoints{{t1, extension.asArrowTable()}}; REQUIRE(expoints_a.size() == 9); REQUIRE(extension.size() == 9); @@ -81,7 +81,7 @@ TEST_CASE("TestTableSpawner") excpts.projectors[0] = test::x * test::x + test::y * test::y + test::z * test::z; auto extension_2 = ExcPointsCfgExtension{o2::framework::spawner>({t1}, o2::aod::Hash<"ExcPoints"_h>::str, excpts.projectors.data(), excpts.projector, excpts.schema)}; - auto excpoints = ExcPoints{{t1, extension_2.asArrowTable()}, 0}; + auto excpoints = ExcPoints{{t1, extension_2.asArrowTable()}}; rex = extension.begin(); auto rex_2 = extension_2.begin(); From 2121de315daeaedd7042554ad7374917aaa4ab38 Mon Sep 17 00:00:00 2001 From: ddobrigk Date: Wed, 22 Jul 2026 19:06:15 +0200 Subject: [PATCH 25/47] Data model fix: avoid classifying TPC loopers as physical primary (#15609) * Transparent fix proposal draft 1 * Please consider the following formatting changes * Fix whitespace * Address only the bug * Adjustments: fix bugs, tuning * Improve name of ProtectedFlags * Please consider the following formatting changes --------- Co-authored-by: ALICE Action Bot --- .../include/Framework/AnalysisDataModel.h | 46 +++++++++++++------ Framework/Core/include/Framework/DataTypes.h | 17 +++++++ 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/Framework/Core/include/Framework/AnalysisDataModel.h b/Framework/Core/include/Framework/AnalysisDataModel.h index c8dd33fba62ee..5478f12a27424 100644 --- a/Framework/Core/include/Framework/AnalysisDataModel.h +++ b/Framework/Core/include/Framework/AnalysisDataModel.h @@ -1984,34 +1984,52 @@ DECLARE_SOA_EXPRESSION_COLUMN(Y, y, float, //! Particle rapidity, conditionally (aod::mcparticle::e - aod::mcparticle::pz)))); } // namespace mcparticle +namespace mcparticle_v2 +{ +// for improved getters with protection against incorrect physical primary tagging +// note: this has to be declared in a separate namespace so it does not conflict with existing +// derived data table declarations in O2Physics +DECLARE_SOA_DYNAMIC_COLUMN(IsPhysicalPrimary, isPhysicalPrimary, //! True if particle is considered a physical primary according to the ALICE definition + [](uint8_t input_flags, float vx, float vy) -> bool { return (o2::aod::mcparticle::Tools::removeIsPhysicalPrimaryBit(input_flags, vx, vy) & o2::aod::mcparticle::enums::PhysicalPrimary) == o2::aod::mcparticle::enums::PhysicalPrimary; }); + +// avoid that the stored flags are provided unprotected via +// the getter '.flags': analysers will get the correct bit map transparently +DECLARE_SOA_COLUMN(Flags, storedFlags, uint8_t); //! ALICE specific flags, see MCParticleFlags. Do not use directly. Use the dynamic columns, e.g. producedByGenerator() +DECLARE_SOA_DYNAMIC_COLUMN(ProtectedFlags, flags, //! protected against + [](uint8_t input_flags, float vx, float vy) -> uint8_t { return o2::aod::mcparticle::Tools::removeIsPhysicalPrimaryBit(input_flags, vx, vy); }); + +} // namespace mcparticle_v2 + DECLARE_SOA_TABLE_FULL(StoredMcParticles_000, "McParticles", "AOD", "MCPARTICLE", //! MC particle table, version 000 o2::soa::Index<>, mcparticle::McCollisionId, - mcparticle::PdgCode, mcparticle::StatusCode, mcparticle::Flags, + mcparticle::PdgCode, mcparticle::StatusCode, mcparticle_v2::Flags, mcparticle::Mother0Id, mcparticle::Mother1Id, mcparticle::Daughter0Id, mcparticle::Daughter1Id, mcparticle::Weight, mcparticle::Px, mcparticle::Py, mcparticle::Pz, mcparticle::E, mcparticle::Vx, mcparticle::Vy, mcparticle::Vz, mcparticle::Vt, mcparticle::PVector, - mcparticle::ProducedByGenerator, - mcparticle::FromBackgroundEvent, - mcparticle::GetGenStatusCode, - mcparticle::GetHepMCStatusCode, - mcparticle::GetProcess, - mcparticle::IsPhysicalPrimary); + mcparticle::ProducedByGenerator, + mcparticle::FromBackgroundEvent, + mcparticle::GetGenStatusCode, + mcparticle::GetHepMCStatusCode, + mcparticle::GetProcess, + mcparticle_v2::ProtectedFlags, + mcparticle_v2::IsPhysicalPrimary); DECLARE_SOA_TABLE_FULL_VERSIONED(StoredMcParticles_001, "McParticles", "AOD", "MCPARTICLE", 1, //! MC particle table, version 001 o2::soa::Index<>, mcparticle::McCollisionId, - mcparticle::PdgCode, mcparticle::StatusCode, mcparticle::Flags, + mcparticle::PdgCode, mcparticle::StatusCode, mcparticle_v2::Flags, mcparticle::MothersIds, mcparticle::DaughtersIdSlice, mcparticle::Weight, mcparticle::Px, mcparticle::Py, mcparticle::Pz, mcparticle::E, mcparticle::Vx, mcparticle::Vy, mcparticle::Vz, mcparticle::Vt, mcparticle::PVector, - mcparticle::ProducedByGenerator, - mcparticle::FromBackgroundEvent, - mcparticle::GetGenStatusCode, - mcparticle::GetHepMCStatusCode, - mcparticle::GetProcess, - mcparticle::IsPhysicalPrimary); + mcparticle::ProducedByGenerator, + mcparticle::FromBackgroundEvent, + mcparticle::GetGenStatusCode, + mcparticle::GetHepMCStatusCode, + mcparticle::GetProcess, + mcparticle_v2::ProtectedFlags, + mcparticle_v2::IsPhysicalPrimary); DECLARE_SOA_EXTENDED_TABLE(McParticles_000, StoredMcParticles_000, "EXMCPARTICLE", 0, //! Basic MC particle properties mcparticle::Phi, diff --git a/Framework/Core/include/Framework/DataTypes.h b/Framework/Core/include/Framework/DataTypes.h index 3d49d6d3c03d0..98a40e8f561b0 100644 --- a/Framework/Core/include/Framework/DataTypes.h +++ b/Framework/Core/include/Framework/DataTypes.h @@ -13,6 +13,7 @@ #include "CommonConstants/LHCConstants.h" +#include #include #include #include @@ -158,6 +159,22 @@ enum MCParticleFlags : uint8_t { }; } // namespace o2::aod::mcparticle::enums +namespace o2::aod::mcparticle +{ +constexpr float maxRadiusForPhysicalPrimary{5.f}; + +struct Tools { + // trivial helper to remove Physical Primary bit + static uint8_t removeIsPhysicalPrimaryBit(uint8_t input_flags, float vx, float vy) + { + if ((std::hypot(vx, vy) > o2::aod::mcparticle::maxRadiusForPhysicalPrimary) && ((input_flags & o2::aod::mcparticle::enums::PhysicalPrimary) == o2::aod::mcparticle::enums::PhysicalPrimary)) { + input_flags = input_flags & ~enums::PhysicalPrimary; // remove physical primary bit, keep others + } + return input_flags; + } +}; +} // namespace o2::aod::mcparticle + namespace o2::aod::run2 { enum Run2EventSelectionCut { From 06158bae44c607cf6c570d24cf58a910bc799744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Jacazio?= Date: Wed, 22 Jul 2026 20:33:36 +0200 Subject: [PATCH 26/47] [ALICE3] Remove ACTS tracker from CMakeLists until there (#15622) Removed conditional source file inclusion for TrackerACTS. --- .../ALICE3/GlobalReconstruction/reconstruction/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/Detectors/Upgrades/ALICE3/GlobalReconstruction/reconstruction/CMakeLists.txt b/Detectors/Upgrades/ALICE3/GlobalReconstruction/reconstruction/CMakeLists.txt index 1dfcb7a22f725..9cc7222d413e7 100644 --- a/Detectors/Upgrades/ALICE3/GlobalReconstruction/reconstruction/CMakeLists.txt +++ b/Detectors/Upgrades/ALICE3/GlobalReconstruction/reconstruction/CMakeLists.txt @@ -16,7 +16,6 @@ endif() o2_add_library(ALICE3GlobalReconstruction TARGETVARNAME targetName SOURCES src/TimeFrame.cxx - $<$:src/TrackerACTS.cxx> PUBLIC_LINK_LIBRARIES O2::ITStracking O2::GPUCommon From 7f4b3dd3a5b6b6a66ad889ca4f7562b8e73865d5 Mon Sep 17 00:00:00 2001 From: Anton Alkin Date: Thu, 23 Jul 2026 10:12:44 +0200 Subject: [PATCH 27/47] DPL Analysis: cddb tables using FairMQ side channel metadata (#15516) --- .../CCDBSupport/src/AnalysisCCDBHelpers.cxx | 87 ++++-- .../CCDBSupport/src/CCDBFetcherHelper.cxx | 10 +- Framework/Core/include/Framework/ASoA.h | 93 ++++++- .../include/Framework/AnalysisDataModel.h | 5 + .../Core/include/Framework/AnalysisTask.h | 248 +++++++++++------- Framework/Core/include/Framework/ArrowTypes.h | 5 + .../Core/include/Framework/DataAllocator.h | 15 +- .../include/Framework/FairMQDeviceProxy.h | 11 + Framework/Core/src/FairMQDeviceProxy.cxx | 21 ++ 9 files changed, 369 insertions(+), 126 deletions(-) diff --git a/Framework/CCDBSupport/src/AnalysisCCDBHelpers.cxx b/Framework/CCDBSupport/src/AnalysisCCDBHelpers.cxx index 21fdae4a57760..0b554a9dfdd7f 100644 --- a/Framework/CCDBSupport/src/AnalysisCCDBHelpers.cxx +++ b/Framework/CCDBSupport/src/AnalysisCCDBHelpers.cxx @@ -11,6 +11,7 @@ #include "AnalysisCCDBHelpers.h" #include "CCDBFetcherHelper.h" +#include "Framework/ArrowTypes.h" #include "Framework/DataProcessingStats.h" #include "Framework/DeviceSpec.h" #include "Framework/TimingInfo.h" @@ -22,6 +23,7 @@ #include "Framework/DanglingEdgesContext.h" #include "Framework/ConfigContext.h" #include "Framework/ConfigParamsHelper.h" +#include #include #include #include @@ -109,20 +111,49 @@ AlgorithmSpec AnalysisCCDBHelpers::fetchFromCCDB(ConfigContext const& /*ctx*/) auto it = ccdbUrls.find(m.name); fieldMetadata->Append("url", it != ccdbUrls.end() ? it->second : m.defaultValue.asString()); auto columnName = m.name.substr(strlen("ccdb:")); +#if (FAIRMQ_VERSION_DEC >= 111000) + fields.emplace_back(std::make_shared(columnName, soa::asArrowDataType(), false, fieldMetadata)); +#else fields.emplace_back(std::make_shared(columnName, arrow::binary_view(), false, fieldMetadata)); +#endif } schemas.emplace_back(std::make_shared(fields, schemaMetadata)); } +#if (FAIRMQ_VERSION_DEC >= 111000) + std::vector>> allbuilders; +#else + std::vector>> allbuilders; +#endif + allbuilders.resize([&schemas]() { size_t size = 0; for (auto& schema : schemas) { size += schema->num_fields(); }; return size; }()); + auto* pool = arrow::default_memory_pool(); + + int idx = 0; + int sidx = 0; + for (auto const& schema : schemas) { + for (auto const& _ : schema->fields()) { +#if (FAIRMQ_VERSION_DEC >= 111000) + auto value_builder = std::make_shared(); + allbuilders[idx] = std::make_pair(sidx, std::make_shared(pool, std::move(value_builder), 3)); +#else + allbuilders[idx] = std::make_pair(sidx, std::make_shared()); +#endif + ++idx; + } + ++sidx; + } + std::shared_ptr helper = std::make_shared(); CCDBFetcherHelper::initialiseHelper(*helper, options); std::unordered_map bindings; fillValidRoutes(*helper, spec.outputs, bindings); - return adaptStateless([schemas, bindings, helper](InputRecord& inputs, DataTakingContext& dtc, DataAllocator& allocator, TimingInfo& timingInfo, DataProcessingStats& stats) { + return adaptStateless([schemas, bindings, helper, allbuilders](InputRecord& inputs, DataTakingContext& dtc, DataAllocator& allocator, TimingInfo& timingInfo, DataProcessingStats& stats) { O2_SIGNPOST_ID_GENERATE(sid, ccdb); O2_SIGNPOST_START(ccdb, sid, "fetchFromAnalysisCCDB", "Fetching CCDB objects for analysis%" PRIu64, (uint64_t)timingInfo.timeslice); - for (auto& schema : schemas) { + std::ranges::for_each(allbuilders, [](auto& builder) { builder.second->Reset(); }); + for (auto i = 0U; i < schemas.size(); ++i) { + auto& schema = schemas[i]; std::vector ops; auto inputBinding = *schema->metadata()->Get("sourceTable"); auto inputMatcher = DataSpecUtils::fromString(*schema->metadata()->Get("sourceMatcher")); @@ -134,20 +165,32 @@ AlgorithmSpec AnalysisCCDBHelpers::fetchFromCCDB(ConfigContext const& /*ctx*/) auto table = inputs.get(inputMatcher)->asArrowTable(); // FIXME: make the fTimestamp column configurable. auto timestampColumn = table->GetColumnByName("fTimestamp"); + auto reserveSize = timestampColumn->length(); O2_SIGNPOST_EVENT_EMIT_INFO(ccdb, sid, "fetchFromAnalysisCCDB", "There are %zu bindings available", bindings.size()); - for (auto& binding : bindings) { + for (auto const& binding : bindings) { O2_SIGNPOST_EVENT_EMIT_INFO(ccdb, sid, "fetchFromAnalysisCCDB", "* %{public}s: %d", binding.first.c_str(), binding.second); } int outputRouteIndex = bindings.at(outRouteDesc); auto& spec = helper->routes[outputRouteIndex].matcher; - std::vector> builders; - for (auto const& _ : schema->fields()) { - builders.emplace_back(std::make_shared()); + auto concrete = DataSpecUtils::asConcreteDataMatcher(spec); + Output output{concrete.origin, concrete.description, concrete.subSpec}; + auto builders = allbuilders | std::views::filter([&i](auto const& builder) { return builder.first == i; }); + unsigned int numBuilders = std::ranges::count_if(allbuilders, [&i](auto const& builder) { return builder.first == i; }); + arrow::Status status; + std::ranges::for_each(builders, [&status, &reserveSize](auto& builder) { + if (reserveSize > builder.second->capacity()) { + status &= builder.second->Reserve(reserveSize - builder.second->capacity()); + } + }); + if (!status.ok()) { + throw framework::runtime_error_f("Failed to reserve arrays: ", status.ToString().c_str()); } + std::vector lastIds(numBuilders, DataAllocator::CacheId{.value = -1, .handle = -1, .segment = -1}); + for (auto ci = 0; ci < timestampColumn->num_chunks(); ++ci) { std::shared_ptr chunk = timestampColumn->chunk(ci); auto const* timestamps = chunk->data()->GetValuesSafe(1); @@ -171,15 +214,30 @@ AlgorithmSpec AnalysisCCDBHelpers::fetchFromCCDB(ConfigContext const& /*ctx*/) O2_SIGNPOST_START(ccdb, sid, "handlingResponses", "Got %zu responses from server.", responses.size()); - if (builders.size() != responses.size()) { - LOGP(fatal, "Not enough responses (expected {}, found {})", builders.size(), responses.size()); + if (numBuilders != responses.size()) { + LOGP(fatal, "Not enough responses (expected {}, found {})", numBuilders, responses.size()); } arrow::Status result; - for (size_t bi = 0; bi < responses.size(); bi++) { - auto& builder = builders[bi]; + + int bi = 0; + for (auto& builder : builders) { auto& response = responses[bi]; + auto& lastId = lastIds[bi]; + if (response.id.value != lastId.value) { + lastId.value = response.id.value; + allocator.adoptFromCache(output, response.id, header::gSerializationMethodCCDB); + } +#if (FAIRMQ_VERSION_DEC >= 111000) + result &= builder.second->Append(); + auto* value_builder = dynamic_cast(builder.second->value_builder()); + result &= value_builder->Append(response.id.handle); + result &= value_builder->Append(response.id.segment); + result &= value_builder->Append(response.size); +#else char const* address = reinterpret_cast(response.id.value); - result &= builder->Append(std::string_view(address, response.size)); + result &= builder.second->Append(std::string_view(address, response.size)); +#endif + ++bi; } if (!result.ok()) { LOGP(fatal, "Error adding results from CCDB"); @@ -188,12 +246,9 @@ AlgorithmSpec AnalysisCCDBHelpers::fetchFromCCDB(ConfigContext const& /*ctx*/) } } arrow::ArrayVector arrays; - for (auto& builder : builders) { - arrays.push_back(*builder->Finish()); - } + std::ranges::for_each(builders, [&arrays](auto& builder) { arrays.push_back(*builder.second->Finish()); }); auto outTable = arrow::Table::Make(schema, arrays); - auto concrete = DataSpecUtils::asConcreteDataMatcher(spec); - allocator.adopt(Output{concrete.origin, concrete.description, concrete.subSpec}, outTable); + allocator.adopt(output, outTable); } stats.updateStats({(int)ProcessingStatsId::CCDB_CACHE_FETCHED_BYTES, DataProcessingStats::Op::Set, (int64_t)helper->totalFetchedBytes}); diff --git a/Framework/CCDBSupport/src/CCDBFetcherHelper.cxx b/Framework/CCDBSupport/src/CCDBFetcherHelper.cxx index 6fa5b88222ea3..e4a9556c59b17 100644 --- a/Framework/CCDBSupport/src/CCDBFetcherHelper.cxx +++ b/Framework/CCDBSupport/src/CCDBFetcherHelper.cxx @@ -167,7 +167,6 @@ auto CCDBFetcherHelper::populateCacheWith(std::shared_ptr con DataTakingContext& dtc, DataAllocator& allocator) -> std::vector { - int objCnt = -1; // We use the timeslice, so that we hook into the same interval as the rest of the // callback. static bool isOnline = isOnlineRun(dtc); @@ -175,10 +174,9 @@ auto CCDBFetcherHelper::populateCacheWith(std::shared_ptr con auto sid = _o2_signpost_id_t{(int64_t)timingInfo.timeslice}; O2_SIGNPOST_START(ccdb, sid, "populateCacheWith", "Starting to populate cache with CCDB objects"); std::vector responses; - for (auto& op : ops) { + for (auto const& op : ops) { int64_t timestampToUse = op.timestamp; O2_SIGNPOST_EVENT_EMIT(ccdb, sid, "populateCacheWith", "Fetching object for route %{public}s", DataSpecUtils::describe(op.spec).data()); - objCnt++; auto concrete = DataSpecUtils::asConcreteDataMatcher(op.spec); Output output{concrete.origin, concrete.description, concrete.subSpec}; auto&& v = allocator.makeVector(output); @@ -198,7 +196,7 @@ auto CCDBFetcherHelper::populateCacheWith(std::shared_ptr con concrete.origin.as(), concrete.description.as(), int(concrete.subSpec)); } } - for (auto m : op.metadata) { + for (auto const& m : op.metadata) { O2_SIGNPOST_EVENT_EMIT(ccdb, sid, "populateCacheWith", "Adding metadata %{public}s: %{public}s to the request", m.key.data(), m.value.data()); metadata[m.key] = m.value; } @@ -215,7 +213,7 @@ auto CCDBFetcherHelper::populateCacheWith(std::shared_ptr con uint64_t cachePopulatedAt = url2uuid->second.cachePopulatedAt; // If timestamp is before the time the element was cached or after the claimed validity, we need to check validity, again // when online. - bool cacheExpired = (validUntil <= timestampToUse) || (op.timestamp < cachePopulatedAt); + bool cacheExpired = (validUntil <= (uint64_t)timestampToUse) || ((uint64_t)op.timestamp < cachePopulatedAt); if (isOnline || cacheExpired) { if (!helper->useTFSlice) { checkValidity = chRate > 0 ? (std::abs(int(timingInfo.tfCounter - url2uuid->second.lastCheckedTF)) >= chRate) : (timingInfo.tfCounter % -chRate) == 0; @@ -291,7 +289,7 @@ auto CCDBFetcherHelper::populateCacheWith(std::shared_ptr con O2_SIGNPOST_EVENT_EMIT(ccdb, sid, "populateCacheWith", "Reusing %{public}s for %{public}s (DPL id %" PRIu64 ")", path.data(), headers["ETag"].data(), cacheId.value); helper->mapURL2UUID[path].cacheHit++; responses.emplace_back(Response{.id = cacheId, .size = helper->mapURL2UUID[path].size, .request = nullptr}); - allocator.adoptFromCache(output, cacheId, header::gSerializationMethodCCDB); + // allocator.adoptFromCache(output, cacheId, header::gSerializationMethodCCDB); // the outputBuffer was not used, can we destroy it? } O2_SIGNPOST_END(ccdb, sid, "populateCacheWith", "Finished populating cache with CCDB objects"); diff --git a/Framework/Core/include/Framework/ASoA.h b/Framework/Core/include/Framework/ASoA.h index 9e9b1d40f4008..c3918f23711b0 100644 --- a/Framework/Core/include/Framework/ASoA.h +++ b/Framework/Core/include/Framework/ASoA.h @@ -28,6 +28,7 @@ #include "Framework/ArrowTableSlicingCache.h" // IWYU pragma: export #include "Framework/SliceCache.h" // IWYU pragma: export #include "Framework/VariantHelpers.h" // IWYU pragma: export +#include #include #include // IWYU pragma: export #include // IWYU pragma: export @@ -40,9 +41,17 @@ #include #include // IWYU pragma: export +namespace fair::mq::shmem +{ +struct MetaHeader; +} + namespace o2::framework { using ListVector = std::vector>; +#if (FAIRMQ_VERSION_DEC >= 111000) +using PointerReconstructor = std::function; +#endif std::string cutString(std::string&& str); std::string strToUpper(std::string&& str); @@ -1016,6 +1025,9 @@ struct ColumnDataHolder { arrow::ChunkedArray* second; }; +template +concept needs_ptr_rec = C::needs_ptr_rec; + template struct TableIterator : IP, C... { public: @@ -1184,6 +1196,21 @@ struct TableIterator : IP, C... { { doSetCurrentInternal(internal_index_columns_t{}, table); } +#if (FAIRMQ_VERSION_DEC >= 111000) + void setPointerReconstructor(framework::PointerReconstructor const& pointerReconstructor) + { + [&pointerReconstructor, this](framework::pack) { + ([&pointerReconstructor, this]() { + if constexpr (needs_ptr_rec) { + if (pointerReconstructor) { + CC::ptrRec = &pointerReconstructor; + } + } + }.template operator()(), + ...); + }(all_columns{}); + } +#endif private: /// Helper to move at the end of columns which actually have an iterator. @@ -2158,7 +2185,12 @@ class Table { return self_t{mArrowTableRef.makeEmpty()}; } - +#if (FAIRMQ_VERSION_DEC >= 111000) + void setPointerReconstructor(framework::PointerReconstructor const& pointerReconstructor) + { + mBegin.setPointerReconstructor(pointerReconstructor); + } +#endif private: template arrow::ChunkedArray* lookupColumn() @@ -2340,6 +2372,57 @@ consteval static std::string_view namespace_prefix() }; \ [[maybe_unused]] static constexpr o2::framework::expressions::BindingNode _Getter_ { _Label_, _Name_::hash, o2::framework::expressions::selectArrowType<_Type_>() } +#if (FAIRMQ_VERSION_DEC >= 111000) +#define DECLARE_SOA_CCDB_COLUMN_FULL(_Name_, _Label_, _Getter_, _ConcreteType_, _CCDBQuery_) \ + struct _Name_ : o2::soa::Column { \ + static constexpr const char* mLabel = _Label_; \ + static constexpr const char* query = _CCDBQuery_; \ + static constexpr const uint32_t hash = crc32(namespace_prefix<_Name_>(), std::string_view{#_Getter_}); \ + static constexpr bool needs_ptr_rec = true; \ + std::function const* ptrRec = nullptr; \ + using base = o2::soa::Column; \ + using type = int64_t[3]; \ + using column_t = _Name_; \ + _Name_(arrow::ChunkedArray const* column) \ + : o2::soa::Column(o2::soa::ColumnIterator(column)) \ + { \ + } \ + \ + _Name_() = default; \ + _Name_(_Name_ const& other) = default; \ + _Name_& operator=(_Name_ const& other) = default; \ + \ + decltype(auto) _Getter_() const \ + { \ + auto& [handle, segment, size] = *mColumnIterator; \ + auto span = std::span{(*ptrRec)(fair::mq::shmem::MetaHeader{ \ + static_cast(size), \ + 0, handle, 0, 0, \ + static_cast(segment), true}), \ + static_cast(size)}; \ + if constexpr (std::same_as<_ConcreteType_, std::span>) { \ + return span; \ + } else { \ + static std::byte* payload = nullptr; \ + static _ConcreteType_* deserialised = nullptr; \ + static TClass* c = TClass::GetClass(#_ConcreteType_); \ + if (payload != (std::byte*)span.data()) { \ + payload = (std::byte*)span.data(); \ + delete deserialised; \ + TBufferFile f(TBufferFile::EMode::kRead, span.size(), (char*)span.data(), kFALSE); \ + deserialised = (_ConcreteType_*)soa::extractCCDBPayload((char*)payload, span.size(), c, "ccdb_object"); \ + } \ + return *deserialised; \ + } \ + } \ + \ + decltype(auto) \ + get() const \ + { \ + return _Getter_(); \ + } \ + }; +#else #define DECLARE_SOA_CCDB_COLUMN_FULL(_Name_, _Label_, _Getter_, _ConcreteType_, _CCDBQuery_) \ struct _Name_ : o2::soa::Column, _Name_> { \ static constexpr const char* mLabel = _Label_; \ @@ -2382,6 +2465,7 @@ consteval static std::string_view namespace_prefix() return _Getter_(); \ } \ }; +#endif #define DECLARE_SOA_CCDB_COLUMN(_Name_, _Getter_, _ConcreteType_, _CCDBQuery_) \ DECLARE_SOA_CCDB_COLUMN_FULL(_Name_, "f" #_Name_, _Getter_, _ConcreteType_, _CCDBQuery_) @@ -3694,7 +3778,12 @@ class FilteredBase : public T { return mCached; } - +#if (FAIRMQ_VERSION_DEC >= 111000) + void setPointerReconstructor(framework::PointerReconstructor const& pointerReconstructor) + { + mFilteredBegin.setPointerReconstructor(pointerReconstructor); + } +#endif private: void resetRanges() { diff --git a/Framework/Core/include/Framework/AnalysisDataModel.h b/Framework/Core/include/Framework/AnalysisDataModel.h index 5478f12a27424..40bc8f651098a 100644 --- a/Framework/Core/include/Framework/AnalysisDataModel.h +++ b/Framework/Core/include/Framework/AnalysisDataModel.h @@ -26,6 +26,11 @@ #include "SimulationDataFormat/MCGenProperties.h" #include "Framework/PID.h" +#include +#if (FAIRMQ_VERSION_DEC >= 111000) +#include +#endif + namespace o2 { namespace aod diff --git a/Framework/Core/include/Framework/AnalysisTask.h b/Framework/Core/include/Framework/AnalysisTask.h index e4e6c35d9a7e9..1e483d017cc88 100644 --- a/Framework/Core/include/Framework/AnalysisTask.h +++ b/Framework/Core/include/Framework/AnalysisTask.h @@ -26,6 +26,8 @@ #include "Framework/TypeIdHelpers.h" #include "Framework/ArrowTableSlicingCache.h" #include "Framework/AnalysisDataModel.h" +#include "Framework/DanglingEdgesContext.h" +#include #include #include @@ -302,11 +304,19 @@ struct AnalysisDataProcessorBuilder { } template +#if (FAIRMQ_VERSION_DEC >= 111000) + static void invokeProcess(Task& task, InputRecord& inputs, R matchers, PointerReconstructor const& pointerReconstructor, void (Task::*processingFunction)(Grouping, Associated...), std::vector& infos, ArrowTableSlicingCache& slices, header::DataOrigin newOrigin = header::DataOrigin{"AOD"}) +#else static void invokeProcess(Task& task, InputRecord& inputs, R matchers, void (Task::*processingFunction)(Grouping, Associated...), std::vector& infos, ArrowTableSlicingCache& slices, header::DataOrigin newOrigin = header::DataOrigin{"AOD"}) +#endif { using G = std::decay_t; auto groupingTable = AnalysisDataProcessorBuilder::bindGroupingTable(inputs, matchers, processingFunction, infos); - +#if (FAIRMQ_VERSION_DEC >= 111000) + if constexpr (!is_enumeration) { + groupingTable.setPointerReconstructor(pointerReconstructor); + } +#endif constexpr const int numElements = nested_brace_constructible_size>() / 10; // set filtered tables for partitions with grouping @@ -347,6 +357,12 @@ struct AnalysisDataProcessorBuilder { ...); }, associatedTables); +#if (FAIRMQ_VERSION_DEC >= 111000) + std::apply([&pointerReconstructor](auto&... table) { + (table.setPointerReconstructor(pointerReconstructor), ...); + }, + associatedTables); +#endif auto binder = [&task, &groupingTable, &associatedTables](auto& x) mutable { x.bindExternalIndices(&groupingTable, &std::get>(associatedTables)...); @@ -377,6 +393,12 @@ struct AnalysisDataProcessorBuilder { auto slicer = GroupSlicer(groupingTable, associatedTables, slices, newOrigin); for (auto& slice : slicer) { auto associatedSlices = slice.associatedTables(); +#if (FAIRMQ_VERSION_DEC >= 111000) + std::apply([&pointerReconstructor](auto&... table) { + (table.setPointerReconstructor(pointerReconstructor), ...); + }, + associatedSlices); +#endif overwriteInternalIndices(associatedSlices, associatedTables); std::apply( [&binder](auto&... x) mutable { @@ -580,118 +602,144 @@ DataProcessorSpec adaptAnalysisTask(ConfigContext const& ctx, Args&&... args) // replace origins in Preslice declarations homogeneous_apply_refs_sized([&newOrigin](auto& element) { return analysis_task_parsers::replaceOrigin(element, newOrigin); }, *task.get()); - auto algo = AlgorithmSpec::InitCallback{[task = task, expressionInfos, inputInfos, newOrigin, newOriginStr](InitContext& ic) mutable { - Cache bindingsKeys; - Cache bindingsKeysUnsorted; - // add preslice declarations to slicing cache definition - homogeneous_apply_refs_sized([&bindingsKeys, &bindingsKeysUnsorted](auto& element) { return analysis_task_parsers::registerCache(element, bindingsKeys, bindingsKeysUnsorted); }, *task.get()); - - homogeneous_apply_refs_sized([&ic](auto&& element) { return analysis_task_parsers::prepareOption(ic, element); }, *task.get()); - homogeneous_apply_refs_sized([&ic](auto&& element) { return analysis_task_parsers::prepareService(ic, element); }, *task.get()); - - auto& callbacks = ic.services().get(); - auto eoscb = [task](EndOfStreamContext& eosContext) { - homogeneous_apply_refs_sized([&eosContext](auto& element) { + auto algo = AlgorithmSpec::InitCallback + { + [task = task, expressionInfos, inputInfos, newOrigin, newOriginStr](InitContext& ic) mutable { + Cache bindingsKeys; + Cache bindingsKeysUnsorted; + // add preslice declarations to slicing cache definition + homogeneous_apply_refs_sized([&bindingsKeys, &bindingsKeysUnsorted](auto& element) { return analysis_task_parsers::registerCache(element, bindingsKeys, bindingsKeysUnsorted); }, *task.get()); + + homogeneous_apply_refs_sized([&ic](auto&& element) { return analysis_task_parsers::prepareOption(ic, element); }, *task.get()); + homogeneous_apply_refs_sized([&ic](auto&& element) { return analysis_task_parsers::prepareService(ic, element); }, *task.get()); + + auto& callbacks = ic.services().get(); + auto eoscb = [task](EndOfStreamContext& eosContext) { + homogeneous_apply_refs_sized([&eosContext](auto& element) { analysis_task_parsers::postRunService(eosContext, element); analysis_task_parsers::postRunOutput(eosContext, element); return true; }, - *task.get()); - eosContext.services().get().readyToQuit(QuitRequest::Me); - }; - - callbacks.set(eoscb); - - /// call the task's init() function first as it may manipulate the task's elements - if constexpr (requires { task->init(ic); }) { - task->init(ic); - } - - /// update configurables in filters and partitions - homogeneous_apply_refs_sized( - [&ic](auto& element) -> bool { return analysis_task_parsers::updatePlaceholders(ic, element); }, - *task.get()); - /// create expression trees for filters gandiva trees matched to schemas and store the pointers into expressionInfos - homogeneous_apply_refs_sized([&expressionInfos](auto& element) { - return analysis_task_parsers::createExpressionTrees(expressionInfos, element); - }, - *task.get()); + *task.get()); + eosContext.services().get().readyToQuit(QuitRequest::Me); + }; - /// parse process functions to enable requested grouping caches - note that at this state process configurables have their final values - if constexpr (requires { &T::process; }) { - AnalysisDataProcessorBuilder::cacheFromArgs(&T::process, true, bindingsKeys, bindingsKeysUnsorted); - } - homogeneous_apply_refs_sized( - [&bindingsKeys, &bindingsKeysUnsorted](auto& x) { - return AnalysisDataProcessorBuilder::requestCacheFromArgs(x, bindingsKeys, bindingsKeysUnsorted); - }, - *task.get()); + callbacks.set(eoscb); - /// replace origin in slicing caches - std::ranges::transform(bindingsKeys, bindingsKeys.begin(), [&newOrigin](Entry& entry) { - if ((entry.matcher.origin == header::DataOrigin{"AOD"}) && (newOrigin != header::DataOrigin{"AOD"})) { - entry.matcher = replaceOrigin(entry.matcher, newOrigin); + /// call the task's init() function first as it may manipulate the task's elements + if constexpr (requires { task->init(ic); }) { + task->init(ic); } - return entry; - }); - std::ranges::transform(bindingsKeysUnsorted, bindingsKeysUnsorted.begin(), [&newOrigin](Entry& entry) { - if ((entry.matcher.origin == header::DataOrigin{"AOD"}) && (newOrigin != header::DataOrigin{"AOD"})) { - entry.matcher = replaceOrigin(entry.matcher, newOrigin); - } - return entry; - }); - ic.services().get().setCaches(std::move(bindingsKeys)); - ic.services().get().setCachesUnsorted(std::move(bindingsKeysUnsorted)); - ic.services().get().setOrigin(newOrigin); - - return [task, expressionInfos, inputInfos, newOrigin](ProcessingContext& pc) mutable { - // load the ccdb object from their cache - homogeneous_apply_refs_sized([&pc](auto& element) { return analysis_task_parsers::newDataframeCondition(pc.inputs(), element); }, *task.get()); - // reset partitions once per dataframe - homogeneous_apply_refs_sized([](auto& element) { return analysis_task_parsers::newDataframePartition(element); }, *task.get()); - // reset selections for the next dataframe - std::ranges::for_each(expressionInfos, [](auto& info) { info.resetSelection = true; }); - // reset pre-slice for the next dataframe - auto& slices = pc.services().get(); - homogeneous_apply_refs_sized([&slices](auto& element) { - return analysis_task_parsers::updateSliceInfo(element, slices); + /// update configurables in filters and partitions + homogeneous_apply_refs_sized( + [&ic](auto& element) -> bool { return analysis_task_parsers::updatePlaceholders(ic, element); }, + *task.get()); + /// create expression trees for filters gandiva trees matched to schemas and store the pointers into expressionInfos + homogeneous_apply_refs_sized([&expressionInfos](auto& element) { + return analysis_task_parsers::createExpressionTrees(expressionInfos, element); }, - *(task.get())); - // initialize local caches - homogeneous_apply_refs_sized([&pc](auto& element) { return analysis_task_parsers::initializeCache(pc, element); }, *(task.get())); - // prepare outputs - homogeneous_apply_refs_sized([&pc](auto& element) { return analysis_task_parsers::prepareOutput(pc, element); }, *task.get()); - // execute run() - if constexpr (requires { task->run(pc); }) { - task->run(pc); - } - // execute process() + *task.get()); + + /// parse process functions to enable requested grouping caches - note that at this state process configurables have their final values if constexpr (requires { &T::process; }) { - auto loc = std::ranges::find_if(inputInfos, [](auto const& info) { return info.hash == o2::framework::TypeIdHelpers::uniqueId(); }); - auto matchers = loc == inputInfos.end() ? std::vector>{} : loc->matchers; - AnalysisDataProcessorBuilder::invokeProcess(*(task.get()), pc.inputs(), matchers, &T::process, expressionInfos, slices, newOrigin); + AnalysisDataProcessorBuilder::cacheFromArgs(&T::process, true, bindingsKeys, bindingsKeysUnsorted); } - // execute optional process() homogeneous_apply_refs_sized( - [&pc, &expressionInfos, &task, &slices, &inputInfos, &newOrigin](auto& x) { - if constexpr (is_process_configurable) { - if (x.value == true) { - auto loc = std::ranges::find_if(inputInfos, [](auto const& info) { return info.hash == o2::framework::TypeIdHelpers::uniqueId(); }); - auto matchers = loc == inputInfos.end() ? std::vector>{} : loc->matchers; - AnalysisDataProcessorBuilder::invokeProcess(*task.get(), pc.inputs(), matchers, x.process, expressionInfos, slices, newOrigin); - return true; - } - return false; - } - return false; + [&bindingsKeys, &bindingsKeysUnsorted](auto& x) { + return AnalysisDataProcessorBuilder::requestCacheFromArgs(x, bindingsKeys, bindingsKeysUnsorted); }, *task.get()); - // prepare delayed outputs - homogeneous_apply_refs_sized([&pc](auto& element) { return analysis_task_parsers::prepareDelayedOutput(pc, element); }, *task.get()); - // finalize outputs - homogeneous_apply_refs_sized([&pc](auto& element) { return analysis_task_parsers::finalizeOutput(pc, element); }, *task.get()); - }; - }}; + + /// replace origin in slicing caches + std::ranges::transform(bindingsKeys, bindingsKeys.begin(), [&newOrigin](Entry& entry) { + if ((entry.matcher.origin == header::DataOrigin{"AOD"}) && (newOrigin != header::DataOrigin{"AOD"})) { + entry.matcher = replaceOrigin(entry.matcher, newOrigin); + } + return entry; + }); + std::ranges::transform(bindingsKeysUnsorted, bindingsKeysUnsorted.begin(), [&newOrigin](Entry& entry) { + if ((entry.matcher.origin == header::DataOrigin{"AOD"}) && (newOrigin != header::DataOrigin{"AOD"})) { + entry.matcher = replaceOrigin(entry.matcher, newOrigin); + } + return entry; + }); + + ic.services().get().setCaches(std::move(bindingsKeys)); + ic.services().get().setCachesUnsorted(std::move(bindingsKeysUnsorted)); + ic.services().get().setOrigin(newOrigin); +#if (FAIRMQ_VERSION_DEC >= 111000) + PointerReconstructor pointerReconstructor(nullptr); + bool hasCCDBTables = !ic.services().get().requestedTIMs.empty(); + + return [task, expressionInfos, inputInfos, newOrigin, hasCCDBTables, pointerReconstructor](ProcessingContext& pc) mutable { + if (hasCCDBTables && (!pointerReconstructor)) { + auto& proxy = pc.services().get(); + auto& spec = pc.services().get().requestedTIMs.front(); + pointerReconstructor = proxy.getShmPointerReconstructor(spec, 0); + } +#else + return [task, expressionInfos, inputInfos, newOrigin](ProcessingContext& pc) mutable { +#endif + // load the ccdb object from their cache + homogeneous_apply_refs_sized([&pc](auto& element) { return analysis_task_parsers::newDataframeCondition(pc.inputs(), element); }, *task.get()); + // reset partitions once per dataframe + homogeneous_apply_refs_sized([](auto& element) { return analysis_task_parsers::newDataframePartition(element); }, *task.get()); + // reset selections for the next dataframe + std::ranges::for_each(expressionInfos, [](auto& info) { info.resetSelection = true; }); + // reset pre-slice for the next dataframe + auto& slices = pc.services().get(); + homogeneous_apply_refs_sized([&slices](auto& element) { + return analysis_task_parsers::updateSliceInfo(element, slices); + }, + *(task.get())); + // initialize local caches + homogeneous_apply_refs_sized([&pc](auto& element) { return analysis_task_parsers::initializeCache(pc, element); }, *(task.get())); + // prepare outputs + homogeneous_apply_refs_sized([&pc](auto& element) { return analysis_task_parsers::prepareOutput(pc, element); }, *task.get()); + // execute run() + if constexpr (requires { task->run(pc); }) { + task->run(pc); + } + // execute process() + if constexpr (requires { &T::process; }) { + auto loc = std::ranges::find_if(inputInfos, [](auto const& info) { return info.hash == o2::framework::TypeIdHelpers::uniqueId(); }); + auto matchers = loc == inputInfos.end() ? std::vector>{} : loc->matchers; +#if (FAIRMQ_VERSION_DEC >= 111000) + AnalysisDataProcessorBuilder::invokeProcess(*(task.get()), pc.inputs(), matchers, pointerReconstructor, &T::process, expressionInfos, slices, newOrigin); +#else + AnalysisDataProcessorBuilder::invokeProcess(*(task.get()), pc.inputs(), matchers, &T::process, expressionInfos, slices, newOrigin); +#endif + } + // execute optional process() + homogeneous_apply_refs_sized( +#if (FAIRMQ_VERSION_DEC >= 111000) + [&pc, &expressionInfos, &task, &slices, &inputInfos, &newOrigin, &pointerReconstructor](auto& x) { +#else + [&pc, &expressionInfos, &task, &slices, &inputInfos, &newOrigin](auto& x) { +#endif + if constexpr (is_process_configurable) { + if (x.value == true) { + auto loc = std::ranges::find_if(inputInfos, [](auto const& info) { return info.hash == o2::framework::TypeIdHelpers::uniqueId(); }); + auto matchers = loc == inputInfos.end() ? std::vector>{} : loc->matchers; +#if (FAIRMQ_VERSION_DEC >= 111000) + AnalysisDataProcessorBuilder::invokeProcess(*task.get(), pc.inputs(), matchers, pointerReconstructor, x.process, expressionInfos, slices, newOrigin); +#else + AnalysisDataProcessorBuilder::invokeProcess(*task.get(), pc.inputs(), matchers, x.process, expressionInfos, slices, newOrigin); +#endif + return true; + } + return false; + } + return false; + }, + *task.get()); + // prepare delayed outputs + homogeneous_apply_refs_sized([&pc](auto& element) { return analysis_task_parsers::prepareDelayedOutput(pc, element); }, *task.get()); + // finalize outputs + homogeneous_apply_refs_sized([&pc](auto& element) { return analysis_task_parsers::finalizeOutput(pc, element); }, *task.get()); + }; + } + }; return { name, diff --git a/Framework/Core/include/Framework/ArrowTypes.h b/Framework/Core/include/Framework/ArrowTypes.h index cd85db72315e2..6e0c18c42f873 100644 --- a/Framework/Core/include/Framework/ArrowTypes.h +++ b/Framework/Core/include/Framework/ArrowTypes.h @@ -136,6 +136,11 @@ struct arrow_array_for { using type = arrow::FixedSizeListArray; using value_type = int8_t; }; +template +struct arrow_array_for { + using type = arrow::FixedSizeListArray; + using value_type = int64_t; +}; #define ARROW_VECTOR_FOR(_type_) \ template <> \ diff --git a/Framework/Core/include/Framework/DataAllocator.h b/Framework/Core/include/Framework/DataAllocator.h index 8d0660dfbbaec..c3a0d58f0c9c8 100644 --- a/Framework/Core/include/Framework/DataAllocator.h +++ b/Framework/Core/include/Framework/DataAllocator.h @@ -42,6 +42,10 @@ // Do not change this for a full inclusion of fair::mq::Device. #include +#include +#if (FAIRMQ_VERSION_DEC >= 111000) +#include +#endif namespace arrow { @@ -520,6 +524,8 @@ class DataAllocator struct CacheId { int64_t value; + int64_t handle; + int64_t segment; }; enum struct CacheStrategy : int { @@ -531,7 +537,7 @@ class DataAllocator CacheId adoptContainer(const Output& /*spec*/, ContainerT& /*container*/, CacheStrategy /* cache = false */, o2::header::SerializationMethod /* method = header::gSerializationMethodNone*/) { static_assert(always_static_assert_v, "Container cannot be moved. Please make sure it is backed by a o2::pmr::FairMQMemoryResource"); - return {0}; + return {0, 0, 0}; } /// Adopt a PMR container. Notice that the container must be moveable and @@ -623,12 +629,17 @@ DataAllocator::CacheId DataAllocator::adoptContainer(const Output& spec, Contain payloadMessage->GetSize() // ); - CacheId cacheId{0}; // + CacheId cacheId{0, 0, 0}; // if (cache == CacheStrategy::Always) { // The message will be shallow cloned in the cache. Since the // clone is indistinguishable from the original, we can keep sending // the original. cacheId.value = context.addToCache(payloadMessage); +#if (FAIRMQ_VERSION_DEC >= 111000) + auto meta = dynamic_cast(payloadMessage.get())->GetMeta(); + cacheId.handle = meta.fHandle; + cacheId.segment = meta.fSegmentId; +#endif } context.add(std::move(headerMessage), std::move(payloadMessage), routeIndex); diff --git a/Framework/Core/include/Framework/FairMQDeviceProxy.h b/Framework/Core/include/Framework/FairMQDeviceProxy.h index dbdade465f09c..8ab93d83dc1c2 100644 --- a/Framework/Core/include/Framework/FairMQDeviceProxy.h +++ b/Framework/Core/include/Framework/FairMQDeviceProxy.h @@ -20,6 +20,10 @@ #include "Framework/InputRoute.h" #include "Framework/ForwardRoute.h" #include +#include +#if (FAIRMQ_VERSION_DEC >= 111000) +#include +#endif #include namespace o2::header @@ -29,6 +33,9 @@ struct DataHeader; namespace o2::framework { +#if (FAIRMQ_VERSION_DEC >= 111000) +using PointerReconstructor = std::function; +#endif /// Helper class to hide fair::mq::Device headers in the DataAllocator header. /// This is done because fair::mq::Device brings in a bunch of boost.mpl / /// boost.fusion stuff, slowing down compilation times enourmously. @@ -58,6 +65,10 @@ class FairMQDeviceProxy [[nodiscard]] ChannelIndex getForwardChannelIndexByName(std::string const& channelName) const; /// Retrieve the channel index from a given OutputSpec and the associated timeslice [[nodiscard]] ChannelIndex getOutputChannelIndex(OutputSpec const& spec, size_t timeslice) const; +#if (FAIRMQ_VERSION_DEC >= 111000) + /// Retrieve the pointer-reconstruction function for the shm manager for a given input spec + [[nodiscard]] PointerReconstructor getShmPointerReconstructor(InputSpec const& spec, size_t timeslice); +#endif /// Retrieve the channel index from a given OutputSpec and the associated timeslice void getMatchingForwardChannelIndexes(std::vector& result, header::DataHeader const& header, size_t timeslice) const; /// ChannelIndex from a RouteIndex diff --git a/Framework/Core/src/FairMQDeviceProxy.cxx b/Framework/Core/src/FairMQDeviceProxy.cxx index e121084b866a2..b1cd9c9352829 100644 --- a/Framework/Core/src/FairMQDeviceProxy.cxx +++ b/Framework/Core/src/FairMQDeviceProxy.cxx @@ -364,4 +364,25 @@ void FairMQDeviceProxy::bind(std::vector const& outputs, std::vecto } mStateChangeCallback = newStatePending; } + +#if (FAIRMQ_VERSION_DEC >= 111000) +PointerReconstructor FairMQDeviceProxy::getShmPointerReconstructor(InputSpec const& spec, size_t timeslice) +{ + assert(mInputRoutes.size() == mInputs.size()); + ChannelIndex c{-1}; + for (size_t ri = 0; ri < mInputs.size(); ++ri) { + auto& route = mInputs[ri]; + + LOG(debug) << "matching: " << DataSpecUtils::describe(spec) << " to route " << DataSpecUtils::describe(route.matcher); + if ((spec == route.matcher) && (timeslice == route.timeslice)) { + c = mInputRoutes[ri].channel; + break; + } + } + if (c.value != ChannelIndex::INVALID) { + return {[transport = getInputChannel(c)->Transport()](fair::mq::shmem::MetaHeader&& meta) { return reinterpret_cast(fair::mq::shmem::GetDataAddressFromHandle(*transport, meta)); }}; + } + return {}; +} +#endif } // namespace o2::framework From f8a99bf5d4b4100b40f1d4f796ae8bb1584aa9de Mon Sep 17 00:00:00 2001 From: Giulio Eulisse <10544+ktf@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:31:05 +0200 Subject: [PATCH 28/47] Drop unused / obsolete include (#15629) --- Framework/Core/test/test_Root2ArrowTable.cxx | 1 - 1 file changed, 1 deletion(-) diff --git a/Framework/Core/test/test_Root2ArrowTable.cxx b/Framework/Core/test/test_Root2ArrowTable.cxx index dacb54eb5ecdf..ea97f6bcd8d3e 100644 --- a/Framework/Core/test/test_Root2ArrowTable.cxx +++ b/Framework/Core/test/test_Root2ArrowTable.cxx @@ -31,7 +31,6 @@ #include #include #include -#include #include #include From 2a2edeea3f50dae8144d8b765cde57b9d4a6fadf Mon Sep 17 00:00:00 2001 From: David Rohr Date: Thu, 23 Jul 2026 14:41:30 +0200 Subject: [PATCH 29/47] Increase LUT table size and make it configurable (#15630) --- GPU/Workflow/src/GPUWorkflowSpec.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GPU/Workflow/src/GPUWorkflowSpec.cxx b/GPU/Workflow/src/GPUWorkflowSpec.cxx index d928cfdd502c9..01a82ebdba361 100644 --- a/GPU/Workflow/src/GPUWorkflowSpec.cxx +++ b/GPU/Workflow/src/GPUWorkflowSpec.cxx @@ -260,8 +260,8 @@ void GPURecoWorkflowSpec::init(InitContext& ic) if (mConfig->configCalib.matLUT == nullptr) { LOGF(fatal, "Error loading matlut file"); } - } else { - mConfig->configProcessing.lateO2MatLutProvisioningSize = 50 * 1024 * 1024; + } else if (mConfig->configProcessing.lateO2MatLutProvisioningSize <= 0) { + mConfig->configProcessing.lateO2MatLutProvisioningSize = 55 * 1024 * 1024; } if (mSpecConfig.readTRDtracklets) { From a78221e0d4c00ef7141c616c092db1bbab9bf939 Mon Sep 17 00:00:00 2001 From: Marian Ivanov Date: Fri, 24 Jul 2026 01:18:25 +0200 Subject: [PATCH 30/47] TPC TimeSeries: propagate min-momentum, min-cluster, max-tgl to workflow (#15618) * TPC TimeSeries: propagate min-momentum, min-cluster, max-tgl to workflow Add configurable environment variables for o2-tpc-time-series-workflow: TPCTIMESERIES_MIN_MOMENTUM (default: 0.2) TPCTIMESERIES_MIN_CLUSTER (default: 80) TPCTIMESERIES_MAX_TGL (default: 1.4) Defaults match TPCTimeSeriesSpec.cxx hardcoded values. Previously these cuts were not configurable from the workflow script. * TPC TimeSeries: propagate track selection and multiplicity cuts to workflow Add configurable environment variables for o2-tpc-time-series-workflow: TPCTIMESERIES_MIN_MOMENTUM (default: 0.2) TPCTIMESERIES_MIN_CLUSTER (default: 80) TPCTIMESERIES_MAX_TGL (default: 1.4) TPCTIMESERIES_MULT_MAX (default: 50000) Defaults match TPCTimeSeriesSpec.cxx hardcoded values. Previously these cuts were not configurable from the workflow script. --------- Co-authored-by: miranov25 --- prodtests/full-system-test/calib-workflow.sh | 8 ++++++++ 1 file changed, 8 insertions(+) mode change 100755 => 100644 prodtests/full-system-test/calib-workflow.sh diff --git a/prodtests/full-system-test/calib-workflow.sh b/prodtests/full-system-test/calib-workflow.sh old mode 100755 new mode 100644 index 3c05ca6cda303..72f7a5aa47056 --- a/prodtests/full-system-test/calib-workflow.sh +++ b/prodtests/full-system-test/calib-workflow.sh @@ -55,6 +55,10 @@ if [[ $CALIB_ASYNC_EXTRACTTPCCURRENTS == 1 ]]; then fi if [[ $CALIB_ASYNC_EXTRACTTIMESERIES == 1 ]] ; then : ${CALIB_ASYNC_SAMPLINGFACTORTIMESERIES:=0.001} + : ${TPCTIMESERIES_MIN_MOMENTUM:=0.2} + : ${TPCTIMESERIES_MIN_CLUSTER:=80} + : ${TPCTIMESERIES_MAX_TGL:=1.4} + : ${TPCTIMESERIES_MULT_MAX:=50000} if [[ -n ${CALIB_ASYNC_ENABLEUNBINNEDTIMESERIES:-} ]]; then CONFIG_TPCTIMESERIES+=" --enable-unbinned-root-output --sample-unbinned-tsallis --threads ${TPCTIMESERIES_THREADS:-1}" fi @@ -69,6 +73,10 @@ if [[ $CALIB_ASYNC_EXTRACTTIMESERIES == 1 ]] ; then fi : ${TPCTIMESERIES_SOURCES:=$TRACK_SOURCES} CONFIG_TPCTIMESERIES+=" --track-sources $TPCTIMESERIES_SOURCES" + CONFIG_TPCTIMESERIES+=" --min-momentum ${TPCTIMESERIES_MIN_MOMENTUM}" + CONFIG_TPCTIMESERIES+=" --min-cluster ${TPCTIMESERIES_MIN_CLUSTER}" + CONFIG_TPCTIMESERIES+=" --max-tgl ${TPCTIMESERIES_MAX_TGL}" + CONFIG_TPCTIMESERIES+=" --mult-max ${TPCTIMESERIES_MULT_MAX}" add_W o2-tpc-time-series-workflow "${CONFIG_TPCTIMESERIES}" fi From 5a8b038a790c17031f21515a75bcf7a4ca39e561 Mon Sep 17 00:00:00 2001 From: David Rohr Date: Tue, 21 Jul 2026 13:31:59 +0200 Subject: [PATCH 31/47] GPU: Fix CUDA/HIP API incompatibility with new ROCm --- GPU/GPUTracking/Base/cuda/GPUReconstructionCUDA.cu | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/GPU/GPUTracking/Base/cuda/GPUReconstructionCUDA.cu b/GPU/GPUTracking/Base/cuda/GPUReconstructionCUDA.cu index cd9bab59e397b..9ac021974b8a1 100644 --- a/GPU/GPUTracking/Base/cuda/GPUReconstructionCUDA.cu +++ b/GPU/GPUTracking/Base/cuda/GPUReconstructionCUDA.cu @@ -594,7 +594,11 @@ void GPUReconstructionCUDA::PrintKernelOccupancies() int32_t maxBlocks = 0, threads = 0, suggestedBlocks = 0, nRegs = 0, sMem = 0; GPUChkErr(cudaSetDevice(mDeviceId)); for (uint32_t i = 0; i < mInternals->kernelFunctions.size(); i++) { - GPUChkErr(cuOccupancyMaxPotentialBlockSize(&suggestedBlocks, &threads, *mInternals->kernelFunctions[i], 0, 0, 0)); // NOLINT: failure in clang-tidy +#if !defined(__HIPCC__) || (defined(__clang_major__) && __clang_major__ < 23) // CUDA + GPUChkErr(cuOccupancyMaxPotentialBlockSize(&suggestedBlocks, &threads, *mInternals->kernelFunctions[i], 0, 0, 0)); +#else + GPUChkErr(cuOccupancyMaxPotentialBlockSize(&suggestedBlocks, &threads, *mInternals->kernelFunctions[i], 0, 0)); +#endif GPUChkErr(cuOccupancyMaxActiveBlocksPerMultiprocessor(&maxBlocks, *mInternals->kernelFunctions[i], threads, 0)); GPUChkErr(cuFuncGetAttribute(&nRegs, CU_FUNC_ATTRIBUTE_NUM_REGS, *mInternals->kernelFunctions[i])); GPUChkErr(cuFuncGetAttribute(&sMem, CU_FUNC_ATTRIBUTE_SHARED_SIZE_BYTES, *mInternals->kernelFunctions[i])); From 227fd404a6eebeac460a033b68cbe0959890ca27 Mon Sep 17 00:00:00 2001 From: David Rohr Date: Tue, 21 Jul 2026 13:32:12 +0200 Subject: [PATCH 32/47] GPU CMake: Improve ROCm path detection --- dependencies/FindO2GPU.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies/FindO2GPU.cmake b/dependencies/FindO2GPU.cmake index 1c3b997f64272..bd495138ab3a6 100644 --- a/dependencies/FindO2GPU.cmake +++ b/dependencies/FindO2GPU.cmake @@ -10,7 +10,7 @@ # or submit itself to any jurisdiction. # NOTE!!!! - Whenever this file is changed, move it over to alidist/resources -# FindO2GPU.cmake Version 17 +# FindO2GPU.cmake Version 18 set(CUDA_COMPUTETARGET_DEFAULT_FULL 80-real 86-real 89-real 120-real 75-virtual) set(HIP_AMDGPUTARGET_DEFAULT_FULL gfx906;gfx908) @@ -318,7 +318,7 @@ if(ENABLE_HIP) set(CMAKE_HIP_STANDARD_REQUIRED TRUE) set(TMP_ROCM_DIR_LIST "${CMAKE_PREFIX_PATH}:$ENV{CMAKE_PREFIX_PATH}") string(REPLACE ":" ";" TMP_ROCM_DIR_LIST "${TMP_ROCM_DIR_LIST}") - list(FILTER TMP_ROCM_DIR_LIST INCLUDE REGEX rocm) + list(FILTER TMP_ROCM_DIR_LIST INCLUDE REGEX /rocm/lib/cmake) list(POP_FRONT TMP_ROCM_DIR_LIST TMP_ROCM_DIR) get_filename_component(TMP_ROCM_DIR ${TMP_ROCM_DIR}/../../ ABSOLUTE) if (NOT DEFINED CMAKE_HIP_COMPILER) From cb6266d2bce997874af4681304ad011c179ed905 Mon Sep 17 00:00:00 2001 From: David Rohr Date: Thu, 23 Jul 2026 10:57:17 +0200 Subject: [PATCH 33/47] Fix linkDefs (needed for ROOT 6.40) --- .../src/FT0DCSMonitoringLinkDef.h | 2 ++ .../src/FV0ReconstructionLinkDef.h | 2 ++ Detectors/O2TrivialMC/src/O2TrivialMCLinkDef.h | 3 +++ Detectors/ZDC/raw/CMakeLists.txt | 3 --- Detectors/ZDC/raw/src/ZDCRawLinkDef.h | 18 ------------------ 5 files changed, 7 insertions(+), 21 deletions(-) delete mode 100644 Detectors/ZDC/raw/src/ZDCRawLinkDef.h diff --git a/Detectors/FIT/FT0/dcsmonitoring/src/FT0DCSMonitoringLinkDef.h b/Detectors/FIT/FT0/dcsmonitoring/src/FT0DCSMonitoringLinkDef.h index c85dd2d378ccb..50a9e7f35e25f 100644 --- a/Detectors/FIT/FT0/dcsmonitoring/src/FT0DCSMonitoringLinkDef.h +++ b/Detectors/FIT/FT0/dcsmonitoring/src/FT0DCSMonitoringLinkDef.h @@ -15,4 +15,6 @@ #pragma link off all classes; #pragma link off all functions; +#pragma link C++ class o2::ft0::FT0DCSConfigReader + ; + #endif diff --git a/Detectors/FIT/FV0/reconstruction/src/FV0ReconstructionLinkDef.h b/Detectors/FIT/FV0/reconstruction/src/FV0ReconstructionLinkDef.h index c85dd2d378ccb..f4551100c8cc5 100644 --- a/Detectors/FIT/FV0/reconstruction/src/FV0ReconstructionLinkDef.h +++ b/Detectors/FIT/FV0/reconstruction/src/FV0ReconstructionLinkDef.h @@ -15,4 +15,6 @@ #pragma link off all classes; #pragma link off all functions; +#pragma link C++ class o2::fv0::BaseRecoTask + ; + #endif diff --git a/Detectors/O2TrivialMC/src/O2TrivialMCLinkDef.h b/Detectors/O2TrivialMC/src/O2TrivialMCLinkDef.h index c85dd2d378ccb..51c7e396a49b7 100644 --- a/Detectors/O2TrivialMC/src/O2TrivialMCLinkDef.h +++ b/Detectors/O2TrivialMC/src/O2TrivialMCLinkDef.h @@ -15,4 +15,7 @@ #pragma link off all classes; #pragma link off all functions; +#pragma link C++ class o2::mc::O2TrivialMCEngine + ; +#pragma link C++ class o2::mc::O2TrivialMCApplication + ; + #endif diff --git a/Detectors/ZDC/raw/CMakeLists.txt b/Detectors/ZDC/raw/CMakeLists.txt index 5f4983d6fc31b..da11ea3d9810b 100644 --- a/Detectors/ZDC/raw/CMakeLists.txt +++ b/Detectors/ZDC/raw/CMakeLists.txt @@ -25,9 +25,6 @@ o2_add_library(ZDCRaw O2::ZDCBase O2::ZDCSimulation) -o2_target_root_dictionary(ZDCRaw - HEADERS include/ZDCRaw/DumpRaw.h) - o2_add_executable(raw-parser COMPONENT_NAME zdc SOURCES src/raw-parser.cxx diff --git a/Detectors/ZDC/raw/src/ZDCRawLinkDef.h b/Detectors/ZDC/raw/src/ZDCRawLinkDef.h deleted file mode 100644 index c85dd2d378ccb..0000000000000 --- a/Detectors/ZDC/raw/src/ZDCRawLinkDef.h +++ /dev/null @@ -1,18 +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. - -#ifdef __CLING__ - -#pragma link off all globals; -#pragma link off all classes; -#pragma link off all functions; - -#endif From 1709369443c1f85a627cec4c0e56defb39505563 Mon Sep 17 00:00:00 2001 From: David Rohr Date: Thu, 23 Jul 2026 10:58:04 +0200 Subject: [PATCH 34/47] Fix includes for ROOT 6.40 (missing in cxx, and can no longer forward-declare) --- CCDB/include/CCDB/CcdbApi.h | 2 +- Generators/include/Generators/GeneratorFromFile.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CCDB/include/CCDB/CcdbApi.h b/CCDB/include/CCDB/CcdbApi.h index 4dab11d5972d8..acb0ad10f6c9a 100644 --- a/CCDB/include/CCDB/CcdbApi.h +++ b/CCDB/include/CCDB/CcdbApi.h @@ -39,7 +39,7 @@ class TJAlienCredentials; #include "CCDB/CCDBDownloader.h" class TFile; -class TGrid; +#include namespace o2 { diff --git a/Generators/include/Generators/GeneratorFromFile.h b/Generators/include/Generators/GeneratorFromFile.h index 9bf1d4911008b..ad93814467fc3 100644 --- a/Generators/include/Generators/GeneratorFromFile.h +++ b/Generators/include/Generators/GeneratorFromFile.h @@ -19,12 +19,12 @@ #include "Generators/GeneratorFromO2KineParam.h" #include "SimulationDataFormat/MCEventHeader.h" #include +#include #include class TBranch; class TFile; class TParticle; -class TGrid; namespace o2 { From 10eb61ecb63e4a906bd3ed57f9ec609dd21a856a Mon Sep 17 00:00:00 2001 From: David Rohr Date: Thu, 23 Jul 2026 10:58:18 +0200 Subject: [PATCH 35/47] Do not use deprecated ROOT interfaces --- Detectors/PHOS/calib/src/PHOSRunbyrunCalibrator.cxx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Detectors/PHOS/calib/src/PHOSRunbyrunCalibrator.cxx b/Detectors/PHOS/calib/src/PHOSRunbyrunCalibrator.cxx index 63e51f06c0e64..0c4e740168ffe 100644 --- a/Detectors/PHOS/calib/src/PHOSRunbyrunCalibrator.cxx +++ b/Detectors/PHOS/calib/src/PHOSRunbyrunCalibrator.cxx @@ -213,9 +213,9 @@ void PHOSRunbyrunCalibrator::writeHistos() { // Merge collected in different slots histograms - TF1 fRatio("ratio", this, &PHOSRunbyrunCalibrator::CBRatio, 0, 1, 6, "PHOSRunbyrunCalibrator", "CBRatio"); - TF1 fBg("background", this, &PHOSRunbyrunCalibrator::bg, 0, 1, 6, "PHOSRunbyrunCalibrator", "bg"); - TF1 fSignal("signal", this, &PHOSRunbyrunCalibrator::CBSignal, 0, 1, 6, "PHOSRunbyrunCalibrator", "CBSignal"); + TF1 fRatio("ratio", this, &PHOSRunbyrunCalibrator::CBRatio, 0, 1, 6); + TF1 fBg("background", this, &PHOSRunbyrunCalibrator::bg, 0, 1, 6); + TF1 fSignal("signal", this, &PHOSRunbyrunCalibrator::CBSignal, 0, 1, 6); // fit inv mass distributions for (int mod = 0; mod < 4; mod++) { From 51d8507439564840878828062f2c14d80ccc9571 Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Fri, 24 Jul 2026 23:42:53 +0200 Subject: [PATCH 36/47] Add geometrical TPC protection against misfired loopers (#15621) --- Generators/include/Generators/TPCLoopers.h | 11 +++++ .../include/Generators/TPCLoopersParam.h | 1 + Generators/src/Generator.cxx | 5 ++ Generators/src/TPCLoopers.cxx | 47 +++++++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/Generators/include/Generators/TPCLoopers.h b/Generators/include/Generators/TPCLoopers.h index a144a947fc11b..3e8685257b829 100644 --- a/Generators/include/Generators/TPCLoopers.h +++ b/Generators/include/Generators/TPCLoopers.h @@ -107,8 +107,16 @@ class GenTPCLoopers void SetAdjust(float adjust = 0.f); + void setGeomProtection(bool protect); + + // check if a vertex lies in the TPC volume where ionisation can be recorded + bool isInTPCActiveVolume(double vx, double vy) const; + unsigned int getNLoopers() const { return (mNLoopersPairs + mNLoopersCompton); } + // loopers dropped by the geometrical protection since the last reset + unsigned int getNSkipped() const { return mNSkippedPairs + mNSkippedCompton; } + private: std::unique_ptr mONNX_pair = nullptr; std::unique_ptr mONNX_compton = nullptr; @@ -137,6 +145,9 @@ class GenTPCLoopers double mTimeEnd = 0.0; // Time limit for the last event float mLoopsFractionPairs = 0.08; // Fraction of loopers from Pairs int mInteractionRate = 50000; // Interaction rate in Hz + bool mGeomProtection = true; // Skip loopers generated outside the TPC active volume + unsigned int mNSkippedPairs = 0; // Pairs dropped by the geometrical protection + unsigned int mNSkippedCompton = 0; // Compton electrons dropped by the geometrical protection }; #endif // GENERATORS_WITH_TPCLOOPERS diff --git a/Generators/include/Generators/TPCLoopersParam.h b/Generators/include/Generators/TPCLoopersParam.h index 87e4510d6e617..db75c1311d020 100644 --- a/Generators/include/Generators/TPCLoopersParam.h +++ b/Generators/include/Generators/TPCLoopersParam.h @@ -45,6 +45,7 @@ struct GenTPCLoopersParam : public o2::conf::ConfigurableParamHelpersetGeomProtection(loopersParam.geomProtection); const auto& intrate = loopersParam.intrate; // Configure the generator with flat gas loopers defined per orbit with clusters/track info // If intrate is negative (default), automatic IR from collisioncontext.root will be used @@ -260,6 +261,10 @@ Bool_t mParticles.insert(mParticles.end(), looperParticles.begin(), looperParticles.end()); LOG(debug) << "Added " << looperParticles.size() << " looper particles"; + const auto skippedLoopers = mTPCLoopersGen->getNSkipped(); + if (skippedLoopers > 0) { + LOG(debug) << "Geometrical protection skipped " << skippedLoopers << " loopers outside the TPC active volume"; + } } #endif return kTRUE; diff --git a/Generators/src/TPCLoopers.cxx b/Generators/src/TPCLoopers.cxx index 41551ab483660..a587f1fc8d1c9 100644 --- a/Generators/src/TPCLoopers.cxx +++ b/Generators/src/TPCLoopers.cxx @@ -126,6 +126,40 @@ namespace o2 namespace eventgen { +namespace +{ +// Radial limits of the region from which a looper can still reach the TPC +// sensitive gas. The field cage positions are those used by the +// "ExcludeFCGap" selection in o2::tpc::Detector::ProcessHits() +// A looper can enter the sensitive gas from just outside it, so an additional margin is set +// with a factor two over the largest radial excursion which was observed in validation (~4.2 cm). +// +// No cut is applied on z because loopers spiral the field lines and a vertex as far as |z| = 283 cm +// feeds hits into the gas. A cut here would discard loopers that produce TPC signals. +constexpr double kFcLxIn = 82.428409; // cm, inner field cage strips +constexpr double kRodROut = 254.25 + 2.2; // cm, outer field cage rods plus their radial size +constexpr double kLooperRadialReach = 10.; // cm, margin for the helix sweep +constexpr double kTPCActiveRMin = kFcLxIn - kLooperRadialReach; +constexpr double kTPCActiveRMax = kRodROut + kLooperRadialReach; +} // namespace + +bool GenTPCLoopers::isInTPCActiveVolume(double vx, double vy) const +{ + const double vt = std::sqrt(vx * vx + vy * vy); + return (vt >= kTPCActiveRMin && vt <= kTPCActiveRMax); +} + +void GenTPCLoopers::setGeomProtection(bool protect) +{ + mGeomProtection = protect; + if (mGeomProtection) { + LOG(debug) << "TPC loopers geometrical protection: ON (accepting vertices with " + << kTPCActiveRMin << " <= Vt <= " << kTPCActiveRMax << " cm)"; + } else { + LOG(warning) << "TPC loopers geometrical protection: OFF - loopers will be generated outside the TPC active volume as well."; + } +} + GenTPCLoopers::GenTPCLoopers(std::string model_pairs, std::string model_compton, std::string poisson, std::string gauss, std::string scaler_pair, std::string scaler_compton) @@ -267,11 +301,20 @@ std::vector GenTPCLoopers::importParticles() std::vector particles; const double mass_e = TDatabasePDG::Instance()->GetParticle(11)->Mass(); const double mass_p = TDatabasePDG::Instance()->GetParticle(-11)->Mass(); + mNSkippedPairs = 0; + mNSkippedCompton = 0; // Get looper pairs from the event for (auto& pair : mGenPairs) { double px_e, py_e, pz_e, px_p, py_p, pz_p; double vx, vy, vz, time; double e_etot, p_etot; + // The generative model is not currently fully constrained to the TPC geometry, so it places + // significant fraction of the vertices outside the drift gas. + // These are now dropped before they reach the transport. + if (mGeomProtection && !isInTPCActiveVolume(pair[6], pair[7])) { + mNSkippedPairs++; + continue; + } px_e = pair[0]; py_e = pair[1]; pz_e = pair[2]; @@ -301,6 +344,10 @@ std::vector GenTPCLoopers::importParticles() double px, py, pz; double vx, vy, vz, time; double etot; + if (mGeomProtection && !isInTPCActiveVolume(compton[3], compton[4])) { + mNSkippedCompton++; + continue; + } px = compton[0]; py = compton[1]; pz = compton[2]; From 9a3938a24db6833082437d41dd91cf83f3afb287 Mon Sep 17 00:00:00 2001 From: Mario Ciacco Date: Sat, 25 Jul 2026 02:23:43 +0200 Subject: [PATCH 37/47] remove overlaps in OTOF staves (#15633) --- Detectors/Upgrades/ALICE3/IOTOF/simulation/src/Layer.cxx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Detectors/Upgrades/ALICE3/IOTOF/simulation/src/Layer.cxx b/Detectors/Upgrades/ALICE3/IOTOF/simulation/src/Layer.cxx index f2e42e1bce172..0c249f6fefe18 100644 --- a/Detectors/Upgrades/ALICE3/IOTOF/simulation/src/Layer.cxx +++ b/Detectors/Upgrades/ALICE3/IOTOF/simulation/src/Layer.cxx @@ -331,14 +331,9 @@ void OTOFLayer::createLayer(TGeoVolume* motherVolume) << " is not divisible by modulesPerStaveX=" << modulesPerStaveX; } const int modulesPerStaveZ = mModulesPerStave / modulesPerStaveX; - const double moduleOverlapZ = 0.7; // cm, 7 mm longitudinal overlap from oTOF V2 specs const double moduleSizeX = staveSizeX / modulesPerStaveX; const double moduleSizeY = staveSizeY; - const double moduleSizeZ = (staveSizeZ + (modulesPerStaveZ - 1) * moduleOverlapZ) / modulesPerStaveZ; - const double modulePitchZ = moduleSizeZ - moduleOverlapZ; - if (modulePitchZ <= 0.0) { - LOG(fatal) << "Invalid oTOF module overlap " << moduleOverlapZ << " cm for module size " << moduleSizeZ << " cm"; - } + const double moduleSizeZ = staveSizeZ / modulesPerStaveZ; TGeoBBox* module = new TGeoBBox(moduleSizeX * 0.5, moduleSizeY * 0.5, moduleSizeZ * 0.5); TGeoVolume* moduleVol = new TGeoVolume(moduleName, module, medAir); setModuleStyle(moduleVol); @@ -389,7 +384,7 @@ void OTOFLayer::createLayer(TGeoVolume* motherVolume) for (int j = 0; j < modulesPerStaveZ; ++j) { LOGP(info, "oTOF: Creating module {}/{} for stave {}/{}", i + 1, modulesPerStaveX, j + 1, modulesPerStaveZ); const double tx = (i + 0.5) * moduleSizeX - 0.5 * staveSizeX; - const double tz = -0.5 * staveSizeZ + 0.5 * moduleSizeZ + j * modulePitchZ; + const double tz = -0.5 * staveSizeZ + (j + 0.5) * moduleSizeZ; auto* translation = new TGeoTranslation(tx, 0, tz); staveVol->AddNode(moduleVol, 1 + i * modulesPerStaveZ + j, translation); } From 2bd5701c9f84ec1f7dce183fe919bfc7bc4e85f0 Mon Sep 17 00:00:00 2001 From: David Rohr Date: Fri, 24 Jul 2026 23:49:20 +0200 Subject: [PATCH 38/47] GPU Workflow: Fix possible deadlock when stopping without receiving EndOfSteam with DoublePipeline --- GPU/GPUTracking/Base/GPUReconstruction.cxx | 10 ++++ GPU/GPUTracking/Base/GPUReconstruction.h | 6 ++ GPU/GPUTracking/Base/GPUReconstructionCPU.cxx | 2 +- GPU/GPUTracking/Global/GPUChainTracking.cxx | 30 +++++----- GPU/GPUTracking/Global/GPUChainTracking.h | 4 +- .../Global/GPUChainTrackingClusterizer.cxx | 6 +- GPU/GPUTracking/Interface/GPUO2Interface.cxx | 25 +++++--- GPU/GPUTracking/Interface/GPUO2Interface.h | 1 + .../Interface/GPUO2InterfaceConfiguration.h | 4 +- .../Standalone/Benchmark/standalone.cxx | 12 ++-- GPU/Workflow/src/GPUWorkflowInternal.h | 5 +- GPU/Workflow/src/GPUWorkflowPipeline.cxx | 60 +++++++++++++++---- 12 files changed, 115 insertions(+), 50 deletions(-) diff --git a/GPU/GPUTracking/Base/GPUReconstruction.cxx b/GPU/GPUTracking/Base/GPUReconstruction.cxx index ac67c617872bd..eaf3afbdc9cf3 100644 --- a/GPU/GPUTracking/Base/GPUReconstruction.cxx +++ b/GPU/GPUTracking/Base/GPUReconstruction.cxx @@ -66,6 +66,7 @@ struct GPUReconstructionPipelineContext { std::queue pipelineQueue; std::mutex mutex; std::condition_variable cond; + bool workerRunning = false; bool terminate = false; }; } // namespace o2::gpu @@ -1094,6 +1095,7 @@ void GPUReconstruction::RunPipelineWorker() { std::unique_lock lk(mPipelineContext->mutex); mPipelineContext->cond.wait(lk, [this] { return this->mPipelineContext->pipelineQueue.size() > 0; }); + mPipelineContext->workerRunning = true; } GPUReconstructionPipelineQueue* q; { @@ -1111,6 +1113,8 @@ void GPUReconstruction::RunPipelineWorker() q->done = true; } q->c.notify_one(); + mPipelineContext->workerRunning = false; + mPipelineContext->cond.notify_one(); } if (GetProcessingSettings().debugLevel >= 3) { GPUInfo("Pipeline worker ended"); @@ -1122,6 +1126,12 @@ void GPUReconstruction::TerminatePipelineWorker() EnqueuePipeline(true); } +void GPUReconstruction::DrainPipeline() +{ + std::unique_lock lk(mPipelineContext->mutex); + mPipelineContext->cond.wait(lk, [this] { return this->mPipelineContext->pipelineQueue.empty() && !this->mPipelineContext->workerRunning; }); +} + int32_t GPUReconstruction::EnqueuePipeline(bool terminate) { ClearAllocatedMemory(true); diff --git a/GPU/GPUTracking/Base/GPUReconstruction.h b/GPU/GPUTracking/Base/GPUReconstruction.h index 4479eb696808e..d85c29371f8fb 100644 --- a/GPU/GPUTracking/Base/GPUReconstruction.h +++ b/GPU/GPUTracking/Base/GPUReconstruction.h @@ -98,6 +98,11 @@ class GPUReconstruction static constexpr GeometryType geometryType = GeometryType::O2; #endif + enum retValValue : uint32_t { ok = 0, + error = 1, + doExit = 2, + nonFatalErrorCode = 3, + abort = 4 }; static DeviceType GetDeviceType(const char* type); enum InOutPointerType : uint32_t { CLUSTER_DATA = 0, SECTOR_OUT_TRACK = 1, @@ -159,6 +164,7 @@ class GPUReconstruction int32_t CheckErrorCodes(bool cpuOnly = false, bool forceShowErrors = false, std::vector>* fillErrors = nullptr); void RunPipelineWorker(); void TerminatePipelineWorker(); + void DrainPipeline(); // Helpers for memory allocation GPUMemoryResource& Res(int16_t num) { return mMemoryResources[num]; } diff --git a/GPU/GPUTracking/Base/GPUReconstructionCPU.cxx b/GPU/GPUTracking/Base/GPUReconstructionCPU.cxx index 9fbe9e1171af3..ac2c2d0a78a78 100644 --- a/GPU/GPUTracking/Base/GPUReconstructionCPU.cxx +++ b/GPU/GPUTracking/Base/GPUReconstructionCPU.cxx @@ -245,7 +245,7 @@ int32_t GPUReconstructionCPU::RunChains() retVal = mChains[i]->RunChain(); } } - if (retVal != 0 && retVal != 2) { + if (retVal != GPUReconstruction::retValValue::ok && retVal != GPUReconstruction::retValValue::doExit) { return retVal; } mTimerTotal.Stop(); diff --git a/GPU/GPUTracking/Global/GPUChainTracking.cxx b/GPU/GPUTracking/Global/GPUChainTracking.cxx index 067c4dff92810..480ec76408584 100644 --- a/GPU/GPUTracking/Global/GPUChainTracking.cxx +++ b/GPU/GPUTracking/Global/GPUChainTracking.cxx @@ -677,7 +677,7 @@ int32_t GPUChainTracking::RunChain() const bool needQA = GPUQA::QAAvailable() && (GetProcessingSettings().runQA || (GetProcessingSettings().eventDisplay && (mIOPtrs.nMCInfosTPC || GetProcessingSettings().runMC))); if (needQA && GetQA()->IsInitialized() == false) { if (GetQA()->InitQA(GetProcessingSettings().runQA <= 0 ? -GetProcessingSettings().runQA : gpudatatypes::gpuqa::tasksAutomatic)) { - return 1; + return GPUReconstruction::retValValue::error; } } if (needQA) { @@ -693,7 +693,7 @@ int32_t GPUChainTracking::RunChain() mRec->PrepareEvent(); } catch (const std::bad_alloc& e) { GPUError("Memory Allocation Error"); - return (1); + return GPUReconstruction::retValValue::error; } mRec->getGeneralStepTimer(GeneralStep::Prepare).Stop(); @@ -707,11 +707,11 @@ int32_t GPUChainTracking::RunChain() if (mIOPtrs.tpcCompressedClusters) { if (runRecoStep(RecoStep::TPCDecompression, &GPUChainTracking::RunTPCDecompression)) { - return 1; + return GPUReconstruction::retValValue::error; } } else if (mIOPtrs.tpcPackedDigits || mIOPtrs.tpcZS) { if (runRecoStep(RecoStep::TPCClusterFinding, &GPUChainTracking::RunTPCClusterizer, false)) { - return 1; + return GPUReconstruction::retValValue::error; } } @@ -720,17 +720,17 @@ int32_t GPUChainTracking::RunChain() } if (mIOPtrs.clustersNative && runRecoStep(RecoStep::TPCConversion, &GPUChainTracking::ConvertNativeToClusterData)) { - return 1; + return GPUReconstruction::retValValue::error; } mRec->PushNonPersistentMemory(qStr2Tag("TPCSLCD1")); // 1st stack level for TPC tracking sector data mTPCSectorScratchOnStack = true; if (runRecoStep(RecoStep::TPCSectorTracking, &GPUChainTracking::RunTPCTrackingSectors)) { - return 1; + return GPUReconstruction::retValValue::error; } if (runRecoStep(RecoStep::TPCMerging, &GPUChainTracking::RunTPCTrackingMerger, false)) { - return 1; + return GPUReconstruction::retValValue::error; } if (mTPCSectorScratchOnStack) { mRec->PopNonPersistentMemory(RecoStep::TPCSectorTracking, qStr2Tag("TPCSLCD1")); // Release 1st stack level, TPC sector data not needed after merger @@ -750,16 +750,16 @@ int32_t GPUChainTracking::RunChain() } } if (runRecoStep(RecoStep::TPCCompression, &GPUChainTracking::RunTPCCompression)) { - return 1; + return GPUReconstruction::retValValue::error; } } if (runRecoStep(RecoStep::TRDTracking, &GPUChainTracking::RunTRDTracking)) { - return 1; + return GPUReconstruction::retValValue::error; } if (runRecoStep(RecoStep::Refit, &GPUChainTracking::RunRefit)) { - return 1; + return GPUReconstruction::retValValue::error; } if (!GetProcessingSettings().doublePipeline) { // Synchronize with output copies running asynchronously @@ -770,9 +770,9 @@ int32_t GPUChainTracking::RunChain() mRec->SetNActiveThreads(-1); } - int32_t retVal = 0; + int32_t retVal = GPUReconstruction::retValValue::ok; if (CheckErrorCodes(false, false, mRec->getErrorCodeOutput())) { // TODO: Eventually, we should use GPUReconstruction::CheckErrorCodes - retVal = 3; + retVal = GPUReconstruction::retValValue::nonFatalErrorCode; if (!GetProcessingSettings().ignoreNonFatalGPUErrors) { return retVal; } @@ -820,7 +820,7 @@ int32_t GPUChainTracking::RunChainFinalize() GPUInfo("Starting Event Display..."); if (mEventDisplay->StartDisplay()) { GPUError("Error starting Event Display"); - return (1); + return GPUReconstruction::retValValue::error; } mDisplayRunning = true; } else { @@ -857,7 +857,7 @@ int32_t GPUChainTracking::RunChainFinalize() mDisplayRunning = false; GetProcessingSettings().eventDisplay->DisplayExit(); const_cast(GetProcessingSettings()).eventDisplay = nullptr; // TODO: fixme - eventDisplay should probably not be put into ProcessingSettings in the first place - return (2); + return GPUReconstruction::retValValue::doExit; } GetProcessingSettings().eventDisplay->setDisplayControl(0); GPUInfo("Loading next event..."); @@ -865,7 +865,7 @@ int32_t GPUChainTracking::RunChainFinalize() mEventDisplay->BlockTillNextEvent(); } - return 0; + return GPUReconstruction::retValValue::ok; } int32_t GPUChainTracking::FinalizePipelinedProcessing() diff --git a/GPU/GPUTracking/Global/GPUChainTracking.h b/GPU/GPUTracking/Global/GPUChainTracking.h index 3c3531530372a..759aaf818028e 100644 --- a/GPU/GPUTracking/Global/GPUChainTracking.h +++ b/GPU/GPUTracking/Global/GPUChainTracking.h @@ -195,7 +195,7 @@ class GPUChainTracking : public GPUChain void SetCalibObjects(const GPUCalibObjects& obj); void SetUpdateCalibObjects(const GPUCalibObjectsConst& obj, const GPUNewCalibValues& vals); void SetSubOutputControl(int32_t i, GPUOutputControl* v) { mSubOutputControls[i] = v; } - void SetFinalInputCallback(std::function v) { mWaitForFinalInputs = v; } + void SetFinalInputCallback(std::function v) { mWaitForFinalInputs = v; } const GPUSettingsDisplay* mConfigDisplay = nullptr; // Abstract pointer to Standalone Display Configuration Structure const GPUSettingsQA* mConfigQA = nullptr; // Abstract pointer to Standalone QA Configuration Structure @@ -321,7 +321,7 @@ class GPUChainTracking : public GPUChain std::mutex mMutexUpdateCalib; std::unique_ptr mPipelineFinalizationCtx; GPUChainTrackingFinalContext* mPipelineNotifyCtx = nullptr; - std::function mWaitForFinalInputs; + std::function mWaitForFinalInputs; int32_t OutputStream() const { return mRec->NStreams() - 2; } }; diff --git a/GPU/GPUTracking/Global/GPUChainTrackingClusterizer.cxx b/GPU/GPUTracking/Global/GPUChainTrackingClusterizer.cxx index 681914414d401..a558ed85c5516 100644 --- a/GPU/GPUTracking/Global/GPUChainTrackingClusterizer.cxx +++ b/GPU/GPUTracking/Global/GPUChainTrackingClusterizer.cxx @@ -769,7 +769,7 @@ int32_t GPUChainTracking::RunTPCClusterizer(bool synchronizeOutput) #endif if (RunTPCClusterizer_prepare(mPipelineNotifyCtx && GetProcessingSettings().doublePipelineClusterizer, extraADCs)) { - return 1; + return GPUReconstruction::retValValue::error; } if (GetProcessingSettings().autoAdjustHostThreads && !doGPU) { mRec->SetNActiveThreads(mRec->MemoryScalers()->nTPCdigits / 6000); @@ -1471,7 +1471,9 @@ int32_t GPUChainTracking::RunTPCClusterizer(bool synchronizeOutput) notifyForeignChainFinished(); } if (mWaitForFinalInputs && iSectorBase >= 30 && (int32_t)iSectorBase < 30 + GetProcessingSettings().nTPCClustererLanes) { - mWaitForFinalInputs(); + if (mWaitForFinalInputs()) { + return GPUReconstruction::retValValue::abort; + } synchronizeCalibUpdate = DoQueuedUpdates(0, false); } } diff --git a/GPU/GPUTracking/Interface/GPUO2Interface.cxx b/GPU/GPUTracking/Interface/GPUO2Interface.cxx index ced3016dc15b1..184597b12ceba 100644 --- a/GPU/GPUTracking/Interface/GPUO2Interface.cxx +++ b/GPU/GPUTracking/Interface/GPUO2Interface.cxx @@ -137,6 +137,11 @@ void GPUO2Interface::Deinitialize() mNContexts = 0; } +void GPUO2Interface::DrainPipeline() +{ + mCtx[0].mRec->DrainPipeline(); +} + void GPUO2Interface::DumpEvent(int32_t nEvent, GPUTrackingInOutPointers* data, uint32_t iThread, const char* dir) { const auto oldPtrs = mCtx[iThread].mChain->mIOPtrs; @@ -185,19 +190,23 @@ int32_t GPUO2Interface::RunTracking(GPUTrackingInOutPointers* data, GPUInterface } }; - auto inputWaitCallback = [this, iThread, inputUpdateCallback, &data, &outputs, &setOutputs]() { + auto inputWaitCallback = [this, iThread, inputUpdateCallback, &data, &outputs, &setOutputs]() -> int32_t { GPUTrackingInOutPointers* updatedData; GPUInterfaceOutputs* updatedOutputs; + int32_t retVal = 0; if (inputUpdateCallback->callback) { - inputUpdateCallback->callback(updatedData, updatedOutputs); - mCtx[iThread].mChain->mIOPtrs = *updatedData; - outputs = updatedOutputs; - data = updatedData; - setOutputs(outputs); + retVal = inputUpdateCallback->callback(updatedData, updatedOutputs); + if (retVal == 0) { + mCtx[iThread].mChain->mIOPtrs = *updatedData; + outputs = updatedOutputs; + data = updatedData; + setOutputs(outputs); + } } if (inputUpdateCallback->notifyCallback) { inputUpdateCallback->notifyCallback(); } + return retVal; }; if (inputUpdateCallback) { @@ -210,8 +219,8 @@ int32_t GPUO2Interface::RunTracking(GPUTrackingInOutPointers* data, GPUInterface } int32_t retVal = mCtx[iThread].mRec->RunChains(); - if (retVal == 2) { - retVal = 0; // 2 signals end of event display, ignore + if (retVal == GPUReconstruction::retValValue::doExit) { + retVal = GPUReconstruction::retValValue::ok; // Ignore exit signal from event display } if (mConfig->configQA.shipToQC && mCtx[iThread].mChain->QARanForTF()) { outputs->qa.hist1 = &mCtx[iThread].mChain->GetQA()->getHistograms1D(); diff --git a/GPU/GPUTracking/Interface/GPUO2Interface.h b/GPU/GPUTracking/Interface/GPUO2Interface.h index ca56018908b41..eed7c119f5328 100644 --- a/GPU/GPUTracking/Interface/GPUO2Interface.h +++ b/GPU/GPUTracking/Interface/GPUO2Interface.h @@ -71,6 +71,7 @@ class GPUO2Interface int32_t Initialize(const GPUO2InterfaceConfiguration& config); void Deinitialize(); + void DrainPipeline(); int32_t RunTracking(GPUTrackingInOutPointers* data, GPUInterfaceOutputs* outputs = nullptr, uint32_t iThread = 0, GPUInterfaceInputUpdate* inputUpdateCallback = nullptr); void Clear(bool clearOutputs, uint32_t iThread = 0); diff --git a/GPU/GPUTracking/Interface/GPUO2InterfaceConfiguration.h b/GPU/GPUTracking/Interface/GPUO2InterfaceConfiguration.h index 0f8a3784f0a88..155048859a507 100644 --- a/GPU/GPUTracking/Interface/GPUO2InterfaceConfiguration.h +++ b/GPU/GPUTracking/Interface/GPUO2InterfaceConfiguration.h @@ -58,8 +58,8 @@ struct GPUInterfaceOutputs : public GPUTrackingOutputs { }; struct GPUInterfaceInputUpdate { - std::function callback; // Callback which provides final data ptrs / outputRegions after Clusterization stage - std::function notifyCallback; // Callback called to notify that Clusterization state has finished without update + std::function callback; // Callback which provides final data ptrs / outputRegions after Clusterization stage + std::function notifyCallback; // Callback called to notify that Clusterization state has finished without update }; // Full configuration structure with all available settings of GPU... diff --git a/GPU/GPUTracking/Standalone/Benchmark/standalone.cxx b/GPU/GPUTracking/Standalone/Benchmark/standalone.cxx index cfa3ad7c81b83..c452e973d97e0 100644 --- a/GPU/GPUTracking/Standalone/Benchmark/standalone.cxx +++ b/GPU/GPUTracking/Standalone/Benchmark/standalone.cxx @@ -685,11 +685,11 @@ int32_t RunBenchmark(GPUReconstruction* recUse, GPUChainTracking* chainTrackingU } } - if (tmpRetVal == 0 || tmpRetVal == 2) { + if (tmpRetVal == GPUReconstruction::retValValue::ok || tmpRetVal == GPUReconstruction::retValValue::doExit) { OutputStat(chainTrackingUse, iRun == 0 ? nTracksTotal : nullptr, iRun == 0 ? nClustersTotal : nullptr); } - if (tmpRetVal == 0 && configStandalone.testSyncAsync) { + if (tmpRetVal == GPUReconstruction::retValValue::ok && configStandalone.testSyncAsync) { vecpod compressedTmpMem(chainTracking->mIOPtrs.tpcCompressedClusters->totalDataSize); memcpy(compressedTmpMem.data(), (const void*)chainTracking->mIOPtrs.tpcCompressedClusters, chainTracking->mIOPtrs.tpcCompressedClusters->totalDataSize); o2::tpc::CompressedClusters tmp(*chainTracking->mIOPtrs.tpcCompressedClusters); @@ -717,7 +717,7 @@ int32_t RunBenchmark(GPUReconstruction* recUse, GPUChainTracking* chainTrackingU recAsync->SetResetTimers(iRun < configStandalone.runsInit); } tmpRetVal = recAsync->RunChains(); - if (tmpRetVal == 0 || tmpRetVal == 2) { + if (tmpRetVal == GPUReconstruction::retValValue::ok || tmpRetVal == GPUReconstruction::retValValue::doExit) { OutputStat(chainTrackingAsync, nullptr, nullptr); } recAsync->ClearAllocatedMemory(); @@ -726,14 +726,14 @@ int32_t RunBenchmark(GPUReconstruction* recUse, GPUChainTracking* chainTrackingU recUse->ClearAllocatedMemory(); } - if (tmpRetVal == 2) { + if (tmpRetVal == GPUReconstruction::retValValue::doExit) { configStandalone.continueOnError = 0; // Forced exit from event display loop configStandalone.noprompt = 1; } - if (tmpRetVal == 3 && configStandalone.proc.ignoreNonFatalGPUErrors) { + if (tmpRetVal == GPUReconstruction::retValValue::nonFatalErrorCode && configStandalone.proc.ignoreNonFatalGPUErrors) { printf("GPU Standalone Benchmark: Non-FATAL GPU error occured, ignoring\n"); } else if (tmpRetVal && !configStandalone.continueOnError) { - if (tmpRetVal != 2) { + if (tmpRetVal != GPUReconstruction::retValValue::doExit) { printf("GPU Standalone Benchmark: Error occured\n"); } return 1; diff --git a/GPU/Workflow/src/GPUWorkflowInternal.h b/GPU/Workflow/src/GPUWorkflowInternal.h index 1ad6f3df13f5a..aa6ab80af576b 100644 --- a/GPU/Workflow/src/GPUWorkflowInternal.h +++ b/GPU/Workflow/src/GPUWorkflowInternal.h @@ -64,11 +64,12 @@ struct GPURecoWorkflowSpec_PipelineInternals { fair::mq::Device* fmqDevice = nullptr; volatile fair::mq::State fmqState = fair::mq::State::Undefined, fmqPreviousState = fair::mq::State::Undefined; - volatile bool endOfStreamAsyncReceived = false; + volatile bool endOfStreamAsyncWaiting = true; volatile bool endOfStreamDplReceived = false; volatile bool runStarted = false; volatile bool shouldTerminate = false; - std::mutex stateMutex; + volatile bool pipelineAbort = false; + std::mutex stateMutex, receiveMutex; std::condition_variable stateNotify; std::thread receiveThread; diff --git a/GPU/Workflow/src/GPUWorkflowPipeline.cxx b/GPU/Workflow/src/GPUWorkflowPipeline.cxx index f0aeb8089e27a..9ceda4adb043e 100644 --- a/GPU/Workflow/src/GPUWorkflowPipeline.cxx +++ b/GPU/Workflow/src/GPUWorkflowPipeline.cxx @@ -116,11 +116,15 @@ void GPURecoWorkflowSpec::enqueuePipelinedJob(GPUTrackingInOutPointers* ptrs, GP context->jobInputUpdateCallback = std::make_unique(); if (!inputFinal) { - context->jobInputUpdateCallback->callback = [context](GPUTrackingInOutPointers*& data, GPUInterfaceOutputs*& outputs) { + context->jobInputUpdateCallback->callback = [context, this](GPUTrackingInOutPointers*& data, GPUInterfaceOutputs*& outputs) -> int32_t { std::unique_lock lk(context->jobInputFinalMutex); - context->jobInputFinalNotify.wait(lk, [context]() { return context->jobInputFinal; }); + context->jobInputFinalNotify.wait(lk, [context, this]() { return context->jobInputFinal || mPipeline->pipelineAbort; }); + if (mPipeline->pipelineAbort) { + return 1; + } data = context->jobPtrs; outputs = context->jobOutputRegions; + return 0; }; } context->jobInputUpdateCallback->notifyCallback = [this]() { @@ -264,7 +268,33 @@ void GPURecoWorkflowSpec::handlePipelineEndOfStream(EndOfStreamContext& ec) void GPURecoWorkflowSpec::handlePipelineStop() { if (mSpecConfig.enableDoublePipeline == 1) { - mPipeline->mayInjectTFId = 0; + { + std::unique_lock lk(mPipeline->queueMutex); + mPipeline->pipelineAbort = mPipeline->pipelineQueue.size(); + } + if (mPipeline->pipelineAbort) { + mPipeline->pipelineQueue.front()->jobInputFinalNotify.notify_one(); + mGPUReco->DrainPipeline(); + { + std::unique_lock lk(mPipeline->queueMutex); + mPipeline->pipelineQueue = {}; + } + { + std::lock_guard lk(mPipeline->completionPolicyMutex); + mPipeline->completionPolicyQueue = {}; + } + mPipeline->pipelineAbort = false; + { + std::lock_guard lk(mPipeline->stateMutex); + mPipeline->endOfStreamAsyncWaiting = false; + mPipeline->mNTFReceived = 0; + mPipeline->runStarted = false; + } + } + { + std::unique_lock lk(mPipeline->mayInjectMutex); + mPipeline->mayInjectTFId = 0; + } } } @@ -273,16 +303,19 @@ void GPURecoWorkflowSpec::receiveFMQStateCallback(fair::mq::State newState) { std::lock_guard lk(mPipeline->stateMutex); if (mPipeline->fmqState != fair::mq::State::Running && newState == fair::mq::State::Running) { - mPipeline->endOfStreamAsyncReceived = false; + mPipeline->endOfStreamAsyncWaiting = true; mPipeline->endOfStreamDplReceived = false; } mPipeline->fmqPreviousState = mPipeline->fmqState; mPipeline->fmqState = newState; + } + mPipeline->stateNotify.notify_all(); + { + std::lock_guard lk(mPipeline->receiveMutex); if (newState == fair::mq::State::Exiting) { mPipeline->fmqDevice->UnsubscribeFromStateChange(GPURecoWorkflowSpec_FMQCallbackKey); } } - mPipeline->stateNotify.notify_all(); } void GPURecoWorkflowSpec::RunReceiveThread() @@ -293,7 +326,7 @@ void GPURecoWorkflowSpec::RunReceiveThread() int32_t recvTimeot = 1000; fair::mq::MessagePtr msg; LOG(debug) << "Waiting for out of band message"; - auto shouldReceive = [this]() { return ((mPipeline->fmqState == fair::mq::State::Running || (mPipeline->fmqState == fair::mq::State::Ready && mPipeline->fmqPreviousState == fair::mq::State::Running)) && !mPipeline->endOfStreamAsyncReceived); }; + auto shouldReceive = [this]() { return ((mPipeline->fmqState == fair::mq::State::Running || (mPipeline->fmqState == fair::mq::State::Ready && mPipeline->fmqPreviousState == fair::mq::State::Running)) && mPipeline->endOfStreamAsyncWaiting); }; do { { std::unique_lock lk(mPipeline->stateMutex); @@ -304,7 +337,7 @@ void GPURecoWorkflowSpec::RunReceiveThread() } try { do { - std::unique_lock lk(mPipeline->stateMutex); + std::unique_lock lk(mPipeline->receiveMutex); if (!shouldReceive()) { break; } @@ -327,10 +360,13 @@ void GPURecoWorkflowSpec::RunReceiveThread() } if (m->flagEndOfStream) { LOG(info) << "Received end-of-stream from out-of-band channel"; - std::lock_guard lk(mPipeline->stateMutex); - mPipeline->endOfStreamAsyncReceived = true; - mPipeline->mNTFReceived = 0; - mPipeline->runStarted = false; + { + std::lock_guard lk(mPipeline->stateMutex); + mPipeline->endOfStreamAsyncWaiting = false; + mPipeline->mNTFReceived = 0; + mPipeline->runStarted = false; + } + mPipeline->stateNotify.notify_all(); continue; } @@ -342,7 +378,7 @@ void GPURecoWorkflowSpec::RunReceiveThread() { std::unique_lock lk(mPipeline->stateMutex); - mPipeline->stateNotify.wait(lk, [this]() { return (mPipeline->runStarted && !mPipeline->endOfStreamAsyncReceived) || mPipeline->shouldTerminate; }); + mPipeline->stateNotify.wait(lk, [this]() { return (mPipeline->runStarted && mPipeline->endOfStreamAsyncWaiting) || mPipeline->shouldTerminate; }); if (!mPipeline->runStarted) { continue; } From 68019b1ac7a5031e9a0af70bd20c44168ac3ae9d Mon Sep 17 00:00:00 2001 From: David Rohr Date: Sat, 25 Jul 2026 00:25:02 +0200 Subject: [PATCH 39/47] GPU Workflow: Fix API usage of fairmq message --- GPU/Workflow/src/GPUWorkflowPipeline.cxx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/GPU/Workflow/src/GPUWorkflowPipeline.cxx b/GPU/Workflow/src/GPUWorkflowPipeline.cxx index 9ceda4adb043e..305772331abb3 100644 --- a/GPU/Workflow/src/GPUWorkflowPipeline.cxx +++ b/GPU/Workflow/src/GPUWorkflowPipeline.cxx @@ -199,15 +199,17 @@ int32_t GPURecoWorkflowSpec::handlePipeline(ProcessingContext& pc, GPUTrackingIn } size_t prepareBufferSize = sizeof(pipelinePrepareMessage) + ptrsTotal * sizeof(size_t) * 4; - std::vector messageBuffer(prepareBufferSize / sizeof(size_t)); - pipelinePrepareMessage& preMessage = *(pipelinePrepareMessage*)messageBuffer.data(); + fair::mq::MessagePtr payload(device->NewMessage()); + payload->Rebuild(prepareBufferSize, fair::mq::Alignment(sizeof(size_t))); + auto* messageBuffer = (size_t*)payload->GetData(); + pipelinePrepareMessage& preMessage = *(pipelinePrepareMessage*)messageBuffer; preMessage.magicWord = preMessage.MAGIC_WORD; preMessage.timeSliceId = tinfo.timeslice; preMessage.pointersTotal = ptrsTotal; preMessage.flagEndOfStream = false; memcpy((void*)&preMessage.tfSettings, (const void*)ptrs.settingsTF, sizeof(preMessage.tfSettings)); - size_t* ptrBuffer = messageBuffer.data() + sizeof(preMessage) / sizeof(size_t); + size_t* ptrBuffer = messageBuffer + sizeof(preMessage) / sizeof(size_t); size_t ptrsCopied = 0; int32_t lastRegion = -1; for (uint32_t i = 0; i < GPUTrackingInOutZS::NSECTORS; i++) { @@ -238,9 +240,7 @@ int32_t GPURecoWorkflowSpec::handlePipeline(ProcessingContext& pc, GPUTrackingIn } auto channel = device->GetChannels().find("gpu-prepare-channel"); - fair::mq::MessagePtr payload(device->NewMessage()); LOG(info) << "Sending gpu-reco-workflow prepare message of size " << prepareBufferSize; - payload->Rebuild(messageBuffer.data(), prepareBufferSize, nullptr, nullptr); channel->second[0].Send(payload); return 2; } @@ -255,12 +255,13 @@ void GPURecoWorkflowSpec::handlePipelineEndOfStream(EndOfStreamContext& ec) } if (mSpecConfig.enableDoublePipeline == 2) { auto* device = ec.services().get().device(); - pipelinePrepareMessage preMessage; - preMessage.flagEndOfStream = true; - auto channel = device->GetChannels().find("gpu-prepare-channel"); fair::mq::MessagePtr payload(device->NewMessage()); + payload->Rebuild(sizeof(pipelinePrepareMessage), fair::mq::Alignment(alignof(pipelinePrepareMessage))); + auto* preMessage = (pipelinePrepareMessage*)payload->GetData(); + new (preMessage) pipelinePrepareMessage; + preMessage->flagEndOfStream = true; + auto channel = device->GetChannels().find("gpu-prepare-channel"); LOG(info) << "Sending end-of-stream message over out-of-bands channel"; - payload->Rebuild(&preMessage, sizeof(preMessage), nullptr, nullptr); channel->second[0].Send(payload); } } From 980546b33b5cca8e51ffe85da7aeb129f740e9c1 Mon Sep 17 00:00:00 2001 From: Igor Altsybeev Date: Mon, 27 Jul 2026 09:50:01 +0200 Subject: [PATCH 40/47] move OT barrel service 'disk' z from 132 to 142 cm to have space for readout cards (#15635) --- Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKServices.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKServices.cxx b/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKServices.cxx index e3855dbde6535..fb436cd473021 100644 --- a/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKServices.cxx +++ b/Detectors/Upgrades/ALICE3/TRK/simulation/src/TRKServices.cxx @@ -922,7 +922,7 @@ void TRKServices::createOTServicesPeacock(TGeoVolume* motherVolume) // geometry of service "disk" for OT barrel double rMinOTbarrelServices = 45.0; // cm, radius of first OT barrel layer double rMaxOTbarrelServices = 78.0; // cm, radius of last OT barrel layer - double zOTbarrelServices = 132.0; // cm, approximate position of OT services in z + double zOTbarrelServices = 142.0; // cm, approximate position of OT services in z // geometry of service "tubes" for OT barrel float rMinOuterBarrelTubeServices = rMaxOTbarrelServices; // cm, IA, May 11, 2026: temporary radius (?) From 233cc2e80b11cf550a814950b800550eb941b2d2 Mon Sep 17 00:00:00 2001 From: shahor02 Date: Mon, 27 Jul 2026 16:04:45 +0400 Subject: [PATCH 41/47] Do not request unused Propagator (#15638) --- Detectors/TPC/workflow/src/CalibratordEdxSpec.cxx | 1 - 1 file changed, 1 deletion(-) diff --git a/Detectors/TPC/workflow/src/CalibratordEdxSpec.cxx b/Detectors/TPC/workflow/src/CalibratordEdxSpec.cxx index dea1d85899675..fc4c38b51bcd9 100644 --- a/Detectors/TPC/workflow/src/CalibratordEdxSpec.cxx +++ b/Detectors/TPC/workflow/src/CalibratordEdxSpec.cxx @@ -226,7 +226,6 @@ DataProcessorSpec getCalibratordEdxSpec(const o2::base::Propagator::MatCorrType enableAskMatLUT, // askMatLUT o2::base::GRPGeomRequest::None, // geometry inputs, - true, true); return DataProcessorSpec{ "tpc-calibrator-dEdx", From 4e941926285371453409ac89f690d793296d6db9 Mon Sep 17 00:00:00 2001 From: Mario Ciacco Date: Tue, 28 Jul 2026 05:59:56 +0200 Subject: [PATCH 42/47] [ALICE3] TF3: approaching TF3 ASIC: use 8-chip modules + update chip segmentation (#15639) * approach TF3 ASIC size by using 8-chip modules + update chip segmentation parameters * fix parameter + update number of sensors in macro --- .../base/include/IOTOFBase/IOTOFBaseParam.h | 7 +++-- .../ALICE3/IOTOF/macros/CheckDigitsIOTOF.C | 31 +++++++------------ .../ALICE3/IOTOF/simulation/src/Layer.cxx | 10 +++--- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/Detectors/Upgrades/ALICE3/IOTOF/base/include/IOTOFBase/IOTOFBaseParam.h b/Detectors/Upgrades/ALICE3/IOTOF/base/include/IOTOFBase/IOTOFBaseParam.h index 22454e0db0f48..7d3dbdc1bd1dc 100644 --- a/Detectors/Upgrades/ALICE3/IOTOF/base/include/IOTOFBase/IOTOFBaseParam.h +++ b/Detectors/Upgrades/ALICE3/IOTOF/base/include/IOTOFBase/IOTOFBaseParam.h @@ -41,7 +41,7 @@ struct ChipSpecifics { struct ITOFChipSpecifics : ChipSpecifics { ITOFChipSpecifics() { - NCols = 258; + NCols = 129; NRows = 271; PitchCol = 250.00e-4; PitchRow = 100.00e-4; @@ -53,11 +53,12 @@ struct ITOFChipSpecifics : ChipSpecifics { struct OTOFChipSpecifics : ChipSpecifics { OTOFChipSpecifics() { - NCols = 517; + NCols = 125; NRows = 243; PitchCol = 250.00e-4; PitchRow = 100.00e-4; - PassiveEdgeSide = 106.48e-4; + PassiveEdgeTop = 50.e-4; + PassiveEdgeSide = 115.8e-4; SensorLayerThicknessEff = 50.e-4; SensorLayerThickness = 50.e-4; } diff --git a/Detectors/Upgrades/ALICE3/IOTOF/macros/CheckDigitsIOTOF.C b/Detectors/Upgrades/ALICE3/IOTOF/macros/CheckDigitsIOTOF.C index cd9e806ca24e7..26ffd08697d56 100644 --- a/Detectors/Upgrades/ALICE3/IOTOF/macros/CheckDigitsIOTOF.C +++ b/Detectors/Upgrades/ALICE3/IOTOF/macros/CheckDigitsIOTOF.C @@ -101,10 +101,6 @@ void CheckDigitsIOTOF(std::string digifile = "tf3digits.root", std::string hitfi auto* gman = o2::iotof::GeometryTGeo::Instance(); gman->fillMatrixCache(o2::math_utils::bit2Mask(o2::math_utils::TransformType::L2G)); - // IOTOF response plane y = half sensor thickness - float depthMax = iotofPars.sensorThickness; // fallback (no CCDB) - const float yPlaneIOTOF = depthMax / 2.; - // Hits TFile* hitFile = TFile::Open(hitfile.data()); TTree* hitTree = (TTree*)hitFile->Get("o2sim"); @@ -162,7 +158,6 @@ void CheckDigitsIOTOF(std::string digifile = "tf3digits.root", std::string hitfi Int_t subDetID = gman->getIOTOFLayer(iDetID); Float_t x = 0.f, y = 0.f, z = 0.f; - Float_t x_flat = 0.f, z_flat = 0.f; // Float_t t = (*digArr)[iDigit].getTime(); @@ -207,13 +202,9 @@ void CheckDigitsIOTOF(std::string digifile = "tf3digits.root", std::string hitfi locHS.SetCoordinates(xyzLocS.X(), xyzLocS.Y(), xyzLocS.Z()); o2::math_utils::Vector3D locHE; /// Hit, end pos locHE.SetCoordinates(xyzLocE.X(), xyzLocE.Y(), xyzLocE.Z()); - o2::math_utils::Vector3D locHF; - // IOTOF: Interpolate to response plane - float x0 = locHS.X(), y0 = locHS.Y(), z0 = locHS.Z(); - float dltx = locHE.X() - x0, dlty = locHE.Y() - y0, dltz = locHE.Z() - z0; - float r = (std::abs(dlty) > 1e-9f) ? (yPlaneIOTOF - y0) / dlty : 0.5f; - locH.SetCoordinates(x0 + r * dltx, yPlaneIOTOF, z0 + r * dltz); + // IOTOF: Interpolate to mid point + locH.SetCoordinates(0.5 * (locHS.X() + locHE.X()), 0.5 * (locHS.Y() + locHE.Y()), 0.5 * (locHS.Z() + locHE.Z())); int row = 0, col = 0; float xlc = 0., zlc = 0.; @@ -226,7 +217,7 @@ void CheckDigitsIOTOF(std::string digifile = "tf3digits.root", std::string hitfi row, col, /// row and col retrieved from the hit: hit global position -> hit local position -> detector position (row, col) locH.X(), locH.Z(), /// x and z of the hit in the local reference frame: hit global position -> hit local position xlc, zlc, /// x and z of the hit in the local frame: hit global position -> hit local position -> detector position (row, col) -> local position - locHS.X() - locD.X(), locHS.Z() - locD.Z()); /// difference in x and z between the hit and the digit in the local frame + locH.X() - locD.X(), locH.Z() - locD.Z()); /// difference in x and z between the hit and the digit in the local frame nt2->Fill(chipID, gloD.Z(), locHS.X() - locHE.X(), locHS.Z() - locHE.Z()); /// differences between local hit start and hit end positions } // end loop on digits array @@ -237,21 +228,21 @@ void CheckDigitsIOTOF(std::string digifile = "tf3digits.root", std::string hitfi auto canvXY = new TCanvas("canvXY", "", 1600, 800); canvXY->Divide(2, 1); canvXY->cd(1); - nt->Draw("y:x>>h_y_vs_x_IOTOF(1000, -100, 100, 1000, -100, 100)", "id >= 0 && id < 14352", "colz"); + nt->Draw("y:x>>h_y_vs_x_IOTOF(1000, -100, 100, 1000, -100, 100)", "id >= 0 && id < 53568", "colz"); canvXY->cd(2); - nt->Draw("y:z>>h_y_vs_z_IOTOF(1000, -400, 400, 1000, -100, 100)", "id >= 0 && id < 14352", "colz"); + nt->Draw("y:z>>h_y_vs_z_IOTOF(1000, -400, 400, 1000, -100, 100)", "id >= 0 && id < 53568", "colz"); canvXY->SaveAs("tf3digits_y_vs_x_vs_z.pdf"); // z distributions auto canvZ = new TCanvas("canvZ", "", 800, 800); canvZ->cd(); - nt->Draw("z>>h_z_IOTOF(500, -70, 70)", "id >= 0 && id < 14352 "); + nt->Draw("z>>h_z_IOTOF(500, -70, 70)", "id >= 0 && id < 53568 "); canvZ->SaveAs("tf3digits_z.pdf"); // dz distributions (difference between local position of digits and hits in x and z) auto canvdZ = new TCanvas("canvdZ", "", 800, 800); canvdZ->cd(); - nt->Draw("dz>>h_dz_ML(500, -0.05, 0.05)", "id >= 0 && id < 14352 "); + nt->Draw("dz>>h_dz_ML(500, -0.05, 0.05)", "id >= 0 && id < 53568 "); canvdZ->SaveAs("tf3digits_dz.pdf"); canvdZ->SaveAs("tf3digits_dz.root"); @@ -259,13 +250,13 @@ void CheckDigitsIOTOF(std::string digifile = "tf3digits.root", std::string hitfi auto canvdXdZ = new TCanvas("canvdXdZ", "", 1600, 800); canvdXdZ->Divide(2, 1); canvdXdZ->cd(1); - nt->Draw("dx:dz>>h_dx_vs_dz_ITOF(600, -0.03, 0.03, 600, -0.03, 0.03)", "id >= 0 && id < 960", "colz"); + nt->Draw("dx:dz>>h_dx_vs_dz_ITOF(600, -0.03, 0.03, 600, -0.03, 0.03)", "id >= 0 && id < 1920", "colz"); addTLines(0.01); auto h = (TH2F*)gPad->GetPrimitive("h_dx_vs_dz_ITOF"); Info("ITOF", "RMS(dx)=%.1f mu", h->GetRMS(2) * 1e4); Info("ITOF", "RMS(dz)=%.1f mu", h->GetRMS(1) * 1e4); canvdXdZ->cd(2); - nt->Draw("dx:dz>>h_dx_vs_dz_OTOF(600, -0.03, 0.03, 600, -0.03, 0.03)", "id >= 960 && id < 14352", "colz"); + nt->Draw("dx:dz>>h_dx_vs_dz_OTOF(600, -0.03, 0.03, 600, -0.03, 0.03)", "id >= 1920 && id < 53568", "colz"); addTLines(0.01); h = (TH2F*)gPad->GetPrimitive("h_dx_vs_dz_OTOF"); Info("OTOF", "RMS(dx)=%.1f mu", h->GetRMS(2) * 1e4); @@ -278,13 +269,13 @@ void CheckDigitsIOTOF(std::string digifile = "tf3digits.root", std::string hitfi canvdXdZHit->Divide(2, 1); canvdXdZHit->cd(1); LOG(info) << "dxH, dzH"; - nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_ITOF(300, -0.03, 0.03, 300, -0.03, 0.03)", "id >= 0 && id < 960", "colz"); + nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_ITOF(300, -0.03, 0.03, 300, -0.03, 0.03)", "id >= 0 && id < 1920", "colz"); addTLines(0.01); h = (TH2F*)gPad->GetPrimitive("h_dxH_vs_dzH_ITOF"); Info("ITOF", "RMS(dxH)=%.1f mu", h->GetRMS(2) * 1e4); Info("ITOF", "RMS(dzH)=%.1f mu", h->GetRMS(1) * 1e4); canvdXdZHit->cd(2); - nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_OTOF(300, -0.03, 0.03, 300, -0.03, 0.03)", "id >= 960 && id < 14352", "colz"); + nt2->Draw("dxH:dzH>>h_dxH_vs_dzH_OTOF(300, -0.03, 0.03, 300, -0.03, 0.03)", "id >= 1920 && id < 53568", "colz"); addTLines(0.01); h = (TH2F*)gPad->GetPrimitive("h_dxH_vs_dzH_OTOF"); Info("OTOF", "RMS(dxH)=%.1f mu", h->GetRMS(2) * 1e4); diff --git a/Detectors/Upgrades/ALICE3/IOTOF/simulation/src/Layer.cxx b/Detectors/Upgrades/ALICE3/IOTOF/simulation/src/Layer.cxx index 0c249f6fefe18..f63c93d37ce75 100644 --- a/Detectors/Upgrades/ALICE3/IOTOF/simulation/src/Layer.cxx +++ b/Detectors/Upgrades/ALICE3/IOTOF/simulation/src/Layer.cxx @@ -178,7 +178,7 @@ void ITOFLayer::createLayer(TGeoVolume* motherVolume) setStaveStyle(staveVol); // Now we create the volume for a single module (sensor + chip) - const int modulesPerStaveX = 1; // we assume that each stave is divided in 2 modules along the x direction + const int modulesPerStaveX = 1; // we assume that each stave is divided in 1 modules along the x direction const double moduleSizeX = staveSizeX / modulesPerStaveX; // cm const double moduleSizeY = staveSizeY; // cm const double moduleSizeZ = staveSizeZ / mModulesPerStave; // cm @@ -188,7 +188,7 @@ void ITOFLayer::createLayer(TGeoVolume* motherVolume) // Now we create the volume of the chip, which is the same for all modules const int chipsPerModuleX = 2; // we assume that each module is divided in 2 chips along the x direction - const int chipsPerModuleZ = 2; // we assume that each module is divided in 2 chips along the z direction + const int chipsPerModuleZ = 4; // we assume that each module is divided in 2 chips along the z direction const double chipSizeX = moduleSizeX / chipsPerModuleX; // cm const double chipSizeY = moduleSizeY; // cm const double chipSizeZ = moduleSizeZ / chipsPerModuleZ; // cm @@ -324,13 +324,13 @@ void OTOFLayer::createLayer(TGeoVolume* motherVolume) setStaveStyle(staveVol); // Now we create the volume for a single module (sensor + chip) - // oTOF V2 is a 2xN matrix of modules per stave with overlap along z. + // oTOF V2 is a 2xN matrix. const int modulesPerStaveX = 2; if (mModulesPerStave % modulesPerStaveX != 0) { LOG(fatal) << "Invalid oTOF module layout: total modules per stave " << mModulesPerStave << " is not divisible by modulesPerStaveX=" << modulesPerStaveX; } - const int modulesPerStaveZ = mModulesPerStave / modulesPerStaveX; + const int modulesPerStaveZ = 2 * mModulesPerStave / modulesPerStaveX; const double moduleSizeX = staveSizeX / modulesPerStaveX; const double moduleSizeY = staveSizeY; const double moduleSizeZ = staveSizeZ / modulesPerStaveZ; @@ -340,7 +340,7 @@ void OTOFLayer::createLayer(TGeoVolume* motherVolume) // Now we create the volume of the chip, which is the same for all modules const int chipsPerModuleX = 2; // we assume that each module is divided in 2 chips along the x direction - const int chipsPerModuleZ = 2; // we assume that each module is divided in 2 chips along the z direction + const int chipsPerModuleZ = 4; // we assume that each module is divided in 2 chips along the z direction const double chipSizeX = moduleSizeX / chipsPerModuleX; // cm const double chipSizeY = moduleSizeY; // cm const double chipSizeZ = moduleSizeZ / chipsPerModuleZ; // cm From 2d7c7319560425dbbfbbb8a0460cac4681cbdb08 Mon Sep 17 00:00:00 2001 From: Autumn McKee Date: Tue, 28 Jul 2026 10:00:32 +0200 Subject: [PATCH 43/47] Propagate bulk read errors (#15624) --- Framework/AnalysisSupport/src/TTreePlugin.cxx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Framework/AnalysisSupport/src/TTreePlugin.cxx b/Framework/AnalysisSupport/src/TTreePlugin.cxx index 1a6f48ebef5b4..fa291ce8cbebc 100644 --- a/Framework/AnalysisSupport/src/TTreePlugin.cxx +++ b/Framework/AnalysisSupport/src/TTreePlugin.cxx @@ -211,6 +211,9 @@ auto readBoolValues = [](uint8_t* target, ReadOps& op, TBufferFile& rootBuffer) while (readEntries < op.rootBranchEntries) { auto beginValue = readEntries; readLast = op.branch->GetBulkRead().GetBulkEntries(readEntries, rootBuffer); + if (readLast < 0) { + throw runtime_error_f("Error while reading branch %s starting from %d.", op.branch->GetName(), readEntries); + } int size = readLast * op.listSize; readEntries += readLast; for (int i = beginValue; i < beginValue + size; ++i) { @@ -228,7 +231,18 @@ auto readVLAValues = [](uint8_t* target, ReadOps& op, ReadOps const& offsetOp, T rootBuffer.Reset(); while (readEntries < op.rootBranchEntries) { auto readLast = op.branch->GetBulkRead().GetEntriesSerialized(readEntries, rootBuffer); + if (readLast < 0) { + throw runtime_error_f("Error while reading branch %s starting from %d.", op.branch->GetName(), readEntries); + } + if (readEntries + readLast > op.rootBranchEntries) { + throw runtime_error_f("Invalid read range for branch %s: starting from %d, read %d entries, total entries %lld.", + op.branch->GetName(), readEntries, readLast, static_cast(op.rootBranchEntries)); + } int size = offsets[readEntries + readLast] - offsets[readEntries]; + if (size < 0) { + throw runtime_error_f("Invalid offset range for branch %s: offsets[%d]=%d, offsets[%d]=%d.", + op.branch->GetName(), readEntries, offsets[readEntries], readEntries + readLast, offsets[readEntries + readLast]); + } readEntries += readLast; bigEndianCopy(target, rootBuffer.GetCurrent(), size, op.typeSize); target += (ptrdiff_t)(size * op.typeSize); From b6647b4fb95a8c2793835da476cc5c1e5fea7501 Mon Sep 17 00:00:00 2001 From: shahor02 Date: Tue, 28 Jul 2026 15:46:26 +0400 Subject: [PATCH 44/47] Fix in Propagator::initFieldFromGRP (#15637) The field normally should not be deleted since this would invalidate the field pointers cached elsewhere (e.g. by the double version of the Propagator if it was requested). Instead, the existing field should be scaled by the newly provided current values. The exception is the marginal case when the scaling is not possible, e.g. when switching from 5kGauss map to 2kGauss (which should be avoided for other reasons). In this case the field will be recreated and the warning will be printed about possible invalidation of cached pointers. --- .../Base/include/DetectorsBase/Propagator.h | 2 +- Detectors/Base/src/Propagator.cxx | 77 ++++++++++--------- 2 files changed, 43 insertions(+), 36 deletions(-) diff --git a/Detectors/Base/include/DetectorsBase/Propagator.h b/Detectors/Base/include/DetectorsBase/Propagator.h index 75b9446aebade..377094cc368b8 100644 --- a/Detectors/Base/include/DetectorsBase/Propagator.h +++ b/Detectors/Base/include/DetectorsBase/Propagator.h @@ -181,9 +181,9 @@ class PropagatorImpl return &instance; } static int initFieldFromGRP(const o2::parameters::GRPMagField* grp, bool verbose = false); - static int initFieldFromGRP(const o2::parameters::GRPObject* grp, bool verbose = false); static int initFieldFromGRP(const std::string grpFileName = "", bool verbose = false); + static int initFieldFromGRP(float currL3, float currDip, bool uniform, bool verbose = false); #endif GPUd() MatBudget getMatBudget(MatCorrType corrType, const o2::math_utils::Point3D& p0, const o2::math_utils::Point3D& p1) const; diff --git a/Detectors/Base/src/Propagator.cxx b/Detectors/Base/src/Propagator.cxx index 208b9bf138688..a465bbc312c77 100644 --- a/Detectors/Base/src/Propagator.cxx +++ b/Detectors/Base/src/Propagator.cxx @@ -84,7 +84,6 @@ int PropagatorImpl::initFieldFromGRP(const std::string grpFileName, boo if (verbose) { grp->print(); } - return initFieldFromGRP(grp); } @@ -92,27 +91,7 @@ int PropagatorImpl::initFieldFromGRP(const std::string grpFileName, boo template int PropagatorImpl::initFieldFromGRP(const o2::parameters::GRPObject* grp, bool verbose) { - /// init mag field from GRP data and attach it to TGeoGlobalMagField - - if (TGeoGlobalMagField::Instance()->IsLocked()) { - if (TGeoGlobalMagField::Instance()->GetField()->TestBit(o2::field::MagneticField::kOverrideGRP)) { - LOG(warning) << "ExpertMode!!! GRP information will be ignored"; - LOG(warning) << "ExpertMode!!! Running with the externally locked B field"; - return 0; - } else { - LOG(info) << "Destroying existing B field instance"; - delete TGeoGlobalMagField::Instance(); - } - } - auto fld = o2::field::MagneticField::createFieldMap(grp->getL3Current(), grp->getDipoleCurrent(), o2::field::MagneticField::kConvLHC, grp->getFieldUniformity()); - TGeoGlobalMagField::Instance()->SetField(fld); - TGeoGlobalMagField::Instance()->Lock(); - if (verbose) { - LOG(info) << "Running with the B field constructed out of GRP"; - LOG(info) << "Access field via TGeoGlobalMagField::Instance()->Field(xyz,bxyz) or via"; - LOG(info) << "auto o2field = static_cast( TGeoGlobalMagField::Instance()->GetField() )"; - } - return 0; + return initFieldFromGRP(grp->getL3Current(), grp->getDipoleCurrent(), grp->getFieldUniformity(), verbose); } //____________________________________________________________ @@ -120,25 +99,53 @@ template int PropagatorImpl::initFieldFromGRP(const o2::parameters::GRPMagField* grp, bool verbose) { /// init mag field from GRP data and attach it to TGeoGlobalMagField + return initFieldFromGRP(grp->getL3Current(), grp->getDipoleCurrent(), grp->getFieldUniformity(), verbose); +} - if (TGeoGlobalMagField::Instance()->IsLocked()) { - if (TGeoGlobalMagField::Instance()->GetField()->TestBit(o2::field::MagneticField::kOverrideGRP)) { - LOG(warning) << "ExpertMode!!! GRP information will be ignored"; - LOG(warning) << "ExpertMode!!! Running with the externally locked B field"; - return 0; +//____________________________________________________________ +template +int PropagatorImpl::initFieldFromGRP(float currL3, float currDip, bool uniform, bool verbose) +{ + /// init mag field from GRP data and attach it to TGeoGlobalMagField or rescale already updated field + auto fldGlo = static_cast(TGeoGlobalMagField::Instance()->GetField()); + if (fldGlo) { // global field object was already initialized, reuse it if it is locked (as it normally should be) + float _currL3(currL3), _currDip(currDip); + auto newFieldType = fldGlo->getFieldMapScale(_currL3, _currDip, uniform); + bool sameFieldType = newFieldType == fldGlo->getMapType(); + if (!sameFieldType) { + LOGP(warn, "Existing B-field type {} cannot be rescaled to type {} requested by the GRP", int(fldGlo->getMapType()), int(newFieldType)); + } + if (TGeoGlobalMagField::Instance()->IsLocked() && sameFieldType) { + if (Instance()->mField && Instance()->mField != fldGlo) { // just make sure that cached field is the same as the global one + std::string name{"PropagatorF"}; + if constexpr (std::is_same_v) { + std::string name{"PropagatorD"}; + } + LOGP(fatal, "Magnetic field pointer cached in the {} instance differs from the gloabal field pointer", name); + } + if (verbose) { + LOGP(info, "Rescaling magnetic field to currents L3: {}, Dipole: {}, UniformityFlag: {}", currL3, currDip, uniform); + } + fldGlo->rescaleField(currL3, currDip, uniform); } else { - LOG(info) << "Destroying existing B field instance"; + LOGP(warn, "Destroying existing B field instance. This may invalidate field pointer cached in other objects"); delete TGeoGlobalMagField::Instance(); + Instance()->mField = nullptr; + Instance()->mFieldFast = nullptr; + fldGlo = nullptr; } } - auto fld = o2::field::MagneticField::createFieldMap(grp->getL3Current(), grp->getDipoleCurrent(), o2::field::MagneticField::kConvLHC, grp->getFieldUniformity()); - TGeoGlobalMagField::Instance()->SetField(fld); - TGeoGlobalMagField::Instance()->Lock(); - if (verbose) { - LOG(info) << "Running with the B field constructed out of GRP"; - LOG(info) << "Access field via TGeoGlobalMagField::Instance()->Field(xyz,bxyz) or via"; - LOG(info) << "auto o2field = static_cast( TGeoGlobalMagField::Instance()->GetField() )"; + if (!fldGlo) { + fldGlo = o2::field::MagneticField::createFieldMap(currL3, currDip, o2::field::MagneticField::kConvLHC, uniform); + TGeoGlobalMagField::Instance()->SetField(fldGlo); + TGeoGlobalMagField::Instance()->Lock(); + if (verbose) { + LOG(info) << "Running with the B field constructed out of GRP"; + LOG(info) << "Access field via TGeoGlobalMagField::Instance()->Field(xyz,bxyz) or via"; + LOG(info) << "auto o2field = static_cast( TGeoGlobalMagField::Instance()->GetField() )"; + } } + Instance()->updateField(); return 0; } From 0de4119b067ec438db5abac0bf42c23d63f3281a Mon Sep 17 00:00:00 2001 From: Matthias Kleiner <48915672+matthias-kleiner@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:11:19 +0200 Subject: [PATCH 45/47] TPC: Adding macros to create correction maps on the Grid (#15644) staticMapCreatorCPM.C creates the correction map from the unbinned residuals via the crossing point method SmoothingExtrapolate.C Performs gaussian smoothing and extrapolation to small unmeasured radii voxResQA.C Makes default QA plots TPCFastTransformInitCPM.C converts the correction maps to the final splines --- .../calibration/SpacePoints/CMakeLists.txt | 31 + .../SpacePoints/macro/SmoothingExtrapolate.C | 1002 +++++++ .../SpacePoints/macro/staticMapCreatorCPM.C | 2564 +++++++++++++++++ .../calibration/SpacePoints/macro/voxResQA.C | 445 +++ GPU/TPCFastTransformation/CMakeLists.txt | 19 + .../macro/TPCFastTransformInitCPM.C | 740 +++++ 6 files changed, 4801 insertions(+) create mode 100644 Detectors/TPC/calibration/SpacePoints/macro/SmoothingExtrapolate.C create mode 100644 Detectors/TPC/calibration/SpacePoints/macro/staticMapCreatorCPM.C create mode 100644 Detectors/TPC/calibration/SpacePoints/macro/voxResQA.C create mode 100644 GPU/TPCFastTransformation/macro/TPCFastTransformInitCPM.C diff --git a/Detectors/TPC/calibration/SpacePoints/CMakeLists.txt b/Detectors/TPC/calibration/SpacePoints/CMakeLists.txt index 47bb9c09a9951..ac33a0b632dab 100644 --- a/Detectors/TPC/calibration/SpacePoints/CMakeLists.txt +++ b/Detectors/TPC/calibration/SpacePoints/CMakeLists.txt @@ -43,9 +43,40 @@ o2_add_test_root_macro(macro/staticMapCreator.C PUBLIC_LINK_LIBRARIES O2::SpacePoints LABELS tpc COMPILE_ONLY) +o2_add_test_root_macro(macro/staticMapCreatorCPM.C + PUBLIC_LINK_LIBRARIES O2::SpacePoints + O2::CCDB + O2::Algorithm + O2::Framework + O2::CommonConstants + O2::DataFormatsParameters + O2::ReconstructionDataFormats + O2::DetectorsBase + LABELS tpc COMPILE_ONLY) + +o2_add_test_root_macro(macro/SmoothingExtrapolate.C + PUBLIC_LINK_LIBRARIES O2::SpacePoints + O2::Algorithm + O2::TPCCalibration + O2::CCDB + O2::TPCBaseRecSim + LABELS tpc COMPILE_ONLY) + +o2_add_test_root_macro(macro/voxResQA.C + PUBLIC_LINK_LIBRARIES O2::SpacePoints + O2::TPCBaseRecSim + O2::CommonUtils + O2::Framework + LABELS tpc COMPILE_ONLY) + install(FILES macro/staticMapCreator.C DESTINATION share/macro/) +install(FILES macro/staticMapCreatorCPM.C + macro/SmoothingExtrapolate.C + macro/voxResQA.C + DESTINATION share/macro/) + o2_add_test(TrackResiduals COMPONENT_NAME calibration PUBLIC_LINK_LIBRARIES O2::SpacePoints diff --git a/Detectors/TPC/calibration/SpacePoints/macro/SmoothingExtrapolate.C b/Detectors/TPC/calibration/SpacePoints/macro/SmoothingExtrapolate.C new file mode 100644 index 0000000000000..2af42e68898ea --- /dev/null +++ b/Detectors/TPC/calibration/SpacePoints/macro/SmoothingExtrapolate.C @@ -0,0 +1,1002 @@ +// 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. + +#if !defined(__CLING__) || defined(__ROOTCLING__) + +#include +#include +#include +#include "TFile.h" +#include "TSystem.h" +#include "TTree.h" +#include "TF1.h" +#include "TGraph.h" +#include "TProfile.h" +#include "Math/MinimizerOptions.h" + +#include "Algorithm/RangeTokenizer.h" +#include "SpacePoints/TrackResiduals.h" +#include "TPCCalibration/TPCFastSpaceChargeCorrectionHelper.h" +#include "CCDB/BasicCCDBManager.h" +#include "TPCCalibration/TPCScaler.h" +#if __has_include("TPCBaseRecSim/CDBTypes.h") +#include "TPCBaseRecSim/CDBTypes.h" +#else +#include "TPCBase/CDBTypes.h" +#endif + +#endif + +using namespace o2::tpc; +using namespace o2::gpu; + +//------------------------------------------------------------------------------------------------------------ +static const Float_t RowX[153] = + { + 85.225, 85.975, 86.725, 87.475, 88.225, 88.975, 89.725, 90.475, 91.225, 91.975, 92.725, 93.475, 94.225, 94.975, 95.725, 96.475, + 97.225, 97.975, 98.725, 99.475, 100.225, 100.975, 101.725, 102.475, 103.225, 103.975, 104.725, 105.475, 106.225, 106.975, 107.725, + 108.475, 109.225, 109.975, 110.725, 111.475, 112.225, 112.975, 113.725, 114.475, 115.225, 115.975, 116.725, 117.475, 118.225, 118.975, + 119.725, 120.475, 121.225, 121.975, 122.725, 123.475, 124.225, 124.975, 125.725, 126.475, 127.225, 127.975, 128.725, 129.475, 130.225, + 130.975, 131.725, 135.200, 136.200, 137.200, 138.200, 139.200, 140.200, 141.200, 142.200, 143.200, 144.200, 145.200, 146.200, 147.200, + 148.200, 149.200, 150.200, 151.200, 152.200, 153.200, 154.200, 155.200, 156.200, 157.200, 158.200, 159.200, 160.200, 161.200, 162.200, + 163.200, 164.200, 165.200, 166.200, 167.200, 168.200, 171.400, 172.600, 173.800, 175.000, 176.200, 177.400, 178.600, 179.800, 181.000, + 182.200, 183.400, 184.600, 185.800, 187.000, 188.200, 189.400, 190.600, 191.800, 193.000, 194.200, 195.400, 196.600, 197.800, 199.000, + 200.200, 201.400, 202.600, 203.800, 205.000, 206.200, 209.650, 211.150, 212.650, 214.150, 215.650, 217.150, 218.650, 220.150, 221.650, + 223.150, 224.650, 226.150, 227.650, 229.150, 230.650, 232.150, 233.650, 235.150, 236.650, 238.150, 239.650, 241.150, 242.650, 244.150, + 245.650, 246.650}; // last value added +//------------------------------------------------------------------------------------------------------------ + +//---------------------------------------------------------------------------------------- +Double_t PolyFitFunc(Double_t* x_val, Double_t* par) +{ + Double_t x, y, par0, par1, par2, par3, par4, par5; + par0 = par[0]; + par1 = par[1]; + par2 = par[2]; + par3 = par[3]; + par4 = par[4]; + par5 = par[5]; + x = x_val[0]; + y = par0 + par1 * x + par2 * x * x + par3 * x * x * x + par4 * x * x * x * x + par5 * x * x * x * x * x; + return y; +} +//---------------------------------------------------------------------------------------- + +// Function to create Gaussian filter +void vec_FilterCreation(std::vector>>& vec_GKernel, Int_t Delta_X, Int_t Delta_Y, Int_t Delta_Z, Double_t sigma) +{ + // initialising standard deviation to 1.0 + // double sigma = 1.0; + double r, s = 2.0 * sigma * sigma; + + // sum is for normalization + double sum = 0.0; + + // generating 5x5 kernel + for (int x = -Delta_X; x <= Delta_X; x++) { + for (int y = -Delta_Y; y <= Delta_Y; y++) { + for (int z = -Delta_Z; z <= Delta_Z; z++) { + r = sqrt(x * x + y * y + z * z); + vec_GKernel[x + Delta_X][y + Delta_Y][z + Delta_Z] = (exp(-(r * r) / s)) / (M_PI * s); + sum += vec_GKernel[x + Delta_X][y + Delta_Y][z + Delta_Z]; + } + } + } + + // normalising the Kernel + for (int i = 0; i < (Delta_X * 2 + 1); ++i) { + for (int j = 0; j < (Delta_Y * 2 + 1); ++j) { + for (int k = 0; k < (Delta_Z * 2 + 1); ++k) { + vec_GKernel[i][j][k] /= sum; + } + } + } +} + +// Mean TPC scaler (IDC) values for one timestamp, from the standard CCDB CalScaler/CalScalerWeights +// objects. Used for the offline IDC join below -- see the "Offline IDC join" block in +// SmoothingExtrapolate() for why this is done here rather than during map creation. +bool getScalerValues(o2::ccdb::BasicCCDBManager& ccdbmgr, long tfTimeInMS, float& scA, float& scC) +{ + auto* scalerTree = ccdbmgr.getForTimeStamp(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::CalScaler), long(std::ceil(tfTimeInMS))); + auto* scalerWeights = ccdbmgr.getForTimeStamp(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::CalScalerWeights), long(std::ceil(tfTimeInMS))); + + if (!scalerTree) { + LOGP(error, "Could not get 'TPC/Calib/Scaler' for time stamp {}", tfTimeInMS); + return false; + } + // The caller sets setFatalWhenNull(false), so a missing object comes back as nullptr rather than + // aborting -- the weights have to be checked before being dereferenced below. + if (!scalerWeights) { + LOGP(error, "Could not get 'TPC/Calib/ScalerWeights' for time stamp {}", tfTimeInMS); + return false; + } + + o2::tpc::TPCScaler scaler; + scaler.setFromTree(*(scalerTree)); + scaler.setScalerWeights(*scalerWeights); + scaler.useWeights(true); + scaler.setIonDriftTimeMS(500); + + static bool defaultScalerReported = false; + static bool badScalerValueReported = false; + if (scaler.getRun() == 0) { + if (!defaultScalerReported) { + LOGP(error, "Retrieved default scaler entry 'TPC/Calib/Scaler' for time stamp {}", tfTimeInMS); + defaultScalerReported = true; + } + return false; + } + + scA = scaler.getMeanScaler(tfTimeInMS, o2::tpc::Side::A); + scC = scaler.getMeanScaler(tfTimeInMS, o2::tpc::Side::C); + + if ((scA <= 0) || (scC <= 0)) { + if (!badScalerValueReported) { + LOGP(error, "Bad scaler value, first seen for time stamp {}, scA: {}, scC: {}", tfTimeInMS, scA, scC); + badScalerValueReported = true; + } + scA = 0; + scC = 0; + return false; + } + + return true; +} + +void SmoothingExtrapolate(const char* fileName = "debugVoxRes.root", TString fileOutName = "SmoothVoxRes.root", + int do_smoothing = 1, int do_extrapolation = 2, + int A11maxZ2X = -1, bool maskIA11 = false, + Int_t N_bins_X_GF = 1, Int_t N_bins_Y_GF = 2, + Int_t N_bins_Z_GF = 0, Float_t sigma_GF = 1.2) +{ + + // do_extrapolation: + // 0 -> no extrapolation to low radii done + // 1 -> only smoothed values are extrapolated + // 2 -> smoothed and raw values are extrapolated + // 3 -> only raw values are extrapolated + + // Example, smoothing and extrapolating both smoothed and raw values: + // SmoothingExtrapolate("voxRes.__.it0.root", "voxRes._smooth.root", 1, 2, -1, false, 1, 2, 0, 1.2) + + const float maxDeltaCut = 25.0; // maximum value for any Delta to be accepted + const float min_statistics = 20; + const float max_extrapolation_value = 20.0; // maximum value for extrapolation in DX, DY, DZ + //---------------------------------------------------------------- + // input + if (gSystem->AccessPathName(fileName)) { + LOGP(error, "input file {} does not exist", fileName); + return; + } + + auto file = std::unique_ptr(TFile::Open(fileName, "READ")); + if (!file || !file->IsOpen()) { + LOGP(error, "input file {} does not exist", fileName); + return; + } + + TTree* voxResTree = nullptr; + file->cd(); + gDirectory->GetObject("voxResTree", voxResTree); + if (!voxResTree) { + LOGP(error, "tree voxResTree does not exist in {}", fileName); + return; + } + + o2::tpc::TrackResiduals::VoxRes* voxRes_map = nullptr; + Long64_t entries_input_map = voxResTree->GetEntries(); + LOGP(info, "entries_input_map: {}", entries_input_map); + voxResTree->SetBranchAddress("voxRes", &voxRes_map); + + // required for the binning that was used + auto userInfo = voxResTree->GetUserInfo(); + if (!userInfo->FindObject("y2xBinning") || !userInfo->FindObject("z2xBinning")) { + LOGP(error, "'y2xBinning' or 'z2xBinning' not found in UserInfo, but required to get the correct binning"); + return; + } + + // Obtain configuration + const SpacePointsCalibConfParam& params = SpacePointsCalibConfParam::Instance(); + if (std::filesystem::exists("scdconfig.ini")) { + params.updateFromFile("scdconfig.ini"); + } + // TrackResiduals::setZ2XBinning() (called below) reads scdcalib.maxZ2X directly and uses it to scale + // the physical z/x bin boundaries -- it is baked into what each z2x voxel index in the input tree + // actually means, not a cosmetic knob. There is no scdconfig.ini on the GRID, so without this the + // code default (1.0) would silently apply instead of whatever stage 1 actually used (production 1.4), + // misaligning this macro's re-derived binning against the tree's real geometry. Must happen BEFORE + // setZ2XBinning() below. See staticMapCreatorCPM.C's UserInfo::Add("maxZ2X", ...) for where this comes + // from. + if (auto* maxZ2XObj = userInfo->FindObject("maxZ2X")) { + const std::string maxZ2XStr = maxZ2XObj->GetTitle(); + o2::conf::ConfigurableParam::setValue("scdcalib.maxZ2X", maxZ2XStr); + LOGP(info, "Set scdcalib.maxZ2X = {} from input UserInfo (matches stage 1)", maxZ2XStr); + } else { + LOGP(warning, + "'maxZ2X' not found in input UserInfo (older input file?) -- using scdcalib.maxZ2X = {} " + "(scdconfig.ini/code default), which may NOT match the value stage 1 actually used to " + "build this tree's z2x binning!", + params.maxZ2X); + } + + LOGP(info, "----- Dumping configuration values START -----"); + params.printKeyValues(); + LOGP(info, "----- Dumping configuration values END -----"); + + LOGP(info, "Get binning from userInfo"); + o2::tpc::TrackResiduals trackResiduals; + auto y2xBins = o2::RangeTokenizer::tokenize(userInfo->FindObject("y2xBinning")->GetTitle()); + auto z2xBins = o2::RangeTokenizer::tokenize(userInfo->FindObject("z2xBinning")->GetTitle()); + trackResiduals.setY2XBinning(y2xBins); + trackResiduals.setZ2XBinning(z2xBins); + trackResiduals.init(); + LOGP(info, "trackResiduals initialized"); + //---------------------------------------------------------------- + + //---------------------------------------------------------------- + // Offline IDC join: staticMapCreatorCPM.C on the GRID intentionally does not access IDCs + // live (useCTPLumi=2 -- CCDB access from inside the hot per-TF loop was slow/unreliable), so + // meanIDC/medianIDC in the input's UserInfo are placeholder 0. Per-TF timestamps are recorded + // regardless (the "OrbitLumiInfo" tree's timeMSsel branch) specifically so this can be joined back + // offline, with a properly-cached CCDB client. Timestamps are sorted before querying: + // TPCScaler/TPCScalerWeights CCDB objects are valid over a time range, and BasicCCDBManager's cache + // (setCaching(true)) only hits when consecutive queries land in the same validity window -- + // unsorted access would bounce between windows and force a real CCDB fetch almost every call. + float meanIDCReal = 0.f; + float medianIDCReal = 0.f; + { + TTree* orbitLumiTree = nullptr; + file->GetObject("OrbitLumiInfo", orbitLumiTree); + if (!orbitLumiTree || orbitLumiTree->GetEntries() == 0) { + LOGP(warning, "Offline IDC join: no 'OrbitLumiInfo' tree (or it's empty) in the input file -- meanIDC/medianIDC stay 0"); + } else { + std::vector* timeMSselPtr = nullptr; + orbitLumiTree->SetBranchAddress("timeMSsel", &timeMSselPtr); + orbitLumiTree->GetEntry(0); + if (!timeMSselPtr || timeMSselPtr->empty()) { + LOGP(warning, "Offline IDC join: 'timeMSsel' branch missing or empty -- meanIDC/medianIDC stay 0"); + } else { + std::vector sortedTimes(*timeMSselPtr); + std::sort(sortedTimes.begin(), sortedTimes.end()); + + auto& ccdbmgr = o2::ccdb::BasicCCDBManager::instance(); + ccdbmgr.setCaching(true); + ccdbmgr.setFatalWhenNull(false); + ccdbmgr.setURL("http://alice-ccdb.cern.ch"); + + std::vector averageIDCs; + averageIDCs.reserve(sortedTimes.size()); + for (long tfTimeInMS : sortedTimes) { + float scA = 0.f, scC = 0.f; + if (getScalerValues(ccdbmgr, tfTimeInMS, scA, scC)) { + averageIDCs.emplace_back((scA + scC) / 2.f); + } + } + if (averageIDCs.empty()) { + LOGP(warning, "Offline IDC join: no valid scaler values found for any of {} TFs -- meanIDC/medianIDC stay 0", sortedTimes.size()); + } else { + double sum = 0.0; + for (float v : averageIDCs) { + sum += v; + } + meanIDCReal = static_cast(sum / averageIDCs.size()); + medianIDCReal = static_cast(TMath::Median(static_cast(averageIDCs.size()), averageIDCs.data())); + LOGP(info, "Offline IDC join: {} of {} TFs gave a valid scaler value, meanIDC={}, medianIDC={}", + averageIDCs.size(), sortedTimes.size(), meanIDCReal, medianIDCReal); + } + } + } + } + //---------------------------------------------------------------- + + //---------------------------------------------------------------- + // Get voxel map binning from map + + const int nXBins = trackResiduals.getNXBins(); + const int nY2XBins = trackResiduals.getNY2XBins(); + const int nZ2XBins = trackResiduals.getNZ2XBins(); + LOGP(info, "binning X,Y2X,Z2X: {}, {}, {}", nXBins, nY2XBins, nZ2XBins); + //---------------------------------------------------------------- + + //---------------------------------------------------------------- + // output + // Create a new file + a clone of old tree in new file + TFile* outputfile = new TFile(fileOutName.Data(), "RECREATE"); + LOGP(info, "Output file: {} created", fileOutName.Data()); + + o2::tpc::TrackResiduals::VoxRes mVoxelResultsOut{}; ///< the results from mVoxelResults are copied in here to be able to stream them + o2::tpc::TrackResiduals::VoxRes* mVoxelResultsOutPtr{&mVoxelResultsOut}; ///< pointer to set the branch address to for the output + std::unique_ptr mTreeOut; + //---------------------------------------------------------------- + + //---------------------------------------------------------------- + ROOT::Math::MinimizerOptions::SetDefaultMinimizer("GSLSimAn"); + + TF1* func_PolyFitFunc = new TF1("func_PolyFitFunc", PolyFitFunc, 0, 150, 6); + TF1* func_PolyFitFunc_raw = new TF1("func_PolyFitFunc_raw", PolyFitFunc, 0, 150, 6); + TProfile* tp_DX_vs_X_raw = new TProfile("tp_DX_vs_X_raw", "tp_DX_vs_X_raw", 250, 0, 250); + TProfile* tp_Stat_vs_X = new TProfile("tp_Stat_vs_X", "tp_Stat_vs_X;row;", 250, 0, 250); + TProfile* tp_Stat_vs_X_single = new TProfile("tp_Stat_vs_X_single", "tp_Stat_vs_X;row;", 250, 0, 250); + TProfile* tp_DX_vs_X_single = new TProfile("tp_DX_vs_X_single", "tp_DX_vs_X;row;", 250, 0, 250); + TGraph* tg_Stat_vs_X_slice = new TGraph(); + TProfile* tp_DX_vs_Row_raw = new TProfile("tp_DX_vs_Row_raw", "tp_DX_vs_Row_raw", 250, 0, 250); + TProfile* tp_DX_vs_X_smooth = new TProfile("tp_DX_vs_X_smooth", "tp_DX_vs_X_smooth", 250, 0, 250); + TProfile* tp_DX_vs_X_smooth_extr = new TProfile("tp_DX_vs_X_smooth_extr", "tp_DX_vs_X_smooth_extr", 250, 0, 250); + TProfile* tp_DY_vs_X_smooth_extr = new TProfile("tp_DY_vs_X_smooth_extr", "tp_DY_vs_X_smooth_extr", 250, 0, 250); + TProfile* tp_DZ_vs_X_smooth_extr_A = new TProfile("tp_DZ_vs_X_smooth_extr_A", "tp_DZ_vs_X_smooth_extr_A", 250, 0, 250); + TProfile* tp_DZ_vs_X_smooth_extr_C = new TProfile("tp_DZ_vs_X_smooth_extr_C", "tp_DZ_vs_X_smooth_extr_C", 250, 0, 250); + TProfile* tp_DZ_vs_X_smooth_A = new TProfile("tp_DZ_vs_X_smooth_A", "tp_DZ_vs_X_smooth_A", 250, 0, 250); + TProfile* tp_DZ_vs_X_smooth_C = new TProfile("tp_DZ_vs_X_smooth_C", "tp_DZ_vs_X_smooth_C", 250, 0, 250); + TProfile* tp_DZ_vs_X_raw_A = new TProfile("tp_DZ_vs_X_raw_A", "tp_DZ_vs_X_raw_A", 250, 0, 250); + TProfile* tp_DZ_vs_X_raw_C = new TProfile("tp_DZ_vs_X_raw_C", "tp_DZ_vs_X_raw_C", 250, 0, 250); + TProfile* tp_Stat_vs_row = new TProfile("tp_Stat_vs_row", "tp_Stat_vs_row;row;", 152, 0, 152); + int n_bins_z_phi_sector = 36 * nY2XBins * nZ2XBins; + TH1D* h_x_start_fit_vs_z_phi_sector = new TH1D("h_x_start_fit_vs_z_phi_sector", "h_x_start_fit_vs_z_phi_sector", n_bins_z_phi_sector, 0, n_bins_z_phi_sector); + //---------------------------------------------------------------- + + //-------------------------------------------------------------------------- + // Prepare output data + std::vector>>>> vec_DXYZ_vox; + std::vector>>>> vec_DXYZ_vox_GF; + vec_DXYZ_vox.resize(6); + vec_DXYZ_vox_GF.resize(6); + for (Int_t i_xyz = 0; i_xyz < 6; i_xyz++) { + vec_DXYZ_vox[i_xyz].resize(36); + vec_DXYZ_vox_GF[i_xyz].resize(36); + for (Int_t i_sector = 0; i_sector < 36; i_sector++) { + vec_DXYZ_vox[i_xyz][i_sector].resize(152); + vec_DXYZ_vox_GF[i_xyz][i_sector].resize(152); + for (Int_t voxX = 0; voxX < 152; voxX++) { + vec_DXYZ_vox[i_xyz][i_sector][voxX].resize((nY2XBins)); + vec_DXYZ_vox_GF[i_xyz][i_sector][voxX].resize((nY2XBins)); + for (Int_t voxY = 0; voxY < (nY2XBins); voxY++) { + vec_DXYZ_vox[i_xyz][i_sector][voxX][voxY].resize((nZ2XBins)); + vec_DXYZ_vox_GF[i_xyz][i_sector][voxX][voxY].resize((nZ2XBins)); + for (Int_t voxZ = 0; voxZ < (nZ2XBins); voxZ++) { + vec_DXYZ_vox[i_xyz][i_sector][voxX][voxY][voxZ] = 0.0; + vec_DXYZ_vox_GF[i_xyz][i_sector][voxX][voxY][voxZ] = 0.0; + } + } + } + } + } + std::vector>> vec_max_X_fit; + vec_max_X_fit.resize(36); + for (Int_t i_sector = 0; i_sector < 36; i_sector++) { + vec_max_X_fit[i_sector].resize(nY2XBins); + for (Int_t voxY = 0; voxY < (nY2XBins); voxY++) { + vec_max_X_fit[i_sector][voxY].resize(nZ2XBins); + for (Int_t voxZ = 0; voxZ < (nZ2XBins); voxZ++) { + vec_max_X_fit[i_sector][voxY][voxZ] = 0.0; + } + } + } + + for (Long64_t jentry = 0; jentry < entries_input_map; jentry++) { + voxResTree->GetEntry(jentry); + + const auto bvox_X = voxRes_map->bvox[o2::tpc::TrackResiduals::VoxX]; // bin number in x (= pad row) + const auto bvox_F = voxRes_map->bvox[o2::tpc::TrackResiduals::VoxF]; // bin number in y/x 0..14 + const auto bvox_Z = voxRes_map->bvox[o2::tpc::TrackResiduals::VoxZ]; // bin number in z/x 0..4 + const int sector = (int)voxRes_map->bsec; + const float xAV = voxRes_map->stat[o2::tpc::TrackResiduals::VoxX]; + const float z2xAV = voxRes_map->stat[o2::tpc::TrackResiduals::VoxZ]; + const float zAV = z2xAV * xAV; + + vec_DXYZ_vox[0][sector][bvox_X][bvox_F][bvox_Z] = voxRes_map->D[0]; // dX + vec_DXYZ_vox[1][sector][bvox_X][bvox_F][bvox_Z] = voxRes_map->D[1]; // dY + vec_DXYZ_vox[2][sector][bvox_X][bvox_F][bvox_Z] = voxRes_map->D[2]; // dZ + vec_DXYZ_vox[3][sector][bvox_X][bvox_F][bvox_Z] = voxRes_map->stat[3]; // #entries + vec_DXYZ_vox[4][sector][bvox_X][bvox_F][bvox_Z] = xAV; // xAV + vec_DXYZ_vox[5][sector][bvox_X][bvox_F][bvox_Z] = zAV; // zAV + + vec_DXYZ_vox_GF[0][sector][bvox_X][bvox_F][bvox_Z] = voxRes_map->DS[0]; // dXS + vec_DXYZ_vox_GF[1][sector][bvox_X][bvox_F][bvox_Z] = voxRes_map->DS[1]; // dYS + vec_DXYZ_vox_GF[2][sector][bvox_X][bvox_F][bvox_Z] = voxRes_map->DS[2]; // dZS + vec_DXYZ_vox_GF[3][sector][bvox_X][bvox_F][bvox_Z] = voxRes_map->stat[3]; // #entries + vec_DXYZ_vox_GF[4][sector][bvox_X][bvox_F][bvox_Z] = xAV; // xAV + vec_DXYZ_vox_GF[5][sector][bvox_X][bvox_F][bvox_Z] = zAV; // zAV + + tp_Stat_vs_row->Fill(bvox_X, voxRes_map->stat[3]); + float voxX_pos = RowX[bvox_X]; + if (fabs(bvox_F - (int)(nY2XBins / 2)) <= 1) { + tp_Stat_vs_X->Fill(voxX_pos, voxRes_map->stat[3]); + } + if (bvox_Z == 0) { + // if(voxX_pos < (85.0+32.0)) + { + tp_DX_vs_X_raw->Fill(voxX_pos, voxRes_map->D[0]); + tp_DX_vs_Row_raw->Fill(bvox_X, voxRes_map->D[0]); + if (sector < 18) { + tp_DZ_vs_X_raw_A->Fill(voxX_pos, voxRes_map->D[2]); + } else { + tp_DZ_vs_X_raw_C->Fill(voxX_pos, voxRes_map->D[2]); + } + } + } + } + + //-------------------------------------------------------------------------------- + tp_Stat_vs_row->GetXaxis()->SetRangeUser(20, 62); + const float meanEntries = tp_Stat_vs_row->GetMean(2); + tp_Stat_vs_row->GetXaxis()->SetRangeUser(2, 1); + const int startRowGoodEntries = tp_Stat_vs_row->FindFirstBinAbove(meanEntries * 0.7) - 1; + + // don't trust bins with too low statistics + tp_DX_vs_X_raw->GetXaxis()->SetRangeUser(RowX[startRowGoodEntries], RowX[63]); + // const float max_DX = tp_DX_vs_X_raw ->GetBinContent(tp_DX_vs_X_raw->GetMaximumBin()); + // const float max_X_DX = tp_DX_vs_X_raw ->GetBinCenter(tp_DX_vs_X_raw->GetMaximumBin()); + tp_DX_vs_Row_raw->GetXaxis()->SetRangeUser(startRowGoodEntries, 63); + // const int max_Row_DX = tp_DX_vs_Row_raw ->GetBinCenter(tp_DX_vs_Row_raw->GetMaximumBin()); + + float max_X_stat = 0.0; + for (int ibin = (tp_Stat_vs_X->GetNbinsX() - 3); ibin >= 0; ibin--) { + float X_val = tp_Stat_vs_X->GetBinCenter(ibin); + float stat = tp_Stat_vs_X->GetBinContent(ibin); + float DX = tp_DX_vs_X_raw->GetBinContent(ibin); + if (X_val < (85.0 + 32.0)) { + Double_t stat_previous[3] = {tp_Stat_vs_X->GetBinContent(ibin + 1), tp_Stat_vs_X->GetBinContent(ibin + 2), tp_Stat_vs_X->GetBinContent(ibin + 3)}; + Double_t Xpos_previous[3] = {tp_Stat_vs_X->GetBinCenter(ibin + 1), tp_Stat_vs_X->GetBinCenter(ibin + 2), tp_Stat_vs_X->GetBinCenter(ibin + 3)}; + if ((stat - stat_previous[0]) < 0.0 && (stat - stat_previous[1]) < 0.0 && (stat - stat_previous[2]) < 0.0 && stat > 0.0) { + Double_t ratio_stat[3] = {stat / stat_previous[0], stat / stat_previous[1], stat / stat_previous[2]}; + if (ratio_stat[0] < 0.8 && ratio_stat[1] < 0.5 && ratio_stat[2] < 0.5) { + max_X_stat = Xpos_previous[2]; + break; + } + } + if (fabs(stat < 0.1)) { + max_X_stat = Xpos_previous[2]; + break; + } + } + } + + const float max_X_DX = max_X_stat; + int max_Row_DX = 0; + + // find corresponding row + for (int i = 0; i < 50; ++i) { + if (RowX[i] > max_X_DX) { + break; + } + max_Row_DX = i; + } + const float max_DX = tp_DX_vs_X_raw->GetBinContent(tp_DX_vs_X_raw->FindBin(max_X_DX)); + + LOGP(info, "max DX value: {:.3f}, X-position of max DX value: {:.3f} (row: {}), first row checked: {}, mean entries: {:.2f}, max_X_stat: {:.3f}", max_DX, max_X_DX, max_Row_DX, startRowGoodEntries, meanEntries, max_X_stat); + //-------------------------------------------------------------------------------- + + //-------------------------------------------------------------------------------- + LOGP(info, "Calculating extrapolation fit start values for every phi slice"); + for (Int_t i_sector = 0; i_sector < 36; i_sector++) { + for (Int_t voxY = 0; voxY < (nY2XBins); voxY++) { + for (Int_t voxZ = 0; voxZ < (nZ2XBins); voxZ++) { + // fill the TGraph used for fitting + int ipoint = 0; + for (Int_t voxX = 0; voxX <= 151; voxX++) { + float voxX_pos = RowX[voxX]; + float statistics = vec_DXYZ_vox_GF[3][i_sector][voxX][voxY][voxZ]; + float DX = vec_DXYZ_vox_GF[0][i_sector][voxX][voxY][voxZ]; + tg_Stat_vs_X_slice->SetPoint(ipoint, voxX_pos, statistics); + ipoint++; + + if (i_sector == 11 && voxY == 10 && voxZ == 27) { + tp_Stat_vs_X_single->Fill(voxX_pos, statistics); + tp_DX_vs_X_single->Fill(voxX_pos, DX); + } + } + + float max_X_stat = 0.0; + for (int ibin = (tg_Stat_vs_X_slice->GetN() - 3); ibin >= 0; ibin--) { + double X_val = 0.0; + double stat = 0.0; + tg_Stat_vs_X_slice->GetPoint(ibin, X_val, stat); + if (stat < min_statistics) + continue; + if (X_val < (85.0 + 32.0)) { + Double_t stat_previous[3] = {0.0, 0.0, 0.0}; + Double_t Xpos_previous[3] = {0.0, 0.0, 0.0}; + + tg_Stat_vs_X_slice->GetPoint(ibin + 1, Xpos_previous[0], stat_previous[0]); + tg_Stat_vs_X_slice->GetPoint(ibin + 2, Xpos_previous[1], stat_previous[1]); + tg_Stat_vs_X_slice->GetPoint(ibin + 3, Xpos_previous[2], stat_previous[2]); + + if ((stat - stat_previous[0]) < 0.0 && (stat - stat_previous[1]) < 0.0 && (stat - stat_previous[2]) < 0.0 && stat > 0.0) { + Double_t ratio_stat[3] = {stat / stat_previous[0], stat / stat_previous[1], stat / stat_previous[2]}; + // if(i_sector == 9 && voxY == 19 && voxZ == 27) + //{ + // } + if (ratio_stat[0] < 0.8 && ratio_stat[1] < 0.5 && ratio_stat[2] < 0.5) { + // first check if there aren't any bins at lower radii with statistics + int flag_low_bin = 0; + for (int ibinB = ibin - 1; ibinB >= 0; ibinB--) { + double X_valB = 0.0; + double statB = 0.0; + tg_Stat_vs_X_slice->GetPoint(ibinB, X_valB, statB); + if (statB > 0.0 && statB / stat_previous[0] > 0.8) { + ibin = ibinB; + flag_low_bin = 1; + break; + } + } + if (!flag_low_bin) { + max_X_stat = Xpos_previous[2]; + break; + } + } + } + if (fabs(stat < 0.1)) { + // first check if there aren't any bins at lower radii with statistics + int flag_low_bin = 0; + for (int ibinB = ibin - 1; ibinB >= 0; ibinB--) { + double X_valB = 0.0; + double statB = 0.0; + tg_Stat_vs_X_slice->GetPoint(ibinB, X_valB, statB); + if (statB > 0.0) { + ibin = ibinB; + flag_low_bin = 1; + break; + } + } + if (!flag_low_bin) { + max_X_stat = Xpos_previous[2]; + break; + } + } + } + } + + if (max_X_stat < 85.0) { + max_X_stat = max_X_DX; // set to average value + } + int i_bin_z_phi_sector = voxZ * 36 * nY2XBins + i_sector * nY2XBins + voxY; + h_x_start_fit_vs_z_phi_sector->SetBinContent(i_bin_z_phi_sector, max_X_stat); + tg_Stat_vs_X_slice->Set(0); + + vec_max_X_fit[i_sector][voxY][voxZ] = max_X_stat; + } // end of Z loop + } // end of Y loop + } // end of sector loop + LOGP(info, "Done calculating extrapolation fit start values for every phi slice"); + //-------------------------------------------------------------------------------- + + // ========================================================================= + // treat acceptance edge in z-direction + // replace values very close to or beyond the pad plane by a value a bit further away + const float maxZ = 242.f; + for (Int_t i_sector = 0; i_sector < 36; i_sector++) { + for (Int_t voxX = 0; voxX < nXBins; voxX++) { + for (Int_t voxY = 0; voxY < nY2XBins; voxY++) { + float lastValue[4] = {0.f, 0.f, 0.f, 0.f}; + float lastValueS[4] = {0.f, 0.f, 0.f, 0.f}; + float lastValueA11[4] = {0.f, 0.f, 0.f, 0.f}; + float lastValueSA11[4] = {0.f, 0.f, 0.f, 0.f}; + for (Int_t voxZ = 0; voxZ < nZ2XBins; voxZ++) { + const float absZ = std::abs(vec_DXYZ_vox[5][i_sector][voxX][voxY][voxZ]); + // if (i_sector==0&&voxX>149&&voxY==5) { + //} + if (absZ < maxZ) { + for (int i = 0; i < 4; ++i) { + lastValue[i] = vec_DXYZ_vox[i][i_sector][voxX][voxY][voxZ]; + lastValueS[i] = vec_DXYZ_vox_GF[i][i_sector][voxX][voxY][voxZ]; + } + } else { + for (int i = 0; i < 4; ++i) { + vec_DXYZ_vox[i][i_sector][voxX][voxY][voxZ] = lastValue[i]; + vec_DXYZ_vox_GF[i][i_sector][voxX][voxY][voxZ] = lastValueS[i]; + } + } + + if (i_sector == 11 && A11maxZ2X > -1) { + if (voxZ <= A11maxZ2X) { + for (int i = 0; i < 4; ++i) { + lastValueA11[i] = vec_DXYZ_vox[i][i_sector][voxX][voxY][voxZ]; + lastValueSA11[i] = + vec_DXYZ_vox_GF[i][i_sector][voxX][voxY][voxZ]; + } + } else { + for (int i = 0; i < 4; ++i) { + vec_DXYZ_vox[i][i_sector][voxX][voxY][voxZ] = lastValueA11[i]; + vec_DXYZ_vox_GF[i][i_sector][voxX][voxY][voxZ] = + lastValueSA11[i]; + } + } + } + } + } + } + } + + //---------------------------------------------------------------- + // Gaussian filtering + + if (do_smoothing) { + LOGP(info, "Gaussian filtering started"); + std::vector>> vec_GKernel; + vec_GKernel.resize(N_bins_X_GF * 2 + 1); + for (Int_t i_X = 0; i_X < (Int_t)vec_GKernel.size(); i_X++) { + vec_GKernel[i_X].resize(N_bins_Y_GF * 2 + 1); + for (Int_t i_Y = 0; i_Y < (Int_t)vec_GKernel[i_X].size(); i_Y++) { + vec_GKernel[i_X][i_Y].resize(N_bins_Z_GF * 2 + 1); + } + } + vec_FilterCreation(vec_GKernel, N_bins_X_GF, N_bins_Y_GF, N_bins_Z_GF, sigma_GF); + + for (Int_t i_sector = 0; i_sector < 36; i_sector++) { + std::vector>>> arr_values; + std::vector>>> arr_values_used; + arr_values.resize(3); + arr_values_used.resize(3); + + for (Int_t i_xyz = 0; i_xyz < 3; i_xyz++) { + arr_values[i_xyz].resize(N_bins_X_GF * 2 + 1); + arr_values_used[i_xyz].resize(N_bins_X_GF * 2 + 1); + for (Int_t i_X = 0; i_X < (Int_t)arr_values[i_xyz].size(); i_X++) { + arr_values[i_xyz][i_X].resize(N_bins_Y_GF * 2 + 1); + arr_values_used[i_xyz][i_X].resize(N_bins_Y_GF * 2 + 1); + for (Int_t i_Y = 0; i_Y < (Int_t)arr_values[i_xyz][i_X].size(); i_Y++) { + arr_values[i_xyz][i_X][i_Y].resize(N_bins_Z_GF * 2 + 1); + arr_values_used[i_xyz][i_X][i_Y].resize(N_bins_Z_GF * 2 + 1); + for (Int_t i_Z = 0; i_Z < (Int_t)arr_values[i_xyz][i_X][i_Y].size(); i_Z++) { + arr_values[i_xyz][i_X][i_Y][i_Z] = 0.0; + arr_values_used[i_xyz][i_X][i_Y][i_Z] = 0.0; + } + } + } + } + + // CRU 0 : 000 - 016 (IROC) + // CRU 1 : 017 - 031 (IROC) + // CRU 2 : 032 - 047 (IROC) + // CRU 3 : 048 - 062 (IROC) + + // CRU 4 : 063 - 080 (OROC 1) + // CRU 5 : 081 - 096 (OROC 1) + + // CRU 6 : 097 - 112 (OROC 2) + // CRU 7 : 113 - 126 (OROC 2) + + // CRU 8 : 127 - 139 (OROC 3) + // CRU 9 : 140 - 151 (OROC 3) + + std::vector> vec_ROC_row; + vec_ROC_row.resize(4); + for (int iRoc = 0; iRoc < 4; iRoc++) { + vec_ROC_row[iRoc].resize(2); + } + vec_ROC_row[0][0] = 0; + vec_ROC_row[0][1] = 62; + vec_ROC_row[1][0] = 63; + vec_ROC_row[1][1] = 96; + vec_ROC_row[2][0] = 97; + vec_ROC_row[2][1] = 126; + vec_ROC_row[3][0] = 127; + vec_ROC_row[3][1] = 151; + + for (int iRoc = 0; iRoc < 4; iRoc++) { + int minRow = vec_ROC_row[iRoc][0]; + if (do_extrapolation && (iRoc == 0)) { + minRow = max_Row_DX + 2; // don't smooth over low radii large distortions + } + for (Int_t voxX = minRow; voxX <= vec_ROC_row[iRoc][1]; voxX++) { + for (Int_t voxY = 0; voxY < (nY2XBins); voxY++) { + for (Int_t voxZ = 0; voxZ < (nZ2XBins); voxZ++) { + Float_t sum_weight[3] = {0.0}; + Float_t sum_values[3] = {0.0}; + for (Int_t index_voxXB = -N_bins_X_GF; index_voxXB <= N_bins_X_GF; index_voxXB++) { + Int_t voxXB = voxX + index_voxXB; + if (voxXB < vec_ROC_row[iRoc][0]) + continue; + if (voxXB > vec_ROC_row[iRoc][1]) + continue; + for (Int_t index_voxYB = -N_bins_Y_GF; index_voxYB <= N_bins_Y_GF; index_voxYB++) { + Int_t voxYB = voxY + index_voxYB; + if (voxYB < 0) + continue; + if (voxYB >= nY2XBins) + continue; + for (Int_t index_voxZB = -N_bins_Z_GF; index_voxZB <= N_bins_Z_GF; index_voxZB++) { + Int_t voxZB = voxZ + index_voxZB; + if (voxZB < 0) + continue; + if (voxZB >= (nZ2XBins)) + continue; + float statistics = vec_DXYZ_vox[3][i_sector][voxXB][voxYB][voxZB]; + if ((int)statistics == 0) + continue; + if (TMath::IsNaN(vec_DXYZ_vox[0][i_sector][voxXB][voxYB][voxZB])) + continue; // NaN check + if (TMath::IsNaN(vec_DXYZ_vox[1][i_sector][voxXB][voxYB][voxZB])) + continue; // NaN check + if (TMath::IsNaN(vec_DXYZ_vox[2][i_sector][voxXB][voxYB][voxZB])) + continue; // NaN check + if (fabs(vec_DXYZ_vox[0][i_sector][voxXB][voxYB][voxZB]) > maxDeltaCut) + continue; + if (fabs(vec_DXYZ_vox[1][i_sector][voxXB][voxYB][voxZB]) > maxDeltaCut) + continue; + if (fabs(vec_DXYZ_vox[2][i_sector][voxXB][voxYB][voxZB]) > maxDeltaCut) + continue; + for (Int_t i_xyz = 0; i_xyz < 3; i_xyz++) { + arr_values_used[i_xyz][index_voxXB + N_bins_X_GF][index_voxYB + N_bins_Y_GF][index_voxZB + N_bins_Z_GF] = 1.0; + arr_values[i_xyz][index_voxXB + N_bins_X_GF][index_voxYB + N_bins_Y_GF][index_voxZB + N_bins_Z_GF] = vec_GKernel[index_voxXB + N_bins_X_GF][index_voxYB + N_bins_Y_GF][index_voxZB + N_bins_Z_GF] * vec_DXYZ_vox[i_xyz][i_sector][voxXB][voxYB][voxZB]; + sum_weight[i_xyz] += vec_GKernel[index_voxXB + N_bins_X_GF][index_voxYB + N_bins_Y_GF][index_voxZB + N_bins_Z_GF]; + sum_values[i_xyz] += arr_values[i_xyz][index_voxXB + N_bins_X_GF][index_voxYB + N_bins_Y_GF][index_voxZB + N_bins_Z_GF]; + if (TMath::IsNaN(vec_DXYZ_vox[i_xyz][i_sector][voxXB][voxYB][voxZB])) { + LOGP(error, "NaN vec_DXYZ_vox detected in xyz {}, sec {}, voxX {}, voxY {}, voxZ {}", i_xyz, i_sector, voxXB, voxYB, voxZB); + } + if (TMath::IsNaN(sum_values[i_xyz])) { + LOGP(error, "NaN sum_values detected in xyz {}, sec {}, voxX {}, voxY {}, voxZ {}", i_xyz, i_sector, voxXB, voxYB, voxZB); + } + } + } + } + } + + for (Int_t i_xyz = 0; i_xyz < 3; i_xyz++) { + if (sum_weight[i_xyz] > 0.0) { + sum_values[i_xyz] /= sum_weight[i_xyz]; + vec_DXYZ_vox_GF[i_xyz][i_sector][voxX][voxY][voxZ] = sum_values[i_xyz]; + if (TMath::IsNaN(vec_DXYZ_vox_GF[i_xyz][i_sector][voxX][voxY][voxZ])) { + LOGP(error, "NaN detected during smoothing process in xyz {}, sec {}, voxX {}, voxY {}, voxZ {}", i_xyz, i_sector, voxX, voxY, voxZ); + } + + float voxX_pos = RowX[voxX]; + if (voxZ == 0) { + if (i_xyz == 0) + tp_DX_vs_X_smooth->Fill(voxX_pos, sum_values[i_xyz]); + if (i_sector < 18) { + if (i_xyz == 2) + tp_DZ_vs_X_smooth_A->Fill(voxX_pos, sum_values[i_xyz]); + } else { + if (i_xyz == 2) + tp_DZ_vs_X_smooth_C->Fill(voxX_pos, sum_values[i_xyz]); + } + } + } + } + } + } + } + } + } // end loop over sectors + } // do_smoothing + //---------------------------------------------------------------- + + //---------------------------------------------------------------- + // Do extrapolation to low radii + if (do_extrapolation > 0) { + LOGP(info, "Starting extrapolation to small radii"); + // const float start_fit = max_X_DX + 0.0; + // const float stop_fit = max_X_DX + 10.0; + TGraph* tg_data_for_fit = new TGraph(); + TGraph* tg_data_for_fit_raw = new TGraph(); + for (Int_t i_sector = 0; i_sector < 36; i_sector++) { + for (Int_t voxY = 0; voxY < (nY2XBins); voxY++) { + for (Int_t voxZ = 0; voxZ < (nZ2XBins); voxZ++) { + const float start_fit = vec_max_X_fit[i_sector][voxY][voxZ] + 0.0; + const float stop_fit = vec_max_X_fit[i_sector][voxY][voxZ] + 10.0; + // find maximum dX + // float maxXdX = 0; + // float maxdX = 0; + // for(Int_t voxX = startRowGoodEntries; voxX < 63; voxX++) // enough to search in IROC + //{ + // const float dX = vec_DXYZ_vox[0][i_sector][voxX][voxY][voxZ]; + // if (dX > maxdX) { + // maxdX = dX; + // maxXdX = RowX[voxX]; + //} + //} + // const float start_fit = maxXdX + 1.0; + // const float stop_fit = maxXdX + 10.0; + + for (Int_t i_xyz = 0; i_xyz < 3; i_xyz++) { + // fill the TGraph used for fitting + int ipoint = 0; + int meanStat = 1; // statistics to use in extrapolation region + for (Int_t voxX = 0; voxX <= 151; voxX++) { + // float voxX_pos = vec_DXYZ_vox_GF[4][i_sector][voxX][voxY][voxZ]; + float voxX_pos = RowX[voxX]; + if (voxX_pos < start_fit) + continue; + if (voxX_pos > stop_fit) + break; + float statistics = vec_DXYZ_vox_GF[3][i_sector][voxX][voxY][voxZ]; + if (statistics < min_statistics) + continue; + float DXYZval = vec_DXYZ_vox_GF[i_xyz][i_sector][voxX][voxY][voxZ]; + tg_data_for_fit->SetPoint(ipoint, voxX_pos, DXYZval); + float DXYZval_raw = vec_DXYZ_vox[i_xyz][i_sector][voxX][voxY][voxZ]; + tg_data_for_fit_raw->SetPoint(ipoint, voxX_pos, DXYZval_raw); + ipoint++; + } + if (ipoint < 5) + continue; + + // fit the TGraph + for (Int_t i = 0; i < 6; i++) { + func_PolyFitFunc->SetParameter(i, 0.0); + func_PolyFitFunc->SetParError(i, 0.0); + func_PolyFitFunc_raw->SetParameter(i, 0.0); + func_PolyFitFunc_raw->SetParError(i, 0.0); + if (i > 2) { + func_PolyFitFunc->FixParameter(i, 0.0); + func_PolyFitFunc_raw->FixParameter(i, 0.0); + } + } + func_PolyFitFunc->SetParameter(0, 0.2); + func_PolyFitFunc->SetParameter(1, 0.3); + func_PolyFitFunc->SetParameter(2, 0.4); + func_PolyFitFunc->SetRange(start_fit, stop_fit); + tg_data_for_fit->Fit("func_PolyFitFunc", "QWMN", "", start_fit, stop_fit); + + func_PolyFitFunc_raw->SetParameter(0, 0.2); + func_PolyFitFunc_raw->SetParameter(1, 0.3); + func_PolyFitFunc_raw->SetParameter(2, 0.4); + func_PolyFitFunc_raw->SetRange(start_fit, stop_fit); + tg_data_for_fit_raw->Fit("func_PolyFitFunc_raw", "QWMN", "", start_fit, stop_fit); + + // if (ipoint>0) { + // meanStat /= ipoint; + // } + + // do the low radii extrapolation + for (Int_t voxX = 0; voxX <= 151; voxX++) { + // float voxX_pos = vec_DXYZ_vox_GF[4][i_sector][voxX][voxY][voxZ]; + float voxX_pos = RowX[voxX]; + if (voxX_pos > start_fit) { + break; + } + double extrapolation_value = func_PolyFitFunc->Eval(voxX_pos); + if (fabs(extrapolation_value) > max_extrapolation_value) + extrapolation_value = TMath::Sign(1, extrapolation_value) * max_extrapolation_value; + vec_DXYZ_vox_GF[i_xyz][i_sector][voxX][voxY][voxZ] = extrapolation_value; + vec_DXYZ_vox_GF[3][i_sector][voxX][voxY][voxZ] = meanStat; // set statistics -> important for spline creation + + double extrapolation_value_raw = func_PolyFitFunc_raw->Eval(voxX_pos); + if (fabs(extrapolation_value_raw) > max_extrapolation_value) + extrapolation_value_raw = TMath::Sign(1, extrapolation_value_raw) * max_extrapolation_value; + vec_DXYZ_vox[i_xyz][i_sector][voxX][voxY][voxZ] = extrapolation_value_raw; + vec_DXYZ_vox[3][i_sector][voxX][voxY][voxZ] = meanStat; // set statistics -> important for spline creation + } + + tg_data_for_fit->Set(0); + tg_data_for_fit_raw->Set(0); + } // end of xyz + + for (Int_t voxX = 0; voxX <= 151; voxX++) { + if (voxZ == 0) // for QA + { + float voxX_pos = RowX[voxX]; + tp_DX_vs_X_smooth_extr->Fill(voxX_pos, vec_DXYZ_vox_GF[0][i_sector][voxX][voxY][voxZ]); + if (i_sector < 18) { + tp_DZ_vs_X_smooth_extr_A->Fill(voxX_pos, vec_DXYZ_vox_GF[2][i_sector][voxX][voxY][voxZ]); + } else { + tp_DZ_vs_X_smooth_extr_C->Fill(voxX_pos, vec_DXYZ_vox_GF[2][i_sector][voxX][voxY][voxZ]); + } + } + } + } + } + } + } + //---------------------------------------------------------------- + + //---------------------------------------------------------------- + // IROC A11 maskign + if (maskIA11) { + int i_sector = 11; + for (Int_t voxY = 0; voxY < (nY2XBins); voxY++) { + for (Int_t voxZ = 0; voxZ < (nZ2XBins); voxZ++) { + for (Int_t voxX = 0; voxX <= 62; voxX++) { + for (Int_t i_xyz = 0; i_xyz < 4; i_xyz++) { + vec_DXYZ_vox_GF[i_xyz][i_sector][voxX][voxY][voxZ] = 0; + vec_DXYZ_vox[i_xyz][i_sector][voxX][voxY][voxZ] = 0; + } + } + } + } + } + + //---------------------------------------------------------------- + mTreeOut = std::make_unique("voxResTree", "Voxel results and statistics"); + mTreeOut->Branch("voxRes", &mVoxelResultsOutPtr); + // copy user info + auto userInfoOut = mTreeOut->GetUserInfo(); + for (auto o : *userInfo) { + userInfoOut->Add(o->Clone()); + } + // Overwrite the placeholder meanIDC/medianIDC just cloned above with the real offline-joined + // values computed in the "Offline IDC join" block earlier in this function. + if (auto* stale = userInfoOut->FindObject("meanIDC")) { + userInfoOut->Remove(stale); + delete stale; + } + if (auto* stale = userInfoOut->FindObject("medianIDC")) { + userInfoOut->Remove(stale); + delete stale; + } + userInfoOut->Add(new TNamed("meanIDC", std::to_string(meanIDCReal).data())); + userInfoOut->Add(new TNamed("medianIDC", std::to_string(medianIDCReal).data())); + userInfoOut->Add(new TNamed("startRowGoodEntries", std::to_string(startRowGoodEntries).data())); + userInfoOut->Add(new TNamed("maxDX", std::to_string(max_DX).data())); + userInfoOut->Add(new TNamed("maxDX_lx", std::to_string(max_X_DX).data())); + userInfoOut->Add(new TNamed("maxDX_row", std::to_string(max_Row_DX).data())); + + // copy aliases + if (voxResTree->GetListOfAliases()) { + for (auto o : *voxResTree->GetListOfAliases()) { + mTreeOut->SetAlias(o->GetName(), o->GetTitle()); + } + } + + for (Long64_t jentry = 0; jentry < entries_input_map; jentry++) { + voxResTree->GetEntry(jentry); + + auto bvox_X = voxRes_map->bvox[o2::tpc::TrackResiduals::VoxX]; // bin number in x (= pad row) + auto bvox_F = voxRes_map->bvox[o2::tpc::TrackResiduals::VoxF]; // bin number in y/x 0..14 + auto bvox_Z = voxRes_map->bvox[o2::tpc::TrackResiduals::VoxZ]; // bin number in z/x 0..4 + int sector = (int)voxRes_map->bsec; + // Int_t index_map = bvox_X+152*bvox_F+152*(nY2XBins)*bvox_Z+152*(nY2XBins)*(nZ2XBins)*sector; + + mVoxelResultsOut = *voxRes_map; // copy the entry from the input map + + for (int ixyz = 0; ixyz < 3; ixyz++) { + if (do_extrapolation == 1 || do_extrapolation == 2) + mVoxelResultsOut.DS[ixyz] = (float)vec_DXYZ_vox_GF[ixyz][sector][bvox_X][bvox_F][bvox_Z]; // overwrite the smoothed values + if (do_extrapolation == 2 || do_extrapolation == 3) + mVoxelResultsOut.D[ixyz] = (float)vec_DXYZ_vox[ixyz][sector][bvox_X][bvox_F][bvox_Z]; // overwrite the raw values + } + if (do_extrapolation == 1 || do_extrapolation == 2) + mVoxelResultsOut.stat[3] = (float)vec_DXYZ_vox_GF[3][sector][bvox_X][bvox_F][bvox_Z]; + if (do_extrapolation == 2 || do_extrapolation == 3) + mVoxelResultsOut.stat[3] = (float)vec_DXYZ_vox[3][sector][bvox_X][bvox_F][bvox_Z]; + for (int ixyz = 0; ixyz < 3; ixyz++) { + if (TMath::IsNaN(mVoxelResultsOut.DS[ixyz])) // NaN + { + LOGP(error, "NaN detected in smoothed value xyz {}, sec {}, voxX {}, voxY {}, voxZ {}", ixyz, sector, bvox_X, bvox_F, bvox_Z); + mVoxelResultsOut.DS[ixyz] = 0.0; + mVoxelResultsOut.stat[3] = 0; + mVoxelResultsOut.flags |= TrackResiduals::Masked; + } else { + mVoxelResultsOut.flags |= TrackResiduals::SmoothDone; + } + if (TMath::IsNaN(mVoxelResultsOut.D[ixyz])) // NaN + { + LOGP(error, "NaN detected in raw value xyz {}, sec {}, voxX {}, voxY {}, voxZ {}", ixyz, sector, bvox_X, bvox_F, bvox_Z); + mVoxelResultsOut.D[ixyz] = 0.0; + } + } + mTreeOut->Fill(); + } + //---------------------------------------------------------------- + + //---------------------------------------------------------------- + outputfile->cd(); + mTreeOut->Write(); + mTreeOut.release(); + tp_DX_vs_X_raw->Write(); + tp_DX_vs_X_smooth->Write(); + tp_DX_vs_X_smooth_extr->Write(); + tp_DZ_vs_X_raw_A->Write(); + tp_DZ_vs_X_raw_C->Write(); + tp_DZ_vs_X_smooth_A->Write(); + tp_DZ_vs_X_smooth_C->Write(); + tp_DZ_vs_X_smooth_extr_A->Write(); + tp_DZ_vs_X_smooth_extr_C->Write(); + tp_Stat_vs_X->Write(); + tp_Stat_vs_row->Write(); + h_x_start_fit_vs_z_phi_sector->Write(); + tp_Stat_vs_X_single->Write(); + tp_DX_vs_X_single->Write(); + outputfile->Close(); + //---------------------------------------------------------------- +} diff --git a/Detectors/TPC/calibration/SpacePoints/macro/staticMapCreatorCPM.C b/Detectors/TPC/calibration/SpacePoints/macro/staticMapCreatorCPM.C new file mode 100644 index 0000000000000..dea2b2f98c2ef --- /dev/null +++ b/Detectors/TPC/calibration/SpacePoints/macro/staticMapCreatorCPM.C @@ -0,0 +1,2564 @@ +// 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. + +#if !defined(__CLING__) || defined(__ROOTCLING__) +#include "CCDB/CcdbApi.h" +#include "CCDB/BasicCCDBManager.h" + +#include "TSystem.h" +#include "Algorithm/RangeTokenizer.h" +#include "Framework/Logger.h" +#include "CommonConstants/LHCConstants.h" +#include "SpacePoints/SpacePointsCalibConfParam.h" +#include "SpacePoints/TrackResiduals.h" +#include "SpacePoints/TrackInterpolation.h" +#include "DataFormatsParameters/GRPMagField.h" +#include "DataFormatsParameters/GRPLHCIFData.h" +#include "DataFormatsTPC/Defs.h" +#include "ReconstructionDataFormats/GlobalTrackID.h" +#include "DetectorsBase/MatLayerCylSet.h" +#include "DetectorsBase/Propagator.h" +#include "TPCBase/Mapper.h" +#include "ReconstructionDataFormats/TrackUtils.h" + +#include +#include +#include +#include +#include "TTreePerfStats.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +// For multiple threads +#include +#include +#include +#include +#include +#include "TROOT.h" + +// Compile-time detection of TrackData::filterFlag / UnbinnedResid::rejected, for back-compat with O2 +// builds that predate them. +template +struct HasFilterFlagMember : std::false_type { +}; +template +struct HasFilterFlagMember().filterFlag)>> : std::true_type { +}; + +template +struct HasRejectedMember : std::false_type { +}; +template +struct HasRejectedMember().rejected)>> : std::true_type { +}; + +template +bool hasPositiveFilterFlag(const T& t) +{ + if constexpr (HasFilterFlagMember::value) { + return t.filterFlag > 0; + } else { + return false; + } +} + +template +bool isRejectedResidual(const T& t) +{ + if constexpr (HasRejectedMember::value) { + return t.rejected; + } else { + return false; + } +} + +#else + +#error This macro must run in compiled mode + +#endif + +using namespace o2::tpc; +using GID = o2::dataformats::GlobalTrackID; +namespace fs = std::filesystem; + +constexpr int NSectors = SECTORSPERSIDE * SIDES; +constexpr int NRows = Mapper::PADROWS; + +// Portable peak-RSS report via getrusage(), which works on both Linux and macOS (unlike parsing +// /proc/self/status, which is Linux-only procfs). ru_maxrss's UNIT differs by platform though (bytes on +// macOS, KB on Linux) -- handled below. Only reports peak resident-set size (one number), not the +// separate VmRSS/VmPeak/VmSize that /proc/self/status exposes on Linux. +void printMemoryUsage(const std::string& label = "") +{ + struct rusage ru; + if (getrusage(RUSAGE_SELF, &ru) != 0) { + return; + } +#ifdef __APPLE__ + const double maxRssGB = ru.ru_maxrss / 1024.0 / 1024.0 / 1024.0; // macOS: ru_maxrss in bytes +#else + const double maxRssGB = ru.ru_maxrss / 1024.0 / 1024.0; // Linux: ru_maxrss in KB +#endif + if (label.empty()) { + LOGP(info, "VmPeakRSS {:.2f} GB", maxRssGB); + } else { + LOGP(info, "[{}] VmPeakRSS {:.2f} GB", label, maxRssGB); + } +} + +template +double calculateMean(const std::vector& vec) +{ + if (vec.empty()) { + LOGP(error, "vector is empty"); + return 0.; + } + + return std::accumulate(vec.begin(), vec.end(), 0.0) / vec.size(); +} + +template +double calculateMedian(std::vector vec) +{ + if (vec.empty()) { + LOGP(error, "vector is empty"); + return 0.; + } + + const size_t size = vec.size(); + const size_t midIndex = size / 2; + + std::nth_element(vec.begin(), vec.begin() + midIndex, vec.end()); + + if (size % 2 != 0) { + return vec[midIndex]; + } + + return (vec[midIndex - 1] + vec[midIndex]) / 2.0; +} + +// Lightweight double-precision 3-vector for the hot per-residual/per-voxel-flush loop, ~10x faster +// than TVector3 here (0.550s -> 0.054s over 20M calls) for numerically identical math -- matters +// because getIntCircles() runs while the per-voxel mutex is held. Double (not float) because this is +// live geometric computation (sqrt- and division-heavy) rather than storage -- contrast Vec3f below, +// which is storage-only and fine in single precision. +struct Vec3d { + double x = 0.0, y = 0.0, z = 0.0; + double X() const { return x; } + double Y() const { return y; } + double Z() const { return z; } + void SetXYZ(double xx, double yy, double zz) + { + x = xx; + y = yy; + z = zz; + } + double Perp() const { return std::sqrt(x * x + y * y); } + double Mag() const { return std::sqrt(x * x + y * y + z * z); } + void RotateZ(double angle) + { + const double c = std::cos(angle), s = std::sin(angle); + const double xn = x * c - y * s, yn = x * s + y * c; + x = xn; + y = yn; + } + Vec3d& operator-=(const Vec3d& o) + { + x -= o.x; + y -= o.y; + z -= o.z; + return *this; + } + Vec3d& operator+=(const Vec3d& o) + { + x += o.x; + y += o.y; + z += o.z; + return *this; + } + Vec3d& operator*=(double a) + { + x *= a; + y *= a; + z *= a; + return *this; + } +}; +inline Vec3d operator-(const Vec3d& a, const Vec3d& b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; } +inline Vec3d operator*(const Vec3d& a, double s) { return {a.x * s, a.y * s, a.z * s}; } + +//------------------------------------------------------------------------------------------------------------ +// Crossing point of two circles in the transverse plane. +// +// Two sentinel returns, both of which callers reject via their own transverse-radius band cut: +// (9999, 9999, 9999) -- the circles do not intersect at all (Perp() far above any accepted radius) +// (0, 0, 0) -- they intersect, but neither solution falls in the valid TPC radial band below +// A third, implicit outcome is NaN: when d lands within a few ulps of a tangency bound (d == r1+r2 or +// d == |r1-r2|) the guards below still pass, but rounding can drive the sqrt argument marginally +// negative, since a == r1 exactly at both bounds in exact arithmetic. NaN then fails the callers' +// band cut too (every comparison against it is false), so such a degenerate pair is simply dropped -- +// which is the wanted behaviour, hence no clamping here. +Vec3d getIntCircles(double r1, double r2, Vec3d circleCenter1, Vec3d circleCenter2, double voxX, double voxY) +{ + Vec3d vecSp; // default-constructed to (0, 0, 0), which is the "no solution accepted" sentinel + + const Vec3d dVec = (circleCenter1 - circleCenter2); + const double d = dVec.Perp(); // dist. circle center 1 & 2 + + if (d > r1 + r2) { + // no solutions, the circles are separate + vecSp.SetXYZ(9999, 9999, 9999); + return vecSp; + } + if (d < std::fabs(r1 - r2)) { + // no solutions, one circle is contained in the other + vecSp.SetXYZ(9999, 9999, 9999); + return vecSp; + } + if (d < 0.0001) { + // no solutions, same circle center + vecSp.SetXYZ(9999, 9999, 9999); + return vecSp; + } + + const double dx = circleCenter2.X() - circleCenter1.X(); + const double dy = circleCenter2.Y() - circleCenter1.Y(); + + const double a = (r1 * r1 - r2 * r2 + d * d) / (2 * d); + const double h = std::sqrt(r1 * r1 - a * a); + + // Midpoint of the chord joining the two intersection points. Deliberately written as a reciprocal + // multiply rather than `a * dx / d`: floating-point reciprocal-then-multiply and multiply-then-divide + // are not guaranteed to give the same last-bit result. Keep this exact form. + const double Mx = circleCenter1.X() + (1 / d) * a * dx; + const double My = circleCenter1.Y() + (1 / d) * a * dy; + + const double sp1x = Mx + h * dy / d; + const double sp2x = Mx - h * dy / d; + + const double sp1y = My - h * dx / d; + const double sp2y = My + h * dx / d; + + // The two solutions take the z of the circle they came from, not a common z. + const double sp1z = circleCenter1.Z(); + const double sp2z = circleCenter2.Z(); + + const double sp1Perp = std::sqrt(sp1x * sp1x + sp1y * sp1y); + const double sp2Perp = std::sqrt(sp2x * sp2x + sp2y * sp2y); + + const bool sp1In = (sp1Perp > 60.0 && sp1Perp < 280.0); + const bool sp2In = (sp2Perp > 60.0 && sp2Perp < 280.0); + + if (sp1In && sp2In) { + // Both solutions are physically plausible -- pick whichever is closer to the voxel center rather + // than an arbitrary, data-independent choice that could just as easily discard the better of two + // valid solutions. + const double d1 = std::sqrt((sp1x - voxX) * (sp1x - voxX) + (sp1y - voxY) * (sp1y - voxY)); + const double d2 = std::sqrt((sp2x - voxX) * (sp2x - voxX) + (sp2y - voxY) * (sp2y - voxY)); + if (d1 <= d2) { + vecSp.SetXYZ(sp1x, sp1y, sp1z); + } else { + vecSp.SetXYZ(sp2x, sp2y, sp2z); + } + } else if (sp1In) { + vecSp.SetXYZ(sp1x, sp1y, sp1z); + } else if (sp2In) { + vecSp.SetXYZ(sp2x, sp2y, sp2z); + } + // else: neither in band, vecSp stays at its default (0,0,0) sentinel + + return vecSp; +} +//------------------------------------------------------------------------------------------------------------ + +struct range { + long from{-1}; + long to{-1}; + + bool operator<(const range& other) + { + return from < other.from; + } + + void sort() + { + if (from > to) { + std::swap(from, to); + } + } +}; + +// ---- Fastest-replica selection for alien:// residual files (used by getInputFileList below) ---- +// Different Storage Elements serving the same LFN can have very different real-world throughput +// depending on where this job actually lands on the network (verified: ALICE::FZK::SE faster than +// ALICE::CERN::EOS from one machine, the reverse from another) -- a name-based heuristic can't predict +// that, so probe with a real timed read instead. Residual files are O(10GB), so getting this wrong once +// costs far more than the probe itself. +struct SEReplica { + std::string se; + std::string pfn; +}; + +std::vector getAlienReplicas(const std::string& plainLFN) +{ + std::vector result; + std::string cmd = "alien.py whereis -r " + plainLFN + " 2>/dev/null"; + std::unique_ptr pipe(popen(cmd.c_str(), "r"), pclose); + if (!pipe) { + return result; + } + std::array buffer; + std::string output; + while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { + output += buffer.data(); + } + + auto trim = [](std::string& s) { + s.erase(0, s.find_first_not_of(" \t")); + s.erase(s.find_last_not_of(" \t\r\n") + 1); + }; + std::istringstream iss(output); + std::string line; + while (std::getline(iss, line)) { + auto sePos = line.find("SE =>"); + auto pfnPos = line.find("pfn =>"); + if (sePos == std::string::npos || pfnPos == std::string::npos) { + continue; + } + std::string se = line.substr(sePos + 5, pfnPos - sePos - 5); + std::string pfn = line.substr(pfnPos + 6); + trim(se); + trim(pfn); + result.push_back({se, pfn}); + } + return result; +} + +// Generates a tiny standalone probe macro on disk (ROOT requires the file stem to match the top-level +// function name) that opens one alien:// replica and reads a few real entries from the 'unbinnedResid' +// tree (the same tree/branch doFileProcessing itself reads, with the same two dominant unused +// sub-branches disabled -- see there), timing the real transfer via TFile::GetBytesRead(). Prints +// "PROBE_OK " or "PROBE_FAIL" to stdout -- this is invoked as a subprocess by +// probeOneReplicaSubprocess below, never called in-process. +std::string writeProbeChildMacro(const std::string& funcName) +{ + static const char* templateSrc = + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \"SpacePoints/TrackInterpolation.h\"\n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "\n" + "void @FUNC@(const char* plainLFN, const char* se, Long64_t probeEntries = 3)\n" + "{\n" + " // This runs in a fresh, separate ROOT process (see probeOneReplicaSubprocess) -- unlike the parent\n" + " // process, gGrid is never already set up here, so it must be connected explicitly.\n" + " if (!gGrid && !TGrid::Connect(\"alien://\")) {\n" + " printf(\"PROBE_FAIL\\n\");\n" + " return;\n" + " }\n" + " std::string url = std::string(\"alien://\") + plainLFN + \"?se=\" + se;\n" + " std::unique_ptr f(TFile::Open(url.c_str(), \"READ\"));\n" + " if (!f || f->IsZombie()) { printf(\"PROBE_FAIL\\n\"); return; }\n" + " TTreeReader reader(\"unbinnedResid\", f.get());\n" + " TTree* residTree = reader.GetTree();\n" + " if (!residTree) { printf(\"PROBE_FAIL\\n\"); return; }\n" + " TTreeReaderValue> res(reader, \"res\");\n" + " residTree->SetBranchStatus(\"res.tgSlp\", 0);\n" + " residTree->SetBranchStatus(\"res.channel\", 0);\n" + "\n" + " Long64_t bytesBefore = f->GetBytesRead();\n" + " auto t0 = std::chrono::steady_clock::now();\n" + " Long64_t nRead = 0;\n" + " for (Long64_t i = 0; i < probeEntries && reader.Next(); ++i) {\n" + " if (static_cast(res.GetSetupStatus()) < 0) break;\n" + " (void)res->size();\n" + " ++nRead;\n" + " }\n" + " auto t1 = std::chrono::steady_clock::now();\n" + " Long64_t bytesRead = f->GetBytesRead() - bytesBefore;\n" + " if (nRead == 0 || bytesRead <= 0) { printf(\"PROBE_FAIL\\n\"); return; }\n" + " double sec = std::chrono::duration(t1 - t0).count();\n" + " printf(\"PROBE_OK %lld %f\\n\", (long long)bytesRead, sec);\n" + "}\n"; + + std::string src(templateSrc); + const std::string placeholder = "@FUNC@"; + auto pos = src.find(placeholder); + if (pos != std::string::npos) { + src.replace(pos, placeholder.size(), funcName); + } + + const std::string path = "/tmp/" + funcName + ".C"; + std::ofstream out(path); + out << src; + out.close(); + return path; +} + +// Probes one candidate replica in a SEPARATE OS PROCESS, bounded by the `timeout` command, so a genuine +// hang (not just a slow-but-working transfer) can be killed without touching this process's own +// TGrid/JAlien connection. An in-process std::async-based timeout was tried and rejected: +// std::future's destructor from std::launch::async BLOCKS until the task finishes regardless of what +// wait_for() returned, so it doesn't actually bound wall-clock time -- and leaking the future to dodge +// that reopens a real concurrent-JAlien-access crash. A real OS process boundary is the only mechanism +// that is both a genuine timeout AND safe against that crash. +// +// Confirmed necessary by a real GRID failure: a job hung completely (~1% CPU for 15 minutes, no +// progress) inside an in-process probe's first candidate until AliEn's idle-CPU watchdog killed the +// whole job. A bounded probe lets it fall through to the next candidate instead of losing the slot. +bool probeOneReplicaSubprocess(const std::string& plainLFN, const std::string& se, double& bytesPerSec, + int timeoutSec = 30, Long64_t probeEntries = 3) +{ + bytesPerSec = -1.0; + const std::string funcName = fmt::format("probeSEChild{}", static_cast(getpid())); + const std::string macroPath = writeProbeChildMacro(funcName); + + const std::string cmd = fmt::format( + "timeout {}s root.exe -b -q -l -x '{}(\"{}\", \"{}\", {})' 2>&1", + timeoutSec, macroPath, plainLFN, se, probeEntries); + + std::string output; + { + std::unique_ptr pipe(popen(cmd.c_str(), "r"), pclose); + if (pipe) { + std::array buffer; + while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { + output += buffer.data(); + } + } + } + std::remove(macroPath.c_str()); + + // Parse line-by-line rather than a single sscanf over the whole output -- ROOT's own startup banner + // and "Info in " lines precede the actual PROBE_OK/PROBE_FAIL line. + std::istringstream iss(output); + std::string line; + while (std::getline(iss, line)) { + long long bytesRead = 0; + double sec = 0.0; + if (sscanf(line.c_str(), "PROBE_OK %lld %lf", &bytesRead, &sec) == 2) { + if (sec > 0 && bytesRead > 0) { + bytesPerSec = static_cast(bytesRead) / sec; + return true; + } + return false; + } + if (line.rfind("PROBE_FAIL", 0) == 0) { + return false; + } + } + return false; // empty/no matching line -- timed out (killed by `timeout`) or crashed +} + +// Probes each candidate replica in turn (via probeOneReplicaSubprocess above) and returns the SE with +// the highest measured throughput, or "" if every candidate failed or timed out. +std::string probeFastestSE(const std::string& plainLFN, const std::vector& replicas, + Long64_t probeEntries = 3, int timeoutSec = 30) +{ + std::string bestSE; + double bestBytesPerSec = -1.0; + for (const auto& r : replicas) { + double bps = -1.0; + if (!probeOneReplicaSubprocess(plainLFN, r.se, bps, timeoutSec, probeEntries)) { + LOGP(warning, "SE probe: {} failed or timed out after {}s", r.se, timeoutSec); + continue; + } + LOGP(info, "SE probe: {} -> {:.1f} MB/s", r.se, bps / (1024.0 * 1024.0)); + if (bps > bestBytesPerSec) { + bestBytesPerSec = bps; + bestSE = r.se; + } + } + return bestSE; +} + +std::vector loadRunTimeSpans(const std::string& flname, int onlyRun, const std::string& selection); +std::vector getInputFileList(const std::string& fileInput) +{ + std::vector fileList; + std::vector fileListVerified; + // check if only one input file (a txt file contaning a list of files is provided) + if (fileInput.length() > 3 && fileInput.substr(fileInput.length() - 3, 3) == "txt") { + LOGP(info, "Reading files from input file list {}", fileInput); + std::ifstream is(fileInput); + std::istream_iterator start(is); + std::istream_iterator end; + fileList.insert(fileList.begin(), start, end); + } else { + fileList.push_back(fileInput); + } + + // fastestSE is probed once, for the first alien:// file, and reused for the rest of this slot's files + // (same production run -> same SE set, in practice), avoiding a ~10GB-file probe cost per file. If a + // later file isn't actually hosted on the cached SE, doFileProcessing's own open has a fallback to + // unforced resolution -- so a stale cache can't silently drop a file, only cost a little speed for + // that one file. + std::string fastestSE; + for (auto file : fileList) { + if ((file.find("alien://") == 0) && !gGrid && !TGrid::Connect("alien://")) { + LOGP(fatal, "Failed to open alien connection"); + } + if (gSystem->Getenv("FORCESE") && !TString(file.data()).EndsWith(gSystem->Getenv("FORCESE"))) { + file += "?se="; + file += gSystem->Getenv("FORCESE"); + } else if (!gSystem->Getenv("FORCESE") && file.rfind("alien://", 0) == 0) { + if (fastestSE.empty()) { + std::string plainLFN = file.substr(std::string("alien://").size()); + auto slash = plainLFN.find_first_not_of('/'); + plainLFN = (slash == std::string::npos) ? "/" : "/" + plainLFN.substr(slash); + auto replicas = getAlienReplicas(plainLFN); + if (!replicas.empty()) { + fastestSE = (replicas.size() == 1) ? replicas.front().se : probeFastestSE(plainLFN, replicas); + if (!fastestSE.empty()) { + LOGP(info, "Auto-selected fastest SE {} for this slot's alien:// files", fastestSE); + } + } + } + if (!fastestSE.empty()) { + file += "?se="; + file += fastestSE; + } + } + fileListVerified.push_back(file); + } + + if (fileListVerified.size() == 0) { + LOGP(error, "No input files to process"); + } + return fileListVerified; +} + +bool revalidateTrack(const TrackData& trk, const SpacePointsCalibConfParam& params) +{ + + if (hasPositiveFilterFlag(trk)) { + return false; + } + + if (fabs(trk.par.getTgl()) > params.maxZ2X) { + return false; + } + if (trk.nClsITS < params.minITSNCls) { + return false; + } + if (trk.nClsTPC < params.minTPCNCls) { + return false; + } + // No TRD-based cuts here (neither on the tracklet count nor on chi2TRD): this macro does not use TRD. + // track quality cuts + if (trk.chi2ITS / trk.nClsITS > params.maxITSChi2) { + return false; + } + if (trk.chi2TPC / trk.nClsTPC > params.maxTPCChi2) { + return false; + } + + if (params.cutOnDCA) { + auto propagator = o2::base::Propagator::Instance(); + // o2::track::TrackPar trkPar(trk.x, trk.alpha, trk.p); // use this line, in case ClassDef version of TrackData < 4 + o2::track::TrackPar trkPar = trk.par; + if (!propagator->propagateToX(trkPar, 0, propagator->getNominalBz())) { + return false; + } + if (trkPar.getX() * trkPar.getX() + trkPar.getY() * trkPar.getY() > params.maxDCA * params.maxDCA) { + return false; + } + } + return true; +} + +// Check that a TTreeReaderValue was actually bound to a branch of the expected type. +// Unlike SetBranchAddress (which silently tolerated a missing/mismatching branch), dereferencing a +// TTreeReaderValue that failed to set up returns a null proxy and segfaults, so this has to be +// checked once per file before the data is used. The setup status is only final after the first +// entry has been loaded, so call this after SetEntry(). +template +bool checkReaderValue(const TTreeReaderValue& value, const int iThread, const std::string& fileName) +{ + // all failure codes of ESetupStatus are negative (kSetupMatch is 0, the other success codes are positive) + const auto status = value.GetSetupStatus(); + if (static_cast(status) < 0) { + LOGP(warning, "[Thread{}] Branch '{}' could not be set up (setup status {}) in file {}", iThread, value.GetBranchName(), static_cast(status), fileName); + return false; + } + return true; +} + +// Pool size per voxel/charge. Compile-time rather than a runtime parameter so that circleCenters and +// circleRadii below can be fixed-size std::array instead of heap-allocated std::vector: with ~3M +// voxels in a realistic bin configuration, per-voxel heap allocations add up to roughly 4.2 GB of +// resident memory and 12M+ small mallocs for these two members alone. Changing the pool size means +// editing this line and recompiling. +constexpr int NPool = 15; + +// Warm-up threshold for the no-input-map DZ rolling-average correction: minimum cumulative NP/PP/NN +// sample count (see VoxelData below) required before a voxel's residualsAll[0] (dX) is trusted enough +// to shift the Z propagation. See the call site in doFileProcessing for the full reasoning. +constexpr int MinDxSamplesForZCorr = 20; + +// Storage for circleCenters: pure scratch coordinates (never serialized/drawn), populated from +// float-precision inputs (xycircle.xC/yC, track/voxel positions) in the first place, so float is +// enough -- consistent with circleRadii already being float. Converts implicitly to Vec3d at the point +// of use (getIntCircles computes in double internally regardless of the float input). +struct Vec3f { + float x = 0.f, y = 0.f, z = 0.f; + operator Vec3d() const { return Vec3d{x, y, z}; } +}; + +// shared circle pool for one voxel; one instance per voxel (not per thread), guarded by its own mutex +struct VoxelData { + std::mutex mtx; //! per-voxel mutex, shared across threads + std::array, 2> circleCenters; // [charge] was vec_TV3_circle_center_thread + std::array, 2> circleRadii; // [charge] was vec_TV3_circle_radius_thread + std::array poolCounter{}; // [charge] was vec_counter_thread + + std::array residualsAll{}; // was vec_residualsAll_thread; [2] (Z) is a running sum, see counterZAll + std::array residualsNP{}; // was vec_residualsNP_thread + std::array residualsPP{}; // was vec_residualsPP_thread + std::array residualsNN{}; // was vec_residualsNN_thread + int counterNP = 0; // was vec_residuals_counterNP_thread + int counterPP = 0; // was vec_residuals_counterPP_thread + int counterNN = 0; // was vec_residuals_counterNN_thread + int counterZAll = 0; // was vec_residuals_counterZAll_thread +}; + +// One TimeFrame's data, fully OWNED (copied out of the TTreeReaderValues rather than referencing them). +// TTreeReaderValue::operator* reuses the same underlying storage on every SetEntry -- a background +// producer thread reading TF N+1 into that storage while the consumer is still processing TF N's tracks +// would race and corrupt data, the same class of hazard already found once in this file (a lazy- +// deserialization race across concurrent track-worker threads). Copying the three vectors out per TF +// avoids that; the copy itself is small next to the per-TF track-processing time. +struct TFPackage { + int iEntry = 0; + std::vector trackRefsVec; + std::vector trackDataVec; + std::vector unbinnedResidualsVec; +}; + +// Bounded single-producer/single-consumer queue of TFPackages. Lets one background thread stay a few +// TimeFrames ahead of the (I/O-free -- see doFileProcessing's own track-loop-parallelism notes) track +// processing, so a TTreeCache refill on some later TF can overlap with the current TF's track-worker +// compute instead of blocking it. This is meant to hide the periodic TTreeCache-refill spikes this +// pipeline's I/O shows in practice, and only pays off paired with a moderate TTreeCache size -- too +// large a cache makes individual refills bigger than any reasonable queue depth can absorb. +class BoundedTFQueue +{ + public: + explicit BoundedTFQueue(size_t maxDepth) : mMaxDepth(maxDepth) {} + + // Producer side. Blocks while the queue is already at capacity. + void push(std::unique_ptr pkg) + { + std::unique_lock lock(mMtx); + mNotFull.wait(lock, [this] { return mQueue.size() < mMaxDepth; }); + mQueue.push_back(std::move(pkg)); + lock.unlock(); + mNotEmpty.notify_one(); + } + + // Producer side, called once (file fully read, or the maxTracks quota was reached). + void setDone() + { + { + std::lock_guard lock(mMtx); + mDone = true; + } + mNotEmpty.notify_one(); + } + + // Consumer side. Returns nullptr once the producer is done AND the queue has been fully drained. + std::unique_ptr pop() + { + std::unique_lock lock(mMtx); + mNotEmpty.wait(lock, [this] { return !mQueue.empty() || mDone; }); + if (mQueue.empty()) { + return nullptr; + } + auto pkg = std::move(mQueue.front()); + mQueue.pop_front(); + lock.unlock(); + mNotFull.notify_one(); + return pkg; + } + + // Diagnostic only: how many packages are sitting ready right now. Lets the consumer log whether the + // producer is comfortably ahead (queue usually near mMaxDepth) or struggling to keep up (queue usually + // near empty) -- distinguishes "the buffer is the wrong depth" from "the producer itself is too slow + // to ever fill it, no matter how deep it is". + size_t size() const + { + std::lock_guard lock(mMtx); + return mQueue.size(); + } + + private: + const size_t mMaxDepth; + mutable std::mutex mMtx; + std::condition_variable mNotEmpty; + std::condition_variable mNotFull; + std::deque> mQueue; + bool mDone = false; +}; + +// How many TimeFrames the background producer may stay ahead of track-processing. Absorbs some of the +// periodic TTreeCache-refill spikes this pipeline's I/O shows in practice, though 10 (the default +// below) isn't enough to fully hide the biggest ones -- a much larger depth would be needed for that, +// at a real memory cost (a single TF can carry tens of thousands of tracks). Overridable via +// SCDCALIB_TF_QUEUE_DEPTH (no recompile) to tune against a real workload's spike size. +constexpr size_t TFQueueDepthDefault = 10; + +void doFileProcessing(const int iThread, + const int nFileThreads, + const int maxTrackWorkers, + const long firstTFTime, + const long lastTFTime, + const bool invertBadRange, + const float maxdEdx, + const float maxdEdxExp, + const float maxDevdEdxOverExp, + const float skipEdgePads, + std::vector& nEdgeClustersSkipped_thread, + std::vector& nTracksSkippedByBadRangeList_thread, + std::vector& nTFs_thread, + std::vector& nTFsSkippedByBadRangeList_thread, + std::vector& nTFsSkippedByTimeWindow_thread, + const std::string voxMapInput, + const GID::mask_t sources, + const int64_t orbitResetTimeMS, + const float magfieldvalue, + const std::vector fileList, + const int maxTracksPerSlice, + const Long64_t maxTracks, + std::atomic& nTracksProcessed, + const std::array, NSectors>& voxelResults, // read-only input correction map, shared across threads (no per-thread copy) + std::vector>& badRanges_thread, + const TrackResiduals& trackResiduals, // read-only: findVoxelBin/getVoxelCoordinates/getGlbVoxBin are all const, safe to share across threads + const float maxDistIntCls, + const int nY2XBins, + const int nZ2XBins, + std::vector& voxels, // flat [sec*152*nY2XBins*nZ2XBins + ix*nY2XBins*nZ2XBins + iy*nZ2XBins + iz] -- shared across threads, one instance per voxel + std::vector& totalBytesReadPerf_thread, + std::vector& lumiEntriesCTP_thread, + std::vector& lumiSumCTP_thread, + std::vector>& orbitsSel_thread, + std::vector>& ctpLumiSel_thread, + std::vector>& timeMSsel_thread) +{ + // Get Mapper + const Mapper& mapper = Mapper::instance(); + + // yMaxCentrePadByRow only depends on the pad row (152 values) -- precomputed once per file-thread + // here, gated on skipEdgePads since that's its only use, rather than via a Mapper lookup per residual. + std::array yMaxCentrePadByRow{}; + if (skipEdgePads) { + for (int irow = 0; irow < NRows; ++irow) { + yMaxCentrePadByRow[irow] = mapper.getPadCentre(o2::tpc::PadPos(irow, 0)).Y() - mapper.getPadRegionInfo(o2::tpc::Mapper::REGION[irow]).getPadWidth() / 2; + } + } + + // Obtain configuration per thread + const SpacePointsCalibConfParam& params_thread = SpacePointsCalibConfParam::Instance(); + + // Per-thread input handles and I/O-monitoring state. These are plain locals: each file-thread only ever + // touches its own, so there is nothing to share with the other threads or hand back to the caller. + // + // DECLARATION ORDER IS LOAD-BEARING. Locals are destroyed in reverse order of declaration, and these + // objects reference each other: a TTreeReaderValue refers to its TTreeReader, which refers to a TTree + // owned by the TFile, and TTreePerfStats refers to that tree too. Declaring the file first and the + // reader values last therefore tears them down in the only safe order -- values, then perf stats, then + // readers, then the file. Do not reorder these. + std::unique_ptr inputFile; + std::unique_ptr treeUnbinnedResiduals; + std::unique_ptr treeTrackData; + std::unique_ptr treeRecords; + std::unique_ptr perfStats; + std::unique_ptr>> unbinnedResiduals; // unbinned residuals input + std::unique_ptr>> trackRefs; // track references for the unbinned residuals + std::unique_ptr>> trackData; // additional track info (chi2, nClusters, track parameters) + std::unique_ptr>> orbits; // first orbit of each TF in the input data + std::unique_ptr> lumiTF; // lumi info + + // Previous I/O sample, for the instantaneous-rate log inside the TF loop. + Long64_t perfLastBytes = 0; + std::chrono::steady_clock::time_point perfLastSample; + + int trackCounter_local{0}; + + // Track-loop parallelism, WITHIN this one file-thread only -- active when nFileThreads==1 (GRID/ + // alien:// mode, forced by getInputFileList() for TGrid safety) or when there's only one input file + // (otherwise every other core would sit idle). Safe because the track/cluster loop body touches no + // TGrid/CCDB/file I/O (SetEntry() already happened before this point), only in-memory TF data plus the + // same per-voxel mutex (vox.mtx) multi-file-thread mode already relies on. Each worker gets its own + // copy of every piece of mutated state (RNG, scratch coordinates, counters) -- sharing any of it + // across workers would be a silent data race. Otherwise nTrackWorkers is forced to 1 (serial, via + // worker index 0) to avoid oversubscribing on top of the file-threads. + int nTrackWorkers = 1; + if (nFileThreads == 1 || fileList.size() == 1) { + if (maxTrackWorkers > 0) { + // Explicit override (e.g. the number of cores actually allocated to a batch/GRID job). + // hardware_concurrency() reports the machine's core count, not the job's allocation -- an 8-core + // GRID job auto-detects 32 and oversubscribes 4x -- so when the caller knows, trust it instead. + nTrackWorkers = maxTrackWorkers; + } else { + unsigned hc = std::thread::hardware_concurrency(); + nTrackWorkers = (hc == 0) ? 8 : static_cast(hc); + if (nTrackWorkers > 32) { + nTrackWorkers = 32; + } + } + } + LOGP(info, "[Thread_{}] Using {} track-worker thread(s) for the track loop", iThread, nTrackWorkers); + std::vector clsPosWorker(nTrackWorkers); + // Per-worker scratch: this track's position at the current row. + std::vector trackPosAtRow_worker(nTrackWorkers); + std::vector nEdgeClustersSkipped_worker(nTrackWorkers, 0); + std::vector trackCounter_local_worker(nTrackWorkers, 0); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // ===| FILE LOOP |=================================================================================================================== + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // This thread's share of the input files, as an explicit work list: this is what lets a file that + // looks unhealthy be pushed to the BACK and retried later instead of being processed now or dropped -- + // see the health checks below for why deferring beats both. Appending while iterating by index is + // safe: the bound is re-read every iteration and no iterators are held. + std::vector workList; + for (int i = iThread; i < int(fileList.size()); i += nFileThreads) { + workList.push_back(i); + } + // How many times each file has already been deferred, so a persistently sick one cannot cycle forever. + std::vector deferCount(fileList.size(), 0); + int maxFileDeferrals = 1; + if (const char* envMaxDeferrals = gSystem->Getenv("SCDCALIB_MAX_FILE_DEFERRALS")) { + maxFileDeferrals = std::atoi(envMaxDeferrals); + } + // Health-probe timings (seconds) of the files accepted so far, used as this job's own baseline. An + // absolute threshold cannot work here: measured healthy timings differ by ~6x between GRID regimes, + // so what matters is how a file compares to the others in ITS OWN slot, not to a constant. + std::vector probeTimes; + double probeSlowFactor = 5.0; + if (const char* envSlowFactor = gSystem->Getenv("SCDCALIB_PROBE_SLOW_FACTOR")) { + probeSlowFactor = std::atof(envSlowFactor); + } + // Fallback used until enough samples exist for a median to mean anything (and if the very first file + // of a slot is the sick one, this is the only thing standing between us and the stall). + double probeAbsMaxSec = 15.0; + if (const char* envProbeAbsMax = gSystem->Getenv("SCDCALIB_PROBE_ABS_MAX_SEC")) { + probeAbsMaxSec = std::atof(envProbeAbsMax); + } + // Floor under the median-relative threshold once >=3 samples exist. Without it, a fast-regime median + // (tens of ms) makes ordinary network jitter look "5x slower than usual" and trip the probe -- and a + // file unlucky enough to trip it twice is dropped for good (see deferFile below), losing real data to + // noise. The probe's actual target is stalls an order of magnitude bigger (~20s against a ~1.5s + // baseline, in the real case this was built around), so a small floor can't mask that while still + // absorbing sub-second jitter in a fast regime. + double probeMinThresholdSec = 2.0; + if (const char* envProbeMinThreshold = gSystem->Getenv("SCDCALIB_PROBE_MIN_THRESHOLD_SEC")) { + probeMinThresholdSec = std::atof(envProbeMinThreshold); + } + + for (size_t iWork = 0; iWork < workList.size(); ++iWork) { + const int iFile = workList[iWork]; + // Get filename from fileList + auto fileName = fileList[iFile]; + + // Check if enough tracks are processed + if ((maxTracks > 0) && (nTracksProcessed.load(std::memory_order_relaxed) > maxTracks)) { + LOGP(info, "[Thread_{}] Maximum number of requested tracks processed {} > {} ({}), will not process further files", iThread, nTracksProcessed.load(std::memory_order_relaxed), maxTracks, maxTracksPerSlice); + break; + } + + if (gSystem->Getenv("FORCESE") && !TString(fileName.data()).EndsWith(gSystem->Getenv("FORCESE"))) { + fileName += "?se="; + fileName += gSystem->Getenv("FORCESE"); + } + + // Open tree and set branches + LOGP(info, "[Thread{}] Processing input file {}", iThread, fileName); + unbinnedResiduals.reset(nullptr); + trackRefs.reset(nullptr); + lumiTF.reset(nullptr); + trackData.reset(nullptr); + orbits.reset(nullptr); + // Must be destroyed here, before the readers/file below: TTreePerfStats registers itself as the + // process-wide gPerfStats global and keeps raw TFile*/TTree* pointers to the file it watches. + // Leaving it alive past this point means gPerfStats still points at this (about to be destroyed) + // file while the NEXT file's setup does real reads -- TFile::ReadBuffer() unconditionally calls + // gPerfStats->FileReadEvent(thatFile, ...), which compares thatFile against the dangling fFile + // pointer; if the allocator hands the new TFile the same address the old one was just freed from + // (a real, common allocator pattern for same-sized immediate reuse), the comparison spuriously + // matches and it dereferences the also-dangling fTree -- a real, reproduced segfault on the 2nd+ + // file of a multi-file run, verified against the installed ROOT's TTreePerfStats.cxx/TFile.cxx + // source. perfStats's own destructor is safe to call here (only clears gPerfStats if it's still the + // registered one; never dereferences fTree/fFile), so this alone fixes it. + perfStats.reset(nullptr); + treeUnbinnedResiduals.reset(nullptr); + treeTrackData.reset(nullptr); + treeRecords.reset(nullptr); + + const auto openStart = std::chrono::steady_clock::now(); + inputFile.reset(TFile::Open(fileName.c_str())); + if ((!inputFile || inputFile->IsZombie())) { + // A forced-SE URL (FORCESE or the auto-picked/cached fastest SE, see getInputFileList) can fail if + // this particular file isn't actually hosted there -- fall back to unforced alien:// resolution + // rather than skipping the file outright, since a stale SE cache would otherwise silently drop data. + auto sePos = fileName.find("?se="); + if (sePos != std::string::npos) { + std::string fallbackName = fileName.substr(0, sePos); + LOGP(warning, "[Thread_{}] Forced-SE open failed for {}, retrying without SE override: {}", iThread, fileName, fallbackName); + inputFile.reset(TFile::Open(fallbackName.c_str())); + } + } + // Gates the "[prefetch diag]" per-TF/per-file I/O diagnostics further below: real signal for + // diagnosing GRID stalls/CPU-idle-watchdog kills (the reason this machinery exists at all), but + // pure noise on a local/Lustre run with many file-threads where none of that risk applies -- a + // 36-file-thread local run was producing an unreadable flood of per-TF consumer lines otherwise. + const bool isAlienFile = fileName.find("alien://") != std::string::npos; + if (isAlienFile) { + if (inputFile && !inputFile->IsZombie()) { + inputFile->SetBufferSize(4000000); + LOGP(info, "[Thread_{}] Set buffer size to {}", iThread, inputFile->GetBufferSize()); + } else { + LOGP(info, "[Thread_{}] TFile {} is empty", iThread, fileName); + } + } + + if (!inputFile || inputFile->IsZombie()) { + LOGP(warning, "[Thread{}] Skipping file {}", iThread, fileName); + continue; + } + + // Deferring an unhealthy-looking file, rather than skipping it. Observed for real: a file that was + // reproducibly slow in one job read at full speed a short time later -- the slowness is transient + // storage-server state, not a property of the file. So dropping it outright throws away data that + // would very likely have been fine. Pushing it to the back of this thread's work list costs exactly + // what skipping costs right now, but gives the server time to recover, and if maxTracks stops the + // job first the file is never touched again at all. The caller always moves on to the next file. + auto deferFile = [&](const char* reason, double measured, double threshold) { + if (deferCount[iFile] < maxFileDeferrals) { + ++deferCount[iFile]; + workList.push_back(iFile); + LOGP(warning, "[Thread{}] {} for {} ({:.1f} s vs {:.1f} s threshold) -- deferring it to the end of this thread's file list (attempt {} of {}); storage slowness has been seen to be transient, so it may read fine later, and maxTracks may stop the job before we return to it", + iThread, reason, fileName, measured, threshold, deferCount[iFile], maxFileDeferrals); + return; + } + LOGP(warning, "[Thread{}] {} for {} ({:.1f} s vs {:.1f} s threshold) and it has already been deferred {} time(s) -- skipping this file for good", + iThread, reason, fileName, measured, threshold, deferCount[iFile]); + }; + + // --- Unhealthy-replica gate ------------------------------------------------------------------- + // An abnormally slow TFile::Open is the earliest sign a replica's storage server is struggling, + // and the last point we can walk away cheaply: the warm-up read right after this pulls a whole + // TTreeCache-sized chunk in one uninterruptible call, so if the server stalls there the job hangs + // until AliEn's ~15-minute idle-CPU watchdog kills it, discarding all work already done. Skipping + // one file here costs a fraction of a slot's statistics, so the trade is heavily one-sided. 15 s + // threshold, calibrated against real GRID opens (absolute, not relative to this job's own timings + // -- may need revisiting on very different links). Tune via SCDCALIB_MAX_FILE_OPEN_SEC; <= 0 + // disables it. + double maxFileOpenSec = 15.0; + if (const char* envMaxOpenSec = gSystem->Getenv("SCDCALIB_MAX_FILE_OPEN_SEC")) { + maxFileOpenSec = std::atof(envMaxOpenSec); + } + const double openSec = std::chrono::duration(std::chrono::steady_clock::now() - openStart).count(); + if (maxFileOpenSec > 0 && openSec > maxFileOpenSec) { + deferFile("Slow file open (SCDCALIB_MAX_FILE_OPEN_SEC)", openSec, maxFileOpenSec); + continue; + } + + treeUnbinnedResiduals = std::make_unique("unbinnedResid", inputFile.get()); + if (!treeUnbinnedResiduals->GetTree()) { + LOGP(warning, "[Thread{}] Could not get tree 'unbinnedResid' from file {}. Skipping file!", iThread, fileName); + continue; + } + + // GetEntries() only reads TTree header metadata, not branch data -- cheap even on alien://, unlike + // the TTreeCache/branch setup further below. Fetched here (still before that setup) so the fail-fast + // time-window check below can run before anything expensive touches the network. + const auto nTFEntries = treeUnbinnedResiduals->GetEntries(); + if (nTFEntries <= 0) { + LOGP(warning, "[Thread{}] Tree 'unbinnedResid' in file {} has no entries. Skipping file!", iThread, fileName); + continue; + } + + treeRecords = std::make_unique("records", inputFile.get()); + if (!treeRecords->GetTree()) { + LOGP(warning, "[Thread{}] Could not get tree 'records' from file {}. Skipping file!", iThread, fileName); + continue; + } + orbits = std::make_unique>>(*treeRecords, "firstTForbit"); + // Real data has exactly one entry in 'records', but MC input can have several (e.g. one per + // simulation chunk merged into this file) -- each entry's own 'firstTForbit' only covers that + // chunk's TFs, so reading just entry 0 silently truncated the orbit list on MC, tripping the + // length check below and skipping the whole file. Concatenate every entry's vector instead, in + // entry order, to rebuild the same flat, TF-index-ordered list this file's real-data path already + // produced from its single entry (verified for real: MC input observed with 5 'records' entries). + std::vector combinedOrbits; + { + const Long64_t nRecordsEntries = treeRecords->GetEntries(); + bool recordsOk = true; + for (Long64_t ie = 0; ie < nRecordsEntries; ++ie) { + if (treeRecords->SetEntry(ie) != TTreeReader::kEntryValid || + !checkReaderValue(*orbits, iThread, fileName)) { + recordsOk = false; + break; + } + const auto& thisEntryOrbits = **orbits; + combinedOrbits.insert(combinedOrbits.end(), thisEntryOrbits.begin(), thisEntryOrbits.end()); + } + if (!recordsOk) { + LOGP(warning, "[Thread{}] Could not load the orbits from tree 'records' in file {}. Skipping file!", iThread, fileName); + continue; + } + } + // the orbit list is indexed with the TF index of the unbinnedResid tree below, so it has to be at least as long + if (static_cast(combinedOrbits.size()) < nTFEntries) { + LOGP(error, "[Thread{}] 'firstTForbit' has fewer entries than the residual tree has TFs ({} vs {}) in file {}. Skipping file!", iThread, + combinedOrbits.size(), nTFEntries, fileName); + continue; + } + + // Set timeStamp for processing, get this file's [min,max] orbit-derived time range, and find the + // first TF entry actually inside the requested window -- all in one pass over the (already + // downloaded) 'firstTForbit' array. Bounded to the first nTFEntries entries: orbits can have extra + // trailing entries beyond what the residual tree actually has (see the size check above) that don't + // correspond to any real TF and must not feed either the fail-fast check below or the warm-up entry. + uint32_t minFirstOrbit = -1; + uint32_t maxFirstOrbit = 0; + Long64_t warmupEntry = 0; + bool foundWarmupEntry = (firstTFTime <= 0); // no time filter set: entry 0 is always fine to warm up on + { + const auto& orbitsVec = combinedOrbits; + for (Long64_t i = 0; i < nTFEntries; ++i) { + const uint32_t orbit = orbitsVec[i]; + if (orbit < minFirstOrbit) { + minFirstOrbit = orbit; + } + if (orbit > maxFirstOrbit) { + maxFirstOrbit = orbit; + } + if (!foundWarmupEntry) { + const int64_t t = orbitResetTimeMS + orbit * o2::constants::lhc::LHCOrbitMUS * 1.e-3; + if (t >= firstTFTime && t <= lastTFTime) { + warmupEntry = i; + foundWarmupEntry = true; + } + } + } + } + // ---| Fail fast on files with zero overlap with the requested time window |--- + if (firstTFTime > 0) { + const int64_t fileMinTimeMS = orbitResetTimeMS + minFirstOrbit * o2::constants::lhc::LHCOrbitMUS * 1.e-3; + const int64_t fileMaxTimeMS = orbitResetTimeMS + maxFirstOrbit * o2::constants::lhc::LHCOrbitMUS * 1.e-3; + if (fileMaxTimeMS < firstTFTime || fileMinTimeMS > lastTFTime) { + LOGP(warning, "[Thread{}] File {} has no TF inside the requested time window [{}, {}] ms (file spans [{}, {}] ms) -- skipping the whole file without downloading its residual tree", + iThread, fileName, firstTFTime, lastTFTime, fileMinTimeMS, fileMaxTimeMS); + // Counted as if the per-TF loop below had visited and skipped each entry, so nTFs stays a + // consistent denominator for the skip-fraction summary at the end. + nTFs_thread[iThread] += nTFEntries; + nTFsSkippedByTimeWindow_thread[iThread] += nTFEntries; + continue; + } + } + + // --- Read-health probe, BEFORE the TTreeCache is enabled -------------------------------------- + // A slow open alone can miss a replica that opens fine but stalls on the first real read, so this + // probes a small read directly, before SetCacheSize/AddBranchToCache below turn the first read into + // a whole cache-sized fetch that can hang with nothing able to interrupt it. Probes 'trackData' (a + // small member-split branch) rather than the already-read 'records' tree, which sits next to the + // file header and reads fast regardless of whether the replica then stalls on real data. Uses + // TBranch::GetEntry directly rather than a TTreeReader, to avoid creating a second reader on a tree + // that gets its real one further below. A missing branch skips the probe rather than failing it -- + // this is a health check, not a validity check. + { + TTree* probeTree = dynamic_cast(inputFile->Get("trackData")); + TBranch* probeBranch = probeTree ? probeTree->GetBranch("trk.nClsTPC") : nullptr; + if (probeBranch && probeTree->GetEntries() > 0) { + const Long64_t probeEntry = std::min(warmupEntry, probeTree->GetEntries() - 1); + const auto probeStart = std::chrono::steady_clock::now(); + const Int_t probeBytes = probeBranch->GetEntry(probeEntry); + const double probeSec = std::chrono::duration(std::chrono::steady_clock::now() - probeStart).count(); + + // If the read returned nothing, the measurement says nothing about the replica's health -- fall + // through to the normal path rather than judging the file on it. Deliberately fail-open: a probe + // that cannot measure must not be able to reject files, or an unexpected branch layout would + // quietly defer every file in the slot. + if (probeBytes <= 0) { + LOGP(info, "[Thread{}] Read-health probe returned no data for {} (entry {}) -- skipping the health check for this file", iThread, fileName, probeEntry); + } else { + // Baseline: the median probe time of files already accepted in this slot. Below 3 samples a + // median is meaningless, so fall back to the absolute guard. + double probeThreshold = probeAbsMaxSec; + if (probeTimes.size() >= 3) { + std::vector sorted(probeTimes); + std::nth_element(sorted.begin(), sorted.begin() + sorted.size() / 2, sorted.end()); + const double median = sorted[sorted.size() / 2]; + probeThreshold = std::max(probeSlowFactor * median, probeMinThresholdSec); + } + if (probeThreshold > 0 && probeSec > probeThreshold) { + deferFile("Slow read probe (SCDCALIB_PROBE_SLOW_FACTOR/SCDCALIB_PROBE_ABS_MAX_SEC)", probeSec, probeThreshold); + continue; + } + // Only healthy files feed the baseline, so one sick file cannot raise the bar for the next. + probeTimes.push_back(probeSec); + } + } + } + + // I/O throughput monitor -- large cache so async prefetch has room to read many baskets ahead; only + // branches actually accessed get cached/prefetched. TTreePerfStats records raw bytes read from the + // remote file, so its rate is the actual download speed. + // + // 256 MB, not 512 MB: a real GRID job hit an 11+ minute stall refilling a single 512MB chunk (~0.7 + // MB/s vs. 45-100 MB/s for every other chunk on the same SE), then a second stall that never + // recovered, losing the whole job to AliEn's idle-CPU watchdog. A smaller chunk halves the + // worst-case single-chunk wait and lets a persistently slow file be abandoned sooner -- a + // reliability trade-off against 512MB's better throughput (~17% vs ~13% wall-clock reduction) when + // nothing is stalling. Overridable via SCDCALIB_CACHE_SIZE_MB. + { + TTree* residTree = treeUnbinnedResiduals->GetTree(); + int dbgCacheSizeMB = 256; + if (const char* envCacheSizeMB = gSystem->Getenv("SCDCALIB_CACHE_SIZE_MB")) { + dbgCacheSizeMB = std::atoi(envCacheSizeMB); + } + if (isAlienFile) { + LOGP(info, "[Thread{}] [prefetch diag] TTreeCache size = {} MB (SCDCALIB_CACHE_SIZE_MB, default 256)", iThread, dbgCacheSizeMB); + } + residTree->SetCacheSize(static_cast(dbgCacheSizeMB) * 1024 * 1024); + residTree->AddBranchToCache("*", true); + perfStats = std::make_unique(fmt::format("ioperf_{}_{}", iThread, iFile).data(), residTree); + } + perfLastBytes = 0; + perfLastSample = std::chrono::steady_clock::now(); + + unbinnedResiduals = std::make_unique>>(*treeUnbinnedResiduals, "res"); + trackRefs = std::make_unique>>(*treeUnbinnedResiduals, "trackInfo"); + lumiTF = std::make_unique>(*treeUnbinnedResiduals, "CTPLumi"); + + // Skip reading UnbinnedResid/TrackDataCompact members that are never used below. 'res' and + // 'trackInfo' are member-split branches, one sub-branch per struct field, and the unused ones + // dominate the file (res.tgSlp alone can be >1 GB in a single input file), so disabling them means + // they are never transferred at all -- measured ~17% fewer bytes read. Must come AFTER the + // TTreeReaderValues above so that it is the final word on these sub-branches' status. + // Do not disable res.dy/dz/y/z/row/sec/rejected or trackInfo.idxFirstResidual/nResiduals/sourceId -- + // all of those are read below. Note trackInfo.filterFlag (on TrackDataCompact) is unused, while the + // separate trk.filterFlag (on TrackData, read further down) is used; they are different fields. + { + TTree* residTree = treeUnbinnedResiduals->GetTree(); + for (const char* br : {"res.tgSlp", "res.channel", "trackInfo.multStack*", + "trackInfo.nExtDetResid", "trackInfo.filterFlag"}) { + residTree->SetBranchStatus(br, 0); + } + } + + // Load one entry once so the reader values get bound, then verify all branches are there before + // anything below dereferences them. Warms up on warmupEntry (computed above, the first entry + // actually inside the requested time window) rather than always entry 0 -- for a file only partially + // overlapping the window, entry 0 is often outside it, and fetching its residual data would be + // exactly the kind of wasted network read the whole-file fail-fast check above targets, just at the + // scale of one TF. + if (treeUnbinnedResiduals->SetEntry(warmupEntry) != TTreeReader::kEntryValid) { + LOGP(warning, "[Thread{}] Could not load entry {} of 'unbinnedResid' from file {}. Skipping file!", iThread, warmupEntry, fileName); + continue; + } + if (!checkReaderValue(*unbinnedResiduals, iThread, fileName) || + !checkReaderValue(*trackRefs, iThread, fileName) || + !checkReaderValue(*lumiTF, iThread, fileName)) { + LOGP(warning, "[Thread{}] Skipping file {}", iThread, fileName); + continue; + } + + // Re-prime the reader values after the pre-scan (which leaves the reader past the last entry), + // so a stale-read (e.g. the bad-range-skip stats below) at warmupEntry dereferences valid data. + if (treeUnbinnedResiduals->SetEntry(warmupEntry) != TTreeReader::kEntryValid) { + LOGP(warning, "[Thread{}] Could not re-load entry {} of 'unbinnedResid' from file {}. Skipping file!", iThread, warmupEntry, fileName); + continue; + } + + { + treeTrackData = std::make_unique("trackData", inputFile.get()); + if (!treeTrackData->GetTree()) { + LOGP(warning, "[Thread{}] Could not get tree 'trackData' from file {}. Skipping file!", iThread, fileName); + continue; + } + { + TTree* trackDataTree = treeTrackData->GetTree(); + for (const char* br : {"trk.gid*", "trk.chi2TRD", "trk.deltaTOF", "trk.nTrkltsTRD", + "trk.clAvailTOF", "trk.TRDTrkltSlope*", "trk.nExtDetResid", + "trk.clIdx.*", "trk.multStack*"}) { + trackDataTree->SetBranchStatus(br, 0); + } + } + trackData = std::make_unique>>(*treeTrackData, "trk"); + if (treeTrackData->GetEntries() != nTFEntries) { + LOGP(error, "[Thread{}] The input trees with unbinned residuals and track information have a different number of entries ({} vs {}). Skipping file!", iThread, + nTFEntries, treeTrackData->GetEntries()); + continue; + } + // Same TF indexing as 'unbinnedResid' -- warm up on the same entry for the same reason (see above). + if (treeTrackData->SetEntry(warmupEntry) != TTreeReader::kEntryValid || + !checkReaderValue(*trackData, iThread, fileName)) { + LOGP(warning, "[Thread{}] Skipping file {}", iThread, fileName); + continue; + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // ===| TIME FRAME LOOP |============================================================================================================= + // Split into a background PRODUCER thread (skip-checks, SetEntry/warm-up, sync check, lumi/orbit + // bookkeeping, and reading each TF's data) pushing TFPackages into a bounded queue, and this thread + // as CONSUMER, popping a package and dispatching the track workers on it -- see BoundedTFQueue/ + // TFPackage above for why. The producer is the only thread that ever touches + // treeUnbinnedResiduals/treeTrackData/the TTreeReaderValues (exactly one thread doing TGrid/file I/O + // at a time); the consumer never touches them at all, only the packages it pops. + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + size_t tfQueueDepth = TFQueueDepthDefault; + if (const char* envQueueDepth = gSystem->Getenv("SCDCALIB_TF_QUEUE_DEPTH")) { + tfQueueDepth = static_cast(std::atoi(envQueueDepth)); + } + if (isAlienFile) { + LOGP(info, "[Thread{}] [prefetch diag] TFQueueDepth = {} (SCDCALIB_TF_QUEUE_DEPTH, default {})", iThread, tfQueueDepth, TFQueueDepthDefault); + } + BoundedTFQueue tfQueue(tfQueueDepth); + + // Set by the consumer below when a single tfQueue.pop() waited unreasonably long, checked by the + // producer between TF reads (never inside one -- see the long comment at the consumer's check site + // for why). Assumes the fetch has become persistently slow rather than truly dead: a producer stuck + // forever never reaches this check at all and would need a separate, not-yet-built process-level + // watchdog -- this only shortens the "slow but eventually returns" case, not a genuine hang. + std::atomic abandonFile{false}; + double popWaitAbandonMs = 60000.0; // 60s -- matches the scale of the real stalls that motivated this + if (const char* envAbandonMs = gSystem->Getenv("SCDCALIB_POP_WAIT_ABANDON_MS")) { + popWaitAbandonMs = std::atof(envAbandonMs); + } + + std::thread producerThread([&]() { + // A failed SetEntry() (checked below) only means the tree could not be repositioned -- it says + // nothing about whether a given lazily-read branch's basket actually arrived. TTreeReaderValue + // fetches each branch on its own first dereference for the current entry, and a mid-stream storage + // hiccup there doesn't throw: it prints a ROOT error and leaves the underlying object in an + // unspecified state, which then segfaults wherever it's first used with no indication of why + // (observed for real: an xrootd "Operation expired" mid basket-read, immediately followed by a + // SIGSEGV with only the ROOT error line as a clue). Must be called right after the value's first + // dereference for this entry -- GetReadStatus() reports the status of that specific read. + auto checkReadOk = [&](ROOT::Internal::TTreeReaderValueBase& val, const char* branchName, int iEntry) { + if (val.GetReadStatus() != ROOT::Internal::TTreeReaderValueBase::kReadSuccess) { + LOGP(warning, "[Thread{}] Storage read error on branch '{}' at entry {} of file {} (GetReadStatus={}) -- skipping TF!", + iThread, branchName, iEntry, fileName, static_cast(val.GetReadStatus())); + return false; + } + return true; + }; + for (int iEntry = 0; iEntry < nTFEntries; ++iEntry) { + // Checked here, between TF reads, never inside one -- this point is only ever reached right + // after the previous push() succeeded, so the producer is never mid-call when this fires. See + // the consumer's check site for the full reasoning. + if (abandonFile.load(std::memory_order_relaxed)) { + LOGP(warning, "[Thread{}] Abandoning the rest of file {} ({}/{} TFs read) after a persistently slow fetch", iThread, fileName, iEntry, nTFEntries); + break; + } + ++nTFs_thread[iThread]; + + // Periodic I/O throughput progress log. Now reports the producer's own progress through the + // file, which can run ahead of what the consumer has actually finished processing. + if (iEntry % 50 == 0) { + double instMBps = 0.0, totMB = 0.0; + if (perfStats) { + const auto nowSample = std::chrono::steady_clock::now(); + const double dt = std::chrono::duration(nowSample - perfLastSample).count(); + const Long64_t br = perfStats->GetBytesRead(); + instMBps = (dt > 0) ? (br - perfLastBytes) / (1024.0 * 1024.0) / dt : 0.0; + totMB = br / (1024.0 * 1024.0); + perfLastBytes = br; + perfLastSample = nowSample; + } + const Long64_t nProcessedNow = nTracksProcessed.load(std::memory_order_relaxed); + if (maxTracks > 0) { + LOGP(info, "[Thread{}] TF entry {}/{} | read {:.1f} MB, inst. {:.1f} MB/s | tracks processed {}/{} ({:.1f}%)", + iThread, iEntry, nTFEntries, totMB, instMBps, nProcessedNow, maxTracks, 100.0 * nProcessedNow / maxTracks); + } else { + LOGP(info, "[Thread{}] TF entry {}/{} | read {:.1f} MB, inst. {:.1f} MB/s | tracks processed {} (no limit set)", + iThread, iEntry, nTFEntries, totMB, instMBps, nProcessedNow); + } + } + + // Check if enough tracks are processed + if ((maxTracks > 0) && (nTracksProcessed.load(std::memory_order_relaxed) > maxTracks)) { + LOGP(info, "[Thread{}] Maximum number of requested tracks processed {} > {} ({}), will not process further TFs", iThread, nTracksProcessed.load(std::memory_order_relaxed), maxTracks, maxTracksPerSlice); + break; + } + + // ---| check for TF time acceptance |--- + const int64_t tfTimeInMS = orbitResetTimeMS + combinedOrbits[iEntry] * o2::constants::lhc::LHCOrbitMUS * 1.e-3; + if ((firstTFTime > 0) && (tfTimeInMS < firstTFTime || tfTimeInMS > lastTFTime)) { + if (nTFsSkippedByTimeWindow_thread[iThread] == 0) { + // Log once per thread rather than per TF: this can legitimately fire for every TF of every + // file, e.g. when the requested [firstTFTime,lastTFTime] window contains no data at all + // because a time-slice boundary landed past the end of the run. A per-TF log would then + // produce one line per TF for the entire job. + LOGP(warning, "[Thread{}] TF at index {} (time {} ms, orbit {}) outside requested window [{}, {}] ms -- skipping (will keep happening silently for further TFs outside the window, see final summary for the total count)", + iThread, iEntry, tfTimeInMS, combinedOrbits[iEntry], firstTFTime, lastTFTime); + } + ++nTFsSkippedByTimeWindow_thread[iThread]; + continue; + } + // ---| check for time exclusion list |--- + if (badRanges_thread[iThread].size() > 0) { + bool skip = false; + for (const auto& range : badRanges_thread[iThread]) { + if ((combinedOrbits[iEntry] >= range.from) && (combinedOrbits[iEntry] <= range.to)) { + skip = true; + break; + } + } + if (invertBadRange) { + skip = !skip; + } + if (skip) { + nTracksSkippedByBadRangeList_thread[iThread] += (*trackRefs)->size(); + ++nTFsSkippedByBadRangeList_thread[iThread]; + continue; + } + } + if (params_thread.timeFilter) { + if (tfTimeInMS < params_thread.startTimeMS || tfTimeInMS > params_thread.endTimeMS) { + continue; + } + } + + // --- [prefetch diag]: producer-side read/deserialize timing, kept to confirm the periodic-spike + // I/O pattern still looks the same underneath the producer/consumer split -- expected to be + // mostly hidden from the consumer by TFQueueDepth, not eliminated. --- + const auto dbgTIoStart = std::chrono::steady_clock::now(); + + // ---| Read entries |--- + if (treeUnbinnedResiduals->SetEntry(iEntry) != TTreeReader::kEntryValid) { + LOGP(warning, "[Thread{}] Could not load entry {} of 'unbinnedResid' from file {}. Skipping TF!", iThread, iEntry, fileName); + continue; + } + if (treeTrackData->SetEntry(iEntry) != TTreeReader::kEntryValid) { + LOGP(warning, "[Thread{}] Could not load entry {} of 'trackData' from file {}. Skipping TF!", iThread, iEntry, fileName); + continue; + } + + // First dereference of 'trackInfo' for this entry -- triggers the actual branch read; must be + // checked before .size() (or anything else) trusts the result, see checkReadOk above. + (void)(**trackRefs); + if (!checkReadOk(*trackRefs, "trackInfo", iEntry)) { + continue; + } + const auto nTracks = (*trackRefs)->size(); + + // Materialize this TF's 'res' branch here, on the producer thread, before it's copied out below. + // TTreeReaderValue::operator* deserializes lazily on the first dereference per entry. + (void)(**unbinnedResiduals).size(); + if (!checkReadOk(*unbinnedResiduals, "res", iEntry)) { + continue; + } + + const double dbgIoMs = std::chrono::duration(std::chrono::steady_clock::now() - dbgTIoStart).count(); + { + thread_local double dbgSumIoMs = 0.0; + thread_local uint64_t dbgNProduced = 0; + dbgSumIoMs += dbgIoMs; + ++dbgNProduced; + if (isAlienFile && dbgNProduced % 10 == 0) { + LOGP(info, "[Thread{}] [prefetch diag][producer] TF {} nTracks={} ioMs={:.1f} | running: {} TF(s) read, sumIoMs={:.0f}", + iThread, iEntry, nTracks, dbgIoMs, dbgNProduced, dbgSumIoMs); + } + } + + // First dereference of 'trackData' for this entry -- same lazy-read/checkReadOk requirement as + // 'trackInfo'/'res' above. + (void)(**trackData); + if (!checkReadOk(*trackData, "trackData", iEntry)) { + continue; + } + + // the track loop below indexes trackData with the trackRefs index, so both have to be in sync + if ((**trackData).size() < nTracks) { + LOGP(warning, "[Thread{}] TF {} of file {} has fewer track data entries than track references ({} vs {}). Skipping TF!", iThread, iEntry, fileName, + (**trackData).size(), nTracks); + continue; + } + + lumiSumCTP_thread[iThread] += (*lumiTF)->getLumi(); + ++lumiEntriesCTP_thread[iThread]; + + timeMSsel_thread[iThread].emplace_back(tfTimeInMS); + orbitsSel_thread[iThread].emplace_back((*lumiTF)->orbit); + ctpLumiSel_thread[iThread].emplace_back((*lumiTF)->getLumi()); + + // Copy the three vectors out (see TFPackage) and hand the package to the consumer. push() blocks + // here if the queue is already at TFQueueDepth, which is exactly the back-pressure that keeps the + // producer from running arbitrarily far ahead. + auto pkg = std::make_unique(); + pkg->iEntry = iEntry; + pkg->trackRefsVec = **trackRefs; + pkg->trackDataVec = **trackData; + pkg->unbinnedResidualsVec = **unbinnedResiduals; + tfQueue.push(std::move(pkg)); + } + tfQueue.setDone(); + }); + + while (true) { + const auto dbgTPopStart = std::chrono::steady_clock::now(); + std::unique_ptr pkg = tfQueue.pop(); + const double dbgPopWaitMs = std::chrono::duration(std::chrono::steady_clock::now() - dbgTPopStart).count(); + const size_t dbgQueueSizeAfterPop = tfQueue.size(); // how many the producer still has ready right now + + // A pop() this slow only ever returns *after* the producer's own push() for this package already + // succeeded -- i.e. the producer is guaranteed to be between TF reads right now, not blocked + // inside one, so signalling it here can never race a live TFile/TGrid call. Real motivation: a + // real GRID job had one file's chunk refill alone take 11+ minutes (~0.7 MB/s vs. 45-100 MB/s for + // every other chunk on the same SE), then a second chunk on the same file never returned and the + // whole job was killed by AliEn's idle-CPU watchdog. This won't catch that second, truly-dead case + // (the producer never reaches this check then -- needs a separate process-level watchdog, not yet + // built), but it does mean a merely very slow file gets abandoned after one bad chunk instead of + // risking a permanent stall. + if (pkg && dbgPopWaitMs > popWaitAbandonMs && !abandonFile.load(std::memory_order_relaxed)) { + LOGP(warning, "[Thread{}] popWait {:.0f} ms exceeds abandon threshold {:.0f} ms (SCDCALIB_POP_WAIT_ABANDON_MS) -- signalling the producer to give up on the rest of this file, assuming a persistently slow fetch rather than a dead one", + iThread, dbgPopWaitMs, popWaitAbandonMs); + abandonFile.store(true, std::memory_order_relaxed); + } + if (!pkg) { + break; // producer is done and the queue is drained + } + const int iEntry = pkg->iEntry; + const auto nTracks = pkg->trackRefsVec.size(); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // ===| TRACK LOOP |================================================================================================================== + // Body extracted into a per-track lambda so it can be dispatched across nTrackWorkers compute + // threads (see the setup + rationale above the FILE LOOP). A `return` inside this lambda skips to + // the next track -- the lambda body IS one track's worth of work. The cluster loop's own + // `continue`s still mean what they always do: that for-loop lives inside the lambda unchanged. + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + auto processTrack = [&](size_t iTrack, int iWorker) { + const auto& trkInfo = pkg->trackRefsVec[iTrack]; + if (!GID::includesSource(trkInfo.sourceId, sources)) { + return; + } + const auto& trk = pkg->trackDataVec[iTrack]; + if (!revalidateTrack(trk, params_thread)) { + return; + } + + auto propagator = o2::base::Propagator::Instance(); + o2::track::TrackPar trkPar = trk.par; + int sign = trkPar.getSign(); + + int charge = 0; + if (sign > 0) { + charge = 1; + } + + // dE/dx cut + if (maxdEdx > 0 && trk.dEdxTPC > maxdEdx) { + return; + } + + if (maxdEdxExp > 0 || maxDevdEdxOverExp > 0) { + // propagate to the beginning of the inner containment vessel, to use the momentum for dE/dx expected + if (!propagator->PropagateToXBxByBz(trkPar, 63.2, 0.99, 2., o2::base::Propagator::MatCorrType::USEMatCorrLUT)) { // USEMatCorrTGeo, USEMatCorrLUT, USEMatCorrNONE + return; + } + + const auto dEdxExp = o2::track::BetheBlochSolidOpt(trk.par.getP() / trk.par.getPID().getMass()) * 3e4; + + if (maxdEdxExp > 0 && dEdxExp > maxdEdxExp) { + return; + } + + if (maxDevdEdxOverExp > 0 && std::abs(trk.dEdxTPC / dEdxExp - 1) > maxDevdEdxOverExp) { + return; + } + } + if (!propagator->PropagateToXBxByBz(trkPar, 85., 0.99, 2., o2::base::Propagator::MatCorrType::USEMatCorrLUT)) { // USEMatCorrTGeo, USEMatCorrLUT, USEMatCorrNONE + return; + } + + // INCREASE LOCAL TRACK COUNTER + ++trackCounter_local_worker[iWorker]; + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // ===| CLUSTER & RESIDUAL LOOP |===================================================================================================== + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + for (unsigned int i = trkInfo.idxFirstResidual; i < trkInfo.idxFirstResidual + trkInfo.nResiduals; ++i) { + const auto& residIn = pkg->unbinnedResidualsVec[i]; + int sec = residIn.sec; + if (residIn.row >= NRows || sec >= NSectors || sec < 0) { // non TPC residuals have row>=160, though to see them one should loop until i<(trc.clIdx.getFirstEntry() + trc.clIdx.getEntries() + trc.nExtDetResid) + continue; + } + + if (isRejectedResidual(residIn)) { + continue; + } + + float angleSec = TMath::DegToRad() * (10.0 + 20.0 * sec); + std::array bvox; + // cluster position + float xPos = param::RowX[residIn.row]; + float yPos = residIn.y * param::MaxY / 0x7fff + residIn.dy * param::MaxResid / 0x7fff; + float zPos = residIn.z * param::MaxZ / 0x7fff + residIn.dz * param::MaxResid / 0x7fff; + // exclude the edge pads as they are biased! + // get max y-position of edge pad: pad centre last pad - pad width/2 + if (skipEdgePads && std::abs(yPos) > yMaxCentrePadByRow[residIn.row]) { + ++nEdgeClustersSkipped_worker[iWorker]; + continue; + } + + clsPosWorker[iWorker].SetXYZ(xPos, yPos, zPos); + if (!trackResiduals.findVoxelBin(sec, xPos, yPos, zPos, bvox)) { + // we are not inside any voxel + continue; + } + + // circle pool for this voxel is shared across threads -> lock for the rest of this iteration + auto& vox = voxels[((sec * NRows + bvox[2]) * nY2XBins + bvox[1]) * nZ2XBins + bvox[0]]; + std::unique_lock voxLock(vox.mtx); + + // XALEX + if (vox.poolCounter[charge] == NPool) { + continue; + } + + //--------------------------------------------------------- + // Main part of the new code + float xposvox, yoverxpos, zoverxpos; + trackResiduals.getVoxelCoordinates(sec, bvox[2], bvox[1], bvox[0], xposvox, yoverxpos, zoverxpos); + if (fabs(xposvox) < 5.0) { + continue; + } + float yposvox = yoverxpos * xposvox; + float zposvox = zoverxpos * xposvox; + + float dxclsvoxel = clsPosWorker[iWorker].X() - xposvox; + float dyclsvoxel = clsPosWorker[iWorker].Y() - yposvox; + float dzclsvoxel = clsPosWorker[iWorker].Z() - zposvox; + + Vec3d deltaClsVoxel; + deltaClsVoxel.SetXYZ(dxclsvoxel, dyclsvoxel, dzclsvoxel); + + //----------------------------------------- + trkPar.rotate(o2::math_utils::sector2Angle(sec)); + + propagator->PropagateToXBxByBz(trkPar, xPos, 0.99, 2., o2::base::Propagator::MatCorrType::USEMatCorrLUT); // USEMatCorrTGeo, USEMatCorrLUT, USEMatCorrNONE + trackPosAtRow_worker[iWorker].SetXYZ(trkPar.getX(), trkPar.getY(), trkPar.getZ()); + + float sna, csa; + o2::math_utils::CircleXY::value_t> xycircle; + trkPar.getCircleParams(magfieldvalue, xycircle, sna, csa); // in global coordinates + + Vec3d circleCenterEstimate; + circleCenterEstimate.SetXYZ(xycircle.xC, xycircle.yC, 0.0); + circleCenterEstimate.RotateZ(-angleSec); + float radius_estimate = xycircle.rC; + + if ((trackPosAtRow_worker[iWorker] - clsPosWorker[iWorker]).Perp() > maxDistIntCls) { + continue; + } + + //----------------------------------------- + // DeltaZ corrections, two methods + if (voxMapInput.size()) // with input map from first itteration, should be more precise than second method + { + // we already have a correction map available + const auto& voxRes = voxelResults[sec][trackResiduals.getGlbVoxBin(bvox)]; // bvox: z,y,x + float DX_input_map = voxRes.D[TrackResiduals::ResX]; + + propagator->PropagateToXBxByBz(trkPar, xPos - DX_input_map, 0.99, 2., o2::base::Propagator::MatCorrType::USEMatCorrLUT); // USEMatCorrTGeo, USEMatCorrLUT, USEMatCorrNONE + float DZdist = (zPos - trkPar.getZ()); // distortion + // accumulate Z sum (shared across threads for this voxel, guarded by vox.mtx) and increment counter + vox.residualsAll[2] += DZdist; + vox.counterZAll++; + } else { + // without input map, use rolling average -- gate on the cumulative NP/PP/NN counters (never + // reset across flushes, unlike poolCounter) so this only fires once residualsAll[0] has + // actually been computed from a decent number of samples, not merely whenever the in-flight + // pool for this charge happens to be non-empty (poolCounter resets to 0 on every flush, so + // checking it here both under- and over-fires relative to residualsAll[0]'s real validity). + if (vox.counterNP > MinDxSamplesForZCorr || vox.counterPP > MinDxSamplesForZCorr || vox.counterNN > MinDxSamplesForZCorr) { + float DXrollingaverage = vox.residualsAll[0]; + propagator->PropagateToXBxByBz(trkPar, xPos - DXrollingaverage, 0.99, 2., o2::base::Propagator::MatCorrType::USEMatCorrLUT); // USEMatCorrTGeo, USEMatCorrLUT, USEMatCorrNONE + float DZdist = (zPos - trkPar.getZ()); // distortion + vox.residualsAll[2] += DZdist; + vox.counterZAll++; + } + } + //----------------------------------------- + + circleCenterEstimate -= deltaClsVoxel; // shift track to voxel center to avoid smearing within voxel + + int counter = vox.poolCounter[charge]; + vox.circleCenters[charge][counter] = Vec3f{static_cast(circleCenterEstimate.X()), static_cast(circleCenterEstimate.Y()), static_cast(circleCenterEstimate.Z())}; + vox.circleRadii[charge][counter] = radius_estimate; + vox.poolCounter[charge]++; + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // ===| PROCESSING VOXEL |============================================================================================================ + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if ((vox.poolCounter[0] >= NPool || vox.poolCounter[1] >= NPool)) { + //////////////////////////////// + // Same polarity combinations // + //////////////////////////////// + float weightCurvatureInt = 0.0; + Vec3d averageInt; + averageInt.SetXYZ(0.0, 0.0, 0.0); + + for (int counterPos = 0; counterPos < (vox.poolCounter[1] - 1); counterPos++) { + for (int counterPosB = (counterPos + 1); counterPosB < vox.poolCounter[1]; counterPosB++) { + float radiusA = vox.circleRadii[1][counterPos]; + float radiusB = vox.circleRadii[1][counterPosB]; + + if (fabs((radiusA - radiusB) / (radiusA + radiusB)) < 0.2) { + continue; // to similar bending radii + } + + Vec3d intCircles = getIntCircles(radiusA, radiusB, vox.circleCenters[1][counterPos], vox.circleCenters[1][counterPosB], xposvox, yposvox); + if (intCircles.Perp() < 280.0 && intCircles.Perp() > 60.0) { + float distIntCls = (intCircles - clsPosWorker[iWorker]).Perp(); // why? + if (distIntCls > maxDistIntCls) { + continue; + } + + float weight = fabs((radiusA - radiusB) / (radiusA + radiusB)); + float weight_curvature = weight; + + weightCurvatureInt += weight_curvature; + averageInt += intCircles * weight_curvature; + } + } + } + + if (weightCurvatureInt > 0) { + averageInt *= 1.0 / weightCurvatureInt; + + // distortion + // accumulate PP sums (x,y) and increment counter + vox.residualsPP[0] += (xposvox - averageInt.X()); + vox.residualsPP[1] += (yposvox - averageInt.Y()); + vox.counterPP++; + } + + weightCurvatureInt = 0.0; + averageInt.SetXYZ(0.0, 0.0, 0.0); + + for (int counterNeg = 0; counterNeg < (vox.poolCounter[0] - 1); counterNeg++) { + for (int counterNegB = (counterNeg + 1); counterNegB < vox.poolCounter[0]; counterNegB++) { + float radiusA = vox.circleRadii[0][counterNeg]; + float radiusB = vox.circleRadii[0][counterNegB]; + + if (fabs((radiusA - radiusB) / (radiusA + radiusB)) < 0.2) { + continue; // to similar bending radii + } + + Vec3d intCircles = getIntCircles(radiusA, radiusB, vox.circleCenters[0][counterNeg], vox.circleCenters[0][counterNegB], xposvox, yposvox); + if (intCircles.Perp() < 280.0 && intCircles.Perp() > 60.0) { + float distIntCls = (intCircles - clsPosWorker[iWorker]).Perp(); // why? + if (distIntCls > maxDistIntCls) { + continue; + } + + float weight = fabs((radiusA - radiusB) / (radiusA + radiusB)); + float weight_curvature = weight; + + weightCurvatureInt += weight_curvature; + averageInt += intCircles * weight_curvature; + } + } + } + + if (weightCurvatureInt > 0) { + averageInt *= 1.0 / weightCurvatureInt; + + // distortion + // accumulate NN sums (x,y) and increment counter + vox.residualsNN[0] += (xposvox - averageInt.X()); + vox.residualsNN[1] += (yposvox - averageInt.Y()); + vox.counterNN++; + } + + //////////////////////////////////// + // Opposite polarity combinations // + //////////////////////////////////// + weightCurvatureInt = 0.0; + averageInt.SetXYZ(0.0, 0.0, 0.0); + + for (int counterPos = 0; counterPos < vox.poolCounter[1]; counterPos++) { + for (int counterNeg = 0; counterNeg < vox.poolCounter[0]; counterNeg++) { + float radiusA = vox.circleRadii[1][counterPos]; + float radiusB = vox.circleRadii[0][counterNeg]; + Vec3d intCircles = getIntCircles(radiusA, radiusB, vox.circleCenters[1][counterPos], vox.circleCenters[0][counterNeg], xposvox, yposvox); + if (intCircles.Perp() < 280.0 && intCircles.Perp() > 60.0) { + float distIntCls = (intCircles - clsPosWorker[iWorker]).Perp(); // why? + if (distIntCls > maxDistIntCls) { + continue; + } + + // float weight_curvature = (1.0/radiusA)*(1.0/radiusB); // the larger the curvature the more precise the intersection can be calculated + // float weight_curvature = (radiusA)*(radiusB); // the larger the curvature the more precise the intersection can be calculated + + float weight_curvature = 1.0; // (1.0/radiusA)*(1.0/radiusB); + + weightCurvatureInt += weight_curvature; + averageInt += intCircles * weight_curvature; + } + } + } + + if (weightCurvatureInt > 0) { + averageInt *= 1.0 / weightCurvatureInt; + + // distortion + // accumulate NP sums (x,y) and increment counter + vox.residualsNP[0] += (xposvox - averageInt.X()); + vox.residualsNP[1] += (yposvox - averageInt.Y()); + vox.counterNP++; + } + + float weightNP = vox.counterNP * 1.0; + float weightPP = vox.counterPP * 0.01; + float weightNN = vox.counterNN * 0.01; + + float sum_weight = weightNP + weightPP + weightNN; + + if (sum_weight > 0.0f) { + // convert sums to means before weighting + float meanNPx = (vox.counterNP > 0) ? (vox.residualsNP[0] / static_cast(vox.counterNP)) : 0.0f; + float meanNPy = (vox.counterNP > 0) ? (vox.residualsNP[1] / static_cast(vox.counterNP)) : 0.0f; + float meanPPx = (vox.counterPP > 0) ? (vox.residualsPP[0] / static_cast(vox.counterPP)) : 0.0f; + float meanPPy = (vox.counterPP > 0) ? (vox.residualsPP[1] / static_cast(vox.counterPP)) : 0.0f; + float meanNNx = (vox.counterNN > 0) ? (vox.residualsNN[0] / static_cast(vox.counterNN)) : 0.0f; + float meanNNy = (vox.counterNN > 0) ? (vox.residualsNN[1] / static_cast(vox.counterNN)) : 0.0f; + + vox.residualsAll[0] = (weightNP * meanNPx + weightPP * meanPPx + weightNN * meanNNx) / sum_weight; + vox.residualsAll[1] = (weightNP * meanNPy + weightPP * meanPPy + weightNN * meanNNy) / sum_weight; + } + + // reset the pools: only the counter is reset. circleCenters/circleRadii are fixed-size + // std::array now (not std::vector) -- there's no clear()/resize() to even call; + // stale entries beyond the new poolCounter are simply overwritten as the pool refills. + for (int ic = 0; ic < 2; ic++) { + vox.poolCounter[ic] = 0; + } + } + //--------------------------------------------------------- + + // // update COG for voxel bvox (update for X only needed in case binning is not per pad row) + + } // end of cluster loop + }; // end of processTrack lambda + + const auto dbgTCpuStart = std::chrono::steady_clock::now(); + if (nTrackWorkers <= 1) { + for (size_t iTrack = 0; iTrack < nTracks; ++iTrack) { + processTrack(iTrack, 0); + } + } else { + std::vector trackThreads; + trackThreads.reserve(nTrackWorkers); + for (int iWorker = 0; iWorker < nTrackWorkers; ++iWorker) { + trackThreads.emplace_back([&, iWorker]() { + for (size_t iTrack = iWorker; iTrack < nTracks; iTrack += nTrackWorkers) { + processTrack(iTrack, iWorker); + } + }); + } + for (auto& th : trackThreads) { + th.join(); + } + } + const double dbgCpuMs = std::chrono::duration(std::chrono::steady_clock::now() - dbgTCpuStart).count(); + { + // [prefetch diag][consumer]: the number that actually matters. dbgPopWaitMs is how long this + // thread blocked in tfQueue.pop() waiting for the producer -- with the pipeline working, this + // should be near zero most of the time (the producer stays ahead), spiking only when it can't + // keep up (e.g. a cache-refill spike bigger than TFQueueDepth's cushion). + thread_local double dbgSumPopWaitMs = 0.0; + thread_local double dbgSumCpuMs = 0.0; + thread_local uint64_t dbgNTFs = 0; + dbgSumPopWaitMs += dbgPopWaitMs; + dbgSumCpuMs += dbgCpuMs; + ++dbgNTFs; + // Unlike the producer log above, this one had no sampling gate at all -- printed every single + // TF, on every thread, which is the dominant source of "[prefetch diag]" log volume. Matched to + // the producer's every-10th cadence (the cumulative sum* fields lose nothing from sampling) and + // gated on isAlienFile for the same reason as the producer log. + if (isAlienFile && dbgNTFs % 10 == 0) { + LOGP(info, "[Thread{}] [prefetch diag][consumer] TF {} nTracks={} popWaitMs={:.1f} cpuMs={:.1f} queueSizeAfterPop={} | running totals over {} TF(s): sumPopWaitMs={:.0f} sumCpuMs={:.0f}", + iThread, iEntry, nTracks, dbgPopWaitMs, dbgCpuMs, dbgQueueSizeAfterPop, dbgNTFs, dbgSumPopWaitMs, dbgSumCpuMs); + } + } + // Merge this TF's per-worker counters into the file-thread-level counters the rest of + // doFileProcessing (and the caller's merge loop, for nEdgeClustersSkipped_thread) expect. + for (int iWorker = 0; iWorker < nTrackWorkers; ++iWorker) { + trackCounter_local += trackCounter_local_worker[iWorker]; + trackCounter_local_worker[iWorker] = 0; + nEdgeClustersSkipped_thread[iThread] += nEdgeClustersSkipped_worker[iWorker]; + nEdgeClustersSkipped_worker[iWorker] = 0; + } + } // end of TF loop (consumer) + + // producerThread only ever reaches here via tfQueue.setDone() (end of file or maxTracks reached), + // which the consumer's pop() == nullptr check above already waited for -- this join is therefore + // just reclaiming the thread, not a real wait. Must happen before perfStats->Finish()/Print() below, + // since perfStats is only ever touched by the producer thread. + producerThread.join(); + + if (perfStats) { + perfStats->Finish(); + if (isAlienFile) { + perfStats->Print(); // full TTreePerfStats I/O report (incl. "Disk IO = ... MBytes/s") -- GRID-diagnostic only, see isAlienFile above + } + totalBytesReadPerf_thread[iThread] += perfStats->GetBytesRead(); + } + + // Occasionally flush local count to global + if (trackCounter_local >= 1000) { + nTracksProcessed.fetch_add(trackCounter_local, std::memory_order_relaxed); + trackCounter_local = 0; + } + + } // end of file loop + + if (trackCounter_local > 0) { + nTracksProcessed.fetch_add(trackCounter_local, std::memory_order_relaxed); + } +} + +void staticMapCreatorCPM(std::string fileInput = "files.txt", + int runNumber = 527976, + std::string fileOutput = "voxRes.root", + std::string voxMapInput = "", + std::string trackSources = static_cast(GID::ALL), + std::string z2xBinning = "", // empty: default binning; single number: uniform binning; otherwise bin boundaries, e.g. "0.,0.02, 0.04, 0.06, 1" + std::string y2xBinning = "", // empty: default binning; single number: uniform binning; otherwise bin boundaries, e.g. "-1,-0.998, -0.996, ... 0.996, 0.998, 1" + bool useSmoothed = true, // use smoothed residuals as input + bool createSpline = true, // create the splines + int maxTracksPerSlice = -1, // limit the number of total tracks processed to maxTracksPerSlice * nBinsZ2X * nBinxY2X * 36 + int minTracksPerSlice = -1, // request a minimum number of tracks per slice. Otherwise the calibration is not created + std::string badRangeList = "", // list of bad time ranges to be excluded in the calibration + long firstTFTime = -1, // First TF time to accept + long lastTFTime = -1, // Last TF time to accept + float maxdEdx = -1, // dE/dx cut above which tracks will be rejected + float maxdEdxExp = -1, // dE/dx expected cut above which tracks will be rejected + float maxDevdEdxOverExp = -1, // maximum deviation of dE/dx / expected value, above the track is rejected + bool skipEdgePads = 1, // skip edge pads in the calibration, by default on + std::string badRangeSelection = "ALL", // use bad time ranges only for specific comment e.g. C1 + float maxZ2XCut = 1.f, // overrides scdcalib.maxZ2X (the track tgl cut applied in revalidateTrack). + // Defaults to the SpacePointsCalibConfParam compiled-in value of 1.0 + int maxTrackWorkers = -1, // number of threads used for the track loop within one file. <=0 (default) + // auto-detects via hardware_concurrency(), which reports the machine's core + // count rather than the cores actually allocated to a batch job -- pass the + // real allocation explicitly when running on a batch system or the GRID + int nThreads = 8) // number of file-level threads, i.e. how many input files are processed + // concurrently. Forced to 1 for alien:// input regardless, since TGrid is + // not thread-safe (see below) +{ + LOGP(info, "TrackData::filterFlag {}, UnbinnedResid::rejected {}", + HasFilterFlagMember::value ? "available" : "NOT available (old O2, cut skipped)", + HasRejectedMember::value ? "available" : "NOT available (old O2, cut skipped)"); + + // Enable multiple threads + ROOT::EnableThreadSafety(); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // ==== TIME =============================================================================================================== + auto t_start = std::chrono::high_resolution_clock::now(); + // ========================================================================================================================= + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + fair::Logger::SetVerbosity(fair::Verbosity::medium); + fair::Logger::SetConsoleSeverity(fair::Severity::info); + fair::Logger::SetFileSeverity(fair::Severity::info); + + const Mapper& mapper = Mapper::instance(); + + // Obtain configuration + const SpacePointsCalibConfParam& params = SpacePointsCalibConfParam::Instance(); + if (!std::filesystem::exists("scdconfig.ini")) { + LOGP(warning, "Did not find configuration file. Using default parameters and storing them in scdconfig.ini"); + params.writeINI("scdconfig.ini", "scdcalib"); // to write default parameters to a file + } else { + params.updateFromFile("scdconfig.ini"); + } + // Explicit override from the macro's own argument, applied AFTER any scdconfig.ini load so it wins + // regardless of whether one happens to exist in CWD -- see maxZ2XCut's doc comment above. Using the + // string-based setValue overload (implemented out-of-line in ConfigurableParam.cxx), not the + // templated one -- that one needs boost::property_tree fully instantiated at the call site, which + // isn't included here. + o2::conf::ConfigurableParam::setValue("scdcalib.maxZ2X", std::to_string(maxZ2XCut)); + LOGP(info, "----- Dumping configuration values START -----"); + params.printKeyValues(); + LOGP(info, "----- Dumping configuration values END -----"); + + GID::mask_t allowedSources = GID::getSourcesMask("ITS-TPC,ITS-TPC-TRD,ITS-TPC-TOF,ITS-TPC-TRD-TOF"); + GID::mask_t sources = allowedSources & GID::getSourcesMask(trackSources); + + // Get CCDB objects + auto& ccdbmgr = o2::ccdb::BasicCCDBManager::instance(); + ccdbmgr.setCaching(true); + ccdbmgr.setFatalWhenNull(false); + ccdbmgr.setURL("http://alice-ccdb.cern.ch"); + auto runDuration = ccdbmgr.getRunDuration(runNumber); + auto tRun = runDuration.first + (runDuration.second - runDuration.first) / 2; // time stamp for the middle of the run duration + ccdbmgr.setTimestamp(tRun); + + // CTP orbit reset time, does not change during the run + const auto orbitResetTimeNS = ccdbmgr.get>("CTP/Calib/OrbitReset"); + const int64_t orbitResetTimeMS = (*orbitResetTimeNS)[0] * 1e-3; + LOGP(info, "Orbit reset time in MS is {}", orbitResetTimeMS); + + // Geometry, material budget and B-field + auto geoAligned = ccdbmgr.get("GLO/Config/GeometryAligned"); + auto magField = ccdbmgr.get("GLO/Config/GRPMagField"); + const o2::base::MatLayerCylSet* matLut = o2::base::MatLayerCylSet::rectifyPtrFromFile(ccdbmgr.get("GLO/Param/MatLUT")); + o2::base::Propagator::initFieldFromGRP(magField); + auto prop = o2::base::Propagator::Instance(); + prop->setMatLUT(matLut); + float magfieldvalue = static_cast(magField->getNominalL3Field()); + LOGP(info, "Nominal L3 field: {:.3f}", magfieldvalue); + + // GRP LHC and beam type + auto grplhc = ccdbmgr.get("GLO/Config/GRPLHCIF"); + const auto beamA = grplhc->getBeamZ(o2::constants::lhc::BeamA); + const auto beamC = grplhc->getBeamZ(o2::constants::lhc::BeamC); + const auto eCM = grplhc->getSqrtS(); + bool isPbPb = (beamA == 82 && beamC == 82); + LOGP(info, "BeamA: {}, BeamC: {}, isPbPb: {}, Ecm: {}", beamA, beamC, isPbPb, eCM); + + // Input + auto fileList = getInputFileList(fileInput); + + // Single-threaded when reading from AliEn: TGrid/xrootd access is not thread-safe. This overrides the + // nThreads argument. Detected from the actual input file entries rather than from fileInput itself, + // which for a GRID job is a local list of alien:// entries, so that local/Lustre input still gets the + // requested parallelism. + if (!fileList.empty() && fileList[0].rfind("alien://", 0) == 0) { + LOGP(info, "AliEn input detected (alien:// prefix) -- TGrid access is not thread-safe, forcing nThreads=1"); + nThreads = 1; + } + int nFileThreads = nThreads; + LOGP(info, "Using {} threads for processing", nFileThreads); + + // Set up binning + const auto z2xBins = o2::RangeTokenizer::tokenize(z2xBinning); + const auto y2xBins = o2::RangeTokenizer::tokenize(y2xBinning); + + TrackResiduals trackResiduals; + + trackResiduals.setZ2XBinning(z2xBins); + trackResiduals.setY2XBinning(y2xBins); + trackResiduals.init(); + + const int nY2XBins = trackResiduals.getNY2XBins(); + const int nZ2XBins = trackResiduals.getNZ2XBins(); + LOGP(info, "nY2XBins: {}, nZ2XBins: {}", nY2XBins, nZ2XBins); + + const float maxDistIntCls = 25.0; + + std::vector>>>> vec_residualsAll; // sector, voxX, voxF, voxZ, xyz + std::vector>>> vec_residuals_counterAll; // sector, voxX, voxF, voxZ + + vec_residualsAll.resize(NSectors); + vec_residuals_counterAll.resize(NSectors); + for (int isec = 0; isec < NSectors; isec++) { + vec_residualsAll[isec].resize(NRows); + vec_residuals_counterAll[isec].resize(NRows); + for (int ix = 0; ix < NRows; ix++) { + vec_residualsAll[isec][ix].resize(nY2XBins); + vec_residuals_counterAll[isec][ix].resize(nY2XBins); + for (int iy = 0; iy < nY2XBins; iy++) { + vec_residualsAll[isec][ix][iy].resize(nZ2XBins); + vec_residuals_counterAll[isec][ix][iy].resize(nZ2XBins); + for (int iz = 0; iz < nZ2XBins; iz++) { + vec_residualsAll[isec][ix][iy][iz].resize(3); + for (int ixyz = 0; ixyz < 3; ixyz++) { + vec_residualsAll[isec][ix][iy][iz][ixyz] = 0.0; + } + vec_residuals_counterAll[isec][ix][iy][iz] = 0; + } + } + } + } + + const int nSlices = NSectors * trackResiduals.getNY2XBins() * trackResiduals.getNZ2XBins(); + const Long64_t maxTracks = maxTracksPerSlice * nSlices; + const int minTracks = minTracksPerSlice * nSlices; + std::atomic nTracksProcessed{0}; + Long64_t nTracksProcessed_final{0}; + + // Do we have a correction map available that we should apply to the clusters before the map extraction? + std::array, NSectors> voxelResults{}; + if (voxMapInput.size()) { + LOGP(info, "[InputCorrMap]: A correction map has been provided. Will apply the corrections to the cluster residuals"); + LOGP(info, "[InputCorrMap]: Resizing voxelResults to number of voxels"); + for (int iSec = 0; iSec < NSectors; ++iSec) { + voxelResults[iSec].resize(trackResiduals.getNVoxelsPerSector()); + } + TrackResiduals::VoxRes* voxResPtr = nullptr; + std::unique_ptr fIn = std::make_unique(voxMapInput.c_str()); + if (!fIn->IsOpen() || fIn->IsZombie()) { + LOGP(fatal, "[InputCorrMap]: Could not open input file {}", voxMapInput); + } + LOGP(info, "[InputCorrMap]: Getting TTree of voxels"); + std::unique_ptr treeIn; + treeIn.reset((TTree*)fIn->Get("voxResTree")); + if (!treeIn) { + LOGP(fatal, "[InputCorrMap]: Could not extract voxResTree from input file {}", voxMapInput); + } + treeIn->SetBranchAddress("voxRes", &voxResPtr); + LOGP(info, "[InputCorrMap]: Getting voxel results and filling voxelResults"); + for (int iEntry = 0; iEntry < treeIn->GetEntries(); ++iEntry) { + treeIn->GetEntry(iEntry); + auto& voxRes = *voxResPtr; + voxelResults[voxRes.bsec][trackResiduals.getGlbVoxBin(voxRes.bvox)] = voxRes; + } + } + // voxelResults is only read inside doFileProcessing and is not touched between thread start and join, + // so it is shared by const reference instead of copied once per thread (copying would cost + // nFileThreads x 36 x nVoxPerSector VoxRes objects). + LOGP(info, "Sharing the provided input Map with all threads"); + + // Check for bad ranges + std::vector badRanges; + std::vector> badRanges_thread(nFileThreads); + bool invertBadRange = false; + if (badRangeList.length() > 0) { + if (badRangeList[0] == '-') { + LOGP(info, "Inverting badRange list!"); + invertBadRange = true; + badRangeList.erase(0, 1); + } + badRanges = loadRunTimeSpans(badRangeList, runNumber, badRangeSelection); + } + for (int iThread = 0; iThread < nFileThreads; ++iThread) { + badRanges_thread[iThread] = badRanges; + } + + // ---| Lumi estimators |--- + size_t lumiEntriesCTP = 0; + double lumiSumCTP = 0; + + // vector of selected values + std::vector orbitsSel; + // This macro does not read IDC scalers from CCDB (see the "IDC values" note above the OrbitLumiInfo + // tree below). These two stay empty and are written out only to keep the output format stable for + // readers that expect the branches; the values are filled in offline from timeMSsel. + std::vector idcScalerASel; + std::vector idcScalerCSel; + std::vector ctpLumiSel; + std::vector timeMSsel; // time in ms collected over all selected TFs + + //---------------------------- + const std::filesystem::path pFileOutput(fileOutput); + std::string outPath(pFileOutput.parent_path().c_str()); + if (outPath.empty()) { + outPath = "."; + } + + fair::Logger::SetConsoleSeverity(fair::Severity::error); + /////////////////////////// + // Create thread vectors // + /////////////////////////// + // trackResiduals (declared earlier, already configured) is shared read-only across threads -- no per-thread copy needed + + long int nEdgeClustersSkipped{0}; + std::vector nEdgeClustersSkipped_thread(nFileThreads, 0); // Does NEED to be merged + long int nTracksSkippedByBadRangeList{0}; + std::vector nTracksSkippedByBadRangeList_thread(nFileThreads, 0); // Does NEED to be merged + long int nTFs{0}; + long int nTFsSkippedByBadRangeList{0}; + long int nTFsSkippedByTimeWindow{0}; + std::vector nTFs_thread(nFileThreads, 0); // Does NEED to be merged + std::vector nTFsSkippedByBadRangeList_thread(nFileThreads, 0); // Does NEED to be merged + std::vector nTFsSkippedByTimeWindow_thread(nFileThreads, 0); // Does NEED to be merged + + std::vector voxels(NSectors * NRows * nY2XBins * nZ2XBins); // flat [sec][ix][iy][iz] -- shared circle pool, ONE instance for all threads (guarded by per-voxel mutex); sized directly since VoxelData (holds a mutex) cannot be resize()'d after construction // Does NOT need to be merged + + // residualsAll/NP/PP/NN + their counters now live directly in VoxelData (shared, per-voxel mutex), so no per-thread copies or later merge pass are needed for them. + + // The per-file input handles (TFile, TTreeReaders, TTreeReaderValues) and the I/O-rate sampling state + // are plain locals inside doFileProcessing: each thread only ever used its own slot, so they never + // needed to be shared vectors here. Only totalBytesReadPerf is still per-thread, because it is summed + // across threads after the join below. + std::vector totalBytesReadPerf_thread(nFileThreads, 0); // Does NEED to be merged + + std::vector lumiEntriesCTP_thread(nFileThreads, 0); // Does NEED to be merged + std::vector lumiSumCTP_thread(nFileThreads, 0); // Does NEED to be merged + + // vector of selected values + std::vector> orbitsSel_thread(nFileThreads); // Does NEED to be merged + std::vector> ctpLumiSel_thread(nFileThreads); // Does NEED to be merged + std::vector> timeMSsel_thread(nFileThreads); // time in ms collected over all selected TFs // Does NEED to be merged + + fair::Logger::SetConsoleSeverity(fair::Severity::info); + printMemoryUsage("Memory usage after init buffers"); + + // Start threads + std::vector threads(nFileThreads); + for (int i = 0; i < nFileThreads; i++) { + threads[i] = std::thread(doFileProcessing, + i, + nFileThreads, + maxTrackWorkers, + firstTFTime, + lastTFTime, + invertBadRange, + maxdEdx, + maxdEdxExp, + maxDevdEdxOverExp, + skipEdgePads, + std::ref(nEdgeClustersSkipped_thread), + std::ref(nTracksSkippedByBadRangeList_thread), + std::ref(nTFs_thread), + std::ref(nTFsSkippedByBadRangeList_thread), + std::ref(nTFsSkippedByTimeWindow_thread), + voxMapInput, + sources, + orbitResetTimeMS, + magfieldvalue, + fileList, + maxTracksPerSlice, + maxTracks, + std::ref(nTracksProcessed), + std::cref(voxelResults), + std::ref(badRanges_thread), + std::cref(trackResiduals), + maxDistIntCls, + nY2XBins, + nZ2XBins, + std::ref(voxels), + std::ref(totalBytesReadPerf_thread), + std::ref(lumiEntriesCTP_thread), + std::ref(lumiSumCTP_thread), + std::ref(orbitsSel_thread), + std::ref(ctpLumiSel_thread), + std::ref(timeMSsel_thread)); + } + + // Wait for the threads to finish + for (auto& th : threads) { + th.join(); + } + + ////////////////////////////////////////////////////////////////////// + // ===| CODE TO MERGE VECTORS |======================================= + ////////////////////////////////////////////////////////////////////// + // START OF MERGE + nTracksProcessed_final = nTracksProcessed.load(); + Long64_t totalBytesReadPerf = 0; // sum of TTreePerfStats bytes read across all threads/files (network volume) + for (int iThread = 0; iThread < nFileThreads; ++iThread) { + // Merge counters + totalBytesReadPerf += totalBytesReadPerf_thread[iThread]; + lumiEntriesCTP += lumiEntriesCTP_thread[iThread]; + lumiSumCTP += lumiSumCTP_thread[iThread]; + nEdgeClustersSkipped += nEdgeClustersSkipped_thread[iThread]; + nTracksSkippedByBadRangeList += nTracksSkippedByBadRangeList_thread[iThread]; + nTFs += nTFs_thread[iThread]; + nTFsSkippedByBadRangeList += nTFsSkippedByBadRangeList_thread[iThread]; + nTFsSkippedByTimeWindow += nTFsSkippedByTimeWindow_thread[iThread]; + + // Merge vector data + orbitsSel.insert(orbitsSel.end(), + orbitsSel_thread[iThread].begin(), + orbitsSel_thread[iThread].end()); + + ctpLumiSel.insert(ctpLumiSel.end(), + ctpLumiSel_thread[iThread].begin(), + ctpLumiSel_thread[iThread].end()); + + timeMSsel.insert(timeMSsel.end(), + timeMSsel_thread[iThread].begin(), + timeMSsel_thread[iThread].end()); + } + + { + const double totalMBReadPerf = totalBytesReadPerf / (1024.0 * 1024.0); + LOGP(info, "All threads done | read {:.1f} MB total (unbinnedResid, across {} thread(s))", totalMBReadPerf, nFileThreads); + } + + // Compute final binned residuals directly from the shared per-voxel accumulators in `voxels` + // (each thread already accumulated straight into the single shared VoxelData under vox.mtx, + // so there is no per-thread data left to sum over here). + for (int isec = 0; isec < NSectors; isec++) { + for (int iz = 0; iz < nZ2XBins; iz++) { + for (int iy = 0; iy < nY2XBins; iy++) { + for (int ix = 0; ix < NRows; ix++) { + const auto& vox = voxels[((isec * NRows + ix) * nY2XBins + iy) * nZ2XBins + iz]; + + // weights as in original code + double weightNP = vox.counterNP * 1.0; + double weightPP = vox.counterPP * 0.01; + double weightNN = vox.counterNN * 0.01; + double sum_weight = weightNP + weightPP + weightNN; + + // For ixyz == 0,1: weighted average as before + for (int ixyz = 0; ixyz < 2; ++ixyz) { + if (sum_weight > 0.0) { + double meanNP = (vox.counterNP > 0) ? (vox.residualsNP[ixyz] / vox.counterNP) : 0.0; + double meanPP = (vox.counterPP > 0) ? (vox.residualsPP[ixyz] / vox.counterPP) : 0.0; + double meanNN = (vox.counterNN > 0) ? (vox.residualsNN[ixyz] / vox.counterNN) : 0.0; + vec_residualsAll[isec][ix][iy][iz][ixyz] = static_cast((weightNP * meanNP + weightPP * meanPP + weightNN * meanNN) / sum_weight); + } else { + vec_residualsAll[isec][ix][iy][iz][ixyz] = 0.0f; + } + } + // For ixyz == 2: simple mean; vox.residualsAll[2] is the running sum of DZdist, vox.counterZAll the count + vec_residualsAll[isec][ix][iy][iz][2] = (vox.counterZAll > 0) ? static_cast(vox.residualsAll[2] / vox.counterZAll) : 0.0f; + vec_residuals_counterAll[isec][ix][iy][iz] = vox.counterNP + vox.counterPP + vox.counterNN; + } + } + } + } + // END OF MERGE + ////////////////////////////////////////////////////////////////////// + + bool isBadCalib = false; + if ((minTracksPerSlice > 0) && (nTracksProcessed_final < minTracks)) { + LOGP(error, "Processed tracks: {} ({}), max requested tracks: {} ({}), minimum number of tracks not reached {} ({}), calibration will be marked as bad, skipped {} edge clusters, skipped tracks by badRangeList {} ({}), skipped TFs outside requested time window {} of {} ({:.1f}%)", nTracksProcessed_final, nTracksProcessed_final / nSlices, maxTracks, maxTracksPerSlice, minTracks, minTracksPerSlice, nEdgeClustersSkipped, nTracksSkippedByBadRangeList, nTracksSkippedByBadRangeList / nSlices, nTFsSkippedByTimeWindow, nTFs, (nTFs > 0) ? (100.0 * nTFsSkippedByTimeWindow / nTFs) : 0.0); + LOGP(info, "Processed time: {}ms, skipped time by badRangeList {}ms ({})", nTFs * o2::constants::lhc::LHCOrbitMUS * 1.e-3 * 32, nTFsSkippedByBadRangeList * o2::constants::lhc::LHCOrbitMUS * 1.e-3 * 32, float(nTFsSkippedByBadRangeList) / float(nTFs)); + const std::string stem = fs::path(fileOutput.data()).stem().c_str(); + std::ofstream(fmt::format("badCalib.{}", stem)).close(); + isBadCalib = true; + } else { + LOGP(info, "Processed tracks: {} ({}), max requested tracks: {} ({}), skipped {} edge clusters, skipped tracks by badRangeList {} ({}), skipped TFs outside requested time window {} of {} ({:.1f}%)", nTracksProcessed_final, nTracksProcessed_final / nSlices, maxTracks, maxTracksPerSlice, nEdgeClustersSkipped, nTracksSkippedByBadRangeList, nTracksSkippedByBadRangeList / nSlices, nTFsSkippedByTimeWindow, nTFs, (nTFs > 0) ? (100.0 * nTFsSkippedByTimeWindow / nTFs) : 0.0); + LOGP(info, "Processed time: {}ms, skipped time by badRangeList {}ms ({})", nTFs * o2::constants::lhc::LHCOrbitMUS * 1.e-3 * 32, nTFsSkippedByBadRangeList * o2::constants::lhc::LHCOrbitMUS * 1.e-3 * 32, float(nTFsSkippedByBadRangeList) / float(nTFs)); + } + //---------------------------------------------------------------- + + // IDC values: this macro deliberately does not query the TPC scalers from CCDB. Doing so per TF from + // inside the processing loop is slow and unreliable (uncached fetch, object reload on every jump in + // time), so meanIDC/medianIDC are written as 0 placeholders here. The per-TF timestamps needed to + // recover them are written to the OrbitLumiInfo tree below (timeMSsel), so the real values can be + // joined in afterwards, offline, against a properly cached CCDB client. + float meanIDC = 0.f; // not const: written to a TTree branch below, which needs a mutable address + + double meanCTP = 0; + if (lumiEntriesCTP > 0) { + meanCTP = lumiSumCTP / lumiEntriesCTP * (isPbPb ? 2.414 : 1); // 2.414 for PbPb + } + + TFile* outputfile = new TFile(fileOutput.c_str(), "RECREATE"); + LOGP(info, "Output file: {} created", fileOutput); + + o2::tpc::TrackResiduals::VoxRes mVoxelResultsOut{}; ///< the results from mVoxelResults are copied in here to be able to stream them + o2::tpc::TrackResiduals::VoxRes* mVoxelResultsOutPtr{&mVoxelResultsOut}; ///< pointer to set the branch address to for the output + std::unique_ptr mTreeOut; + + // Same tree-alias set as the real TrackResiduals::createOutputFile() (SpacePoints/TrackResiduals.cxx), + // so this tree can be TTree::Draw()'n the same way downstream regardless of which of the two wrote it. + if (trackResiduals.getNVoxelsPerSector() == 0) { + LOGP(warning, "For the tree aliases to work you must initialize the binning before calling createOutputFile()"); + } + mTreeOut = std::make_unique("voxResTree", "voxRes map results and statistics"); + mTreeOut->SetAlias("z2xBin", "bvox[0]"); + mTreeOut->SetAlias("y2xBin", "bvox[1]"); + mTreeOut->SetAlias("xBin", "bvox[2]"); + mTreeOut->SetAlias("z2xAV", "stat[0]"); + mTreeOut->SetAlias("y2xAV", "stat[1]"); + mTreeOut->SetAlias("xAV", "stat[2]"); + mTreeOut->SetAlias("fsector", "bsec+0.5+9.*(y2xAV)/pi"); + mTreeOut->SetAlias("phi", "(bsec%18+0.5+9.*(stat[1])/pi)/9*pi"); + mTreeOut->SetAlias("r", "stat[2]"); + mTreeOut->SetAlias("z", "z2xAV*xAV"); + mTreeOut->SetAlias("dX", "D[0]"); + mTreeOut->SetAlias("dY", "D[1]"); + mTreeOut->SetAlias("dZ", "D[2]"); + mTreeOut->SetAlias("dXS", "DS[0]"); + mTreeOut->SetAlias("dYS", "DS[1]"); + mTreeOut->SetAlias("dZS", "DS[2]"); + mTreeOut->SetAlias("dXE", "E[0]"); + mTreeOut->SetAlias("dYE", "E[1]"); + mTreeOut->SetAlias("dZE", "E[2]"); + mTreeOut->SetAlias("voxelIndex", Form("xBin + %i * (y2xBin + %i * z2xBin) + %i * bsec", trackResiduals.getNXBins(), trackResiduals.getNY2XBins(), trackResiduals.getNVoxelsPerSector())); + mTreeOut->SetAlias("entries", "stat[3]"); + mTreeOut->SetAlias("fitOK", Form("(flags & %u) == %u", TrackResiduals::DistDone, TrackResiduals::DistDone)); + mTreeOut->SetAlias("dispOK", Form("(flags & %u) == %u", TrackResiduals::DispDone, TrackResiduals::DispDone)); + mTreeOut->SetAlias("smtOK", Form("(flags & %u) == %u", TrackResiduals::SmoothDone, TrackResiduals::SmoothDone)); + mTreeOut->SetAlias("masked", Form("(flags & %u) == %u", TrackResiduals::Masked, TrackResiduals::Masked)); + mTreeOut->Branch("voxRes", &mVoxelResultsOutPtr); + + // Placeholder, filled in offline together with meanIDC -- see the note above. + float medianIDC = 0.f; + float medianCTP = static_cast(calculateMedian(ctpLumiSel)); + long medianTimeMS = static_cast(calculateMedian(timeMSsel)); + long meanTimeMS = static_cast(calculateMean(timeMSsel)); + + auto userInfo = mTreeOut->GetUserInfo(); + userInfo->Add(new TNamed("meanIDC", std::to_string(meanIDC).data())); + userInfo->Add(new TNamed("meanCTP", std::to_string(meanCTP).data())); + userInfo->Add(new TNamed("meanTimeMS", std::to_string(meanTimeMS).data())); + userInfo->Add(new TNamed("medianIDC", std::to_string(medianIDC).data())); + userInfo->Add(new TNamed("medianCTP", std::to_string(medianCTP).data())); + userInfo->Add(new TNamed("medianTimeMS", std::to_string(medianTimeMS).data())); + userInfo->Add(new TNamed("y2xBinning", y2xBinning.data())); + userInfo->Add(new TNamed("z2xBinning", z2xBinning.data())); + // TrackResiduals::setZ2XBinning() scales the physical z/x bin boundaries by scdcalib.maxZ2X -- this + // value is baked into what each z2x voxel index means in THIS tree, not just a cosmetic config knob. + // Stage 2 has no scdconfig.ini on the GRID and would otherwise silently reconstruct the binning with + // the O2 code default (1.0) instead of maxZ2XCut (1.4 in production), misaligning its geometry against + // the one this tree's voxels were actually filled with. Stored here so stage 2 can apply the exact + // same value it was built with, not a separately-configured guess. + userInfo->Add(new TNamed("maxZ2X", std::to_string(maxZ2XCut).data())); + userInfo->Add(new TNamed("nSlicesPhiZ", std::to_string(nSlices))); + userInfo->Add(new TNamed("maxTracks", std::to_string(maxTracks))); + userInfo->Add(new TNamed("minTracks", std::to_string(minTracks))); + userInfo->Add(new TNamed("nTracksProcessed", std::to_string(nTracksProcessed_final))); + if (isBadCalib) { + userInfo->Add(new TNamed("badCalib", "1")); + } + + for (int isec = 0; isec < NSectors; isec++) { + for (int iz = 0; iz < nZ2XBins; iz++) { + for (int iy = 0; iy < nY2XBins; iy++) { + for (int ix = 0; ix < NRows; ix++) { + for (int ixyz = 0; ixyz < 3; ixyz++) { + mVoxelResultsOut.D[ixyz] = vec_residualsAll[isec][ix][iy][iz][ixyz]; + mVoxelResultsOut.DS[ixyz] = vec_residualsAll[isec][ix][iy][iz][ixyz]; + mVoxelResultsOut.DC[ixyz] = vec_residualsAll[isec][ix][iy][iz][ixyz]; + mVoxelResultsOut.E[ixyz] = 0.1; + } + + float xposvox, yoverxpos, zoverxpos; + trackResiduals.getVoxelCoordinates(isec, ix, iy, iz, xposvox, yoverxpos, zoverxpos); + + mVoxelResultsOut.stat[0] = static_cast(zoverxpos); // z/x, y/x, x, entries + mVoxelResultsOut.stat[1] = static_cast(yoverxpos); + mVoxelResultsOut.stat[2] = static_cast(xposvox); + mVoxelResultsOut.stat[3] = static_cast(vec_residuals_counterAll[isec][ix][iy][iz]); // number of entries used + + mVoxelResultsOut.EXYCorr = 1.0; + mVoxelResultsOut.dYSigMAD = 1.0; + mVoxelResultsOut.dZSigLTM = 1.0; + + mVoxelResultsOut.bvox[0] = iz; + mVoxelResultsOut.bvox[1] = iy; + mVoxelResultsOut.bvox[2] = ix; + mVoxelResultsOut.bsec = isec; + mVoxelResultsOut.flags = 7; + + mTreeOut->Fill(); + } + } + } + } + + // write orbit and lumi info + outputfile->cd(); + TTree tOrbitLumi("OrbitLumiInfo", "Orbit and Lumi Info"); + int64_t orbitResetMS = orbitResetTimeMS; + tOrbitLumi.Branch("orbitResetTimeMS", &orbitResetMS); + tOrbitLumi.Branch("timeMSsel", &timeMSsel); + tOrbitLumi.Branch("orbitsSel", &orbitsSel); + tOrbitLumi.Branch("idcScalerASel", &idcScalerASel); + tOrbitLumi.Branch("idcScalerCSel", &idcScalerCSel); + tOrbitLumi.Branch("ctpLumiSel", &ctpLumiSel); + tOrbitLumi.Branch("meanIDC", &meanIDC); + tOrbitLumi.Branch("meanCTP", &meanCTP); + tOrbitLumi.Branch("meanTimeMS", &meanTimeMS); + tOrbitLumi.Branch("medianIDC", &medianIDC); + tOrbitLumi.Branch("medianCTP", &medianCTP); + tOrbitLumi.Branch("medianTimeMS", &medianTimeMS); + tOrbitLumi.Fill(); + tOrbitLumi.Write(); + + { + TTree tMetaData("MetaData", "Meta data information"); + + int nSlicesP = nSlices; + int maxTracksP = maxTracks; + int minTracksP = minTracks; + + tMetaData.Branch("runNumber", &runNumber); + tMetaData.Branch("nSlicesPhiZ", &nSlicesP); + tMetaData.Branch("maxTracks", &maxTracksP); + tMetaData.Branch("minTracks", &minTracksP); + tMetaData.Branch("nTracksProcessed", &nTracksProcessed_final); + tMetaData.Branch("fileOutput", &fileOutput); + tMetaData.Branch("voxMapInput", &voxMapInput); + tMetaData.Branch("trackSources", &trackSources); + tMetaData.Branch("z2xBinning", &z2xBinning); + tMetaData.Branch("y2xBinning", &y2xBinning); + tMetaData.Branch("useSmoothed", &useSmoothed); + tMetaData.Branch("createSpline", &createSpline); + tMetaData.Branch("maxTracksPerSlice", &maxTracksPerSlice); + tMetaData.Branch("minTracksPerSlice", &minTracksPerSlice); + tMetaData.Branch("badRangeList", &badRangeList); + tMetaData.Branch("firstTFTime", &firstTFTime); + tMetaData.Branch("lastTFTime", &lastTFTime); + tMetaData.Fill(); + tMetaData.Write(); + } + + outputfile->Write(); + //---------------------------------------------------------------- + + const std::string fileOutputInfo = fmt::format("{}/{}.txt", outPath, pFileOutput.stem().c_str()); + std::ofstream fInfo(fileOutputInfo); + fInfo << "meanIDC: " << meanIDC << "\n"; + fInfo << "meanCTP: " << meanCTP << "\n"; + fInfo << "meanTimeMS: " << meanTimeMS << "\n"; + fInfo << "medianIDC: " << medianIDC << "\n"; + fInfo << "medianCTP: " << medianCTP << "\n"; + fInfo << "medianTimeMS: " << medianTimeMS << "\n"; + fInfo.close(); + LOGP(info, "Found meanIDC: {}", meanIDC); + LOGP(info, "Found meanCTP: {}", meanCTP); + LOGP(info, "Found meanTimeMS: {}", meanTimeMS); + LOGP(info, "Found medianIDC: {}", medianIDC); + LOGP(info, "Found medianCTP: {}", medianCTP); + LOGP(info, "Found medianTimeMS: {}", medianTimeMS); + + // This macro only produces the raw voxel-residual map. Turning that into a TPCFastTransform is a + // separate step, done afterwards by TPCFastTransformInitCPM.C on this output, so that the two can be + // rerun independently. The createSpline argument is kept because it is recorded in the output + // metadata and consumed by that later step. + + // The per-thread input handles are locals inside doFileProcessing and are already released, in + // dependency order, when each thread returns -- nothing to tear down here. + mTreeOut.reset(); + + const auto t_end_1 = std::chrono::high_resolution_clock::now(); + LOGP(info, "Wall-clock time for the whole application: {:.1f} s", + std::chrono::duration(t_end_1 - t_start).count()); + + LOGP(info, "Done processing"); + + printMemoryUsage("Memory usage at end"); +} + +std::vector loadRunTimeSpans(const std::string& flname, int onlyRun, const std::string& selection = "ALL") +{ + std::ifstream inputFile(flname); + if (!inputFile) { + LOGP(fatal, "Failed to open selected run/timespans file {}", flname); + } + LOGP(info, "Reading bad ranges from file {}, for run {}", flname, onlyRun); + auto& ccdbmgr = o2::ccdb::BasicCCDBManager::instance(); + ccdbmgr.setURL("http://alice-ccdb.cern.ch"); + + ccdbmgr.setCaching(true); + ccdbmgr.setFatalWhenNull(false); + + std::vector badRanges; + + std::string line; + size_t cntl = 0, cntr = 0; + int64_t orbitResetTimeMS = 0; + int lastRunOrbitReset = -1; + while (std::getline(inputFile, line)) { + cntl++; + for (char& ch : line) { // Replace semicolons and tabs with spaces for uniform processing + if (ch == ';' || ch == '\t' || ch == ',') { + ch = ' '; + } + } + o2::utils::Str::trim(line); + if (line.size() < 1 || line[0] == '#') { + continue; + } + auto tokens = o2::utils::Str::tokenize(line, ' '); + auto logError = [&cntl, &line]() { LOGP(error, "Expected format for selection is tripplet , failed on line#{}: {}", cntl, line); }; + if (tokens.size() >= 3) { + int run = 0; + long rmin, rmax; + try { + run = std::stoi(tokens[0]); + rmin = std::stol(tokens[1]); + rmax = std::stol(tokens[2]); + } catch (...) { + logError(); + continue; + } + + if (onlyRun != run) { + continue; + } + + if (selection != "ALL") { + bool isSelection = false; + for (int iToken = 3; iToken < int(tokens.size()); ++iToken) { + if (tokens[iToken] == selection) { + isSelection = true; + } + } + if (isSelection == false) { + continue; + } + } + + constexpr long ISTimeStamp = 1514761200000L; + int isTimeStampMin = rmin > ISTimeStamp ? 1 : 0, isTimeStampMax = rmax > ISTimeStamp ? 1 : 0; // values above ISTimeStamp are timestamps (need to be converted to orbits) + if (rmin > rmax) { + LOGP(fatal, "Provided range limits are not in increasing order, entry is {}", line); + } + if (isTimeStampMin != isTimeStampMax) { + LOGP(fatal, "Provided range limits should be both consistent either with orbit number or with unix timestamp in ms, entry is {}", line); + } + if (isTimeStampMin) { + if (lastRunOrbitReset != run) { + LOGP(info, "Input needs conversion from time stamps to orbit"); + const auto [sor, eor] = ccdbmgr.getRunDuration(run); + const long timeMeanRun = (sor + eor) / 2.; + const double lengthRun = (eor - sor); + const auto orbitResetTimeNS = ccdbmgr.getSpecific>("CTP/Calib/OrbitReset", timeMeanRun); + orbitResetTimeMS = (*orbitResetTimeNS)[0] * 1e-3; + + LOGP(info, "Run {}, sor {}, eor {}, duration {} (min)", run, sor, eor, lengthRun / 1000. / 60.); + LOGP(info, "Orbit reset time in MS is {}", orbitResetTimeMS); + lastRunOrbitReset = run; + } + const auto orbitToMS = o2::constants::lhc::LHCOrbitMUS * 1e-3; + const auto rMinIn = rmin, rMaxIn = rmax; + rmin = long((rmin - orbitResetTimeMS) / orbitToMS); + rmax = long(std::ceil((rmax - orbitResetTimeMS) / orbitToMS)); + LOGP(info, "Run {} input range [{} - {}] ms -> [{} - {}] orbits", run, rMinIn, rMaxIn, rmin, rmax); + } + + badRanges.emplace_back(rmin, rmax); + cntr++; + } else { + logError(); + } + } + return badRanges; +} diff --git a/Detectors/TPC/calibration/SpacePoints/macro/voxResQA.C b/Detectors/TPC/calibration/SpacePoints/macro/voxResQA.C new file mode 100644 index 0000000000000..dbc766b2571d4 --- /dev/null +++ b/Detectors/TPC/calibration/SpacePoints/macro/voxResQA.C @@ -0,0 +1,445 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include +#include +#include +#include +#include "TChain.h" +#include "TMath.h" +#include "TCanvas.h" +#include "THStack.h" +#include "TLegend.h" +#include "TProfile.h" +#include "TProfile2D.h" +#include "TObjArray.h" +#include "TStyle.h" +#include "TSystem.h" +#include "TLatex.h" +#include "TPaletteAxis.h" +#include "TPCBase/Utils.h" +#include "TPCBaseRecSim/Painter.h" +#include "CommonUtils/StringUtils.h" +#include "Framework/Logger.h" +#include "SpacePoints/TrackResiduals.h" + +std::string getString(TList* l, const std::string& name, std::string defaultVal = "0"); +int getNbins(const std::string& name, int defaultVal = 20, const char delim = ','); +void setStyle(TH1* hist, int idir); +void setSizes(TAxis* axis, float titleSize, float titleOffset, float labelSize); + +using namespace o2::tpc; +namespace fs = std::filesystem; + +void voxResQA(std::string inFilesCmd, std::string outFileNameAdd = "", bool drawErrors = false, bool useSmoothed = true, int z2xBinSel = -1) +{ + gStyle->SetOptStat(0); + + TObjArray arrCanvases1D; + arrCanvases1D.SetName("voxResQA"); + TObjArray arrCanvases2D; + arrCanvases2D.SetName("voxResQA"); + + auto hsdXvsRowA = new THStack; + auto hsdXvsRowC = new THStack; + auto hsdZvsRowA = new THStack; + auto hsdZvsRowC = new THStack; + + const std::string dXtitle = useSmoothed ? "dXS" : "dX"; + const std::string dYtitle = useSmoothed ? "dYS" : "dY"; + const std::string dZtitle = useSmoothed ? "dZS" : "dZ"; + + const std::string sFiles(gSystem->GetFromPipe(inFilesCmd.data())); + const auto arrFiles = o2::utils::Str::tokenize(sFiles, '\n'); + if (arrFiles.size() == 0) { + return; + } + int idir = 0; + for (const auto& inFile : arrFiles) { + const fs::path inPath(inFile); + const std::string fileTitle(std::string(inPath.stem()).substr(17)); + auto fileIDName = fileTitle; + std::replace(fileIDName.begin(), fileIDName.end(), '.', '_'); + std::replace(fileIDName.begin(), fileIDName.end(), '-', '_'); + TChain cVoxRes("voxResTree"); + cVoxRes.AddFile(inFile.data()); + + if (!cVoxRes.GetBranch("voxRes")) { + LOGP(error, "Could not find branch voxRes in file '{}'", inFile); + continue; + } + + o2::tpc::TrackResiduals::VoxRes* vox{nullptr}; + cVoxRes.SetBranchAddress("voxRes", &vox); + + // retrieve binning + // OBJ: TNamed meanIDC 0.295421 + // OBJ: TNamed meanCTP 118553.744497 + // OBJ: TNamed y2xBinning 20 + // OBJ: TNamed z2xBinning 20 + + cVoxRes.GetEntry(0); + const auto userInfo = cVoxRes.GetTree()->GetUserInfo(); + userInfo->Print(); + const auto y2xBinning = getString(userInfo, "y2xBinning"); + const auto z2xBinning = getString(userInfo, "z2xBinning"); + const auto meanIDC = std::stof(getString(userInfo, "meanIDC")); + const auto meanCTP = std::stof(getString(userInfo, "meanCTP")); + + const int nbinsY2X = getNbins(y2xBinning); + const int nBinsSector = nbinsY2X * 36; + const int nbinsZ2X = getNbins(z2xBinning); + + // ===| 1D histograms |===================================================== + TObjArray arrHists1D; + auto hEntriesDist = new TH1F(("hEntriesDist" + fileIDName).data(), (fileTitle + ";#entries").data(), 500, 0, 5000); + arrHists1D.Add(hEntriesDist); + auto hdXDist = new TH1F(("hdXDist" + fileIDName).data(), (fileTitle + ";" + dXtitle + " (cm)").data(), 100, -10, 20); + arrHists1D.Add(hdXDist); + auto hdYDist = new TH1F(("hdYDist" + fileIDName).data(), (fileTitle + ";" + dYtitle + " (cm)").data(), 100, -10, 10); + arrHists1D.Add(hdYDist); + auto hdZDist = new TH1F(("hdZDist" + fileIDName).data(), (fileTitle + ";" + dZtitle + " (cm)").data(), 100, -10, 10); + arrHists1D.Add(hdZDist); + auto hdYSigmaMAD = new TProfile(("hdYSigmaMAD" + fileIDName).data(), (fileTitle + ";sector;<#sigma_{MAD}(dY)> (cm)").data(), nBinsSector, 0, nBinsSector); + arrHists1D.Add(hdYSigmaMAD); + + auto hdXvsRowA = new TProfile(("hdXvsRowA" + fileIDName).data(), (fileTitle + " (A-Side);row;<" + dXtitle + "> (cm) z2xBin 0").data(), 152, 0, 152); + hsdXvsRowA->Add(hdXvsRowA); + hsdXvsRowA->SetTitle("(A-Side);row; (cm) z2xBin 0"); + setStyle(hdXvsRowA, idir); + + auto hdXvsRowC = new TProfile(("hdXvsRowC" + fileIDName).data(), (fileTitle + " (C-Side);row;<" + dXtitle + "> (cm) z2xBin 0").data(), 152, 0, 152); + hsdXvsRowC->Add(hdXvsRowC); + hsdXvsRowC->SetTitle("(C-Side);row; (cm) z2xBin 0"); + setStyle(hdXvsRowC, idir); + + auto hdZvsRowA = new TProfile(("hdZvsRowA" + fileIDName).data(), (fileTitle + " (A-Side);row;<" + dZtitle + "> (cm) z2xBin 0").data(), 152, 0, 152); + hsdZvsRowA->Add(hdZvsRowA); + hsdZvsRowA->SetTitle("(A-Side);row; (cm) z2xBin 0"); + setStyle(hdZvsRowA, idir); + + auto hdZvsRowC = new TProfile(("hdZvsRowC" + fileIDName).data(), (fileTitle + " (C-Side);row;<" + dZtitle + "> (cm) z2xBin 0").data(), 152, 0, 152); + hsdZvsRowC->Add(hdZvsRowC); + hsdZvsRowC->SetTitle(("(C-Side);row;" + dZtitle + " (cm) z2xBin 0").data()); + setStyle(hdZvsRowC, idir); + + auto hdZvsZ2X = new TProfile(("hdZvsZ2X" + fileIDName).data(), (fileTitle + ";z2x-bin;<" + dZtitle + "> (cm) row 80").data(), 2 * nbinsZ2X, -nbinsZ2X, nbinsZ2X); + arrHists1D.Add(hdZvsZ2X); + + // ===| 2D histograms |===================================================== + TObjArray arrHists2D; + auto hMeanEntries = new TProfile2D(("hMeanEntries_" + fileIDName).data(), (fileTitle + ";sector;row;").data(), nBinsSector, 0, nBinsSector, 152, 0, 152); + arrHists2D.Add(hMeanEntries); + auto hMeanEntrieszRow = new TProfile2D(("hMeanEntrieszRow" + fileIDName).data(), (fileTitle + ";z-bin;row; (cm)").data(), 2 * nbinsZ2X, -nbinsZ2X, nbinsZ2X, 152, 0, 152); + arrHists2D.Add(hMeanEntrieszRow); + auto hSigmaMADSecRow = new TProfile2D(("hSigmaMADSecRow" + fileIDName).data(), (fileTitle + ";sector;row;<#sigma_{MAD}(dY)> (cm)").data(), nBinsSector, 0, nBinsSector, 152, 0, 152); + arrHists2D.Add(hSigmaMADSecRow); + auto hdXSecRow = new TProfile2D(("hdXSecRow" + fileIDName).data(), (fileTitle + ";sector;row;<" + dXtitle + "> (cm)").data(), nBinsSector, 0, nBinsSector, 152, 0, 152); + arrHists2D.Add(hdXSecRow); + auto hdYSecRow = new TProfile2D(("hdYSecRow" + fileIDName).data(), (fileTitle + ";sector;row;<" + dYtitle + "> (cm)").data(), nBinsSector, 0, nBinsSector, 152, 0, 152); + arrHists2D.Add(hdYSecRow); + auto hdZzRow = new TProfile2D(("hdZzRow" + fileIDName).data(), (fileTitle + ";z2x-bin;row;<" + dZtitle + "> (cm)").data(), 2 * nbinsZ2X, -nbinsZ2X, nbinsZ2X, 152, 0, 152); + arrHists2D.Add(hdZzRow); + + TProfile2D* hdXESecRow = nullptr; + TProfile2D* hdYESecRow = nullptr; + TProfile2D* hdZEzRow = nullptr; + if (drawErrors) { + hdXESecRow = new TProfile2D(("hdXESecRow" + fileIDName).data(), (fileTitle + ";sector;row; (cm)").data(), nBinsSector, 0, nBinsSector, 152, 0, 152); + arrHists2D.Add(hdXESecRow); + hdYESecRow = new TProfile2D(("hdYESecRow" + fileIDName).data(), (fileTitle + ";sector;row; (cm)").data(), nBinsSector, 0, nBinsSector, 152, 0, 152); + arrHists2D.Add(hdYESecRow); + hdZEzRow = new TProfile2D(("hdZEzRow" + fileIDName).data(), (fileTitle + ";z-bin;row; (cm)").data(), 2 * nbinsZ2X, -nbinsZ2X, nbinsZ2X, 152, 0, 152); + arrHists2D.Add(hdZEzRow); + } + + /* + OBJ: TNamed z2xBin bvox[0] + OBJ: TNamed y2xBin bvox[1] + OBJ: TNamed xBin bvox[2] + OBJ: TNamed z2xAV stat[0] + OBJ: TNamed y2xAV stat[1] + OBJ: TNamed xAV stat[2] + OBJ: TNamed fsector bsec+0.5+9.*(y2xAV)/pi + OBJ: TNamed phi (bsec%18+0.5+9.*(stat[1])/pi)/9*pi + OBJ: TNamed r stat[2] + OBJ: TNamed z z2xAV*xAV + OBJ: TNamed dX D[0] + OBJ: TNamed dY D[1] + OBJ: TNamed dZ D[2] + OBJ: TNamed dXS DS[0] + OBJ: TNamed dYS DS[1] + OBJ: TNamed dZS DS[2] + OBJ: TNamed dXE E[0] + OBJ: TNamed dYE E[1] + OBJ: TNamed dZE E[2] + OBJ: TNamed voxelIndex xBin + 152 * (y2xBin + 20 * z2xBin) + 60800 * bsec + OBJ: TNamed entries stat[3] + OBJ: TNamed fitOK (flags & 1) == 1 + OBJ: TNamed dispOK (flags & 2) == 2 + OBJ: TNamed smtOK (flags & 4) == 4 + OBJ: TNamed masked (flags & 128) == 128 + */ + + float z2xAV = 0; + + int isNaNdX = 0; + int isNaNdY = 0; + int isNaNdZ = 0; + int isNaNdXS = 0; + int isNaNdYS = 0; + int isNaNdZS = 0; + + for (Long64_t iEntry = 0; iEntry < cVoxRes.GetEntries(); ++iEntry) { + cVoxRes.GetEntry(iEntry); + const auto y2xBin = vox->bvox[1]; + const auto z2xBin = vox->bvox[0]; + const auto xBin = vox->bvox[2]; + const auto bsec = vox->bsec; + const auto entries = vox->stat[3]; + const auto dX = vox->D[0]; + const auto dY = vox->D[1]; + const auto dZ = vox->D[2]; + const auto dXS = vox->DS[0]; + const auto dYS = vox->DS[1]; + const auto dZS = vox->DS[2]; + const auto dXE = vox->E[0]; + const auto dYE = vox->E[1]; + const auto dZE = vox->E[2]; + const auto sectorFine = y2xBin + bsec * nbinsY2X; + const auto z2xBinSides = (z2xBin + 0.5) * (1 - 2 * (bsec > 17)); + const auto z = vox->stat[0] * vox->stat[2]; + + const auto dXdraw = useSmoothed ? dXS : dX; + const auto dYdraw = useSmoothed ? dYS : dY; + const auto dZdraw = useSmoothed ? dZS : dZ; + + isNaNdX += TMath::IsNaN(dX); + isNaNdY += TMath::IsNaN(dY); + isNaNdZ += TMath::IsNaN(dZ); + isNaNdXS += TMath::IsNaN(dXS); + isNaNdYS += TMath::IsNaN(dYS); + isNaNdZS += TMath::IsNaN(dZS); + + // only fill values in the acceptance + if (std::abs(z) < 248) { + if (z2xBinSel < 0 || z2xBinSel == z2xBin) { + if (z2xAV == 0) { + z2xAV = vox->stat[0]; + } + hMeanEntries->Fill(sectorFine, xBin, entries); + hdXSecRow->Fill(sectorFine, xBin, dXdraw); + hdYSecRow->Fill(sectorFine, xBin, dYdraw); + hSigmaMADSecRow->Fill(sectorFine, xBin, vox->dYSigMAD); + hdYSigmaMAD->Fill(sectorFine, vox->dYSigMAD); + } + } + + hMeanEntrieszRow->Fill(z2xBinSides, xBin, entries); + hdZzRow->Fill(z2xBinSides, xBin, dZdraw); + if (drawErrors && (z2xBinSel < 0 || z2xBinSel == z2xBin)) { + hdXESecRow->Fill(sectorFine, xBin, dXE); + hdYESecRow->Fill(sectorFine, xBin, dYE); + hdZEzRow->Fill(z2xBinSides, xBin, dZE); + } + + hEntriesDist->Fill(entries); + hdXDist->Fill(dXdraw); + hdYDist->Fill(dYdraw); + hdZDist->Fill(dZdraw); + if (xBin >= 79 && xBin <= 81) { + hdZvsZ2X->Fill(z2xBinSides, dZdraw); + } + + if (z2xBin == 0) { + if (bsec < 18) { + hdXvsRowA->Fill(xBin, dXdraw); + hdZvsRowA->Fill(xBin, dZdraw); + } else { + hdXvsRowC->Fill(xBin, dXdraw); + hdZvsRowC->Fill(xBin, dZdraw); + } + } + } + + std::vector hXadjust{hMeanEntries, hdXSecRow, hdXESecRow, hdYSecRow, hdYESecRow}; + for (auto h : hXadjust) { + if (!h) { + continue; + } + h->GetXaxis()->SetLimits(0, 36); + // 36 exact divisions means a label for every single sector -- unreadable at this pad size, they + // overlap into an illegible block. 12 (label every 3 sectors) still shows the A-/C-side structure + // without the collision. + h->GetXaxis()->SetNdivisions(12, false); + } + + // hEntriesDist's fixed 0-5000 range is mostly empty for real data (per-voxel entry counts rarely get + // anywhere near 5000) -- zoom to just past the last populated bin instead of showing mostly blank + // axis. + if (hEntriesDist->GetEntries() > 0) { + const int lastBin = hEntriesDist->FindLastBinAbove(0); + if (lastBin > 0) { + hEntriesDist->GetXaxis()->SetRangeUser(0, hEntriesDist->GetXaxis()->GetBinUpEdge(lastBin) * 1.1); + } + } + + // ===| output canvases |=================================================== + // + auto c1D = new TCanvas(("c1D_" + fileIDName + outFileNameAdd).data(), fileTitle.data(), 1500, 900); + arrCanvases1D.Add(c1D); + + int ipad = 1; + c1D->DivideSquare(arrHists1D.GetEntries()); + + for (auto o : arrHists1D) { + c1D->cd(ipad++); + o->Draw(); + const std::string name(o->GetName()); + if (o->IsA() != TProfile::Class()) { + gPad->SetLogy(); + } + // Without this, saveCanvas()'s plain c.SaveAs() (Detectors/TPC/base/src/Utils.cxx) can export a + // pad before its log-scale range is actually recomputed in batch mode -- confirmed real: the + // stored TCanvas in voxResQA*_1D.root has all real data (reopening and redrawing it interactively + // shows every panel fine), but the direct PNG export came out blank. The c2D loop below already + // does this after its own Draw() calls; c1D's was missing it. + gPad->Modified(); + gPad->Update(); + } + + auto c2D = new TCanvas(("c2D_" + fileIDName + outFileNameAdd).data(), fileTitle.data(), 1500, 900); + arrCanvases2D.Add(c2D); + + ipad = 1; + c2D->DivideSquare(arrHists2D.GetEntries()); + + TLatex l; + l.SetTextFont(42); + // Without this, DrawLatex's (x,y) below are interpreted in the pad's USER (data-axis) coordinates, + // not normalized pad fractions -- 0.75/0.85 then lands almost at the frame's bottom-left corner + // (sector~0.75 of 36, row~0.85 of 152), overlapping the plotted content instead of sitting clear of + // it. SetNDC() makes the coordinates pad-fraction-based, and the y moved down (below the frame, + // rather than "0.85" which was never actually near the top). + l.SetNDC(); + + for (auto o : arrHists2D) { + auto h = static_cast(o); + c2D->cd(ipad++); + h->Draw("colz"); + const std::string name(h->GetName()); + if (name.find("hdZzRow") == 0) { + l.DrawLatex(0.4, 0.02, "y2xBin averaged"); + } else { + if (z2xBinSel >= 0) { + l.DrawLatex(0.4, 0.02, fmt::format("z2xBin = {} ({})", z2xBinSel, z2xAV).data()); + } else { + l.DrawLatex(0.4, 0.02, "z2xBin averaged"); + } + } + gPad->Modified(); + gPad->Update(); + + auto palette = (TPaletteAxis*)h->GetListOfFunctions()->FindObject("palette"); + if (palette) { + painter::adjustPalette(h, 0.92); + } + } + + ++idir; + int nNaN = isNaNdX + isNaNdY + isNaNdZ; + int nNaNS = isNaNdXS + isNaNdYS + isNaNdZS; + const auto sNaN = fmt::format("NaN: {} - {} {} {} {}", inFile, nNaN, isNaNdX, isNaNdY, isNaNdZ); + const auto sNaNS = fmt::format("NaNS: {} - {} {} {} {}", inFile, nNaNS, isNaNdXS, isNaNdYS, isNaNdZS); + if (nNaN > 0) { + LOGP(error, "{}", sNaN); + } else { + LOGP(info, "{}", sNaN); + } + if (nNaNS > 0) { + LOGP(error, "{}", sNaNS); + } else { + LOGP(info, "{}", sNaNS); + } + } + + // ===| dX/dX vs row |======================================================== + // + auto cdXZvsRow = new TCanvas(fmt::format("cdXZvsRow{}", outFileNameAdd).data(), "dX/dZ vs row", 1500, 900); + cdXZvsRow->SetRightMargin(0.01); + cdXZvsRow->SetBottomMargin(0.15); + cdXZvsRow->Divide(1, 4, -1, -1); + cdXZvsRow->cd(1); + gPad->SetGrid(); + hsdXvsRowA->Draw("nostack"); + setSizes(hsdXvsRowA->GetYaxis(), 0.1, 0.4, 0.08); + setSizes(hsdXvsRowA->GetXaxis(), 0.1, 0.4, 0.08); + auto leg = gPad->BuildLegend(0.5, 0.5, 0.9, 0.9); + leg->SetMargin(0.05); + cdXZvsRow->cd(2); + gPad->SetGrid(); + hsdXvsRowC->Draw("nostack"); + setSizes(hsdXvsRowC->GetYaxis(), 0.1, 0.4, 0.08); + setSizes(hsdXvsRowC->GetXaxis(), 0.1, 0.4, 0.08); + cdXZvsRow->cd(3); + gPad->SetGrid(); + hsdZvsRowA->Draw("nostack"); + setSizes(hsdZvsRowA->GetYaxis(), 0.1, 0.4, 0.08); + setSizes(hsdZvsRowA->GetXaxis(), 0.1, 0.4, 0.08); + cdXZvsRow->cd(4); + gPad->SetGrid(); + hsdZvsRowC->Draw("nostack"); + setSizes(hsdZvsRowC->GetYaxis(), 0.1, 0.4, 0.08); + setSizes(hsdZvsRowC->GetXaxis(), 0.1, 0.4, 0.08); + + arrCanvases1D.Add(cdXZvsRow); + + // ===| save canvases |======================================================= + // + o2::tpc::utils::saveCanvases(arrCanvases1D, "./", "png,png", fmt::format("voxResQA{}_1D.root", outFileNameAdd.data())); + o2::tpc::utils::saveCanvases(arrCanvases2D, "./", "png,png", fmt::format("voxResQA{}_2D.root", outFileNameAdd.data())); +} + +std::string getString(TList* l, const std::string& name, std::string defaultVal) +{ + if (!l || !l->FindObject(name.data())) { + return defaultVal; + } + return l->FindObject(name.data())->GetTitle(); +} + +int getNbins(const std::string& name, int defaultVal, const char delim) +{ + if (name.find(delim) != name.npos) { + return std::count(name.begin(), name.end(), delim) + 1; + } + return std::stoi(name); +} + +void setStyle(TH1* hist, int idir) +{ + const std::vector colors = {kRed + 2, kOrange + 1, kGreen + 2, kAzure + 10, kBlue + 2, kMagenta + 1}; + const std::vector markers{20, 24, 21, 25, 47, 46, 34, 28}; + const std::vector styles{kSolid, kDashed, kDotted, kDashDotted}; + + hist->SetMarkerSize(1); + hist->SetMarkerColor(colors[idir % colors.size()]); + hist->SetLineColor(colors[idir % colors.size()]); + hist->SetMarkerStyle(markers[idir % markers.size()]); + hist->SetLineStyle(styles[(idir / colors.size()) % styles.size()]); +} + +void setSizes(TAxis* axis, float titleSize, float titleOffset, float labelSize) +{ + axis->SetTitleSize(titleSize); + axis->SetTitleOffset(titleOffset); + axis->SetLabelSize(labelSize); +} diff --git a/GPU/TPCFastTransformation/CMakeLists.txt b/GPU/TPCFastTransformation/CMakeLists.txt index c4fb7c04796f2..d48f7660c45b9 100644 --- a/GPU/TPCFastTransformation/CMakeLists.txt +++ b/GPU/TPCFastTransformation/CMakeLists.txt @@ -88,6 +88,22 @@ if(${ALIGPU_BUILD_TYPE} STREQUAL "O2") ${CMAKE_BINARY_DIR}/stage/include LABELS gpu tpc COMPILE_ONLY) endforeach() + + o2_add_test_root_macro(macro/TPCFastTransformInitCPM.C + PUBLIC_LINK_LIBRARIES O2::TPCFastTransformation + O2::DataFormatsTPC + O2::TPCSimulation + O2::TPCReconstruction + O2::TPCCalibration + O2::SpacePoints + O2::CommonUtils + O2::MathUtils + O2::Algorithm + O2::Framework + PUBLIC_INCLUDE_DIRECTORIES + ${CMAKE_BINARY_DIR}/stage/include + LABELS gpu tpc COMPILE_ONLY) + foreach(m IrregularSpline1DTest.C IrregularSpline2D3DCalibratorTest.C @@ -107,6 +123,9 @@ if(${ALIGPU_BUILD_TYPE} STREQUAL "O2") install(FILES macro/TPCFastTransformInit.C DESTINATION share/macro/) + + install(FILES macro/TPCFastTransformInitCPM.C + DESTINATION share/macro/) endif() if(ALIGPU_BUILD_TYPE STREQUAL "Standalone") diff --git a/GPU/TPCFastTransformation/macro/TPCFastTransformInitCPM.C b/GPU/TPCFastTransformation/macro/TPCFastTransformInitCPM.C new file mode 100644 index 0000000000000..a83efef0cdd75 --- /dev/null +++ b/GPU/TPCFastTransformation/macro/TPCFastTransformInitCPM.C @@ -0,0 +1,740 @@ +// Copyright 2019-2023 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// \file TPCFastTransformInitCPM.C +/// \brief A macro for generating TPC fast transformation +/// out of set of space charge correction voxels +/// +/// \author Sergey Gorbunov +/// + +/// how to run the macro: +/// +/// root -l TPCFastTransformInitCPM.C'("debugVoxRes.root")' +/// + +#if !defined(__CLING__) || defined(__ROOTCLING__) + +#include +#include +#include +#include +#include +#include "TFile.h" +#include "TSystem.h" +#include "TTree.h" +#include "TNtuple.h" +#include "TMath.h" +#include "Riostream.h" + +#include "CommonUtils/TreeStreamRedirector.h" +#include "MathUtils/fit.h" +#include "Algorithm/RangeTokenizer.h" +#include "Framework/Logger.h" +#include "GPU/TPCFastTransform.h" +#include "SpacePoints/TrackResiduals.h" +#include "TPCReconstruction/TPCFastTransformHelperO2.h" +#include "TPCCalibration/TPCFastSpaceChargeCorrectionHelper.h" + +#endif + +struct sMean { + float mean; + float rms; + float meanErr; + float rmsErr; + + ClassDef(sMean, 1); +}; + +#pragma link C++ class sMean + ; +#pragma link C++ class std::vector < sMean> + ; + +using namespace o2::tpc; +using namespace o2::gpu; +namespace mu = o2::math_utils; + +void createFastTransform(std::string outFileName, TTree* voxResTree, o2::tpc::TrackResiduals& trackResiduals, bool useSmoothed, bool invertSigns, float meanIDC = 0.f, float meanCTP = 0.f, int debug = 0, int useCTPLumi = 0); + +void TPCFastTransformInitCPM(const char* fileName = "debugVoxRes.root", + const char* outFileName = "TPCFastTransform_VoxRes.root", + bool useSmoothed = false, + int useCTPLumi = 0, + bool invertSigns = true, + float meanIDC = 0.f, + float meanCTP = 0.f, + int debug = 0, + int nThreads = 8) +{ + + // Initialise TPCFastTransform object from "voxRes" tree of + // o2::tpc::TrackResiduals::VoxRes track residual voxels + // + + /* + To visiualise the results: + + root -l transformDebug.root + corr->Draw("cx:y:z","iRoc==0&&iRow==10","") + grid->Draw("cx:y:z","iRoc==0&&iRow==10","same") + vox->Draw("vx:y:z","iRoc==0&&iRow==10","same") + */ + + o2::tpc::TPCFastSpaceChargeCorrectionHelper::instance()->setNthreads(nThreads); + + if (gSystem->AccessPathName(fileName)) { + LOGP(error, "input file {} does not exist!", fileName); + return; + } + + auto file = std::unique_ptr(TFile::Open(fileName, "READ")); + if (!file || !file->IsOpen()) { + LOGP(error, "could not open input file {}", fileName); + return; + } + + TTree* voxResTree = nullptr; + file->cd(); + gDirectory->GetObject("voxResTree", voxResTree); + if (!voxResTree) { + LOGP(error, "tree voxResTree does not exist!"); + return; + } + auto userInfo = voxResTree->GetUserInfo(); + if (!userInfo->FindObject("y2xBinning") || !userInfo->FindObject("z2xBinning")) { + LOGP(error, "'y2xBinning' or 'z2xBinning' not found in UserInfo, but required to get the correct binning"); + return; + } + + // Obtain configuration + const SpacePointsCalibConfParam& params = SpacePointsCalibConfParam::Instance(); + if (!std::filesystem::exists("scdconfig.ini")) { + LOGP(warning, "Did not find configuration file. Using default parameters and storing them in scdconfig.ini"); + params.writeINI("scdconfig.ini", "scdcalib"); // to write default parameters to a file + } else { + params.updateFromFile("scdconfig.ini"); + } + // TrackResiduals::setZ2XBinning() (called below) reads scdcalib.maxZ2X directly and uses it to scale + // the physical z/x bin boundaries -- baked into what each z2x voxel index in the input tree actually + // means, not a cosmetic knob. There is no scdconfig.ini on the GRID, so without this the code default + // (1.0) would silently apply instead of whatever stage 1 actually used (production 1.4), misaligning + // this macro's re-derived binning against the tree's real geometry. Must happen BEFORE setZ2XBinning() + // below. See staticMapCreatorCPM.C's UserInfo::Add("maxZ2X", ...) for where this comes from -- + // SmoothingExtrapolate.C clones the whole input UserInfo onto its own output, so it survives into this + // macro's input unchanged. + if (auto* maxZ2XObj = userInfo->FindObject("maxZ2X")) { + const std::string maxZ2XStr = maxZ2XObj->GetTitle(); + o2::conf::ConfigurableParam::setValue("scdcalib.maxZ2X", maxZ2XStr); + LOGP(info, "Set scdcalib.maxZ2X = {} from input UserInfo (matches stage 1)", maxZ2XStr); + } else { + LOGP(warning, + "'maxZ2X' not found in input UserInfo (older input file?) -- using scdcalib.maxZ2X = {} " + "(scdconfig.ini/code default), which may NOT match the value stage 1 actually used to " + "build this tree's z2x binning!", + params.maxZ2X); + } + + LOGP(info, "----- Dumping configuration values START -----"); + params.printKeyValues(); + LOGP(info, "----- Dumping configuration values END -----"); + + // required for the binning that was used + o2::tpc::TrackResiduals trackResiduals; + auto y2xBins = o2::RangeTokenizer::tokenize(userInfo->FindObject("y2xBinning")->GetTitle()); + const std::string z2xStr = userInfo->FindObject("z2xBinning")->GetTitle(); + auto z2xBins = o2::RangeTokenizer::tokenize(z2xStr); + LOGP(info, "z2xBins: {}", z2xStr); + trackResiduals.setY2XBinning(y2xBins); + trackResiduals.setZ2XBinning(z2xBins); + trackResiduals.init(); + + auto getFromUserInfo = [userInfo](std::string value, float& valueF) { + if (valueF != 0) { + LOGP(info, "{} set to {} via command line, not reading it from userInfo", value, valueF); + return; + } + + if (!userInfo || !userInfo->FindObject(value.data())) { + LOGP(error, "Could not find value for {} in userInfo", value); + valueF = 0.f; + return; + } + valueF = std::atof(userInfo->FindObject(value.data())->GetTitle()); + LOGP(info, "Found {} = {} in userInfo", value, valueF); + }; + + getFromUserInfo("meanIDC", meanIDC); + getFromUserInfo("meanCTP", meanCTP); + + if ((useCTPLumi != 2 && meanIDC == 0) || meanCTP == 0) { + LOGP(fatal, "meanCTP ({}) or meanIDC ({}) not set!", meanCTP, meanIDC); + } + if (useCTPLumi == 2) { + LOGP(warning, "Explicitly disabled IDCs!"); + } + + createFastTransform(outFileName, voxResTree, trackResiduals, useSmoothed, invertSigns, meanIDC, meanCTP, debug, useCTPLumi); +} + +void createFastTransform(std::string outFileName, TTree* voxResTree, o2::tpc::TrackResiduals& trackResiduals, bool useSmoothed, bool invertSigns, float meanIDC, float meanCTP, int debug, int useCTPLumi) +{ + LOGP(info, "create fast transformation ... "); + std::regex reg(".*FT_voxRes\\.residuals\\.([0-9]{6})_([0-9]{13})_([0-9]{13})_([0-9]+)_([0-9]+).*\\.root"); + std::smatch base_match; + int run = -1; + long validFrom = -1; + long validUntil = -1; + int firstTF = -1; + int lastTF = -1; + if (std::regex_match(outFileName, base_match, reg)) { + run = std::stoi(base_match[1].str()); + validFrom = std::stol(base_match[2].str()); + validUntil = std::stol(base_match[3].str()); + firstTF = std::stol(base_match[4].str()); + lastTF = std::stol(base_match[5].str()); + LOGP(info, "Found run {}, validFrom {}, validUntil {}, firstTF {}, lastTF {}", run, validFrom, validUntil, firstTF, lastTF); + } + + auto* helper = o2::tpc::TPCFastTransformHelperO2::instance(); + + o2::tpc::TPCFastSpaceChargeCorrectionHelper* corrHelper = o2::tpc::TPCFastSpaceChargeCorrectionHelper::instance(); + +#if __has_include("TPCFastTransformPOD.h") + TTree* voxResTreeInverse = nullptr; + o2::gpu::TPCFastSpaceChargeCorrectionMap mapDirect(0, 0), mapInverse(0, 0); + auto corrPtr = corrHelper->createFromTrackResiduals(trackResiduals, voxResTree, voxResTreeInverse, useSmoothed, invertSigns, &mapDirect, &mapInverse); +#else + auto corrPtr = corrHelper->createFromTrackResiduals(trackResiduals, voxResTree, useSmoothed, invertSigns); +#endif + + std::unique_ptr fastTransform(helper->create(0, *corrPtr)); + fastTransform->setLumi(meanCTP); + fastTransform->setIDC(meanIDC); // for SW version with IDC in FastTransfrom + + o2::gpu::TPCFastSpaceChargeCorrection& corr = fastTransform->getCorrection(); + + LOGP(info, "... create fast transformation completed"); + + if (!outFileName.empty()) { + fastTransform->writeToFile(outFileName.data(), "ccdb_object"); + } + + LOGP(info, "verify the results ..."); + + // the difference + + double maxDiff[3] = {0., 0., 0.}; + int maxDiffRoc[3] = {0, 0, 0}; + int maxDiffRow[3] = {0, 0, 0}; + + double sumDiff[3] = {0., 0., 0.}; + long nDiff = 0; + + // a debug file with some NTuples + + TDirectory* currDir = gDirectory; + + const std::filesystem::path pFileOutput(outFileName); + std::string outPath(pFileOutput.parent_path().c_str()); + if (outPath.empty()) { + outPath = "."; + } + const std::string fileOutputDebug = fmt::format("{}/{}.debug.root", outPath, pFileOutput.stem().c_str()); + const std::string fileOutputSummary = fmt::format("{}/{}.summary.root", outPath, pFileOutput.stem().c_str()); + + o2::utils::TreeStreamRedirector summary(fileOutputSummary.data(), "recreate"); + + TFile* debugFile = new TFile(fileOutputDebug.data(), "RECREATE"); + debugFile->cd(); + + // ntuple with the input data: voxel corrections + debugFile->cd(); + TNtuple* debugVox = new TNtuple("vox", "vox", "iRoc:iRow:y2xbin:z2xbin:x:y:z:vx:vy:vz:cx:cy:cz"); + + debugVox->SetMarkerStyle(8); + debugVox->SetMarkerSize(0.8); + debugVox->SetMarkerColor(kBlue); + + currDir->cd(); + + // check the difference in voxels and fill corresp. debug ntuple + + LOGP(info, "verify the results ..."); + + const o2::gpu::TPCFastTransformGeo& geo = helper->getGeometry(); + + o2::tpc::TrackResiduals::VoxRes* v = nullptr; + TBranch* branch = voxResTree->GetBranch("voxRes"); + branch->SetAddress(&v); + branch->SetAutoDelete(kTRUE); + + int nNaNdXV = 0; + int nNaNdYV = 0; + int nNaNdZV = 0; + int nNaNdXC = 0; + int nNaNdYC = 0; + int nNaNdZC = 0; + + int lastSector = -1; + + std::vector statsPerSecMean(36); + std::vector statsPerSecStdDev(36); + std::vector statsPerSecMedian(36); + + std::vector maxDiffPerSec[3]; + + std::vector deviationPerSecLTM95[3]; + std::vector deviationPerSecMedian[3]; + + std::vector entriesStats; + std::vector deviations[3]; + entriesStats.reserve(152 * trackResiduals.getNY2XBins() * trackResiduals.getNZ2XBins()); + for (int i = 0; i < 3; ++i) { + deviations[i].reserve(152 * trackResiduals.getNY2XBins() * trackResiduals.getNY2XBins()); + maxDiffPerSec[i].resize(36); + deviationPerSecLTM95[i].resize(36); + deviationPerSecMedian[i].resize(36); + } + + // retrieve infos from UserInfo + auto getFromUserInfo = [](TList* u, const char* name, int defVal = -1) { + const auto o = u->FindObject(name); + return o ? std::atoi(o->GetTitle()) : defVal; + }; + + auto userInfo = voxResTree->GetUserInfo(); + const int nSlicesPhiZ = getFromUserInfo(userInfo, "nSlicesPhiZ"); + const int maxTracks = getFromUserInfo(userInfo, "maxTracks"); + const int minTracks = getFromUserInfo(userInfo, "minTracks"); + const int nTracksProcessed = getFromUserInfo(userInfo, "nTracksProcessed"); + const bool badCalib = static_cast(getFromUserInfo(userInfo, "badCalib", 0)); + + for (int iVox = 0; iVox < voxResTree->GetEntriesFast(); iVox++) { + + voxResTree->GetEntry(iVox); + + const float voxEntries = v->stat[o2::tpc::TrackResiduals::VoxV]; + const int xBin = v->bvox[o2::tpc::TrackResiduals::VoxX]; // bin number in x (= pad row) + const int y2xBin = v->bvox[o2::tpc::TrackResiduals::VoxF]; // bin number in y/x 0..14 + const int z2xBin = v->bvox[o2::tpc::TrackResiduals::VoxZ]; // bin number in z/x 0..4 + const int iRoc = (int)v->bsec; + const int iRow = (int)xBin; + + const float x = trackResiduals.getX(xBin); // radius of the pad row + const float y2x = trackResiduals.getY2X(xBin, y2xBin); // y/x coordinate of the bin ~-0.15 ... 0.15 + const float z2x = trackResiduals.getZ2X(z2xBin); // z/x coordinate of the bin 0.1 .. 0.9 + const float y = x * y2x; +#if __has_include("TPCFastTransformPOD.h") + const float z = x * z2x * ((iRoc >= geo.getNumberOfSectorsA()) ? -1.f : 1.f); +#else + const float z = x * z2x * ((iRoc >= geo.getNumberOfSlicesA()) ? -1.f : 1.f); +#endif + + float correctionX = useSmoothed ? v->DS[o2::tpc::TrackResiduals::ResX] : v->D[o2::tpc::TrackResiduals::ResX]; + float correctionY = useSmoothed ? v->DS[o2::tpc::TrackResiduals::ResY] : v->D[o2::tpc::TrackResiduals::ResY]; + float correctionZ = useSmoothed ? v->DS[o2::tpc::TrackResiduals::ResZ] : v->D[o2::tpc::TrackResiduals::ResZ]; + + if (invertSigns) { + correctionX *= -1.; + correctionY *= -1.; + correctionZ *= -1.; + } + + entriesStats.emplace_back(voxEntries); + statsPerSecMean[iRoc] += voxEntries; + statsPerSecStdDev[iRoc] += voxEntries * voxEntries; + + nNaNdXV += TMath::IsNaN(correctionX); + nNaNdYV += TMath::IsNaN(correctionY); + nNaNdZV += TMath::IsNaN(correctionZ); + +#if __has_include("TPCFastTransformPOD.h") + float cx, cy, cz; + corr.getCorrectionLocal(iRoc, iRow, y, z, cx, cy, cz); +#else + float u, v, cx, cu, cv, cy, cz; + geo.convLocalToUV(iRoc, y, z, u, v); + corr.getCorrection(iRoc, iRow, u, v, cx, cu, cv); + geo.convUVtoLocal(iRoc, u + cu, v + cv, cy, cz); + cy -= y; + cz -= z; +#endif + + nNaNdXC += TMath::IsNaN(cx); + nNaNdYC += TMath::IsNaN(cy); + nNaNdZC += TMath::IsNaN(cz); + + const float d[3] = {cx - correctionX, cy - correctionY, cz - correctionZ}; + for (int i = 0; i < 3; i++) { + const float dAbs = std::abs(d[i]); + maxDiffPerSec[i][iRoc] = std::max(maxDiffPerSec[i][iRoc], dAbs); + deviations[i].emplace_back(dAbs); + if (std::abs(maxDiff[i]) < dAbs) { + maxDiff[i] = d[i]; + maxDiffRoc[i] = iRoc; + maxDiffRow[i] = iRow; + LOGP(info, "roc {} row {} xyz {} diff {}", iRoc, iRow, i, d[i]); + } + sumDiff[i] += d[i] * d[i]; + } + nDiff++; + + debugVox->Fill(iRoc, iRow, y2xBin, z2xBin, x, y, z, correctionX, correctionY, correctionZ, cx, cy, cz); + + if (lastSector > -1 && lastSector != iRoc) { + if (entriesStats.size() > 0) { + statsPerSecMean[lastSector] /= entriesStats.size(); + statsPerSecStdDev[lastSector] /= entriesStats.size(); + statsPerSecStdDev[lastSector] = (std::sqrt(std::abs(statsPerSecStdDev[lastSector] - statsPerSecMean[lastSector] * statsPerSecMean[lastSector]))); + statsPerSecMedian[lastSector] = mu::median(entriesStats); + } + entriesStats.clear(); + + static std::vector indexDev; + indexDev.resize(deviations[0].size()); + for (int i = 0; i < 3; i++) { + std::array fitRes; + mu::LTMUnbinned(deviations[i], indexDev, fitRes, 0.95); + deviationPerSecLTM95[i][lastSector].mean = fitRes[1]; + deviationPerSecLTM95[i][lastSector].rms = fitRes[2]; + deviationPerSecLTM95[i][lastSector].meanErr = fitRes[3]; + deviationPerSecLTM95[i][lastSector].rmsErr = fitRes[4]; + deviationPerSecMedian[i][lastSector] = mu::median(deviations[i]); + deviations[i].clear(); + } + } + lastSector = iRoc; + } + // last sector + if (lastSector > -1 && entriesStats.size() > 0) { + statsPerSecMean[lastSector] /= entriesStats.size(); + statsPerSecStdDev[lastSector] /= entriesStats.size(); + statsPerSecStdDev[lastSector] /= std::sqrt(std::abs(statsPerSecStdDev[lastSector] - statsPerSecMean[lastSector] * statsPerSecMean[lastSector])); + statsPerSecMedian[lastSector] = mu::median(entriesStats); + entriesStats.clear(); + + std::vector indexDev; + indexDev.resize(deviations[0].size()); + for (int i = 0; i < 3; i++) { + std::array fitRes; + mu::LTMUnbinned(deviations[i], indexDev, fitRes, 0.95); + deviationPerSecLTM95[i][lastSector].mean = fitRes[1]; + deviationPerSecLTM95[i][lastSector].rms = fitRes[2]; + deviationPerSecLTM95[i][lastSector].meanErr = fitRes[3]; + deviationPerSecLTM95[i][lastSector].rmsErr = fitRes[4]; + deviationPerSecMedian[i][lastSector] = mu::median(deviations[i]); + deviations[i].clear(); + } + } + + const int nNaNV = nNaNdXV + nNaNdYV + nNaNdZV; + const int nNaNC = nNaNdXC + nNaNdYC + nNaNdZC; + const auto sNaNV = fmt::format("NaNV: {} {} {} {}", nNaNV, nNaNdXV, nNaNdYV, nNaNdZV); + const auto sNaNC = fmt::format("NaNC: {} {} {} {}", nNaNC, nNaNdXC, nNaNdYC, nNaNdZC); + + if (nNaNV > 0) { + LOGP(error, "{}", sNaNV); + } else { + LOGP(info, "{}", sNaNV); + } + if (nNaNC > 0) { + LOGP(error, "{}", sNaNC); + } else { + LOGP(info, "{}", sNaNC); + } + + summary << "summary" + << "file=" << outFileName + // + << "meanIDC=" << meanIDC + << "meanCTP=" << meanCTP + << "run=" << run + << "validFrom=" << validFrom + << "validUntil=" << validUntil + << "firstTF=" << firstTF + << "lastTF =" << lastTF + // + << "nNaNdXV=" << nNaNdXV + << "nNaNdYV=" << nNaNdYV + << "nNaNdZV=" << nNaNdZV + << "nNaNdXC=" << nNaNdXC + << "nNaNdYC=" << nNaNdYC + << "nNaNdZC=" << nNaNdZC + // + << "statsMean=" << statsPerSecMean + << "statsStdDev=" << statsPerSecStdDev + << "statsMedian=" << statsPerSecMedian + // + << "DdXLTM95=" << deviationPerSecLTM95[0] + << "DdYLTM95=" << deviationPerSecLTM95[1] + << "DdZLTM95=" << deviationPerSecLTM95[2] + << "DdXMedian=" << deviationPerSecMedian[0] + << "DdYMedian=" << deviationPerSecMedian[1] + << "DdZMedian=" << deviationPerSecMedian[2] + << "DdXMax=" << maxDiffPerSec[0] + << "DdYMax=" << maxDiffPerSec[1] + << "DdZMax=" << maxDiffPerSec[2] + // + << "nSlicesPhiZ=" << nSlicesPhiZ + << "maxTracks=" << maxTracks + << "minTracks=" << minTracks + << "nTracksProcessed=" << nTracksProcessed + << "badCalib=" << badCalib + << "\n"; + + summary.Close(); + +#if __has_include("TPCFastTransformPOD.h") + if (debug > 0) { + debugFile->cd(); + TNtuple* ntAll = new TNtuple("all", "all", "sec:row:x:y:z:cx:cy:cz:ix:iy:iz"); + ntAll->SetMarkerStyle(8); + ntAll->SetMarkerSize(0.1); + ntAll->SetMarkerColor(kBlack); + + debugFile->cd(); + TNtuple* ntGrid = new TNtuple("grid", "grid", "sec:row:x:y:z:cx:cy:cz:ix:iy:iz"); + ntGrid->SetMarkerStyle(8); + ntGrid->SetMarkerSize(1.2); + ntGrid->SetMarkerColor(kBlack); + + debugFile->cd(); + TNtuple* ntFitPoints = new TNtuple("fitpoints", "fit points", "sec:row:x:y:z:px:py:pz:cx:cy:cz"); + ntFitPoints->SetMarkerStyle(8); + ntFitPoints->SetMarkerSize(0.4); + ntFitPoints->SetMarkerColor(kRed); + + currDir->cd(); + + auto getInvCorrections = [&](int iSector, int iRow, float realY, float realZ, float& ix, float& iy, float& iz) { + ix = corr.getCorrectionXatRealYZ(iSector, iRow, realY, realZ); + corr.getCorrectionYZatRealYZ(iSector, iRow, realY, realZ, iy, iz); + }; + + auto getAllCorrections = [&](int iSector, int iRow, float y, float z, float& cx, float& cy, float& cz, float& ix, float& iy, float& iz) { + corr.getCorrectionLocal(iSector, iRow, y, z, cx, cy, cz); + getInvCorrections(iSector, iRow, y + cy, z + cz, ix, iy, iz); + }; + + LOGP(info, "create debug ntuples at spline grid points and high granular ..."); + + for (int32_t iSector = 0; iSector < geo.getNumberOfSectors(); iSector++) { + LOGP(info, "debug ntuples for sector {}", iSector); + + for (int32_t iRow = 0; iRow < geo.getNumberOfRows(); iRow++) { + + double x = geo.getRowInfo(iRow).x; + + const auto& gridY = corr.getSplineForRow(iRow).getGridX1(); + const auto& gridZ = corr.getSplineForRow(iRow).getGridX2(); + + { + std::vector points[2], knots[2]; + auto [yMin, yMax] = geo.getRowInfo(iRow).getYrange(); + auto [zMin, zMax] = geo.getZrange(iSector); + + for (int32_t iu = 0; iu < gridY.getNumberOfKnots(); iu++) { + float y, z; + corr.convGridToLocal(iSector, iRow, gridY.getKnot(iu).getU(), 0., y, z); + knots[0].push_back(y); + points[0].push_back(y); + } + for (int32_t iv = 0; iv < gridZ.getNumberOfKnots(); iv++) { + float y, z; + corr.convGridToLocal(iSector, iRow, 0., gridZ.getKnot(iv).getU(), y, z); + knots[1].push_back(z); + points[1].push_back(z); + } + + for (int32_t iyz = 0; iyz <= 1; iyz++) { + std::sort(knots[iyz].begin(), knots[iyz].end()); + std::sort(points[iyz].begin(), points[iyz].end()); + int32_t n = points[iyz].size(); + int nsteps = (iyz == 0) ? 10 : 5; + for (int32_t i = 0; i < n - 1; i++) { + double d = (points[iyz][i + 1] - points[iyz][i]) / nsteps; + for (int32_t ii = 1; ii < nsteps; ii++) { + points[iyz].push_back(points[iyz][i] + d * ii); + } + } + } + points[0].push_back(yMin); + points[0].push_back(yMax); + points[1].push_back(zMin); + points[1].push_back(zMax); + for (int32_t iyz = 0; iyz <= 1; iyz++) { + std::sort(points[iyz].begin(), points[iyz].end()); + } + + for (int32_t iter = 0; iter < 2; iter++) { + std::vector& py = ((iter == 0) ? knots[0] : points[0]); + std::vector& pz = ((iter == 0) ? knots[1] : points[1]); + for (uint32_t iu = 0; iu < py.size(); iu++) { + for (uint32_t iv = 0; iv < pz.size(); iv++) { + float y = py[iu]; + float z = pz[iv]; + float cx{0}, cy{0}, cz{0}, ix{0}, iy{0}, iz{0}; + getAllCorrections(iSector, iRow, y, z, cx, cy, cz, ix, iy, iz); + if (iter == 0) { + ntGrid->Fill(iSector, iRow, x, y, z, cx, cy, cz, ix, iy, iz); + } else { + ntAll->Fill(iSector, iRow, x, y, z, cx, cy, cz, ix, iy, iz); + } + } + } + } + } + + // the data points used in spline fit + auto& fitPoints = mapDirect.getPoints(iSector, iRow); + for (uint32_t ip = 0; ip < fitPoints.size(); ip++) { + auto point = fitPoints[ip]; + float y = point.mY; + float z = point.mZ; + float correctionX = point.mDx; + float correctionY = point.mDy; + float correctionZ = point.mDz; + float cx, cy, cz; + corr.getCorrectionLocal(iSector, iRow, y, z, cx, cy, cz); + ntFitPoints->Fill(iSector, iRow, x, y, z, correctionX, correctionY, correctionZ, cx, cy, cz); + } + } + } + + debugFile->cd(); + ntAll->Write(); + ntGrid->Write(); + ntFitPoints->Write(); + } +#else + if (debug > 0) { + // ntuple with spline grid points + debugFile->cd(); + // ntuple with created TPC corrections + TNtuple* debugCorr = new TNtuple("corr", "corr", "iRoc:iRow:x:y:z:cx:cy:cz"); + + debugCorr->SetMarkerStyle(8); + debugCorr->SetMarkerSize(0.1); + debugCorr->SetMarkerColor(kBlack); + + TNtuple* debugGrid = new TNtuple("grid", "grid", "iRoc:iRow:x:y:z:cx:cy:cz"); + + debugGrid->SetMarkerStyle(8); + debugGrid->SetMarkerSize(1.2); + debugGrid->SetMarkerColor(kBlack); + + // ntuple with data points created from voxels (with data smearing and + // extension to the edges) + TNtuple* debugPoints = new TNtuple("points", "points", "iRoc:iRow:x:y:z:px:py:pz:cx:cy:cz"); + + debugPoints->SetMarkerStyle(8); + debugPoints->SetMarkerSize(0.4); + debugPoints->SetMarkerColor(kRed); + + currDir->cd(); + + LOGP(info, "create debug ntuples at spline grid points and high granular ..."); + + for (int iRoc = 0; iRoc < geo.getNumberOfSlices(); iRoc++) { + LOGP(info, "debug ntuples for roc {}", iRoc); + for (int iRow = 0; iRow < geo.getNumberOfRows(); iRow++) { + + double x = geo.getRowInfo(iRow).x; + + // the correction + + for (double su = 0.; su <= 1.0001; su += 0.01) { + for (double sv = 0.; sv <= 1.0001; sv += 0.1) { + float u, v; + geo.convScaledUVtoUV(iRoc, iRow, su, sv, u, v); + float y, z; + geo.convUVtoLocal(iRoc, u, v, y, z); + float cx, cu, cv; + corr.getCorrection(iRoc, iRow, u, v, cx, cu, cv); + float cy, cz; + geo.convUVtoLocal(iRoc, u + cu, v + cv, cy, cz); + cy -= y; + cz -= z; + debugCorr->Fill(iRoc, iRow, x, y, z, cx, cy, cz); + } + } + + // the spline grid + + const auto& gridU = corr.getSpline(iRoc, iRow).getGridX1(); + const auto& gridV = corr.getSpline(iRoc, iRow).getGridX2(); + for (int iu = 0; iu < gridU.getNumberOfKnots(); iu++) { + // double su = gridU.convUtoX(gridU.getKnot(iu).getU()); + for (int iv = 0; iv < gridV.getNumberOfKnots(); iv++) { + // double sv = gridV.convUtoX(gridV.getKnot(iv).getU()); + float u, v; + corr.convGridToUV(iRoc, iRow, iu, iv, u, v); + float y, z; + geo.convUVtoLocal(iRoc, u, v, y, z); + float cx, cu, cv; + corr.getCorrection(iRoc, iRow, u, v, cx, cu, cv); + float cy, cz; + geo.convUVtoLocal(iRoc, u + cu, v + cv, cy, cz); + cy -= y; + cz -= z; + debugGrid->Fill(iRoc, iRow, x, y, z, cx, cy, cz); + } + } + + // the data points used in spline fit + // (they are kept in + // TPCFastTransformHelperO2::instance()->getCorrectionMap() ) + + o2::gpu::TPCFastSpaceChargeCorrectionMap& map = corrHelper->getCorrectionMap(); + auto& points = map.getPoints(iRoc, iRow); + + for (unsigned int ip = 0; ip < points.size(); ip++) { + auto point = points[ip]; + float y = point.mY; + float z = point.mZ; + float correctionX = point.mDx; + float correctionY = point.mDy; + float correctionZ = point.mDz; + + float u, v, cx, cu, cv, cy, cz; + geo.convLocalToUV(iRoc, y, z, u, v); + corr.getCorrection(iRoc, iRow, u, v, cx, cu, cv); + geo.convUVtoLocal(iRoc, u + cu, v + cv, cy, cz); + cy -= y; + cz -= z; + + debugPoints->Fill(iRoc, iRow, x, y, z, correctionX, correctionY, correctionZ, cx, cy, cz); + } + } + } + + debugFile->cd(); + debugCorr->Write(); + debugGrid->Write(); + debugPoints->Write(); + } +#endif + + for (int i = 0; i < 3; i++) { + sumDiff[i] = sqrt(sumDiff[i]) / nDiff; + } + + LOGP(info, "Max difference in x : {} at ROC {} row {}", maxDiff[0], maxDiffRoc[0], maxDiffRow[0]); + LOGP(info, "Max difference in y : {} at ROC {} row {}", maxDiff[1], maxDiffRoc[1], maxDiffRow[1]); + LOGP(info, "Max difference in z : {} at ROC {} row {}", maxDiff[2], maxDiffRoc[2], maxDiffRow[2]); + LOGP(info, "Mean difference in x,y,z : {} {} {}", sumDiff[0], sumDiff[1], sumDiff[2]); + + corr.testInverse(0); + + debugFile->cd(); + debugVox->Write(); + debugFile->Close(); +} From c226ccb01453a166e6269c106716498f3fe204b5 Mon Sep 17 00:00:00 2001 From: shahor02 Date: Tue, 28 Jul 2026 21:10:47 +0400 Subject: [PATCH 46/47] Use fastAtan2 with protection against radial tracks (#15642) --- Detectors/Base/include/DetectorsBase/MatLayerCyl.h | 2 +- Detectors/Base/include/DetectorsBase/Ray.h | 2 +- Detectors/Base/src/MatLayerCylSet.cxx | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Detectors/Base/include/DetectorsBase/MatLayerCyl.h b/Detectors/Base/include/DetectorsBase/MatLayerCyl.h index e63de51e0a6ca..439bd12e7f673 100644 --- a/Detectors/Base/include/DetectorsBase/MatLayerCyl.h +++ b/Detectors/Base/include/DetectorsBase/MatLayerCyl.h @@ -107,7 +107,7 @@ class MatLayerCyl : public o2::gpu::FlatObject GPUd() int getZBinID(float z) const { int idz = int((z - getZMin()) * getDZInv()); // cannot be negative since before isZOutside is applied - return idz < getNZBins() ? idz : getNZBins() - 1; + return idz < getNZBins() ? (idz > 0 ? idz : 0) : getNZBins() - 1; } // lower boundary of Z slice diff --git a/Detectors/Base/include/DetectorsBase/Ray.h b/Detectors/Base/include/DetectorsBase/Ray.h index a72208c41af0d..0b0c2f2904d27 100644 --- a/Detectors/Base/include/DetectorsBase/Ray.h +++ b/Detectors/Base/include/DetectorsBase/Ray.h @@ -79,7 +79,7 @@ class Ray GPUd() float getPhi(float t) const { - float p = o2::gpu::CAMath::ATan2(mP[1] + t * mD[1], mP[0] + t * mD[0]); + float p = o2::math_utils::fastATan2(mP[1] + t * mD[1], mP[0] + t * mD[0]); // instead of float p = o2::gpu::CAMath::ATan2(mP[1] + t * mD[1], mP[0] + t * mD[0]); o2::math_utils::bringTo02Pi(p); return p; } diff --git a/Detectors/Base/src/MatLayerCylSet.cxx b/Detectors/Base/src/MatLayerCylSet.cxx index c390c8d617326..9d1f0f298b587 100644 --- a/Detectors/Base/src/MatLayerCylSet.cxx +++ b/Detectors/Base/src/MatLayerCylSet.cxx @@ -348,6 +348,12 @@ GPUd() MatBudget MatLayerCylSet::getMatBudget(float x0, float y0, float z0, floa if (tEndPhi == Ray::InvalidT) { break; // ray parallel to radial line, abandon check for phi bin change } + const auto tMarginPhi = 1.e-6f + 1.e-5f * (cross1 - cross2); + // if (!(tEndPhi >= cross2 - tMarginPhi) | !(tEndPhi <= cross1 + tMarginPhi)) { // use non-short-circuit | to reject eventual NANs + if (tEndPhi < cross2 - tMarginPhi || tEndPhi > cross1 + tMarginPhi) { + tEndPhi = cross2; + checkMorePhi = false; + } } auto zID = lr.getZBinID(ray.getZ(tStartPhi)); auto zIDLast = lr.getZBinID(ray.getZ(tEndPhi)); From 792c0d094236d4beb620de00f3ed9f39398794b8 Mon Sep 17 00:00:00 2001 From: Tristan Wenzel Date: Tue, 28 Jul 2026 09:59:50 +0200 Subject: [PATCH 47/47] Multi-threaded material budget LUT creation Introduces multi-threaded creation of the material budget lookup table, reducing it from hours to minutes. Creating the LUT walks every cell of every layer through TGeo and the cells are independent, so they are spread over TBB tasks with one TGeoNavigator per thread. The innermost 20 layers at 60 trials/cell drop from 28 min to 72 s on 28 cores. Effective only with ROOT >= v6-36-10-alice3, which removes a per-query thread-id lookup and the false sharing between per-thread scratch buffers of TGeo shapes. On older ROOT the parallel path is correct, just slower -- it saturates near 12x. All layers map onto a single flat cell index so the load stays balanced despite very different cell counts per layer; a binary search maps a flat index back to (layer, iz, iphi). The worker navigators are given back at the end. meanMaterialBudget() takes an optional navigator: a caller passing its own runs lock-free, a caller passing none shares gGeoManager's and still takes the mutex. Deciding from the argument keeps it local, so process-global state cannot break it. Thread count comes from the new populateFromTGeo() argument, falling back to NTHREADS_MATBUD; the default is the previous serial path. Results are independent of the thread count -- compareMatBudLUT.C checks a parallel LUT against a serial one cell by cell, and they match exactly over all 129523 cells. Supervised-by: Sandro Wenzel --- Detectors/Base/CMakeLists.txt | 5 + .../include/DetectorsBase/GeometryManager.h | 15 ++- .../Base/include/DetectorsBase/MatLayerCyl.h | 4 +- .../include/DetectorsBase/MatLayerCylSet.h | 5 +- Detectors/Base/src/GeometryManager.cxx | 32 ++++-- Detectors/Base/src/MatLayerCyl.cxx | 4 +- Detectors/Base/src/MatLayerCylSet.cxx | 108 +++++++++++++++++- Detectors/Base/test/README.md | 10 ++ Detectors/Base/test/buildMatBudLUT.C | 10 +- Detectors/Base/test/compareMatBudLUT.C | 93 +++++++++++++++ 10 files changed, 260 insertions(+), 26 deletions(-) create mode 100644 Detectors/Base/test/compareMatBudLUT.C diff --git a/Detectors/Base/CMakeLists.txt b/Detectors/Base/CMakeLists.txt index 83a9193274e4f..74d2d02a9246c 100644 --- a/Detectors/Base/CMakeLists.txt +++ b/Detectors/Base/CMakeLists.txt @@ -92,6 +92,7 @@ if(BUILD_SIMULATION) endif() install(FILES test/buildMatBudLUT.C + test/compareMatBudLUT.C test/extractLUTLayers.C test/rescaleLUT.C DESTINATION share/macro/) @@ -100,6 +101,10 @@ o2_add_test_root_macro(test/buildMatBudLUT.C PUBLIC_LINK_LIBRARIES O2::DetectorsBase LABELS detectorsbase) +o2_add_test_root_macro(test/compareMatBudLUT.C + PUBLIC_LINK_LIBRARIES O2::DetectorsBase + LABELS detectorsbase) + o2_add_test_root_macro(test/extractLUTLayers.C PUBLIC_LINK_LIBRARIES O2::DetectorsBase LABELS detectorsbase) diff --git a/Detectors/Base/include/DetectorsBase/GeometryManager.h b/Detectors/Base/include/DetectorsBase/GeometryManager.h index ad1d77b10f49a..5e296a4045984 100644 --- a/Detectors/Base/include/DetectorsBase/GeometryManager.h +++ b/Detectors/Base/include/DetectorsBase/GeometryManager.h @@ -29,6 +29,7 @@ #include class TGeoHMatrix; // lines 11-11 class TGeoManager; // lines 9-9 +class TGeoNavigator; namespace o2 { @@ -96,14 +97,18 @@ class GeometryManager : public TObject ClassDefNV(MatBudgetExt, 1); }; - static o2::base::MatBudget meanMaterialBudget(float x0, float y0, float z0, float x1, float y1, float z1); - static o2::base::MatBudget meanMaterialBudget(const math_utils::Point3D& start, const math_utils::Point3D& end) + /// Mean material budget between two points. Pass a navigator owned by the calling thread to + /// run lock-free from several threads; with nav = nullptr the shared navigator is used under + /// a mutex, as before. + static o2::base::MatBudget meanMaterialBudget(float x0, float y0, float z0, float x1, float y1, float z1, + TGeoNavigator* nav = nullptr); + static o2::base::MatBudget meanMaterialBudget(const math_utils::Point3D& start, const math_utils::Point3D& end, TGeoNavigator* nav = nullptr) { - return meanMaterialBudget(start.X(), start.Y(), start.Z(), end.X(), end.Y(), end.Z()); + return meanMaterialBudget(start.X(), start.Y(), start.Z(), end.X(), end.Y(), end.Z(), nav); } - static o2::base::MatBudget meanMaterialBudget(const math_utils::Point3D& start, const math_utils::Point3D& end) + static o2::base::MatBudget meanMaterialBudget(const math_utils::Point3D& start, const math_utils::Point3D& end, TGeoNavigator* nav = nullptr) { - return meanMaterialBudget(start.X(), start.Y(), start.Z(), end.X(), end.Y(), end.Z()); + return meanMaterialBudget(start.X(), start.Y(), start.Z(), end.X(), end.Y(), end.Z(), nav); } static MatBudgetExt meanMaterialBudgetExt(float x0, float y0, float z0, float x1, float y1, float z1); diff --git a/Detectors/Base/include/DetectorsBase/MatLayerCyl.h b/Detectors/Base/include/DetectorsBase/MatLayerCyl.h index 439bd12e7f673..26de279468477 100644 --- a/Detectors/Base/include/DetectorsBase/MatLayerCyl.h +++ b/Detectors/Base/include/DetectorsBase/MatLayerCyl.h @@ -25,6 +25,8 @@ #include "GPUCommonMath.h" #include "DetectorsBase/MatCell.h" +class TGeoNavigator; + namespace o2 { namespace base @@ -66,7 +68,7 @@ class MatLayerCyl : public o2::gpu::FlatObject void initSegmentation(float rMin, float rMax, float zHalfSpan, int nz, int nphi); void initSegmentation(float rMin, float rMax, float zHalfSpan, float dzMin, float drphiMin); void populateFromTGeo(int ntrPerCell = 10); - void populateFromTGeo(int ip, int iz, int ntrPerCell); + void populateFromTGeo(int ip, int iz, int ntrPerCell, TGeoNavigator* nav = nullptr); void print(bool data = false) const; #endif // !GPUCA_ALIGPUCODE diff --git a/Detectors/Base/include/DetectorsBase/MatLayerCylSet.h b/Detectors/Base/include/DetectorsBase/MatLayerCylSet.h index cba6e5cebcfc8..4408261dc740a 100644 --- a/Detectors/Base/include/DetectorsBase/MatLayerCylSet.h +++ b/Detectors/Base/include/DetectorsBase/MatLayerCylSet.h @@ -73,7 +73,10 @@ class MatLayerCylSet : public o2::gpu::FlatObject #ifndef GPUCA_ALIGPUCODE // this part is unvisible on GPU version void print(bool data = false) const; void addLayer(float rmin, float rmax, float zmax, float dz, float drphi); - void populateFromTGeo(int ntrPerCel = 10); + /// Populate the LUT from TGeo. nThreads > 1 fills the cells in parallel (one TGeoNavigator + /// per thread); nThreads < 0 takes the count from the NTHREADS_MATBUD environment variable. + void populateFromTGeo(int ntrPerCel = 10, int nThreads = -1); + static int getNThreadsFromEnv(); void optimizePhiSlices(float maxRelDiff = 0.05); void dumpToTree(const std::string& outName = "matbudTree.root") const; diff --git a/Detectors/Base/src/GeometryManager.cxx b/Detectors/Base/src/GeometryManager.cxx index a067767752a69..8aaf902aa95c5 100644 --- a/Detectors/Base/src/GeometryManager.cxx +++ b/Detectors/Base/src/GeometryManager.cxx @@ -16,6 +16,7 @@ #include // for TIter #include #include // for TGeoHMatrix +#include // for TGeoNavigator #include // for TGeoNode #include // for TGeoPhysicalNode, TGeoPNEntry #include @@ -398,7 +399,8 @@ GeometryManager::MatBudgetExt GeometryManager::meanMaterialBudgetExt(float x0, f } //_____________________________________________________________________________________ -o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, float z0, float x1, float y1, float z1) +o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, float z0, float x1, float y1, float z1, + TGeoNavigator* nav) { // // Calculate mean material budget and material properties between @@ -414,6 +416,8 @@ o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, floa // // Ported to O2: ruben.shahoyan@cern.ch // + // Multi-threaded execution: pass a navigator owned by the calling thread. + // double length, startD[3] = {x0, y0, z0}; double dir[3] = {x1 - x0, y1 - y0, z1 - z0}; @@ -425,9 +429,17 @@ o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, floa for (int i = 3; i--;) { dir[i] *= invlen; } - std::lock_guard guard(sTGMutex); + // A caller that passes its own navigator owns it exclusively, so no locking is needed. A caller + // that passes none shares gGeoManager's current navigator and must still serialize. Deciding + // this from the argument keeps the choice local: it does not depend on -- and cannot be broken + // by -- process-global state such as TGeoManager::GetMaxThreads(). + std::unique_lock guard(sTGMutex, std::defer_lock); + if (!nav) { + guard.lock(); + nav = gGeoManager->GetCurrentNavigator(); + } // Initialize start point and direction - TGeoNode* currentnode = gGeoManager->InitTrack(startD, dir); + TGeoNode* currentnode = nav->InitTrack(startD, dir); if (!currentnode) { LOG(error) << "start point out of geometry: " << x0 << ':' << y0 << ':' << z0; return o2::base::MatBudget(); // return empty struct @@ -439,11 +451,11 @@ o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, floa // Locate next boundary within length without computing safety. // Propagate either with length (if no boundary found) or just cross boundary - gGeoManager->FindNextBoundaryAndStep(length, kFALSE); + nav->FindNextBoundaryAndStep(length, kFALSE); Double_t stepTot = 0.0; // Step made - Double_t step = gGeoManager->GetStep(); + Double_t step = nav->GetStep(); // If no boundary within proposed length, return current step data - if (!gGeoManager->IsOnBoundary()) { + if (!nav->IsOnBoundary()) { budStep.meanX2X0 = budStep.length / budStep.meanX2X0; return o2::base::MatBudget(budStep); } @@ -458,7 +470,7 @@ o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, floa if (nzero > 3) { // This means navigation has problems on one boundary // Try to cross by making a small step - const double* curPos = gGeoManager->GetCurrentPoint(); + const double* curPos = nav->GetCurrentPoint(); LOG(warning) << "Cannot cross boundary at (" << curPos[0] << ',' << curPos[1] << ',' << curPos[2] << ')'; budTotal.meanRho /= stepTot; budTotal.length = stepTot; @@ -472,14 +484,14 @@ o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, floa if (step >= length) { break; } - currentnode = gGeoManager->GetCurrentNode(); + currentnode = nav->GetCurrentNode(); if (!currentnode) { break; } length -= step; accountMaterial(currentnode->GetVolume()->GetMedium()->GetMaterial(), budStep); - gGeoManager->FindNextBoundaryAndStep(length, kFALSE); - step = gGeoManager->GetStep(); + nav->FindNextBoundaryAndStep(length, kFALSE); + step = nav->GetStep(); } budTotal.meanRho /= stepTot; budTotal.length = stepTot; diff --git a/Detectors/Base/src/MatLayerCyl.cxx b/Detectors/Base/src/MatLayerCyl.cxx index 2efe60235b895..dee179a84ca06 100644 --- a/Detectors/Base/src/MatLayerCyl.cxx +++ b/Detectors/Base/src/MatLayerCyl.cxx @@ -123,7 +123,7 @@ void MatLayerCyl::populateFromTGeo(int ntrPerCell) } //________________________________________________________________________________ -void MatLayerCyl::populateFromTGeo(int ip, int iz, int ntrPerCell) +void MatLayerCyl::populateFromTGeo(int ip, int iz, int ntrPerCell, TGeoNavigator* nav) { /// populate cell with info extracted from TGeometry, using ntrPerCell test tracks per cell @@ -136,7 +136,7 @@ void MatLayerCyl::populateFromTGeo(int ip, int iz, int ntrPerCell) float dzt = zs > 0.f ? 0.25 * dz : -0.25 * dz; // to avoid 90 degree polar angle for (int isp = ntrPerCell; isp--;) { o2::math_utils::sincos(phmn + (isp + 0.5) * getDPhi() / ntrPerCell, sn, cs); - auto bud = o2::base::GeometryManager::meanMaterialBudget(rMin * cs, rMin * sn, zs - dzt, rMax * cs, rMax * sn, zs + dzt); + auto bud = o2::base::GeometryManager::meanMaterialBudget(rMin * cs, rMin * sn, zs - dzt, rMax * cs, rMax * sn, zs + dzt, nav); if (bud.length > 0.) { meanRho += bud.length * bud.meanRho; meanX2X0 += bud.meanX2X0; // we store actually not X2X0 but 1./X0 diff --git a/Detectors/Base/src/MatLayerCylSet.cxx b/Detectors/Base/src/MatLayerCylSet.cxx index 9d1f0f298b587..b85e3042cc379 100644 --- a/Detectors/Base/src/MatLayerCylSet.cxx +++ b/Detectors/Base/src/MatLayerCylSet.cxx @@ -17,7 +17,16 @@ #ifndef GPUCA_ALIGPUCODE // this part is unvisible on GPU version #include "GPUCommonLogger.h" #include +#include #include "CommonUtils/TreeStreamRedirector.h" +#include +#include +#include +#include +#include +#include +#include +#include //#define _DBG_LOC_ // for local debugging only #endif // !GPUCA_ALIGPUCODE @@ -69,9 +78,26 @@ void MatLayerCylSet::addLayer(float rmin, float rmax, float zmax, float dz, floa } //________________________________________________________________________________ -void MatLayerCylSet::populateFromTGeo(int ntrPerCell) +int MatLayerCylSet::getNThreadsFromEnv() { - ///< populate layers, using ntrPerCell test tracks per cell + ///< number of threads requested via NTHREADS_MATBUD, or 1 if unset/invalid + const char* env = std::getenv("NTHREADS_MATBUD"); + if (!env) { + return 1; + } + int n = std::atoi(env); + if (n < 1) { + LOG(warning) << "Ignoring invalid NTHREADS_MATBUD=" << env; + return 1; + } + return n; +} + +//________________________________________________________________________________ +void MatLayerCylSet::populateFromTGeo(int ntrPerCell, int nThreads) +{ + ///< populate layers, using ntrPerCell test tracks per cell. + ///< nThreads < 0 takes the number of threads from the NTHREADS_MATBUD environment variable. assert(mConstructionMask == InProgress); int nlr = getNLayers(); @@ -83,12 +109,86 @@ void MatLayerCylSet::populateFromTGeo(int ntrPerCell) LOG(error) << "The LUT is already populated"; return; } + + if (nThreads < 0) { + nThreads = getNThreadsFromEnv(); + } + + using Clock = std::chrono::steady_clock; + auto seconds = [](Clock::time_point a, Clock::time_point b) { + return std::chrono::duration(b - a).count(); + }; + + if (nThreads <= 1) { + for (int i = 0; i < nlr; i++) { + LOG(info) << "Populating with " << ntrPerCell << " trials Lr " << i; + get()->mLayers[i].print(); + } + const auto tFillStart = Clock::now(); + for (int i = 0; i < nlr; i++) { + get()->mLayers[i].populateFromTGeo(ntrPerCell); + } + const auto tFillEnd = Clock::now(); + finalizeStructures(); + LOG(info) << "LUT fill: 1 thread, cells " << seconds(tFillStart, tFillEnd) << " s"; + return; + } + + // Cells of all layers form one flat index range so that the load is balanced across + // threads even though layers differ a lot in cell count. layerOffsets[i] is the first + // flat index of layer i; a binary search maps a flat index back to (layer, iz, iphi). + std::vector layerOffsets(nlr + 1, 0); for (int i = 0; i < nlr; i++) { - printf("Populating with %d trials Lr %3d ", ntrPerCell, i); + LOG(info) << "Queuing " << ntrPerCell << " trials Lr " << i; get()->mLayers[i].print(); - get()->mLayers[i].populateFromTGeo(ntrPerCell); + const auto& lr = get()->mLayers[i]; + layerOffsets[i + 1] = layerOffsets[i] + size_t(lr.getNZBins()) * lr.getNPhiBins(); } + const size_t totalCells = layerOffsets[nlr]; + + const auto tSetupStart = Clock::now(); + + // TGeo has to be told that several threads will navigate it, and each thread needs its own + // navigator. SetMaxThreads() is one-way -- TGeoManager has no API to return to + // single-threaded mode -- so we do not pretend to restore it; that is harmless because + // meanMaterialBudget() decides whether to lock from its own argument, not from this global. + // The navigators we book are ours, though, so those we do give back. + gGeoManager->SetMaxThreads(nThreads); + + tbb::enumerable_thread_specific threadNavigators( + []() { return gGeoManager->AddNavigator(); }); + + const auto tFillStart = Clock::now(); + { + tbb::global_control threadControl(tbb::global_control::max_allowed_parallelism, nThreads); + tbb::parallel_for(tbb::blocked_range(0, totalCells), + [this, ntrPerCell, &layerOffsets, &threadNavigators](const tbb::blocked_range& range) { + TGeoNavigator* nav = threadNavigators.local(); + for (size_t idx = range.begin(); idx != range.end(); ++idx) { + auto it = std::upper_bound(layerOffsets.begin(), layerOffsets.end(), idx); + const int layerIdx = int(std::distance(layerOffsets.begin(), it)) - 1; + const size_t cellInLayer = idx - layerOffsets[layerIdx]; + auto& layer = this->get()->mLayers[layerIdx]; + const int nphi = layer.getNPhiBins(); + layer.populateFromTGeo(int(cellInLayer % nphi), int(cellInLayer / nphi), ntrPerCell, nav); + } + }); + } + + const auto tFillEnd = Clock::now(); + + for (TGeoNavigator* nav : threadNavigators) { + gGeoManager->RemoveNavigator(nav); + } + finalizeStructures(); + const auto tEnd = Clock::now(); + + // Reported separately because only the middle term scales: the setup walks every volume + // in the geometry (TGeoManager::SetMaxThreads) and the teardown is serial by nature. + LOG(info) << "LUT fill: " << nThreads << " threads, setup " << seconds(tSetupStart, tFillStart) + << " s, cells " << seconds(tFillStart, tFillEnd) + << " s, finalize " << seconds(tFillEnd, tEnd) << " s"; } //________________________________________________________________________________ diff --git a/Detectors/Base/test/README.md b/Detectors/Base/test/README.md index f5f9fd4c04b29..97e8c7f569fd1 100644 --- a/Detectors/Base/test/README.md +++ b/Detectors/Base/test/README.md @@ -13,6 +13,16 @@ root -b -q O2/Detectors/Base/test/buildMatBudLUT.C+ The generation is quite time consuming (may take ~30 min). +It can be filled in parallel, one `TGeoNavigator` per thread, by passing a thread count as the +5th argument of `buildMatBudLUT` or by setting the environment variable: +``` +export NTHREADS_MATBUD=16 +``` +The result does not depend on the number of threads. Scaling beyond a few threads needs +ROOT >= v6-36-10-alice3, which removes the per-query thread-id lookup and the false sharing +between the per-thread scratch buffers of TGeo shapes; with older ROOT the parallel path is +still correct, just slower. + The optimized LUT will be stored in the matbud.root file. Load it as: diff --git a/Detectors/Base/test/buildMatBudLUT.C b/Detectors/Base/test/buildMatBudLUT.C index 860fcbd5da940..44019198685f8 100644 --- a/Detectors/Base/test/buildMatBudLUT.C +++ b/Detectors/Base/test/buildMatBudLUT.C @@ -27,7 +27,10 @@ o2::base::MatLayerCylSet mbLUT; bool testMBLUT(const std::string& lutFile = "matbud.root"); -bool buildMatBudLUT(int nTst = 60, int maxLr = -1, const std::string& outFile = "matbud.root", const std::string& geomName = "o2sim_geometry-aligned.root"); +/// Build the material budget LUT. nThreads < 0 takes the thread count from NTHREADS_MATBUD. +bool buildMatBudLUT(int nTst = 60, int maxLr = -1, const std::string& outFile = "matbud.root", + const std::string& geomNamePrefix = "o2sim", const std::string& opts = "", + int nThreads = -1); struct LrData { float rMin = 0.f; @@ -42,7 +45,8 @@ struct LrData { std::vector lrData; void configLayers(); -bool buildMatBudLUT(int nTst, int maxLr, const std::string& outFile, const std::string& geomNamePrefix, const std::string& opts) +bool buildMatBudLUT(int nTst, int maxLr, const std::string& outFile, const std::string& geomNamePrefix, + const std::string& opts, int nThreads) { auto geomName = o2::base::NameConf::getGeomFileName(geomNamePrefix); if (gSystem->AccessPathName(geomName.c_str())) { // if needed, create geometry @@ -67,7 +71,7 @@ bool buildMatBudLUT(int nTst, int maxLr, const std::string& outFile, const std:: } TStopwatch sw; - mbLUT.populateFromTGeo(nTst); + mbLUT.populateFromTGeo(nTst, nThreads); mbLUT.optimizePhiSlices(); // move to populateFromTGeo mbLUT.flatten(); // move to populateFromTGeo diff --git a/Detectors/Base/test/compareMatBudLUT.C b/Detectors/Base/test/compareMatBudLUT.C new file mode 100644 index 0000000000000..7abaa9bd7182a --- /dev/null +++ b/Detectors/Base/test/compareMatBudLUT.C @@ -0,0 +1,93 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// \file compareMatBudLUT.C +/// \brief Compare two material budget LUTs cell by cell +/// +/// Used to check that filling the LUT in parallel gives the same result as filling it serially: +/// +/// root -b -q 'compareMatBudLUT.C("matbud_serial.root","matbud_parallel.root")' + +#if !defined(__CLING__) || defined(__ROOTCLING__) +#include "DetectorsBase/MatLayerCylSet.h" +#include "GPUCommonLogger.h" +#include +#include +#endif + +/// Returns true if the two LUTs agree everywhere within tol (relative). +bool compareMatBudLUT(const std::string& fileA = "matbud_serial.root", + const std::string& fileB = "matbud_parallel.root", + float tol = 0.f) +{ + auto* lutA = o2::base::MatLayerCylSet::loadFromFile(fileA); + auto* lutB = o2::base::MatLayerCylSet::loadFromFile(fileB); + if (!lutA) { + LOG(error) << "Failed to load LUT from " << fileA; + return false; + } + if (!lutB) { + LOG(error) << "Failed to load LUT from " << fileB; + return false; + } + + if (lutA->getNLayers() != lutB->getNLayers()) { + LOG(error) << "Layer count differs: " << lutA->getNLayers() << " vs " << lutB->getNLayers(); + return false; + } + + size_t nCells = 0, nBad = 0; + double maxRelRho = 0., maxRelX2X0 = 0.; + + for (int il = 0; il < lutA->getNLayers(); il++) { + const auto& la = lutA->getLayer(il); + const auto& lb = lutB->getLayer(il); + if (la.getNZBins() != lb.getNZBins() || la.getNPhiBins() != lb.getNPhiBins()) { + LOG(error) << "Layer " << il << " segmentation differs: " + << la.getNZBins() << "x" << la.getNPhiBins() << " vs " + << lb.getNZBins() << "x" << lb.getNPhiBins(); + return false; + } + for (int iz = 0; iz < la.getNZBins(); iz++) { + for (int ip = 0; ip < la.getNPhiBins(); ip++) { + const auto& ca = la.getCellPhiBin(ip, iz); + const auto& cb = lb.getCellPhiBin(ip, iz); + nCells++; + + auto rel = [](float a, float b) { + const float den = std::max(std::abs(a), std::abs(b)); + return den > 0.f ? std::abs(a - b) / den : 0.f; + }; + const double rRho = rel(ca.meanRho, cb.meanRho); + const double rX = rel(ca.meanX2X0, cb.meanX2X0); + maxRelRho = std::max(maxRelRho, rRho); + maxRelX2X0 = std::max(maxRelX2X0, rX); + + if (rRho > tol || rX > tol) { + if (nBad < 10) { + printf("Lr %3d iz %4d ip %4d : rho %.9g vs %.9g (rel %.3g) | x2x0 %.9g vs %.9g (rel %.3g)\n", + il, iz, ip, ca.meanRho, cb.meanRho, rRho, ca.meanX2X0, cb.meanX2X0, rX); + } + nBad++; + } + } + } + } + + printf("Compared %zu cells over %d layers\n", nCells, lutA->getNLayers()); + printf("Max relative difference: meanRho %.3g, meanX2X0 %.3g (tolerance %.3g)\n", maxRelRho, maxRelX2X0, tol); + if (nBad) { + LOG(error) << nBad << " cells differ beyond tolerance"; + return false; + } + LOG(info) << "LUTs agree"; + return true; +}