From f9c4f980fdd86ee62d78e2b807838992f82c13af Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Guti=C3=A9rrez=20Men=C3=A9ndez?=
 <d.gutierrezmenendez@gsi.de>
Date: Tue, 26 Nov 2024 12:43:00 +0000
Subject: [PATCH] Correct STS channel mapping

---
 algo/detectors/sts/StsRecoUtils.h  |  81 +++++++++++++++++++
 algo/detectors/sts/UnpackMS.cxx    |  19 +++--
 algo/test/CMakeLists.txt           |   1 +
 algo/test/_GTestChannelMapping.cxx | 125 +++++++++++++++++++++++++++++
 4 files changed, 216 insertions(+), 10 deletions(-)
 create mode 100644 algo/detectors/sts/StsRecoUtils.h
 create mode 100644 algo/test/_GTestChannelMapping.cxx

diff --git a/algo/detectors/sts/StsRecoUtils.h b/algo/detectors/sts/StsRecoUtils.h
new file mode 100644
index 0000000000..bc5fc07ba9
--- /dev/null
+++ b/algo/detectors/sts/StsRecoUtils.h
@@ -0,0 +1,81 @@
+/* Copyright (C) 2021 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: David Gutierrez [committer], Dario Ramirez */
+
+#pragma once
+
+#include "Definitions.h"
+
+#include <cassert>
+#include <cstdlib>
+#include <optional>
+
+namespace cbm::algo::sts
+{
+  /** @brief STS Module parameters
+  **/
+  class Module {
+   public:
+    static constexpr u16 CHANNELS_PER_ASIC   = 128;
+    static constexpr u16 ASICS_PER_MODULE    = 16;
+    static constexpr u16 CHANNELS_PER_MODULE = CHANNELS_PER_ASIC * ASICS_PER_MODULE;
+
+    static_assert(CHANNELS_PER_ASIC > 0, "ASICs must have at least one chanhel.");
+    static_assert(ASICS_PER_MODULE > 0, "Modules must have at least one asic.");
+
+    static constexpr u16 ZSTRIP_ASIC       = 8;
+    static constexpr u16 ZSTRIP_HW_CHANNEL = 127;
+    static constexpr u16 ZSTRIP_SW_CHANNEL = 2046;
+    static constexpr u16 ZSTRIP_OFFSET     = ZSTRIP_SW_CHANNEL - 1022;
+
+    /** @brief Channel HW to SW conversion
+     ** @param  elink_chn       Channel number in elink (HW)
+     ** @param  asic_idx        ASIC index in module
+     ** @return channel         Channel number in SW
+     **/
+    static inline std::optional<u16> ChannelInModule(const u16 elink_chn, const u16 asic_idx)
+    {
+      if (elink_chn >= CHANNELS_PER_ASIC || asic_idx >= ASICS_PER_MODULE) return std::nullopt;
+
+      const u16 channel =
+        CHANNELS_PER_ASIC * asic_idx + elink_chn  // n side
+        + (2 * asic_idx >= ASICS_PER_MODULE)
+            * (                                      // p side
+              CHANNELS_PER_ASIC - 1 - 2 * elink_chn  // revert channels in asic
+              - (elink_chn & 0b1) * 2                // shift odd channels to the left
+              + (elink_chn == ZSTRIP_HW_CHANNEL && asic_idx == ZSTRIP_ASIC) * ZSTRIP_OFFSET  // correct z-strip 1
+            );
+
+      return (channel < CHANNELS_PER_MODULE) ? std::make_optional(channel) : std::nullopt;
+    }
+
+    /** @brief Channel SW to HW conversion
+     ** @param  channel             Channel number in module (SW)
+     ** @param  chan_per_asic       Channels per ASIC
+     ** @param  asic_per_module     ASICs per module
+     ** @return elink_chn, asic_idx Channel number in elink (HW) and asic ASIC in module
+     **/
+    static inline std::optional<std::pair<u16, u16>> ChannelInAsic(const u16 channel)
+    {
+      if (channel >= CHANNELS_PER_MODULE) return std::nullopt;
+
+      auto [asic_idx, elink_chn] =
+        std::div(channel  // n side
+                   + (2 * channel >= CHANNELS_PER_MODULE)
+                       * (                                                // p side
+                         -(channel == ZSTRIP_SW_CHANNEL) * ZSTRIP_OFFSET  // correct z-strip 1
+                         + (!(channel & 0b1)) * 2                         // shift even channels to the right
+                         ),
+                 CHANNELS_PER_ASIC);
+
+      elink_chn += (2 * asic_idx >= ASICS_PER_MODULE)
+                   * (                                      // p side
+                     CHANNELS_PER_ASIC - 1 - 2 * elink_chn  // revert channels in asic
+                   );
+
+      return (asic_idx < ASICS_PER_MODULE || elink_chn < CHANNELS_PER_ASIC)
+               ? std::make_optional(std::make_pair(elink_chn, asic_idx))
+               : std::nullopt;
+    }
+  };
+}  // namespace cbm::algo::sts
diff --git a/algo/detectors/sts/UnpackMS.cxx b/algo/detectors/sts/UnpackMS.cxx
index ad00258b2e..077e3c7ab9 100644
--- a/algo/detectors/sts/UnpackMS.cxx
+++ b/algo/detectors/sts/UnpackMS.cxx
@@ -5,10 +5,12 @@
 #include "UnpackMS.h"
 
 #include "AlgoFairloggerCompat.h"
+#include "StsRecoUtils.h"
 #include "StsXyterMessage.h"
 
 #include <cassert>
 #include <cmath>
+#include <cstdint>
 #include <utility>
 #include <vector>
 
@@ -122,18 +124,14 @@ namespace cbm::algo::sts
     }
 
     // --- Check for masked channel
-    if (!elinkPar.fChanMask.empty() && elinkPar.fChanMask[message.GetHitChannel()] == true) {
+    const uint16_t msg_channel = message.GetHitChannel();
+    if (!elinkPar.fChanMask.empty() && elinkPar.fChanMask[msg_channel] == true) {
       return;
     }
 
     // --- Hardware-to-software address
-    uint32_t channel = 0;
-    if (asicNr < fParams.fNumAsicsPerModule / 2) {  // front side (n side); channels counted upward
-      channel = message.GetHitChannel() + fParams.fNumChansPerAsic * asicNr;
-    }
-    else {  // back side (p side); channels counted downward
-      channel = fParams.fNumChansPerAsic * (asicNr + 1) - message.GetHitChannel() - 1;
-    }
+    const auto maybe_channel = Module::ChannelInModule(msg_channel, asicNr);
+    if (!maybe_channel.has_value()) return;
 
     // --- Expand time stamp to time within timeslice (in clock cycle)
     uint64_t messageTime = message.GetHitTimeBinning() + time.currentEpochTime;
@@ -159,10 +157,11 @@ namespace cbm::algo::sts
     }
 
     // --- Create output digi
-    digiVec.emplace_back(elinkPar.fAddress, channel, messageTime, charge);
+    digiVec.emplace_back(elinkPar.fAddress, *maybe_channel, messageTime, charge);
 
     if (fParams.fWriteAux) {
-      aux.fQaDigis.emplace_back(message.IsHitMissedEvts(), elinkPar.fAddress, channel, messageTime, charge, elink);
+      aux.fQaDigis.emplace_back(message.IsHitMissedEvts(), elinkPar.fAddress, *maybe_channel, messageTime, charge,
+                                elink);
     }
   }
   // --------------------------------------------------------------------------
diff --git a/algo/test/CMakeLists.txt b/algo/test/CMakeLists.txt
index a0c7cde46b..e4ab499ebf 100644
--- a/algo/test/CMakeLists.txt
+++ b/algo/test/CMakeLists.txt
@@ -22,6 +22,7 @@ AddBasicTest(_GTestYamlConfig)
 AddBasicTest(_GTestPartitionedVector)
 AddBasicTest(_GTestPartitionedSpan)
 AddBasicTest(_GTestTrdClusterizer)
+AddBasicTest(_GTestChannelMapping)
 
 if (DEFINED ENV{RAW_DATA_PATH})
   set(RAW_DATA_PATH $ENV{RAW_DATA_PATH})
diff --git a/algo/test/_GTestChannelMapping.cxx b/algo/test/_GTestChannelMapping.cxx
new file mode 100644
index 0000000000..754d3b911b
--- /dev/null
+++ b/algo/test/_GTestChannelMapping.cxx
@@ -0,0 +1,125 @@
+/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: David Gutierrez [committer], Dario Ramirez */
+
+#include "Definitions.h"
+#include "detectors/sts/StsRecoUtils.h"
+
+#include <array>
+#include <optional>
+
+#include <gtest.h>
+#include <gtest/gtest-death-test.h>
+#include <gtest/gtest.h>
+
+#define EXPECT_OPT(maybe_val1, val2)                                                                                   \
+  ASSERT_TRUE(maybe_val1.has_value());                                                                                 \
+  EXPECT_EQ(*maybe_val1, val2)
+
+using namespace cbm::algo;
+using namespace cbm::algo::sts;
+
+TEST(_ChannelMapping, FailOnValueLimits)
+{
+  EXPECT_EQ(Module::ChannelInModule(Module::CHANNELS_PER_ASIC + 1, 0), std::nullopt);
+  EXPECT_EQ(Module::ChannelInModule(0, Module::ASICS_PER_MODULE + 1), std::nullopt);
+}
+
+TEST(_ChannelMapping, ReverseFailOnValueLimits)
+{
+  EXPECT_EQ(Module::ChannelInAsic(Module::CHANNELS_PER_MODULE + 1), std::nullopt);
+}
+
+TEST(_ChannelMapping, ModuleEdgesMapping)
+{
+  EXPECT_OPT(Module::ChannelInModule(0, 0), 0);
+  EXPECT_OPT(Module::ChannelInModule(127, 7), 1023);
+  EXPECT_OPT(Module::ChannelInModule(127, 8), 2046);
+  EXPECT_OPT(Module::ChannelInModule(125, 8), 1024);
+  EXPECT_OPT(Module::ChannelInModule(0, 15), 2047);
+}
+
+TEST(_ChannelMapping, ReverseModuleEdgesMapping)
+{
+  {
+    const auto expect = std::make_pair<u16, u16>(0, 0);
+    EXPECT_OPT(Module::ChannelInAsic(0), expect);
+  }
+  {
+    const auto expect = std::make_pair<u16, u16>(127, 7);
+    EXPECT_OPT(Module::ChannelInAsic(1023), expect);
+  }
+  {
+    const auto expect = std::make_pair<u16, u16>(127, 8);
+    EXPECT_OPT(Module::ChannelInAsic(2046), expect);
+  }
+  {
+    const auto expect = std::make_pair<u16, u16>(125, 8);
+    EXPECT_OPT(Module::ChannelInAsic(1024), expect);
+  }
+  {
+    const auto expect = std::make_pair<u16, u16>(0, 15);
+    EXPECT_OPT(Module::ChannelInAsic(2047), expect);
+  }
+}
+
+TEST(_ChannelMapping, ExaustiveMapping)
+{
+  for (u16 channel = 0; channel < Module::CHANNELS_PER_MODULE; channel++) {
+    const auto maybe_hw = Module::ChannelInAsic(channel);
+    ASSERT_TRUE(maybe_hw.has_value());
+
+    const auto maybe_sh = Module::ChannelInModule(maybe_hw->first, maybe_hw->second);
+    EXPECT_OPT(maybe_sh, channel);
+  }
+}
+
+TEST(_ChannelMapping, Compatibility)
+{
+  for (u16 asic = 0; asic < Module::ASICS_PER_MODULE / 2; asic++) {  // n side
+    for (u16 chn = 0; chn < Module::CHANNELS_PER_ASIC; chn++) {
+      const auto legacy   = asic * Module::CHANNELS_PER_ASIC + chn;
+      const auto maybe_sw = Module::ChannelInModule(chn, asic);
+      EXPECT_OPT(maybe_sw, legacy);
+    }
+  }
+
+  for (u16 asic = Module::ASICS_PER_MODULE / 2; asic < Module::ASICS_PER_MODULE; asic++) {  // p side
+    for (u16 chn = 0; chn < Module::CHANNELS_PER_ASIC; chn += 2) {                          // even channels
+      const auto legacy   = (asic + 1) * Module::CHANNELS_PER_ASIC - chn - 1;
+      const auto maybe_sw = Module::ChannelInModule(chn, asic);
+      EXPECT_OPT(maybe_sw, legacy);
+    }
+
+    for (u16 chn = 1; chn < Module::CHANNELS_PER_ASIC; chn += 2) {  // odd channels
+      const auto legacy   = (asic + 1) * Module::CHANNELS_PER_ASIC - chn - 1;
+      const auto maybe_sw = Module::ChannelInModule(chn, asic);
+
+      if ((asic == 8) && (chn == 127)) {  // z-strip
+        EXPECT_OPT(maybe_sw, 2046);
+        EXPECT_EQ(1024, legacy);
+      }
+      else {
+        EXPECT_OPT(maybe_sw, legacy - 2);
+      }
+    }
+  }
+}
+
+TEST(_ChannelMapping, Completness)
+{
+  std::array<bool, Module::CHANNELS_PER_MODULE> visited = {false};
+
+  for (u16 asic = 0; asic < Module::ASICS_PER_MODULE; asic++) {
+    for (u16 chn = 0; chn < Module::CHANNELS_PER_ASIC; chn++) {
+      const auto maybe_sw = Module::ChannelInModule(chn, asic);
+      ASSERT_TRUE(maybe_sw.has_value());
+      ASSERT_LT(*maybe_sw, visited.size());
+      visited[*maybe_sw] = true;
+    }
+  }
+
+  for (const auto chn : visited) {
+    EXPECT_TRUE(chn);
+  }
+}
-- 
GitLab