From a4734cb8d3c05a926cd2702ecde44162cd2fed5c Mon Sep 17 00:00:00 2001
From: "s.zharko@gsi.de" <s.zharko@gsi.de>
Date: Wed, 5 Feb 2025 20:03:41 +0100
Subject: [PATCH 1/4] online: BMON digi calibrator

---
 algo/CMakeLists.txt                  |   1 +
 algo/detectors/bmon/Calibrate.cxx    | 166 +++++++++++++++++++++++++++
 algo/detectors/bmon/Calibrate.h      |  50 ++++++++
 algo/detectors/bmon/CalibrateSetup.h |  67 +++++++++++
 algo/detectors/bmon/UnpackMS.cxx     |   1 +
 algo/global/ParFiles.cxx             |  12 +-
 algo/global/ParFiles.h               |   2 +
 algo/global/Reco.cxx                 |  71 ++++++++++++
 algo/global/Reco.h                   |   2 +
 core/data/bmon/CbmBmonDigi.h         |  27 ++---
 core/data/tof/CbmTofAddress.cxx      |  18 +++
 core/data/tof/CbmTofAddress.h        |  13 ++-
 core/data/tof/CbmTofDigi.h           |  29 ++---
 external/InstallParameter.cmake      |   2 +
 14 files changed, 431 insertions(+), 30 deletions(-)
 create mode 100644 algo/detectors/bmon/Calibrate.cxx
 create mode 100644 algo/detectors/bmon/Calibrate.h
 create mode 100644 algo/detectors/bmon/CalibrateSetup.h

diff --git a/algo/CMakeLists.txt b/algo/CMakeLists.txt
index 09ee075af5..58ac4397e4 100644
--- a/algo/CMakeLists.txt
+++ b/algo/CMakeLists.txt
@@ -95,6 +95,7 @@ set(SRCS
   evselector/DigiEventSelector.cxx
   evselector/DigiEventSelectorConfig.cxx
   unpack/CommonUnpacker.cxx
+  detectors/bmon/Calibrate.cxx
   detectors/sts/ChannelMaskSet.cxx
   detectors/sts/ReadoutConfig.cxx
   detectors/sts/HitfinderChain.cxx
diff --git a/algo/detectors/bmon/Calibrate.cxx b/algo/detectors/bmon/Calibrate.cxx
new file mode 100644
index 0000000000..70d99daeb4
--- /dev/null
+++ b/algo/detectors/bmon/Calibrate.cxx
@@ -0,0 +1,166 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   Calibrate.h
+/// \brief  Calibrator for the BMON digis (implementation)
+/// \since  04.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+#include "Calibrate.h"
+
+#include "AlgoFairloggerCompat.h"
+#include "CbmTofAddress.h"
+#include "util/TimingsFormat.h"
+
+#include <chrono>
+
+using cbm::algo::bmon::Calibrate;
+using cbm::algo::bmon::CalibrateSetup;
+using fles::Subsystem;
+
+// ---------------------------------------------------------------------------------------------------------------------
+//
+Calibrate::Calibrate(CalibrateSetup setup) : fSetup(setup), fSmOffset(1, 0), fRpcOffset(1, 0)
+{
+  // Initialize offset arrays for channel deadtime check
+  int32_t NbSm  = fSetup.NbSm;
+  int32_t NbRpc = fSetup.NbRpc;
+  for (int32_t Sm = 0; Sm < NbSm; Sm++) {
+    fSmOffset.push_back(fSmOffset.back() + NbRpc);
+    for (int32_t Rpc = 0; Rpc < NbRpc; Rpc++) {
+      int32_t NbChan = fSetup.rpcs[Sm * NbRpc + Rpc].chanPar.size();
+      fRpcOffset.push_back(fRpcOffset.back() + 2 * NbChan);  //Factor 2 for channel sides
+    }
+  }
+
+  // **** DEBUG: BEGIN
+  for (size_t iO = 0; iO < fSmOffset.size(); ++iO) {
+    L_(info) << "SM offset: " << iO << ", " << fSmOffset[iO];
+  }
+  for (size_t iO = 0; iO < fRpcOffset.size(); ++iO) {
+    L_(info) << "RPC offset: " << iO << ", " << fRpcOffset[iO];
+  }
+  // **** DEBUG: END
+
+  fChannelDeadTime = std::vector<double>(fRpcOffset.back(), std::numeric_limits<double>::quiet_NaN());
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+//
+Calibrate::resultType Calibrate::operator()(gsl::span<const CbmBmonDigi> digiIn)
+{
+  xpu::push_timer("TofCalibrate");
+  xpu::t_add_bytes(digiIn.size_bytes());
+
+  // --- Output data
+  resultType result = {};
+
+  auto& calDigiOut = result.first;
+  auto& monitor    = result.second;
+  calDigiOut.reserve(digiIn.size());
+
+  std::fill(fChannelDeadTime.begin(), fChannelDeadTime.end(), std::numeric_limits<double>::quiet_NaN());
+
+  for (const auto& digi : digiIn) {
+    uint32_t address = static_cast<uint32_t>(digi.GetAddress());
+    const int32_t SmType{CbmTofAddress::GetSmType(address)};
+    const int32_t Sm{CbmTofAddress::GetSmId(address)};
+    const int32_t Rpc{CbmTofAddress::GetRpcId(address)};
+    const int NbRpc{fSetup.NbRpc};
+
+    int32_t Chan{CbmTofAddress::GetChannelId(address)};
+    int32_t Side{CbmTofAddress::GetChannelSide(address)};
+
+    // NOTE: Follow the hack in the CbmTofEventClusterizer::Exec() for the BMON digis and change the address
+    //       of calibrated digis. In general it is incorrect and needs further investigation.
+    uint32_t calAddress = address;
+    if (Side == 1) {
+      Side = 0;
+      Chan += 6;
+      calAddress = CbmTofAddress::GetUniqueAddress(Sm, Rpc, Chan, Side, SmType);
+    }
+
+    auto& rpcs = fSetup.rpcs;
+    if (SmType != fSetup.SmType || Sm * NbRpc + Rpc >= static_cast<int>(rpcs.size())) {
+      monitor.fDigiCalibUnknownRPC++;
+      L_(error) << "Unknown BMON digi address: " << CbmTofAddress::ToString(calAddress);
+      continue;
+    }
+
+    CalibrateSetup::Rpc& rpcPar      = fSetup.rpcs[Sm * NbRpc + Rpc];
+    CalibrateSetup::Channel& chanPar = rpcPar.chanPar[Chan];
+
+    // Check dead time
+    // FIXME: BMON digis (must) have only one side -> Fix the offsets, when mcbm2024_05 will be fixed
+    const size_t chanIdx  = fRpcOffset[fSmOffset[Sm] + Rpc] + Chan + Side * rpcPar.chanPar.size();
+    const double deadTime = fChannelDeadTime[chanIdx];
+
+    if (!std::isnan(deadTime) && digi.GetTime() <= deadTime) {
+      fChannelDeadTime[chanIdx] = digi.GetTime() + rpcPar.channelDeadtime;
+      monitor.fDigiDeadTimeCount++;
+      continue;
+    }
+    fChannelDeadTime[chanIdx] = digi.GetTime() + rpcPar.channelDeadtime;
+
+    // Create calibrated digi
+    CbmBmonDigi& pCalDigi = calDigiOut.emplace_back(digi);
+    pCalDigi.SetAddress(calAddress);
+
+    // Check if channel sides need to be swapped
+    if (rpcPar.swapChannelSides) {
+      pCalDigi.SetAddress(CbmTofAddress::GetUniqueAddress(Sm, Rpc, Chan, (0 == Side) ? 1 : 0, SmType));
+    }
+
+    // calibrate Digi Time
+    pCalDigi.SetTime(pCalDigi.GetTime() - chanPar.vCPTOff[Side]);
+
+    // subtract Offset
+    const double dTot = std::max(pCalDigi.GetCharge() - chanPar.vCPTotOff[Side], 0.001);
+
+    // calibrate Digi charge (corresponds to TOF ToT)
+    pCalDigi.SetCharge(dTot * chanPar.vCPTotGain[Side]);
+
+    // walk correction
+    std::vector<double>& walk = chanPar.vCPWalk[Side];
+    const double dTotBinSize  = (rpcPar.TOTMax - rpcPar.TOTMin) / rpcPar.numClWalkBinX;
+    int32_t iWx               = std::max((int32_t)((pCalDigi.GetCharge() - rpcPar.TOTMin) / dTotBinSize), 0);
+    iWx                       = std::min(iWx, rpcPar.numClWalkBinX - 1);
+
+    const double dDTot = (pCalDigi.GetCharge() - rpcPar.TOTMin) / dTotBinSize - (double) iWx - 0.5;
+    double dWT         = walk[iWx];
+
+    // linear interpolation to next bin
+    if (dDTot > 0) {
+      if (iWx < rpcPar.numClWalkBinX - 1) {
+        dWT += dDTot * (walk[iWx + 1] - walk[iWx]);
+      }
+    }
+    else {
+      if (0 < iWx) {
+        dWT -= dDTot * (walk[iWx - 1] - walk[iWx]);
+      }
+    }
+    pCalDigi.SetTime(pCalDigi.GetTime() - dWT);  // calibrate Digi Time
+  }
+
+  /// Sort the buffers of hits due to the time offsets applied
+  // (insert-sort faster than std::sort due to pre-sorting)
+  for (std::size_t i = 1; i < calDigiOut.size(); ++i) {
+    CbmTofDigi digi = calDigiOut[i];
+    std::size_t j   = i;
+    while (j > 0 && calDigiOut[j - 1].GetTime() > digi.GetTime()) {
+      calDigiOut[j] = calDigiOut[j - 1];
+      --j;
+    }
+    calDigiOut[j] = digi;
+  }
+
+  //  Kept for possible unsorted input
+  //  std::sort(calDigiOut.begin(), calDigiOut.end(),
+  //  [](const CbmTofDigi& a, const CbmTofDigi& b) -> bool { return a.GetTime() < b.GetTime(); });
+
+  monitor.fTime     = xpu::pop_timer();
+  monitor.fNumDigis = digiIn.size();
+  return result;
+}
diff --git a/algo/detectors/bmon/Calibrate.h b/algo/detectors/bmon/Calibrate.h
new file mode 100644
index 0000000000..291a6882b4
--- /dev/null
+++ b/algo/detectors/bmon/Calibrate.h
@@ -0,0 +1,50 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   Calibrate.h
+/// \brief  Calibratior for the BMON digis
+/// \since  04.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+#pragma once
+
+#include "CbmBmonDigi.h"
+#include "PartitionedVector.h"
+#include "bmon/CalibrateSetup.h"
+#include "tof/Calibrate.h"  // for the monitor data
+#include "tof/Clusterizer.h"
+
+#include <gsl/span>
+#include <optional>
+#include <sstream>
+#include <vector>
+
+#include <xpu/host.h>
+
+namespace cbm::algo::bmon
+{
+  // NOTE: reusing TOF monitor
+  using CalibrateMonitorData = tof::CalibrateMonitorData;
+
+  /// \class Calibrate
+  /// \brief Algorithm to calibrate BMon digis
+  class Calibrate {
+   public:
+    using resultType = std::pair<std::vector<CbmBmonDigi>, CalibrateMonitorData>;
+
+    /// \brief Constructor
+    /// \param params  Calibration parameters
+    explicit Calibrate(CalibrateSetup params);
+
+    /// \brief Calibrates a portion of digis
+    /// \param digiIn  A portion of digis to calibrate
+    resultType operator()(gsl::span<const CbmBmonDigi> digiIn);
+
+   private:
+    CalibrateSetup fSetup;                 ///< Parameters of calibrator
+    std::vector<double> fChannelDeadTime;  ///< Storage for dead time check
+    std::vector<size_t> fSmOffset;         ///< Super module offset
+    std::vector<size_t> fRpcOffset;        ///< RPC offset
+  };
+}  // namespace cbm::algo::bmon
diff --git a/algo/detectors/bmon/CalibrateSetup.h b/algo/detectors/bmon/CalibrateSetup.h
new file mode 100644
index 0000000000..0d9dedd93c
--- /dev/null
+++ b/algo/detectors/bmon/CalibrateSetup.h
@@ -0,0 +1,67 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   CalibrateSetup.h
+/// \brief  Configuration of the calibrator for the BMON digis
+/// \since  04.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+#pragma once
+
+#include "Definitions.h"
+#include "yaml/Property.h"
+
+#include <array>
+#include <map>
+#include <string>
+#include <vector>
+
+namespace cbm::algo::bmon
+{
+  /// \struct  CalibrateSetup
+  /// \brief   BMON calibration per channel
+  struct CalibrateSetup {
+
+    struct Channel {
+      std::vector<double> vCPTOff;
+      std::vector<double> vCPTotGain;
+      std::vector<double> vCPTotOff;
+      std::vector<std::vector<double>> vCPWalk;
+
+      CBM_YAML_PROPERTIES(yaml::Property(&Channel::vCPTOff, "vCPTOff", "CPT offset"),
+                        yaml::Property(&Channel::vCPTotGain, "vCPTotGain", "CP time over threshold gain"),
+                        yaml::Property(&Channel::vCPTotOff, "vCPTotOff", "CP time over threshold offset"),
+                        yaml::Property(&Channel::vCPWalk, "vCPWalk", "CP walk correction", YAML::Block, YAML::Flow));
+    };
+
+    struct Rpc {
+      int32_t numClWalkBinX;
+      double TOTMax;
+      double TOTMin;
+      bool swapChannelSides;
+      double channelDeadtime;
+      std::vector<Channel> chanPar;
+
+      CBM_YAML_PROPERTIES(yaml::Property(&Rpc::numClWalkBinX, "numClWalkBinX", "number of walk correction bins"),
+                        yaml::Property(&Rpc::TOTMax, "TOTMax", "maximum time over threshold"),
+                        yaml::Property(&Rpc::TOTMin, "TOTMin", "minimum time over threshold"),
+                        yaml::Property(&Rpc::swapChannelSides, "swapChannelSides", "flag for swapping channel sides"),
+                        yaml::Property(&Rpc::channelDeadtime, "channelDeadtime", "channel dead time"),
+                        yaml::Property(&Rpc::chanPar, "chanPar", "channel parameters"));
+    };
+
+    /* Members */
+    int32_t SmType;
+    int32_t NbSm;
+    int32_t NbRpc;
+    std::vector<Rpc> rpcs;
+
+    CBM_YAML_PROPERTIES(
+      yaml::Property(&CalibrateSetup::SmType, "SmType", "Super module type"),
+      yaml::Property(&CalibrateSetup::NbSm, "NbSm", "Number of SMs"),
+      yaml::Property(&CalibrateSetup::NbRpc, "NbRpc", "Number of RPCs"),
+      yaml::Property(&CalibrateSetup::rpcs, "rpcs", "Parameters of RPCs"));
+  };
+
+}  // namespace cbm::algo::bmon
diff --git a/algo/detectors/bmon/UnpackMS.cxx b/algo/detectors/bmon/UnpackMS.cxx
index b4fbf123e1..22d56e90d1 100644
--- a/algo/detectors/bmon/UnpackMS.cxx
+++ b/algo/detectors/bmon/UnpackMS.cxx
@@ -5,6 +5,7 @@
 #include "UnpackMS.h"
 
 #include "AlgoFairloggerCompat.h"
+#include "CbmTofAddress.h"
 
 #include <cassert>
 #include <cmath>
diff --git a/algo/global/ParFiles.cxx b/algo/global/ParFiles.cxx
index 8eb07742a6..eabec37b4b 100644
--- a/algo/global/ParFiles.cxx
+++ b/algo/global/ParFiles.cxx
@@ -25,7 +25,9 @@ ParFiles::ParFiles(uint32_t runId)
   switch (setup) {
 
     case Setup::mCBM2022:
-      bmon.readout = "BmonReadout_mcbm2022.yaml";
+      bmon.readout   = "BmonReadout_mcbm2022.yaml";
+      bmon.calibrate = "BmonCalibratePar_mcbm2022.yaml";
+      bmon.hitfinder = "BmonHitfinderPar_mcbm2022.yaml";
 
       sts.readout   = "StsReadout_mcbm2022.yaml";
       sts.chanMask  = "StsChannelMaskSet_mcbm2022.yaml";
@@ -46,7 +48,9 @@ ParFiles::ParFiles(uint32_t runId)
       break;
 
     case Setup::mCBM2024_03:
-      bmon.readout = "BmonReadout_mcbm2024.yaml";
+      bmon.readout   = "BmonReadout_mcbm2024.yaml";
+      bmon.calibrate = "BmonCalibratePar_mcbm2024.yaml";
+      bmon.hitfinder = "BmonHitfinderPar_mcbm2024.yaml";
 
       sts.readout   = "StsReadout_mcbm2024.yaml";
       sts.chanMask  = "StsChannelMaskSet_mcbm2024.yaml";
@@ -67,7 +71,9 @@ ParFiles::ParFiles(uint32_t runId)
       break;
 
     case Setup::mCBM2024_05:
-      bmon.readout = "BmonReadout_mcbm2024.yaml";
+      bmon.readout   = "BmonReadout_mcbm2024.yaml";
+      bmon.calibrate = "mcbm2024_05/BmonCalibratePar.yaml";
+      bmon.hitfinder = "mcbm2024_05/BmonHitfinderPar.yaml";
 
       sts.readout   = "StsReadout_mcbm2024.yaml";
       sts.chanMask  = "StsChannelMaskSet_mcbm2024.yaml";
diff --git a/algo/global/ParFiles.h b/algo/global/ParFiles.h
index 9c966ac637..691e27b39b 100644
--- a/algo/global/ParFiles.h
+++ b/algo/global/ParFiles.h
@@ -26,6 +26,8 @@ namespace cbm::algo
 
     struct {
       fs::path readout;
+      fs::path calibrate;
+      fs::path hitfinder;
     } bmon;
 
     struct {
diff --git a/algo/global/Reco.cxx b/algo/global/Reco.cxx
index e9a0a7a9a0..9bdc35bb6b 100644
--- a/algo/global/Reco.cxx
+++ b/algo/global/Reco.cxx
@@ -14,6 +14,7 @@
 #include "RecoGeneralQa.h"
 #include "StsDigiQa.h"
 #include "TrackingSetup.h"
+#include "bmon/Calibrate.h"
 #include "bmon/ReadoutConfig.h"
 #include "bmon/Unpack.h"
 #include "ca/TrackingChain.h"
@@ -41,6 +42,10 @@
 
 #include <xpu/host.h>
 
+// DEBUG: BEGIN
+#include <set>
+// DEBUG: END
+
 using namespace cbm::algo;
 using fles::Subsystem;
 
@@ -204,6 +209,14 @@ void Reco::Init(const Options& opts)
     fStsHitFinder->SetParameters(hitFinderPars);
   }
 
+  // BMON Hitfinder
+  if (Opts().Has(fles::Subsystem::BMON) && Opts().Has(Step::LocalReco)) {
+    auto calibSetup = yaml::ReadFromFile<bmon::CalibrateSetup>(opts.ParamsDir() / parFiles.bmon.calibrate);
+    fBmonCalibrator = std::make_unique<bmon::Calibrate>(calibSetup);
+
+    //auto hitfindSetup = yaml::ReadFromFile<bmon::HitfindSetup>(opts.ParamsDir() / parFiles.bmon.hitfinder);
+    //fBmonHitFinder     = std::make_unique<bmon::Hitfind>(hitfindSetup);
+  }
 
   // TOF Hitfinder
   if (Opts().Has(fles::Subsystem::TOF) && Opts().Has(Step::LocalReco)) {
@@ -372,6 +385,64 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
       QueueEvbuildMetrics(evbuildMonitor);
     }
 
+    // ***** DEBUG: BEGIN
+    if constexpr (1) {
+      std::set<uint32_t> bmonFoundAddressesUnp;  // found addresses among unpacked bmon digis
+      std::set<uint32_t> bmonFoundAddressesCal;  // found addresses among clalibrated bmon digis
+
+      int nEvents = 20;  //events.size();
+      for (int iE = 0; iE < nEvents; ++iE) {
+        //L_(info) << "-------------- event #" << iE;
+        const auto& event = events[iE];
+        for (const auto& digi : event.fBmon) {
+          bmonFoundAddressesUnp.insert(digi.GetAddress());
+        }
+
+        // Calibrate TOF digis:
+        auto [tofDigis, tofCalMonitor]   = (*fTofCalibrator)(event.fTof);
+        auto [bmonDigis, bmonCalMonitor] = (*fBmonCalibrator)(event.fBmon);
+
+        for (const auto& digi : bmonDigis) {
+          bmonFoundAddressesCal.insert(digi.GetAddress());
+        }
+
+        double bmonMinT = 1e+20;
+        double bmonMaxT = -1e+20;
+        for (const auto& digi : bmonDigis) {
+          //L_(info) << "BMON: " << digi.GetTime();
+          bmonMinT = std::min(bmonMinT, digi.GetTime());
+          bmonMaxT = std::max(bmonMaxT, digi.GetTime());
+        }
+        double tofMinT = 1e+20;
+        double tofMaxT = -1e+20;
+        for (const auto& digi : tofDigis) {
+          //L_(info) << "TOF: " << digi.GetTime();
+          tofMinT = std::min(tofMinT, digi.GetTime());
+          tofMaxT = std::max(tofMaxT, digi.GetTime());
+        }
+        double stsMinT       = 1e+20;
+        double stsMaxT       = -1e+20;
+        const auto& stsDigis = event.fSts;
+        for (const auto& digi : stsDigis) {
+          //L_(info) << "STS: " << digi.GetTime();
+          stsMinT = std::min(stsMinT, digi.GetTime());
+          stsMaxT = std::max(stsMaxT, digi.GetTime());
+        }
+        L_(info) << "BMON time: " << bmonMinT << ", " << bmonMaxT << ", " << (bmonMaxT - bmonMinT);
+        L_(info) << "TOF time: " << tofMinT << ", " << tofMaxT << ", " << (tofMaxT - tofMinT);
+        L_(info) << "STS time: " << stsMinT << ", " << stsMaxT << ", " << (stsMaxT - stsMinT);
+      }
+      L_(info) << "!!!! Found addresses in BMON after unpacking: ";
+      for (auto address : bmonFoundAddressesUnp) {
+        L_(info) << " - " << CbmTofAddress::ToString(address);
+      }
+      L_(info) << "!!!! Found addresses in BMON after calibration: ";
+      for (auto address : bmonFoundAddressesCal) {
+        L_(info) << " - " << CbmTofAddress::ToString(address);
+      }
+    }
+    // ***** DEBUG: END
+
     // --- Filter data for output
     if (Opts().HasOutput(RecoData::DigiTimeslice)) {
       results.bmonDigis  = std::move(digis.fBmon);
diff --git a/algo/global/Reco.h b/algo/global/Reco.h
index 8e26d35271..982a95cfa4 100644
--- a/algo/global/Reco.h
+++ b/algo/global/Reco.h
@@ -27,6 +27,7 @@ namespace cbm::algo
   namespace bmon
   {
     class Unpack;
+    class Calibrate;
   }
 
   namespace much
@@ -146,6 +147,7 @@ namespace cbm::algo
 
     // BMON
     std::unique_ptr<bmon::Unpack> fBmonUnpack;
+    std::unique_ptr<bmon::Calibrate> fBmonCalibrator;
 
     // MUCH
     std::unique_ptr<much::Unpack> fMuchUnpack;
diff --git a/core/data/bmon/CbmBmonDigi.h b/core/data/bmon/CbmBmonDigi.h
index 200e72d0ce..7f0584e2fc 100644
--- a/core/data/bmon/CbmBmonDigi.h
+++ b/core/data/bmon/CbmBmonDigi.h
@@ -111,19 +111,20 @@ public:
 
 
 private:
-  int32_t fAddress = ToIntegralType<ECbmModuleId>(ECbmModuleId::kBmon);  ///< Unique CBM address
-  double fTime     = -1.;                                                ///< Time of signal in BMON [ns]
-  float fCharge    = -1.;                                                ///< Charge
-
-  friend class boost::serialization::access;
-
-  template<class Archive>
-  void serialize(Archive& ar, const unsigned int /*version*/)
-  {
-    ar& fAddress;
-    ar& fTime;
-    ar& fCharge;
-  }
+ // FIXME: SZh 5.2.2025: change address type int32_t -> uint32_t
+ int32_t fAddress = ToIntegralType<ECbmModuleId>(ECbmModuleId::kBmon);  ///< Unique CBM address
+ double fTime     = -1.;                                                ///< Time of signal in BMON [ns]
+ float fCharge    = -1.;                                                ///< Charge
+
+ friend class boost::serialization::access;
+
+ template<class Archive>
+ void serialize(Archive& ar, const unsigned int /*version*/)
+ {
+   ar& fAddress;
+   ar& fTime;
+   ar& fCharge;
+ }
 
 #ifndef NO_ROOT
   ClassDefNV(CbmBmonDigi, 1);
diff --git a/core/data/tof/CbmTofAddress.cxx b/core/data/tof/CbmTofAddress.cxx
index 0ed83293f2..068504c399 100644
--- a/core/data/tof/CbmTofAddress.cxx
+++ b/core/data/tof/CbmTofAddress.cxx
@@ -11,6 +11,9 @@
 
 #include "CbmTofAddress.h"
 
+#include <iomanip>
+#include <sstream>
+
 // It seems C++ standard force the initialization to be in cxx/cpp file (outside of class definition)
 // When not trivial constant values => To check if it should apply also to simple values maybe?
 /** Offset in bits for Super Module Id in the address field  **/
@@ -40,3 +43,18 @@ const int32_t CbmTofAddress::fgkiStripFullIdMask =
   (((1 << fgkSystemBits) - 1)) + (((1 << fgkSmIdBits) - 1) << fgkSmIdOffset)
   + (((1 << fgkSmTypeBits) - 1) << fgkSmTypeOffset) + (((1 << fgkRpcIdBits) - 1) << fgkRpcIdOffset)
   + (((1 << fgkChannelIdBits) - 1) << fgkChannelIdOffset);
+
+std::string CbmTofAddress::ToString(int32_t address)
+{
+  using std::setfill;
+  using std::setw;
+  std::stringstream msg;
+  msg << std::hex << "0x" << setw(8) << setfill('0') << address << std::dec << setfill(' ');
+  msg << ": SmType=" << setw(3) << CbmTofAddress::GetSmType(address);
+  msg << ", Sm=" << setw(2) << CbmTofAddress::GetSmId(address);
+  msg << ", Rpc=" << setw(2) << CbmTofAddress::GetRpcId(address);
+  msg << ", Ch=" << setw(2) << CbmTofAddress::GetChannelId(address);
+  msg << ", Side=" << setw(1) << CbmTofAddress::GetChannelSide(address);
+  msg << ", RpcType=" << setw(2) << CbmTofAddress::GetRpcType(address);
+  return msg.str();
+}
diff --git a/core/data/tof/CbmTofAddress.h b/core/data/tof/CbmTofAddress.h
index f8954d532e..3abf9b1703 100644
--- a/core/data/tof/CbmTofAddress.h
+++ b/core/data/tof/CbmTofAddress.h
@@ -44,6 +44,7 @@
 #include "CbmTofDetectorId_v12b.h"  // for CbmTofDetectorId_v12b
 
 #include <cstdint>
+#include <string>
 
 class CbmTofAddress : public CbmAddress {
 public:
@@ -91,6 +92,11 @@ public:
    ** @return  systemId
    **/
   static int32_t GetRpcId(uint32_t address) { return ((address >> fgkRpcIdOffset) & ((1 << fgkRpcIdBits) - 1)); };
+  /** Get the Rpc Type from the address
+   ** @param address  Unique address
+   ** @return  systemId
+   **/
+  static int32_t GetRpcType(uint32_t address) { return ((address >> fgkRpcTypeOffset) & ((1 << fgkRpcTypeBits) - 1)); };
   /** Get the Channel Id from the address
    ** @param address  Unique address
    ** @return  systemId
@@ -169,8 +175,13 @@ public:
     return GetUniqueAddress(detId.GetSModule(detIdInput), detId.GetCounter(detIdInput), detId.GetCell(detIdInput), 0,
                             detId.GetSMType(detIdInput));
   };
+  /** String representation of the address
+   ** @param address  Unique address
+   ** @return String representation of the address
+   **/
+  static std::string ToString(int32_t address);
 
-private:
+ private:
   /**
    ** To adapt the address sub-fields repartition in size,
    ** you just need to change number of bits of the two sub-fields changing length.
diff --git a/core/data/tof/CbmTofDigi.h b/core/data/tof/CbmTofDigi.h
index 942c6edb18..7448ad1a72 100644
--- a/core/data/tof/CbmTofDigi.h
+++ b/core/data/tof/CbmTofDigi.h
@@ -109,6 +109,7 @@ public:
   /**
           ** @brief Inherited from CbmDigi.
           **/
+  // FIXME: SZh 5.2.2025: change address type int32_t -> uint32_t
   int32_t GetAddress() const { return fuAddress; };
 
 
@@ -160,6 +161,8 @@ public:
   double GetSide() const { return CbmTofAddress::GetChannelSide(GetAddress()); };
 
   /** Modifiers **/
+
+  // FIXME: SZh 5.2.2025: change address type int32_t -> uint32_t
   void SetAddress(int32_t address) { fuAddress = address; };
   void SetAddress(uint32_t Sm, uint32_t Rpc, uint32_t Channel, uint32_t Side = 0, uint32_t SmType = 0);
   void SetTime(double time) { fdTime = time; };
@@ -169,19 +172,19 @@ public:
 
 
 private:
-  double fdTime;       ///< Absolute time [ps]
-  double fdTot;        ///< Tot [ps]
-  uint32_t fuAddress;  ///< Unique channel address
-
-  friend class boost::serialization::access;
-
-  template<class Archive>
-  void serialize(Archive& ar, const unsigned int /*version*/)
-  {
-    ar& fuAddress;
-    ar& fdTime;
-    ar& fdTot;
-  }
+ double fdTime;       ///< Absolute time [ns]
+ double fdTot;        ///< Tot [ns?]
+ uint32_t fuAddress;  ///< Unique channel address
+
+ friend class boost::serialization::access;
+
+ template<class Archive>
+ void serialize(Archive& ar, const unsigned int /*version*/)
+ {
+   ar& fuAddress;
+   ar& fdTime;
+   ar& fdTot;
+ }
 
 #ifndef NO_ROOT
   ClassDefNV(CbmTofDigi, 3);
diff --git a/external/InstallParameter.cmake b/external/InstallParameter.cmake
index 74e6f43126..b368899baf 100644
--- a/external/InstallParameter.cmake
+++ b/external/InstallParameter.cmake
@@ -1,5 +1,7 @@
 set(PARAMETER_VERSION ab9972f137bc52efd58bf3672e670a2d4fbcb1be) # 2025/02/11
 set(PARAMETER_SRC_URL "https://git.cbm.gsi.de/CbmSoft/cbmroot_parameter.git")
+#set(PARAMETER_VERSION 96b2189ac86b3d49c32f63da76e68f66544bba55) # 2025/05/02, BMON hitfinder parameters for online
+#set(PARAMETER_SRC_URL "https://git.cbm.gsi.de/s.zharko/cbmroot_parameter.git")
 
 download_project_if_needed(PROJECT         Parameter_source
                            GIT_REPOSITORY  ${PARAMETER_SRC_URL}
-- 
GitLab


From 3008aca25872492c1448c601b057b988dff41ac4 Mon Sep 17 00:00:00 2001
From: "s.zharko@gsi.de" <s.zharko@gsi.de>
Date: Fri, 7 Feb 2025 18:45:23 +0100
Subject: [PATCH 2/4] BMON calibrator: flexible configuration

---
 algo/detectors/bmon/Calibrate.cxx    | 118 ++++++++++++++-------------
 algo/detectors/bmon/Calibrate.h      |  13 ++-
 algo/detectors/bmon/CalibrateSetup.h |  39 +++++----
 algo/global/Reco.cxx                 |   8 +-
 core/data/tof/CbmTofAddress.h        |   1 +
 external/InstallParameter.cmake      |   2 -
 6 files changed, 93 insertions(+), 88 deletions(-)

diff --git a/algo/detectors/bmon/Calibrate.cxx b/algo/detectors/bmon/Calibrate.cxx
index 70d99daeb4..53b40e6b13 100644
--- a/algo/detectors/bmon/Calibrate.cxx
+++ b/algo/detectors/bmon/Calibrate.cxx
@@ -13,6 +13,7 @@
 #include "CbmTofAddress.h"
 #include "util/TimingsFormat.h"
 
+#include <bitset>
 #include <chrono>
 
 using cbm::algo::bmon::Calibrate;
@@ -21,36 +22,56 @@ using fles::Subsystem;
 
 // ---------------------------------------------------------------------------------------------------------------------
 //
-Calibrate::Calibrate(CalibrateSetup setup) : fSetup(setup), fSmOffset(1, 0), fRpcOffset(1, 0)
+Calibrate::Calibrate(CalibrateSetup setup) : fSetup(setup), fSelectionBitsOffset(0)
 {
   // Initialize offset arrays for channel deadtime check
-  int32_t NbSm  = fSetup.NbSm;
-  int32_t NbRpc = fSetup.NbRpc;
-  for (int32_t Sm = 0; Sm < NbSm; Sm++) {
-    fSmOffset.push_back(fSmOffset.back() + NbRpc);
-    for (int32_t Rpc = 0; Rpc < NbRpc; Rpc++) {
-      int32_t NbChan = fSetup.rpcs[Sm * NbRpc + Rpc].chanPar.size();
-      fRpcOffset.push_back(fRpcOffset.back() + 2 * NbChan);  //Factor 2 for channel sides
+  int nDiamondsInSetup = fSetup.diamonds.size();
+  if (nDiamondsInSetup == 0) {
+    throw std::runtime_error("No diamonds found in the BMON calibration config");
+  }
+  if (!(fSetup.selectionMask) != (nDiamondsInSetup == 1)) {
+    throw std::runtime_error("Wrong diamond selection mask: for a single diamond it must be zero, and for multiple"
+                             " diamonds it must be non-zero");
+  }
+  if (nDiamondsInSetup > 1) {
+    // Define the selection bit offset
+    while (!((fSetup.selectionMask >> fSelectionBitsOffset) % 2)) {
+      ++fSelectionBitsOffset;
     }
+
+    // Sort the diamonds in the setup by their SM or Side or other distinguishing index
+    std::sort(fSetup.diamonds.begin(), fSetup.diamonds.end(), [&](const auto& lhs, const auto& rhs) {
+      return GetDiamondIndex(lhs.refAddress) < GetDiamondIndex(rhs.refAddress);
+    });
   }
 
-  // **** DEBUG: BEGIN
-  for (size_t iO = 0; iO < fSmOffset.size(); ++iO) {
-    L_(info) << "SM offset: " << iO << ", " << fSmOffset[iO];
+  // Remove the channel information from the reference address
+  for (auto& diamond : fSetup.diamonds) {
+    diamond.refAddress = CbmTofAddress::GetModFullId(diamond.refAddress);
   }
-  for (size_t iO = 0; iO < fRpcOffset.size(); ++iO) {
-    L_(info) << "RPC offset: " << iO << ", " << fRpcOffset[iO];
+
+  // Calculate the channel offsets, needed for the dead time
+  fChannelOffset = std::vector<size_t>(nDiamondsInSetup + 1, 0);  // the last element -- total number of channels
+  for (int32_t iD = 0; iD < nDiamondsInSetup; ++iD) {
+    fChannelOffset[iD + 1] = fChannelOffset[iD] + fSetup.diamonds[iD].chanPar.size();
   }
-  // **** DEBUG: END
+  fChannelDeadTime = std::vector<double>(fChannelOffset.back(), std::numeric_limits<double>::quiet_NaN());
 
-  fChannelDeadTime = std::vector<double>(fRpcOffset.back(), std::numeric_limits<double>::quiet_NaN());
+  // **** DEBUG: BEGIN
+  for (size_t iO = 0; iO < fChannelOffset.size(); ++iO) {
+    L_(info) << "Channel offset: diamond=" << iO << ", offset=" << fChannelOffset[iO];
+  }
+  L_(info) << "Selection mask: 0b" << std::bitset<32>(fSetup.selectionMask);
+  L_(info) << "Bits offset: " << static_cast<uint32_t>(fSelectionBitsOffset);
+  L_(info) << "Size of the channel dead time vector: " << fChannelDeadTime.size();
+  // **** DEBUG: END
 }
 
 // ---------------------------------------------------------------------------------------------------------------------
 //
 Calibrate::resultType Calibrate::operator()(gsl::span<const CbmBmonDigi> digiIn)
 {
-  xpu::push_timer("TofCalibrate");
+  xpu::push_timer("BmonCalibrate");
   xpu::t_add_bytes(digiIn.size_bytes());
 
   // --- Output data
@@ -60,79 +81,60 @@ Calibrate::resultType Calibrate::operator()(gsl::span<const CbmBmonDigi> digiIn)
   auto& monitor    = result.second;
   calDigiOut.reserve(digiIn.size());
 
+  // Reset the channel dead time
   std::fill(fChannelDeadTime.begin(), fChannelDeadTime.end(), std::numeric_limits<double>::quiet_NaN());
 
+  const auto& diamonds = fSetup.diamonds;
   for (const auto& digi : digiIn) {
     uint32_t address = static_cast<uint32_t>(digi.GetAddress());
-    const int32_t SmType{CbmTofAddress::GetSmType(address)};
-    const int32_t Sm{CbmTofAddress::GetSmId(address)};
-    const int32_t Rpc{CbmTofAddress::GetRpcId(address)};
-    const int NbRpc{fSetup.NbRpc};
-
-    int32_t Chan{CbmTofAddress::GetChannelId(address)};
-    int32_t Side{CbmTofAddress::GetChannelSide(address)};
-
-    // NOTE: Follow the hack in the CbmTofEventClusterizer::Exec() for the BMON digis and change the address
-    //       of calibrated digis. In general it is incorrect and needs further investigation.
-    uint32_t calAddress = address;
-    if (Side == 1) {
-      Side = 0;
-      Chan += 6;
-      calAddress = CbmTofAddress::GetUniqueAddress(Sm, Rpc, Chan, Side, SmType);
-    }
-
-    auto& rpcs = fSetup.rpcs;
-    if (SmType != fSetup.SmType || Sm * NbRpc + Rpc >= static_cast<int>(rpcs.size())) {
+    int32_t iChannel = CbmTofAddress::GetChannelId(address);
+    size_t iDiamond  = GetDiamondIndex(address);
+    if (iDiamond >= diamonds.size()
+        || static_cast<uint32_t>(CbmTofAddress::GetModFullId(address)) != diamonds[iDiamond].refAddress) {
       monitor.fDigiCalibUnknownRPC++;
-      L_(error) << "Unknown BMON digi address: " << CbmTofAddress::ToString(calAddress);
+      L_(error) << "Unknown BMON digi address: " << CbmTofAddress::ToString(address) << ", iDiamond = " << iDiamond;
       continue;
     }
 
-    CalibrateSetup::Rpc& rpcPar      = fSetup.rpcs[Sm * NbRpc + Rpc];
-    CalibrateSetup::Channel& chanPar = rpcPar.chanPar[Chan];
+    const auto& diamondPar = diamonds[iDiamond];
+    const auto& channelPar = diamondPar.chanPar[iChannel];
 
     // Check dead time
-    // FIXME: BMON digis (must) have only one side -> Fix the offsets, when mcbm2024_05 will be fixed
-    const size_t chanIdx  = fRpcOffset[fSmOffset[Sm] + Rpc] + Chan + Side * rpcPar.chanPar.size();
-    const double deadTime = fChannelDeadTime[chanIdx];
+    const size_t iChannelGlb = fChannelOffset[iDiamond] + iChannel;
+    const double deadTime    = fChannelDeadTime[iChannelGlb];
 
     if (!std::isnan(deadTime) && digi.GetTime() <= deadTime) {
-      fChannelDeadTime[chanIdx] = digi.GetTime() + rpcPar.channelDeadtime;
+      fChannelDeadTime[iChannelGlb] = digi.GetTime() + diamondPar.channelDeadtime;
       monitor.fDigiDeadTimeCount++;
       continue;
     }
-    fChannelDeadTime[chanIdx] = digi.GetTime() + rpcPar.channelDeadtime;
+    fChannelDeadTime[iChannelGlb] = digi.GetTime() + diamondPar.channelDeadtime;
 
     // Create calibrated digi
     CbmBmonDigi& pCalDigi = calDigiOut.emplace_back(digi);
-    pCalDigi.SetAddress(calAddress);
-
-    // Check if channel sides need to be swapped
-    if (rpcPar.swapChannelSides) {
-      pCalDigi.SetAddress(CbmTofAddress::GetUniqueAddress(Sm, Rpc, Chan, (0 == Side) ? 1 : 0, SmType));
-    }
+    pCalDigi.SetAddress(address);
 
     // calibrate Digi Time
-    pCalDigi.SetTime(pCalDigi.GetTime() - chanPar.vCPTOff[Side]);
+    pCalDigi.SetTime(pCalDigi.GetTime() - channelPar.vCPTOff);
 
     // subtract Offset
-    const double dTot = std::max(pCalDigi.GetCharge() - chanPar.vCPTotOff[Side], 0.001);
+    const double dTot = std::max(pCalDigi.GetCharge() - channelPar.vCPTotOff, 0.001);
 
     // calibrate Digi charge (corresponds to TOF ToT)
-    pCalDigi.SetCharge(dTot * chanPar.vCPTotGain[Side]);
+    pCalDigi.SetCharge(dTot * channelPar.vCPTotGain);
 
     // walk correction
-    std::vector<double>& walk = chanPar.vCPWalk[Side];
-    const double dTotBinSize  = (rpcPar.TOTMax - rpcPar.TOTMin) / rpcPar.numClWalkBinX;
-    int32_t iWx               = std::max((int32_t)((pCalDigi.GetCharge() - rpcPar.TOTMin) / dTotBinSize), 0);
-    iWx                       = std::min(iWx, rpcPar.numClWalkBinX - 1);
+    const std::vector<double>& walk = channelPar.vCPWalk;
+    const double dTotBinSize        = (diamondPar.TOTMax - diamondPar.TOTMin) / diamondPar.numClWalkBinX;
+    int32_t iWx                     = std::max((int32_t)((pCalDigi.GetCharge() - diamondPar.TOTMin) / dTotBinSize), 0);
+    iWx                             = std::min(iWx, diamondPar.numClWalkBinX - 1);
 
-    const double dDTot = (pCalDigi.GetCharge() - rpcPar.TOTMin) / dTotBinSize - (double) iWx - 0.5;
+    const double dDTot = (pCalDigi.GetCharge() - diamondPar.TOTMin) / dTotBinSize - (double) iWx - 0.5;
     double dWT         = walk[iWx];
 
     // linear interpolation to next bin
     if (dDTot > 0) {
-      if (iWx < rpcPar.numClWalkBinX - 1) {
+      if (iWx < diamondPar.numClWalkBinX - 1) {
         dWT += dDTot * (walk[iWx + 1] - walk[iWx]);
       }
     }
diff --git a/algo/detectors/bmon/Calibrate.h b/algo/detectors/bmon/Calibrate.h
index 291a6882b4..72d3914794 100644
--- a/algo/detectors/bmon/Calibrate.h
+++ b/algo/detectors/bmon/Calibrate.h
@@ -13,7 +13,6 @@
 #include "PartitionedVector.h"
 #include "bmon/CalibrateSetup.h"
 #include "tof/Calibrate.h"  // for the monitor data
-#include "tof/Clusterizer.h"
 
 #include <gsl/span>
 #include <optional>
@@ -42,9 +41,15 @@ namespace cbm::algo::bmon
     resultType operator()(gsl::span<const CbmBmonDigi> digiIn);
 
    private:
+    /// \brief  A digi address
+    size_t GetDiamondIndex(uint32_t address) const
+    {
+      return ((fSetup.selectionMask & address) >> fSelectionBitsOffset);
+    }
+
     CalibrateSetup fSetup;                 ///< Parameters of calibrator
-    std::vector<double> fChannelDeadTime;  ///< Storage for dead time check
-    std::vector<size_t> fSmOffset;         ///< Super module offset
-    std::vector<size_t> fRpcOffset;        ///< RPC offset
+    std::vector<size_t> fChannelOffset;    ///< Channel offset: offset for the channel index of each diamond
+    std::vector<double> fChannelDeadTime;  ///< Channel dead time
+    uint32_t fSelectionBitsOffset;         ///< Number of bits to ther right from the first bit in the selection mask
   };
 }  // namespace cbm::algo::bmon
diff --git a/algo/detectors/bmon/CalibrateSetup.h b/algo/detectors/bmon/CalibrateSetup.h
index 0d9dedd93c..26803bd010 100644
--- a/algo/detectors/bmon/CalibrateSetup.h
+++ b/algo/detectors/bmon/CalibrateSetup.h
@@ -22,12 +22,12 @@ namespace cbm::algo::bmon
   /// \struct  CalibrateSetup
   /// \brief   BMON calibration per channel
   struct CalibrateSetup {
-
+    // FIXME: remove 'v' from non-vector variable names
     struct Channel {
-      std::vector<double> vCPTOff;
-      std::vector<double> vCPTotGain;
-      std::vector<double> vCPTotOff;
-      std::vector<std::vector<double>> vCPWalk;
+      double vCPTOff;
+      double vCPTotGain;
+      double vCPTotOff;
+      std::vector<double> vCPWalk;
 
       CBM_YAML_PROPERTIES(yaml::Property(&Channel::vCPTOff, "vCPTOff", "CPT offset"),
                         yaml::Property(&Channel::vCPTotGain, "vCPTotGain", "CP time over threshold gain"),
@@ -35,7 +35,8 @@ namespace cbm::algo::bmon
                         yaml::Property(&Channel::vCPWalk, "vCPWalk", "CP walk correction", YAML::Block, YAML::Flow));
     };
 
-    struct Rpc {
+    struct Diamond {
+      uint32_t refAddress;
       int32_t numClWalkBinX;
       double TOTMax;
       double TOTMin;
@@ -43,25 +44,23 @@ namespace cbm::algo::bmon
       double channelDeadtime;
       std::vector<Channel> chanPar;
 
-      CBM_YAML_PROPERTIES(yaml::Property(&Rpc::numClWalkBinX, "numClWalkBinX", "number of walk correction bins"),
-                        yaml::Property(&Rpc::TOTMax, "TOTMax", "maximum time over threshold"),
-                        yaml::Property(&Rpc::TOTMin, "TOTMin", "minimum time over threshold"),
-                        yaml::Property(&Rpc::swapChannelSides, "swapChannelSides", "flag for swapping channel sides"),
-                        yaml::Property(&Rpc::channelDeadtime, "channelDeadtime", "channel dead time"),
-                        yaml::Property(&Rpc::chanPar, "chanPar", "channel parameters"));
+      CBM_YAML_PROPERTIES(
+                        yaml::Property(&Diamond::refAddress, "refAddress", "reference HW address to distinguish this BMON"),
+                        yaml::Property(&Diamond::numClWalkBinX, "numClWalkBinX", "number of walk correction bins"),
+                        yaml::Property(&Diamond::TOTMax, "TOTMax", "maximum time over threshold"),
+                        yaml::Property(&Diamond::TOTMin, "TOTMin", "minimum time over threshold"),
+                        yaml::Property(&Diamond::swapChannelSides, "swapChannelSides", "flag for swapping channel sides"),
+                        yaml::Property(&Diamond::channelDeadtime, "channelDeadtime", "channel dead time"),
+                        yaml::Property(&Diamond::chanPar, "chanPar", "channel parameters"));
     };
 
     /* Members */
-    int32_t SmType;
-    int32_t NbSm;
-    int32_t NbRpc;
-    std::vector<Rpc> rpcs;
+    uint32_t selectionMask;
+    std::vector<Diamond> diamonds;
 
     CBM_YAML_PROPERTIES(
-      yaml::Property(&CalibrateSetup::SmType, "SmType", "Super module type"),
-      yaml::Property(&CalibrateSetup::NbSm, "NbSm", "Number of SMs"),
-      yaml::Property(&CalibrateSetup::NbRpc, "NbRpc", "Number of RPCs"),
-      yaml::Property(&CalibrateSetup::rpcs, "rpcs", "Parameters of RPCs"));
+      yaml::Property(&CalibrateSetup::selectionMask, "selectionMask", "A mask to distinguish between different diamonds"),
+      yaml::Property(&CalibrateSetup::diamonds, "diamonds", "Parameters of each diamond"));
   };
 
 }  // namespace cbm::algo::bmon
diff --git a/algo/global/Reco.cxx b/algo/global/Reco.cxx
index 9bdc35bb6b..b16a463d07 100644
--- a/algo/global/Reco.cxx
+++ b/algo/global/Reco.cxx
@@ -390,7 +390,7 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
       std::set<uint32_t> bmonFoundAddressesUnp;  // found addresses among unpacked bmon digis
       std::set<uint32_t> bmonFoundAddressesCal;  // found addresses among clalibrated bmon digis
 
-      int nEvents = 20;  //events.size();
+      int nEvents = events.size();
       for (int iE = 0; iE < nEvents; ++iE) {
         //L_(info) << "-------------- event #" << iE;
         const auto& event = events[iE];
@@ -428,9 +428,9 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
           stsMinT = std::min(stsMinT, digi.GetTime());
           stsMaxT = std::max(stsMaxT, digi.GetTime());
         }
-        L_(info) << "BMON time: " << bmonMinT << ", " << bmonMaxT << ", " << (bmonMaxT - bmonMinT);
-        L_(info) << "TOF time: " << tofMinT << ", " << tofMaxT << ", " << (tofMaxT - tofMinT);
-        L_(info) << "STS time: " << stsMinT << ", " << stsMaxT << ", " << (stsMaxT - stsMinT);
+        //L_(info) << "BMON time: " << bmonMinT << ", " << bmonMaxT << ", " << (bmonMaxT - bmonMinT);
+        //L_(info) << "TOF time: " << tofMinT << ", " << tofMaxT << ", " << (tofMaxT - tofMinT);
+        //L_(info) << "STS time: " << stsMinT << ", " << stsMaxT << ", " << (stsMaxT - stsMinT);
       }
       L_(info) << "!!!! Found addresses in BMON after unpacking: ";
       for (auto address : bmonFoundAddressesUnp) {
diff --git a/core/data/tof/CbmTofAddress.h b/core/data/tof/CbmTofAddress.h
index 3abf9b1703..d174064bf2 100644
--- a/core/data/tof/CbmTofAddress.h
+++ b/core/data/tof/CbmTofAddress.h
@@ -182,6 +182,7 @@ public:
   static std::string ToString(int32_t address);
 
  private:
+  // FIXME: SZh 7.2.2025: Make these fields constexpr
   /**
    ** To adapt the address sub-fields repartition in size,
    ** you just need to change number of bits of the two sub-fields changing length.
diff --git a/external/InstallParameter.cmake b/external/InstallParameter.cmake
index b368899baf..74e6f43126 100644
--- a/external/InstallParameter.cmake
+++ b/external/InstallParameter.cmake
@@ -1,7 +1,5 @@
 set(PARAMETER_VERSION ab9972f137bc52efd58bf3672e670a2d4fbcb1be) # 2025/02/11
 set(PARAMETER_SRC_URL "https://git.cbm.gsi.de/CbmSoft/cbmroot_parameter.git")
-#set(PARAMETER_VERSION 96b2189ac86b3d49c32f63da76e68f66544bba55) # 2025/05/02, BMON hitfinder parameters for online
-#set(PARAMETER_SRC_URL "https://git.cbm.gsi.de/s.zharko/cbmroot_parameter.git")
 
 download_project_if_needed(PROJECT         Parameter_source
                            GIT_REPOSITORY  ${PARAMETER_SRC_URL}
-- 
GitLab


From 4d3d93c35591bed35f8427be13d8d00442ed3c78 Mon Sep 17 00:00:00 2001
From: "s.zharko@gsi.de" <s.zharko@gsi.de>
Date: Sun, 9 Feb 2025 15:48:30 +0100
Subject: [PATCH 3/4] online: BMON hitfinder

---
 algo/CMakeLists.txt                   |   8 +-
 algo/detectors/bmon/Calibrate.cxx     |   4 +-
 algo/detectors/bmon/Calibrate.h       |   5 +-
 algo/detectors/bmon/CalibrateSetup.h  |   4 +-
 algo/detectors/bmon/Clusterizer.cxx   |  86 ++++++++++++++++++++
 algo/detectors/bmon/Clusterizer.h     |  57 +++++++++++++
 algo/detectors/bmon/ClusterizerPars.h |  25 ++++++
 algo/detectors/bmon/Hit.h             | 103 +++++++++++++++++++++++
 algo/detectors/bmon/Hitfind.cxx       | 112 ++++++++++++++++++++++++++
 algo/detectors/bmon/Hitfind.h         |  60 ++++++++++++++
 algo/detectors/bmon/HitfindSetup.h    |  42 ++++++++++
 algo/global/Reco.cxx                  |  67 ++++-----------
 algo/global/Reco.h                    |   2 +
 core/data/bmon/CbmBmonDigi.h          |  10 ++-
 core/data/tof/CbmTofAddress.cxx       |  12 ++-
 core/data/tof/CbmTofAddress.h         |  76 ++++++++++++++++-
 16 files changed, 606 insertions(+), 67 deletions(-)
 create mode 100644 algo/detectors/bmon/Clusterizer.cxx
 create mode 100644 algo/detectors/bmon/Clusterizer.h
 create mode 100644 algo/detectors/bmon/ClusterizerPars.h
 create mode 100644 algo/detectors/bmon/Hit.h
 create mode 100644 algo/detectors/bmon/Hitfind.cxx
 create mode 100644 algo/detectors/bmon/Hitfind.h
 create mode 100644 algo/detectors/bmon/HitfindSetup.h

diff --git a/algo/CMakeLists.txt b/algo/CMakeLists.txt
index 58ac4397e4..bc9883747a 100644
--- a/algo/CMakeLists.txt
+++ b/algo/CMakeLists.txt
@@ -96,6 +96,11 @@ set(SRCS
   evselector/DigiEventSelectorConfig.cxx
   unpack/CommonUnpacker.cxx
   detectors/bmon/Calibrate.cxx
+  detectors/bmon/Clusterizer.cxx
+  detectors/bmon/Hitfind.cxx
+  detectors/bmon/ReadoutConfig.cxx
+  detectors/bmon/Unpack.cxx
+  detectors/bmon/UnpackMS.cxx
   detectors/sts/ChannelMaskSet.cxx
   detectors/sts/ReadoutConfig.cxx
   detectors/sts/HitfinderChain.cxx
@@ -114,9 +119,6 @@ set(SRCS
   detectors/tof/UnpackMS.cxx
   detectors/tof/Hitfind.cxx
   detectors/tof/TrackingInterface.cxx
-  detectors/bmon/ReadoutConfig.cxx
-  detectors/bmon/Unpack.cxx
-  detectors/bmon/UnpackMS.cxx
   detectors/trd/Cluster.cxx
   detectors/trd/Clusterizer.cxx
   detectors/trd/Cluster2D.cxx
diff --git a/algo/detectors/bmon/Calibrate.cxx b/algo/detectors/bmon/Calibrate.cxx
index 53b40e6b13..1a2a6af79d 100644
--- a/algo/detectors/bmon/Calibrate.cxx
+++ b/algo/detectors/bmon/Calibrate.cxx
@@ -47,7 +47,7 @@ Calibrate::Calibrate(CalibrateSetup setup) : fSetup(setup), fSelectionBitsOffset
 
   // Remove the channel information from the reference address
   for (auto& diamond : fSetup.diamonds) {
-    diamond.refAddress = CbmTofAddress::GetModFullId(diamond.refAddress);
+    diamond.refAddress &= ~CbmTofAddress::GetChannelIdBitmask();
   }
 
   // Calculate the channel offsets, needed for the dead time
@@ -90,7 +90,7 @@ Calibrate::resultType Calibrate::operator()(gsl::span<const CbmBmonDigi> digiIn)
     int32_t iChannel = CbmTofAddress::GetChannelId(address);
     size_t iDiamond  = GetDiamondIndex(address);
     if (iDiamond >= diamonds.size()
-        || static_cast<uint32_t>(CbmTofAddress::GetModFullId(address)) != diamonds[iDiamond].refAddress) {
+        || (address & ~CbmTofAddress::GetChannelIdBitmask()) != diamonds[iDiamond].refAddress) {
       monitor.fDigiCalibUnknownRPC++;
       L_(error) << "Unknown BMON digi address: " << CbmTofAddress::ToString(address) << ", iDiamond = " << iDiamond;
       continue;
diff --git a/algo/detectors/bmon/Calibrate.h b/algo/detectors/bmon/Calibrate.h
index 72d3914794..0e602e6b4c 100644
--- a/algo/detectors/bmon/Calibrate.h
+++ b/algo/detectors/bmon/Calibrate.h
@@ -41,7 +41,8 @@ namespace cbm::algo::bmon
     resultType operator()(gsl::span<const CbmBmonDigi> digiIn);
 
    private:
-    /// \brief  A digi address
+    /// \brief Returns an index of the diamond by the address
+    /// \param address  A hardware address of the digi
     size_t GetDiamondIndex(uint32_t address) const
     {
       return ((fSetup.selectionMask & address) >> fSelectionBitsOffset);
@@ -49,7 +50,7 @@ namespace cbm::algo::bmon
 
     CalibrateSetup fSetup;                 ///< Parameters of calibrator
     std::vector<size_t> fChannelOffset;    ///< Channel offset: offset for the channel index of each diamond
-    std::vector<double> fChannelDeadTime;  ///< Channel dead time
+    std::vector<double> fChannelDeadTime;  ///< Dead time, stored for a channel
     uint32_t fSelectionBitsOffset;         ///< Number of bits to ther right from the first bit in the selection mask
   };
 }  // namespace cbm::algo::bmon
diff --git a/algo/detectors/bmon/CalibrateSetup.h b/algo/detectors/bmon/CalibrateSetup.h
index 26803bd010..eeeb58e178 100644
--- a/algo/detectors/bmon/CalibrateSetup.h
+++ b/algo/detectors/bmon/CalibrateSetup.h
@@ -40,7 +40,6 @@ namespace cbm::algo::bmon
       int32_t numClWalkBinX;
       double TOTMax;
       double TOTMin;
-      bool swapChannelSides;
       double channelDeadtime;
       std::vector<Channel> chanPar;
 
@@ -49,7 +48,6 @@ namespace cbm::algo::bmon
                         yaml::Property(&Diamond::numClWalkBinX, "numClWalkBinX", "number of walk correction bins"),
                         yaml::Property(&Diamond::TOTMax, "TOTMax", "maximum time over threshold"),
                         yaml::Property(&Diamond::TOTMin, "TOTMin", "minimum time over threshold"),
-                        yaml::Property(&Diamond::swapChannelSides, "swapChannelSides", "flag for swapping channel sides"),
                         yaml::Property(&Diamond::channelDeadtime, "channelDeadtime", "channel dead time"),
                         yaml::Property(&Diamond::chanPar, "chanPar", "channel parameters"));
     };
@@ -59,7 +57,7 @@ namespace cbm::algo::bmon
     std::vector<Diamond> diamonds;
 
     CBM_YAML_PROPERTIES(
-      yaml::Property(&CalibrateSetup::selectionMask, "selectionMask", "A mask to distinguish between different diamonds"),
+      yaml::Property(&CalibrateSetup::selectionMask, "selectionMask", "A bit mask to distinguish between different diamonds"),
       yaml::Property(&CalibrateSetup::diamonds, "diamonds", "Parameters of each diamond"));
   };
 
diff --git a/algo/detectors/bmon/Clusterizer.cxx b/algo/detectors/bmon/Clusterizer.cxx
new file mode 100644
index 0000000000..2d48416e35
--- /dev/null
+++ b/algo/detectors/bmon/Clusterizer.cxx
@@ -0,0 +1,86 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   Clusterizer.cxx
+/// \brief  A clusterizer algorithm for BMON (implementation)
+/// \since  07.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+
+#include "bmon/Clusterizer.h"
+
+#include "AlgoFairloggerCompat.h"
+#include "CbmBmonDigi.h"
+#include "CbmTofAddress.h"
+
+#include <algorithm>
+#include <iomanip>
+#include <iostream>
+#include <sstream>
+
+using cbm::algo::bmon::Clusterizer;
+
+// ---------------------------------------------------------------------------------------------------------------------
+//
+Clusterizer::Output_t Clusterizer::operator()(const Clusterizer::Input_t& digis)
+{
+  // Description:
+  // The input array of digis is traced through until the last element. If the current digi and the next digi are
+  // close in time and have neighboring channels, a single hit is produced. Otherwise a hit is produced from a
+  // single digi. The complexity of the algorithm is O(nDigis). Requirements: digis must be sorted in time
+  Output_t res;
+  if (digis.empty()) {
+    return res;
+  }
+
+  auto& hits        = std::get<0>(res);
+  auto& digiIndices = std::get<1>(res);
+  hits.reserve(digis.size());
+  digiIndices.reserve(digis.size());
+  auto itLast = std::prev(digis.end());  // iterator pointing to the last digi
+  bool bUsedWithPrevious{false};         // A flag: if the current digi was used together with the previous one
+  for (auto it = digis.begin(), in = it; it != itLast; ++it) {
+    if (bUsedWithPrevious) {
+      // skip a digi, if it was already used
+      bUsedWithPrevious = false;
+      continue;
+    }
+    in                = std::next(it);
+    const auto& digiT = it->first;
+    const auto& digiN = in->first;
+    if (digiN.GetTime() - digiT.GetTime() < fParams.fdMaxTimeDist
+        && abs(digiN.GetChannel() - digiT.GetChannel()) == 1) {
+      // A hit consisting from two digis is found
+      hits.emplace_back(fParams.fAddress, digiT, digiN);
+      digiIndices.emplace_back(it->second);
+      bUsedWithPrevious = true;
+    }
+    else {
+      // A hit consisting from a single digi
+      hits.emplace_back(fParams.fAddress, digiT);
+      digiIndices.emplace_back(it->second);
+    }
+  }
+  if (!bUsedWithPrevious) {
+    // Create a hit from the last digi
+    hits.emplace_back(fParams.fAddress, itLast->first);
+    digiIndices.emplace_back(itLast->second);
+  }
+  return res;
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+//
+bool Clusterizer::SelectDigi(const CbmBmonDigi& digi) const
+{
+  // Dead channel cut
+  if (fParams.fDeadStrips & (1 << CbmTofAddress::GetChannelId(digi.GetAddress()))) {
+    return false;
+  }
+
+  // ??? Other cuts ??? Charge threshold? Also dead strips can be accounted already on the calibrator level, together
+  //     with the cuts
+
+  return true;
+}
diff --git a/algo/detectors/bmon/Clusterizer.h b/algo/detectors/bmon/Clusterizer.h
new file mode 100644
index 0000000000..86b3c76e3b
--- /dev/null
+++ b/algo/detectors/bmon/Clusterizer.h
@@ -0,0 +1,57 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   Clusterizer.h
+/// \brief  A clusterizer algorithm for BMON
+/// \since  07.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+#pragma once
+
+#include "base/PODVector.h"
+#include "bmon/ClusterizerPars.h"
+#include "bmon/Hit.h"
+
+#include <cmath>
+#include <cstdint>
+#include <memory>
+#include <utility>
+#include <vector>
+
+class CbmBmonDigi;
+
+namespace cbm::algo::bmon
+{
+  /// \class Clusterizer
+  /// \brief A clusterizer algorithm for a BMON
+  ///
+  /// The algorithm is executed on a single hardware module
+  class Clusterizer {
+   public:
+    using Input_t  = std::vector<std::pair<CbmBmonDigi, int32_t>>;     ///< Input type
+    using Output_t = std::pair<std::vector<Hit>, PODVector<int32_t>>;  ///< Output type
+
+    /// \brief Constructor
+    /// \param params RPC parameters
+    explicit Clusterizer(ClusterizerPars params) : fParams(params) {}
+
+    /// \brief Hit building function
+    Output_t operator()(const Input_t& digisInput);
+
+    /// \brief  Applies selection on a digis
+    /// \return true   Digi is selected
+    /// \return false  Digi is cut out
+    bool SelectDigi(const CbmBmonDigi& digi) const;
+
+   private:
+    /// \brief Creates a hit from a single digi
+    Hit CreateHit(const CbmBmonDigi& digi) const;
+
+    /// \brief Creates a hit from two digis
+    Hit CreateHit(const CbmBmonDigi& digiL, const CbmBmonDigi& digiR) const;
+
+
+    ClusterizerPars fParams;  ///< parameters container
+  };
+}  // namespace cbm::algo::bmon
diff --git a/algo/detectors/bmon/ClusterizerPars.h b/algo/detectors/bmon/ClusterizerPars.h
new file mode 100644
index 0000000000..0e2e65ecf9
--- /dev/null
+++ b/algo/detectors/bmon/ClusterizerPars.h
@@ -0,0 +1,25 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   ClusterizerPars.h
+/// \brief  BMON clusterizer parameters
+/// \since  07.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+#pragma once
+
+#include <cstdint>
+#include <vector>
+
+namespace cbm::algo::bmon
+{
+  /// \struct ClusterizerPars
+  /// \brief  Clusterizer parameters for Diamond
+  struct ClusterizerPars {
+    uint32_t fAddress;     ///< Address of the diamond (the channel bit field is 0)
+    uint32_t fDeadStrips;  ///< Dead strip bitmask
+    double fdMaxTimeDist;  ///< Maximum time difference between two consecutive digis to form a single hit
+    double fTimeRes;       ///< Time resolution
+  };
+}  // namespace cbm::algo::bmon
diff --git a/algo/detectors/bmon/Hit.h b/algo/detectors/bmon/Hit.h
new file mode 100644
index 0000000000..e4e1aef243
--- /dev/null
+++ b/algo/detectors/bmon/Hit.h
@@ -0,0 +1,103 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   Hitfind.h
+/// \brief  A BMON hit class
+/// \since  07.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+#pragma once
+
+#include "CbmBmonDigi.h"
+
+#include <boost/serialization/access.hpp>
+
+#include <cstdint>
+#include <vector>
+
+namespace cbm::algo::bmon
+{
+  /// \class  Hit
+  /// \brief  A BMON hit
+  class Hit {
+   public:
+    /// \brief Constructor from a single digi
+    /// \param address  Address of the diamond
+    /// \param digi     A digi
+    Hit(uint32_t address, const CbmBmonDigi& digi) : fAddress(address), fNofChannels(1)
+    {
+      fTime      = digi.GetTime();
+      fTimeError = 0.;  // FIXME: provide a rule to set an error
+    }
+
+    /// \brief Constructor from two digis
+    /// \param address  Address of the diamond
+    /// \param digiL    First digi
+    /// \param digiR    Second digi
+    Hit(uint32_t address, const CbmBmonDigi& digiL, const CbmBmonDigi& digiR) : fAddress(address), fNofChannels(2)
+    {
+      double weightSum = digiL.GetCharge() + digiR.GetCharge();
+      fTime            = (digiL.GetTime() * digiL.GetCharge() + digiR.GetTime() * digiR.GetCharge()) / weightSum;
+      fTimeError       = 0.;  // FIXME: provide a rule to set an error
+    }
+
+    /// \brief Constructor
+    /// \param address   Address of diamond (the channel is not stored)
+    /// \param time      Time of the hit [ns]
+    /// \param timeError Time error of the hit [ns]
+    /// \param nChannels Number of channels used (either one or two)
+    Hit(uint32_t address, double time, double timeError, uint8_t nChannels)
+      : fTime(time)
+      , fTimeError(timeError)
+      , fAddress(address)
+      , fNofChannels(nChannels)
+    {
+    }
+
+    /// \brief Gets hardware address
+    uint32_t GetAddress() const { return fAddress; }
+
+    /// \brief Gets number of channels
+    uint8_t GetNofChannels() const { return fNofChannels; }
+
+    /// \brief Gets time [ns]
+    double GetTime() const { return fTime; }
+
+    /// \brief Gets time error [ns]
+    double GetTimeError() const { return fTimeError; }
+
+    /// \brief Sets address
+    /// \param address Hardware address
+    void SetAddress(uint32_t address) { fAddress = address; }
+
+    /// \brief Sets number of channels
+    /// \param nofChannels Number of channels (digis), used to create a hit
+    void SetNofChannels(uint8_t nofChannels) { fNofChannels = nofChannels; }
+
+    /// \brief Sets time
+    /// \param  time  Hit time [ns]
+    void SetTime(double time) { fTime = time; }
+
+    /// \brief Sets time error
+    /// \param  timeError  Hit time error [ns]
+    void SetTimeError(double timeError) { fTimeError = timeError; }
+
+   private:
+    double fTime{0.};         ///< Time [ns]
+    double fTimeError{0.};    ///< Time error [ns]
+    uint32_t fAddress{0};     ///< Assigned hit address
+    uint8_t fNofChannels{0};  ///< Number of channels used
+
+    /// \brief Boost serialization function
+    friend class boost::serialization::access;
+    template<class Archive>
+    void serialize(Archive& ar, unsigned int /*version*/)
+    {
+      ar& fTime;
+      ar& fTimeError;
+      ar& fAddress;
+      ar& fNofChannels;
+    }
+  };
+}  // namespace cbm::algo::bmon
diff --git a/algo/detectors/bmon/Hitfind.cxx b/algo/detectors/bmon/Hitfind.cxx
new file mode 100644
index 0000000000..b1e6794c8e
--- /dev/null
+++ b/algo/detectors/bmon/Hitfind.cxx
@@ -0,0 +1,112 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   Hitfind.cxx
+/// \brief  A BMON hitfinder steering class (implementation)
+/// \since  07.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+
+#include "Hitfind.h"
+
+#include "AlgoFairloggerCompat.h"
+#include "compat/OpenMP.h"
+#include "util/TimingsFormat.h"
+
+#include <chrono>
+
+using cbm::algo::bmon::ClusterizerPars;
+using cbm::algo::bmon::Hitfind;
+using cbm::algo::bmon::HitfindSetup;
+using fles::Subsystem;
+
+// ---------------------------------------------------------------------------------------------------------------------
+//
+Hitfind::Hitfind(HitfindSetup setup, uint32_t nThreads) : fNofThreads(nThreads)
+{
+  // Create one algorithm per diamond and per thread
+  size_t nDiamondsInSetup{setup.diamonds.size()};
+  if (nDiamondsInSetup == 0) {
+    throw std::runtime_error("No diamonds found in the BMON calibration config");
+  }
+  if (!(setup.selectionMask) != (nDiamondsInSetup == 1)) {
+    throw std::runtime_error("Wrong diamond selection mask: for a single diamond it must be zero, and for multiple"
+                             " diamonds it must be non-zero");
+  }
+
+  if (nDiamondsInSetup > 1) {
+    // Define the selection bit offset
+    while (!((setup.selectionMask >> fSelectionBitsOffset) % 2)) {
+      ++fSelectionBitsOffset;
+    }
+
+    // Sort the diamonds in the setup by their SM or Side or other distinguishing index
+    std::sort(setup.diamonds.begin(), setup.diamonds.end(), [&](const auto& lhs, const auto& rhs) {
+      return GetDiamondIndex(lhs.refAddress) < GetDiamondIndex(rhs.refAddress);
+    });
+  }
+
+  fSelectionBitmask = setup.selectionMask;
+  fDiamondAddress   = PODVector<uint32_t>(nDiamondsInSetup, 0);
+
+  // Store diamond address
+  for (size_t iDiamond = 0; iDiamond < nDiamondsInSetup; ++iDiamond) {
+    fDiamondAddress[iDiamond] = setup.diamonds[iDiamond].refAddress & ~CbmTofAddress::GetChannelIdBitmask();
+  }
+
+  // Create and configure clusterizer algorithms per thread and per diamond
+  fAlgo = std::vector<std::vector<Clusterizer>>(fNofThreads, std::vector<Clusterizer>());
+  for (auto& algoPerThread : fAlgo) {
+    algoPerThread.reserve(nDiamondsInSetup);
+    for (size_t iDiamond = 0; iDiamond < nDiamondsInSetup; ++iDiamond) {
+      auto par               = std::make_unique<ClusterizerPars>();
+      const auto& diamondPar = setup.diamonds[iDiamond];
+      par->fAddress          = diamondPar.refAddress;
+      par->fDeadStrips       = diamondPar.deadStrips;
+      par->fdMaxTimeDist     = diamondPar.maxTimeDist;
+      par->fTimeRes          = diamondPar.timeRes;
+      algoPerThread.emplace_back(std::move(*par));
+    }
+  }
+  L_(info) << "--- Configured hitfinder algorithms for BMON.";
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+//
+Hitfind::Output_t Hitfind::operator()(gsl::span<CbmBmonDigi> digisIn, uint32_t iThread)
+{
+  Output_t res     = {};
+  auto& resHits    = std::get<0>(res);
+  auto& resMoni    = std::get<1>(res);
+  auto& resDigiIds = std::get<2>(res);
+
+  auto& algoPerThread = fAlgo[iThread];
+
+  // Distribute digis over diamonds, apply cuts on this level (maybe the Calibrator is a more proper place for it)
+  size_t nDiamonds  = algoPerThread.size();
+  auto vDigiStorage = std::vector<Clusterizer::Input_t>(nDiamonds, Clusterizer::Input_t(0));
+  for (int32_t iDigi = 0; iDigi < static_cast<int32_t>(digisIn.size()); ++iDigi) {
+    const auto& digi = digisIn[iDigi];
+    size_t iDiamond  = GetDiamondIndex(digi.GetAddress());
+    if (algoPerThread[iDiamond].SelectDigi(digi)) {
+      vDigiStorage[iDiamond].emplace_back(digi, iDigi);
+    }
+  }
+
+  // NOTE: I see no sense in storing a full channel address for different BMON hits,
+  //       so for each hit the diamond address will be assigned: address & ~CbmTofAddress::GetChannelIdBitmask()
+  PODVector<Hit> vHitsFlat;                                    // storage for clusters
+  PODVector<size_t> vNhitsPerDiamond(fDiamondAddress.size());  // number of hits per diamond
+
+  for (size_t iDiamond = 0; iDiamond < algoPerThread.size(); ++iDiamond) {
+    auto [hits, digiIds]       = algoPerThread[iDiamond](vDigiStorage[iDiamond]);
+    vNhitsPerDiamond[iDiamond] = hits.size();
+    vHitsFlat.insert(vHitsFlat.end(), std::make_move_iterator(hits.begin()), std::make_move_iterator(hits.end()));
+    resDigiIds.insert(resDigiIds.end(), std::make_move_iterator(digiIds.begin()),
+                      std::make_move_iterator(digiIds.end()));
+  }
+
+  resHits = PartitionedVector(std::move(vHitsFlat), vNhitsPerDiamond, fDiamondAddress);
+  return res;
+}
diff --git a/algo/detectors/bmon/Hitfind.h b/algo/detectors/bmon/Hitfind.h
new file mode 100644
index 0000000000..68d7d3a9b5
--- /dev/null
+++ b/algo/detectors/bmon/Hitfind.h
@@ -0,0 +1,60 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   Hitfind.h
+/// \brief  BMON hitfinder steering class
+/// \since  07.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+#pragma once
+
+#include "CbmBmonDigi.h"
+#include "PODVector.h"
+#include "PartitionedVector.h"
+#include "bmon/Clusterizer.h"
+#include "bmon/HitfindSetup.h"
+#include "tof/Hitfind.h"  // for tof::HitfindMonitorData
+
+#include <gsl/span>
+#include <optional>
+#include <sstream>
+#include <vector>
+
+#include <xpu/host.h>
+
+namespace cbm::algo::bmon
+{
+  /// \brief TOF hit-finder monitor, re-used for BMON
+  using HitfindMonitorData = tof::HitfindMonitorData;
+
+  /// \class Hitfind
+  /// \brief Hit-finder steering class for BMON
+  class Hitfind {
+   public:
+    /// \brief Output format
+    using Output_t = std::tuple<PartitionedVector<Hit>, HitfindMonitorData, PODVector<int32_t>>;
+
+    /// \brief Constructor
+    /// \param  setup     Setup configuration
+    /// \param  nThreads  Number of threads (for event-based mode)
+    explicit Hitfind(HitfindSetup setup, uint32_t nThreads = 1);
+
+    /// \brief Algorithm execution operator
+    /// \param digiIn   A portion of digis in TS/event
+    /// \param iThread  Index of thread
+    Output_t operator()(gsl::span<CbmBmonDigi> digisIn, uint32_t iThread = 0);
+
+   private:  // members
+    /// \brief Returns an index of the diamond by the address
+    /// \param address  A hardware address of the digi
+    size_t GetDiamondIndex(uint32_t address) const { return ((fSelectionBitmask & address) >> fSelectionBitsOffset); }
+
+    uint32_t fNofThreads;           ///< Number of threads
+    uint32_t fSelectionBitsOffset;  ///< Number of bits to ther right from the first bit in the selection mask
+    uint32_t fSelectionBitmask;     ///< Selection bitmask
+
+    std::vector<std::vector<Clusterizer>> fAlgo;  ///< Clusterizer algorithms [thread][diamond]
+    PODVector<uint32_t> fDiamondAddress;          ///< Diamond address
+  };
+}  // namespace cbm::algo::bmon
diff --git a/algo/detectors/bmon/HitfindSetup.h b/algo/detectors/bmon/HitfindSetup.h
new file mode 100644
index 0000000000..c98cc163ff
--- /dev/null
+++ b/algo/detectors/bmon/HitfindSetup.h
@@ -0,0 +1,42 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   HitfindSetup.h
+/// \brief  Parameters of the BMON hitfinder
+/// \since  06.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+
+#include "Definitions.h"
+#include "yaml/Property.h"
+
+#include <string>
+#include <vector>
+
+namespace cbm::algo::bmon
+{
+  /// \struct HitfindSetup
+  /// \brief  Parameters for the BMON hitfinder
+  struct HitfindSetup {
+    struct Diamond {
+      u32 refAddress;
+      u32 deadStrips;
+      double maxTimeDist;
+      double timeRes;
+
+      CBM_YAML_PROPERTIES(
+                        yaml::Property(&Diamond::refAddress, "refAddress", "reference address of the diamond"),
+                        yaml::Property(&Diamond::deadStrips, "deadStrips", "bit mask for dead strips"),
+                        yaml::Property(&Diamond::maxTimeDist, "maxTimeDist", "maximum time distance"),
+                        yaml::Property(&Diamond::timeRes, "timeRes", "time resolution"));
+    };
+
+    uint32_t selectionMask;
+    std::vector<Diamond> diamonds;
+
+    CBM_YAML_PROPERTIES(
+      yaml::Property(&HitfindSetup::selectionMask, "selectionMask", "A bit mask to distinguish between different diamonds"),
+      yaml::Property(&HitfindSetup::diamonds, "diamonds", "Parameters of diamonds"));
+  };
+}  // namespace cbm::algo::bmon
diff --git a/algo/global/Reco.cxx b/algo/global/Reco.cxx
index b16a463d07..5b2698cb3f 100644
--- a/algo/global/Reco.cxx
+++ b/algo/global/Reco.cxx
@@ -15,6 +15,7 @@
 #include "StsDigiQa.h"
 #include "TrackingSetup.h"
 #include "bmon/Calibrate.h"
+#include "bmon/Hitfind.h"
 #include "bmon/ReadoutConfig.h"
 #include "bmon/Unpack.h"
 #include "ca/TrackingChain.h"
@@ -214,8 +215,8 @@ void Reco::Init(const Options& opts)
     auto calibSetup = yaml::ReadFromFile<bmon::CalibrateSetup>(opts.ParamsDir() / parFiles.bmon.calibrate);
     fBmonCalibrator = std::make_unique<bmon::Calibrate>(calibSetup);
 
-    //auto hitfindSetup = yaml::ReadFromFile<bmon::HitfindSetup>(opts.ParamsDir() / parFiles.bmon.hitfinder);
-    //fBmonHitFinder     = std::make_unique<bmon::Hitfind>(hitfindSetup);
+    auto hitfindSetup = yaml::ReadFromFile<bmon::HitfindSetup>(opts.ParamsDir() / parFiles.bmon.hitfinder);
+    fBmonHitFinder    = std::make_unique<bmon::Hitfind>(hitfindSetup);
   }
 
   // TOF Hitfinder
@@ -386,60 +387,26 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
     }
 
     // ***** DEBUG: BEGIN
-    if constexpr (1) {
-      std::set<uint32_t> bmonFoundAddressesUnp;  // found addresses among unpacked bmon digis
-      std::set<uint32_t> bmonFoundAddressesCal;  // found addresses among clalibrated bmon digis
-
+    if constexpr (0) {
       int nEvents = events.size();
+      size_t nBmonHitsOneChannel{0};
+      size_t nBmonHitsTwoChannels{0};
       for (int iE = 0; iE < nEvents; ++iE) {
-        //L_(info) << "-------------- event #" << iE;
         const auto& event = events[iE];
-        for (const auto& digi : event.fBmon) {
-          bmonFoundAddressesUnp.insert(digi.GetAddress());
-        }
-
         // Calibrate TOF digis:
-        auto [tofDigis, tofCalMonitor]   = (*fTofCalibrator)(event.fTof);
-        auto [bmonDigis, bmonCalMonitor] = (*fBmonCalibrator)(event.fBmon);
-
-        for (const auto& digi : bmonDigis) {
-          bmonFoundAddressesCal.insert(digi.GetAddress());
-        }
-
-        double bmonMinT = 1e+20;
-        double bmonMaxT = -1e+20;
-        for (const auto& digi : bmonDigis) {
-          //L_(info) << "BMON: " << digi.GetTime();
-          bmonMinT = std::min(bmonMinT, digi.GetTime());
-          bmonMaxT = std::max(bmonMaxT, digi.GetTime());
-        }
-        double tofMinT = 1e+20;
-        double tofMaxT = -1e+20;
-        for (const auto& digi : tofDigis) {
-          //L_(info) << "TOF: " << digi.GetTime();
-          tofMinT = std::min(tofMinT, digi.GetTime());
-          tofMaxT = std::max(tofMaxT, digi.GetTime());
-        }
-        double stsMinT       = 1e+20;
-        double stsMaxT       = -1e+20;
-        const auto& stsDigis = event.fSts;
-        for (const auto& digi : stsDigis) {
-          //L_(info) << "STS: " << digi.GetTime();
-          stsMinT = std::min(stsMinT, digi.GetTime());
-          stsMaxT = std::max(stsMaxT, digi.GetTime());
+        auto [bmonDigis, bmonCalMonitor]         = (*fBmonCalibrator)(event.fBmon);
+        auto [bmonHits, hitmonitor, digiindices] = (*fBmonHitFinder)(bmonDigis);
+        for (const auto& hit : bmonHits.Data()) {
+          if (hit.GetNofChannels() == 1) {
+            ++nBmonHitsOneChannel;
+          }
+          else {
+            ++nBmonHitsTwoChannels;
+          }
         }
-        //L_(info) << "BMON time: " << bmonMinT << ", " << bmonMaxT << ", " << (bmonMaxT - bmonMinT);
-        //L_(info) << "TOF time: " << tofMinT << ", " << tofMaxT << ", " << (tofMaxT - tofMinT);
-        //L_(info) << "STS time: " << stsMinT << ", " << stsMaxT << ", " << (stsMaxT - stsMinT);
-      }
-      L_(info) << "!!!! Found addresses in BMON after unpacking: ";
-      for (auto address : bmonFoundAddressesUnp) {
-        L_(info) << " - " << CbmTofAddress::ToString(address);
-      }
-      L_(info) << "!!!! Found addresses in BMON after calibration: ";
-      for (auto address : bmonFoundAddressesCal) {
-        L_(info) << " - " << CbmTofAddress::ToString(address);
       }
+      L_(info) << "!!!! BMON hits with two channels: " << nBmonHitsTwoChannels << " / "
+               << (nBmonHitsTwoChannels + nBmonHitsOneChannel);
     }
     // ***** DEBUG: END
 
diff --git a/algo/global/Reco.h b/algo/global/Reco.h
index 982a95cfa4..19027c8368 100644
--- a/algo/global/Reco.h
+++ b/algo/global/Reco.h
@@ -28,6 +28,7 @@ namespace cbm::algo
   {
     class Unpack;
     class Calibrate;
+    class Hitfind;
   }
 
   namespace much
@@ -148,6 +149,7 @@ namespace cbm::algo
     // BMON
     std::unique_ptr<bmon::Unpack> fBmonUnpack;
     std::unique_ptr<bmon::Calibrate> fBmonCalibrator;
+    std::unique_ptr<bmon::Hitfind> fBmonHitFinder;
 
     // MUCH
     std::unique_ptr<much::Unpack> fMuchUnpack;
diff --git a/core/data/bmon/CbmBmonDigi.h b/core/data/bmon/CbmBmonDigi.h
index 7f0584e2fc..fffed5ca61 100644
--- a/core/data/bmon/CbmBmonDigi.h
+++ b/core/data/bmon/CbmBmonDigi.h
@@ -1,12 +1,13 @@
-/* Copyright (C) 2022 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+/* Copyright (C) 2022-2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
    SPDX-License-Identifier: GPL-3.0-only
-   Authors: Pierre-Alain Loizeau, Volker Friese [committer] */
+   Authors: Pierre-Alain Loizeau, Volker Friese [committer], Sergei Zharko */
 
 
 #ifndef CBMBMONDIGI_H
 #define CBMBMONDIGI_H 1
 
 #include "CbmDefs.h"
+#include "CbmTofAddress.h"
 
 #ifndef NO_ROOT
 #include <Rtypes.h>  // for ClassDef
@@ -85,6 +86,11 @@ public:
    **/
   double GetTime() const { return fTime; }
 
+  /**
+   **  @brief Gets channel ID
+   **  @return Channel ID
+   **/
+  int32_t GetChannel() const { return CbmTofAddress::GetChannelId(fAddress); }
 
   /** @brief Charge
    ** @return Charge
diff --git a/core/data/tof/CbmTofAddress.cxx b/core/data/tof/CbmTofAddress.cxx
index 068504c399..29c1573452 100644
--- a/core/data/tof/CbmTofAddress.cxx
+++ b/core/data/tof/CbmTofAddress.cxx
@@ -1,6 +1,6 @@
-/* Copyright (C) 2013-2020 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+/* Copyright (C) 2013-2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
    SPDX-License-Identifier: GPL-3.0-only
-   Authors: Pierre-Alain Loizeau, Florian Uhlig [committer], Norbert Herrmann */
+   Authors: Pierre-Alain Loizeau, Florian Uhlig [committer], Norbert Herrmann, Segei Zharko */
 
 /** @file CbmTofAddress.cxx
  ** @author Pierre-Alain Loizeau <loizeau@physi.uni-heidelberg.de>
@@ -44,6 +44,14 @@ const int32_t CbmTofAddress::fgkiStripFullIdMask =
   + (((1 << fgkSmTypeBits) - 1) << fgkSmTypeOffset) + (((1 << fgkRpcIdBits) - 1) << fgkRpcIdOffset)
   + (((1 << fgkChannelIdBits) - 1) << fgkChannelIdOffset);
 
+const int32_t CbmTofAddress::fgkSystemIdBitmask    = (1 << CbmAddress::fgkSystemBits) - 1;
+const int32_t CbmTofAddress::fgkSmIdBitmask        = ((1 << fgkSmIdBits) - 1) << fgkSmIdOffset;
+const int32_t CbmTofAddress::fgkSmTypeBitmask      = ((1 << fgkSmTypeBits) - 1) << fgkSmTypeOffset;
+const int32_t CbmTofAddress::fgkRpcIdBitmask       = ((1 << fgkRpcIdBits) - 1) << fgkRpcIdOffset;
+const int32_t CbmTofAddress::fgkChannelSideBitmask = ((1 << fgkChannelSideBits) - 1) << fgkChannelSideOffset;
+const int32_t CbmTofAddress::fgkChannelIdBitmask   = ((1 << fgkChannelIdBits) - 1) << fgkChannelIdOffset;
+const int32_t CbmTofAddress::fgkRpcTypeBitmask     = ((1 << fgkRpcTypeBits) - 1) << fgkRpcTypeOffset;
+
 std::string CbmTofAddress::ToString(int32_t address)
 {
   using std::setfill;
diff --git a/core/data/tof/CbmTofAddress.h b/core/data/tof/CbmTofAddress.h
index d174064bf2..dfa1a18e89 100644
--- a/core/data/tof/CbmTofAddress.h
+++ b/core/data/tof/CbmTofAddress.h
@@ -1,6 +1,6 @@
-/* Copyright (C) 2013-2020 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+/* Copyright (C) 2013-2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
    SPDX-License-Identifier: GPL-3.0-only
-   Authors: Pierre-Alain Loizeau, Florian Uhlig [committer], Norbert Herrmann */
+   Authors: Pierre-Alain Loizeau, Florian Uhlig [committer], Norbert Herrmann, Sergei Zharko */
 
 /** @file CbmTofAddress.h
  ** @author Pierre-Alain Loizeau <loizeau@physi.uni-heidelberg.de>
@@ -54,6 +54,37 @@ public:
   /** Destructor  **/
   virtual ~CbmTofAddress() {};
 
+  /** Component bitmasks **/
+  /** Gets a bitmask for the System bit field
+   ** @return a bitmask with the bits set only for the System bit field
+   **/
+  static int32_t GetSystemIdBitmask() { return fgkSystemIdBitmask; }
+  /** Gets a bitmask for the Super Module ID bit field
+   ** @return a bitmask with the bits set only for the Super Module ID bit field
+   **/
+  static int32_t GetSmIdBitmask() { return fgkSmIdBitmask; }
+  /** Gets a bitmask for the Super Module Type bit field
+   ** @return a bitmask with the bits set only for the Super Module Type bit field
+   **/
+  static int32_t GetSmTypeBitmask() { return fgkSmTypeBitmask; }
+  /** Gets a bitmask for the RPC ID bit field
+   ** @return a bitmask with the bits set only for the RPC ID bit field
+   **/
+  static int32_t GetRpcIdBitmask() { return fgkRpcIdBitmask; }
+  /** Gets a bitmask for the Channel Side bit field
+   ** @return a bitmask with the bits set only for the Channel Side bit field
+   **/
+  static int32_t GetChannelSideBitmask() { return fgkChannelSideBitmask; }
+  /** Gets a bitmask for the Channel ID bit field
+   ** @return a bitmask with the bits set only for the Channel ID bit field
+   **/
+  static int32_t GetChannelIdBitmask() { return fgkChannelIdBitmask; }
+  /** Gets a bitmask for the RPC Type bit field
+   ** @return a bitmask with the bits set only for the RPC Type bit field
+   **/
+  static int32_t GetRpcTypeBitmask() { return fgkRpcTypeBitmask; }
+
+
   /** Field size accessors **/
   /** Number of bits for Super Module Id in the address field
    ** @return Number of bits
@@ -76,6 +107,33 @@ public:
    **/
   static int32_t GetNofChSideBits() { return fgkChannelSideBits; };
 
+
+  /** Bit field offsets **/
+  /** Gets and offset for the System bit field
+   ** @return and offset with the bits set only for the System bit field
+   **/
+  static int32_t GetSmIdOffset() { return fgkSmIdOffset; }
+  /** Gets and offset for the Super Module Type bit field
+   ** @return and offset with the bits set only for the Super Module Type bit field
+   **/
+  static int32_t GetSmTypeOffset() { return fgkSmTypeOffset; }
+  /** Gets and offset for the RPC ID bit field
+   ** @return and offset with the bits set only for the RPC ID bit field
+   **/
+  static int32_t GetRpcIdOffset() { return fgkRpcIdOffset; }
+  /** Gets and offset for the Channel Side bit field
+   ** @return and offset with the bits set only for the Channel Side bit field
+   **/
+  static int32_t GetChannelSideOffset() { return fgkChannelSideOffset; }
+  /** Gets and offset for the Channel ID bit field
+   ** @return and offset with the bits set only for the Channel ID bit field
+   **/
+  static int32_t GetChannelIdOffset() { return fgkChannelIdOffset; }
+  /** Gets and offset for the RPC Type bit field
+   ** @return and offset with the bits set only for the RPC Type bit field
+   **/
+  static int32_t GetRpcTypeOffset() { return fgkRpcTypeOffset; }
+
   /** Maskers **/
   /** Get the Super Module Id from the address
    ** @param address  Unique address
@@ -154,7 +212,9 @@ public:
   {
     return (GetModFullId(addressA) == GetModFullId(addressB)) ? true : false;
   };
-
+  /**
+   ** @brief Sets the channel ID to the address 
+   **/
   static uint32_t ConvertCbmTofDetectorInfo(CbmTofDetectorInfo infoInput)
   {
     // For now assume that the system ID will always be correct
@@ -251,6 +311,16 @@ public:
    ** For the detector strip Id determination (ignore RPC type and side)
    **/
   static const int32_t fgkiStripFullIdMask;
+
+
+  /** Individual bitmasks for each bit field **/
+  static const int32_t fgkSystemIdBitmask;     //<  A bitmask for System ID
+  static const int32_t fgkSmIdBitmask;         //<  A bitmask for SM
+  static const int32_t fgkSmTypeBitmask;       //<  A bitmask for SM Type
+  static const int32_t fgkRpcIdBitmask;        //<  A bitmask for RPC ID
+  static const int32_t fgkChannelSideBitmask;  //<  A bitmask for Channel Side
+  static const int32_t fgkChannelIdBitmask;    //<  A bitmask for Channel ID
+  static const int32_t fgkRpcTypeBitmask;      //<  A bitmask for RPC Type
 };
 
 #endif  // CBMTOFADDRESS_H
-- 
GitLab


From be38224e64b5a74c803bfe45fc0be295efadd4a1 Mon Sep 17 00:00:00 2001
From: "s.zharko@gsi.de" <s.zharko@gsi.de>
Date: Tue, 11 Feb 2025 05:07:31 +0100
Subject: [PATCH 4/4] online: BMON hitfinder QA

---
 algo/CMakeLists.txt                         |   3 +
 algo/ca/qa/CaQa.h                           |   2 +-
 algo/detectors/bmon/Calibrate.cxx           |   4 +-
 algo/detectors/bmon/Calibrate.h             |   2 +-
 algo/detectors/bmon/Clusterizer.cxx         |   1 +
 algo/detectors/bmon/Hitfind.cxx             |   1 +
 algo/detectors/bmon/HitfindSetup.h          |   1 +
 algo/global/Reco.cxx                        |  27 ++---
 algo/global/Reco.h                          |   2 +
 algo/qa/PadConfig.h                         |  19 +++-
 algo/qa/QaTaskHeader.h                      |   7 +-
 algo/qa/hitfind/BmonHitfindQa.cxx           | 112 ++++++++++++++++++++
 algo/qa/hitfind/BmonHitfindQa.h             | 112 ++++++++++++++++++++
 algo/qa/hitfind/BmonHitfindQaParameters.cxx |  58 ++++++++++
 algo/qa/hitfind/BmonHitfindQaParameters.h   |  50 +++++++++
 15 files changed, 378 insertions(+), 23 deletions(-)
 create mode 100644 algo/qa/hitfind/BmonHitfindQa.cxx
 create mode 100644 algo/qa/hitfind/BmonHitfindQa.h
 create mode 100644 algo/qa/hitfind/BmonHitfindQaParameters.cxx
 create mode 100644 algo/qa/hitfind/BmonHitfindQaParameters.h

diff --git a/algo/CMakeLists.txt b/algo/CMakeLists.txt
index bc9883747a..a2a052fa40 100644
--- a/algo/CMakeLists.txt
+++ b/algo/CMakeLists.txt
@@ -154,6 +154,8 @@ set(SRCS
   qa/QaData.cxx
   qa/RecoGeneralQa.cxx
   qa/QaManager.cxx
+  qa/hitfind/BmonHitfindQa.cxx
+  qa/hitfind/BmonHitfindQaParameters.cxx
   qa/unpack/StsDigiQa.cxx
   ca/TrackingSetup.cxx
   ca/TrackingChain.cxx
@@ -191,6 +193,7 @@ target_include_directories(Algo
          ${CMAKE_CURRENT_SOURCE_DIR}/detectors
          ${CMAKE_CURRENT_SOURCE_DIR}/qa
          ${CMAKE_CURRENT_SOURCE_DIR}/qa/unpack
+         ${CMAKE_CURRENT_SOURCE_DIR}/qa/hitfind
          ${CMAKE_CURRENT_SOURCE_DIR}/kf
          ${CMAKE_CURRENT_SOURCE_DIR}/kf/core
          ${CMAKE_CURRENT_SOURCE_DIR}/kf/core/utils
diff --git a/algo/ca/qa/CaQa.h b/algo/ca/qa/CaQa.h
index 655d6fa50b..1c42b60562 100644
--- a/algo/ca/qa/CaQa.h
+++ b/algo/ca/qa/CaQa.h
@@ -54,7 +54,7 @@ namespace cbm::algo::ca
     static constexpr HitSetArray_t<EHitSet> kHitSets = {EHitSet::Input, EHitSet::Used};
 
    public:
-    /// \brief Default destructor
+    /// \brief Constructor
     /// \param pManager  Pointer to the QA manager
     /// \param name      Name of the QA (directory)
     Qa(const std::unique_ptr<qa::Manager>& pManager, std::string_view name) : qa::TaskHeader(pManager, name) {}
diff --git a/algo/detectors/bmon/Calibrate.cxx b/algo/detectors/bmon/Calibrate.cxx
index 1a2a6af79d..d551985490 100644
--- a/algo/detectors/bmon/Calibrate.cxx
+++ b/algo/detectors/bmon/Calibrate.cxx
@@ -77,8 +77,8 @@ Calibrate::resultType Calibrate::operator()(gsl::span<const CbmBmonDigi> digiIn)
   // --- Output data
   resultType result = {};
 
-  auto& calDigiOut = result.first;
-  auto& monitor    = result.second;
+  auto& calDigiOut = std::get<0>(result);
+  auto& monitor    = std::get<1>(result);
   calDigiOut.reserve(digiIn.size());
 
   // Reset the channel dead time
diff --git a/algo/detectors/bmon/Calibrate.h b/algo/detectors/bmon/Calibrate.h
index 0e602e6b4c..644fd20320 100644
--- a/algo/detectors/bmon/Calibrate.h
+++ b/algo/detectors/bmon/Calibrate.h
@@ -30,7 +30,7 @@ namespace cbm::algo::bmon
   /// \brief Algorithm to calibrate BMon digis
   class Calibrate {
    public:
-    using resultType = std::pair<std::vector<CbmBmonDigi>, CalibrateMonitorData>;
+    using resultType = std::tuple<std::vector<CbmBmonDigi>, CalibrateMonitorData>;
 
     /// \brief Constructor
     /// \param params  Calibration parameters
diff --git a/algo/detectors/bmon/Clusterizer.cxx b/algo/detectors/bmon/Clusterizer.cxx
index 2d48416e35..d3a8e3d4ab 100644
--- a/algo/detectors/bmon/Clusterizer.cxx
+++ b/algo/detectors/bmon/Clusterizer.cxx
@@ -67,6 +67,7 @@ Clusterizer::Output_t Clusterizer::operator()(const Clusterizer::Input_t& digis)
     hits.emplace_back(fParams.fAddress, itLast->first);
     digiIndices.emplace_back(itLast->second);
   }
+
   return res;
 }
 
diff --git a/algo/detectors/bmon/Hitfind.cxx b/algo/detectors/bmon/Hitfind.cxx
index b1e6794c8e..d39a46f6c4 100644
--- a/algo/detectors/bmon/Hitfind.cxx
+++ b/algo/detectors/bmon/Hitfind.cxx
@@ -86,6 +86,7 @@ Hitfind::Output_t Hitfind::operator()(gsl::span<CbmBmonDigi> digisIn, uint32_t i
   // Distribute digis over diamonds, apply cuts on this level (maybe the Calibrator is a more proper place for it)
   size_t nDiamonds  = algoPerThread.size();
   auto vDigiStorage = std::vector<Clusterizer::Input_t>(nDiamonds, Clusterizer::Input_t(0));
+
   for (int32_t iDigi = 0; iDigi < static_cast<int32_t>(digisIn.size()); ++iDigi) {
     const auto& digi = digisIn[iDigi];
     size_t iDiamond  = GetDiamondIndex(digi.GetAddress());
diff --git a/algo/detectors/bmon/HitfindSetup.h b/algo/detectors/bmon/HitfindSetup.h
index c98cc163ff..c958cbc17c 100644
--- a/algo/detectors/bmon/HitfindSetup.h
+++ b/algo/detectors/bmon/HitfindSetup.h
@@ -7,6 +7,7 @@
 /// \since  06.02.2025
 /// \author Sergei Zharko <s.zharko@gsi.de>
 
+#pragma once
 
 #include "Definitions.h"
 #include "yaml/Property.h"
diff --git a/algo/global/Reco.cxx b/algo/global/Reco.cxx
index 5b2698cb3f..8460ea28bf 100644
--- a/algo/global/Reco.cxx
+++ b/algo/global/Reco.cxx
@@ -24,6 +24,7 @@
 #include "evbuild/Config.h"
 #include "much/Unpack.h"
 #include "qa/QaManager.h"
+#include "qa/hitfind/BmonHitfindQa.h"
 #include "rich/Unpack.h"
 #include "sts/ChannelMaskSet.h"
 #include "sts/HitfinderChain.h"
@@ -217,6 +218,12 @@ void Reco::Init(const Options& opts)
 
     auto hitfindSetup = yaml::ReadFromFile<bmon::HitfindSetup>(opts.ParamsDir() / parFiles.bmon.hitfinder);
     fBmonHitFinder    = std::make_unique<bmon::Hitfind>(hitfindSetup);
+
+    if (fQaManager != nullptr && Opts().Has(QaStep::RecoBmon)) {
+      fBmonHitFinderQa = std::make_unique<bmon::HitfindQa>(fQaManager, "BmonHitfindEvent");
+      fBmonHitFinderQa->InitParameters(calibSetup, hitfindSetup);
+      fBmonHitFinderQa->Init();
+    }
   }
 
   // TOF Hitfinder
@@ -236,7 +243,7 @@ void Reco::Init(const Options& opts)
 
   // Tracking
   if (Opts().Has(Step::Tracking)) {
-    if (Opts().Has(QaStep::Tracking)) {
+    if (fQaManager != nullptr && Opts().Has(QaStep::Tracking)) {
       fTracking = std::make_unique<TrackingChain>(fQaManager, "CaTimeslice");
     }
     else {
@@ -389,24 +396,18 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
     // ***** DEBUG: BEGIN
     if constexpr (0) {
       int nEvents = events.size();
-      size_t nBmonHitsOneChannel{0};
-      size_t nBmonHitsTwoChannels{0};
       for (int iE = 0; iE < nEvents; ++iE) {
         const auto& event = events[iE];
         // Calibrate TOF digis:
         auto [bmonDigis, bmonCalMonitor]         = (*fBmonCalibrator)(event.fBmon);
-        auto [bmonHits, hitmonitor, digiindices] = (*fBmonHitFinder)(bmonDigis);
-        for (const auto& hit : bmonHits.Data()) {
-          if (hit.GetNofChannels() == 1) {
-            ++nBmonHitsOneChannel;
-          }
-          else {
-            ++nBmonHitsTwoChannels;
-          }
+        auto [bmonHits, hitmonitor, digiIndices] = (*fBmonHitFinder)(bmonDigis);
+        if (fBmonHitFinderQa != nullptr) {
+          fBmonHitFinderQa->RegisterDigis(&bmonDigis);
+          fBmonHitFinderQa->RegisterHits(&bmonHits);
+          fBmonHitFinderQa->RegisterDigiIndices(&digiIndices);
+          fBmonHitFinderQa->Exec();
         }
       }
-      L_(info) << "!!!! BMON hits with two channels: " << nBmonHitsTwoChannels << " / "
-               << (nBmonHitsTwoChannels + nBmonHitsOneChannel);
     }
     // ***** DEBUG: END
 
diff --git a/algo/global/Reco.h b/algo/global/Reco.h
index 19027c8368..6182decdad 100644
--- a/algo/global/Reco.h
+++ b/algo/global/Reco.h
@@ -29,6 +29,7 @@ namespace cbm::algo
     class Unpack;
     class Calibrate;
     class Hitfind;
+    class HitfindQa;
   }
 
   namespace much
@@ -150,6 +151,7 @@ namespace cbm::algo
     std::unique_ptr<bmon::Unpack> fBmonUnpack;
     std::unique_ptr<bmon::Calibrate> fBmonCalibrator;
     std::unique_ptr<bmon::Hitfind> fBmonHitFinder;
+    std::unique_ptr<bmon::HitfindQa> fBmonHitFinderQa;
 
     // MUCH
     std::unique_ptr<much::Unpack> fMuchUnpack;
diff --git a/algo/qa/PadConfig.h b/algo/qa/PadConfig.h
index 4d6ac5376c..104b214426 100644
--- a/algo/qa/PadConfig.h
+++ b/algo/qa/PadConfig.h
@@ -43,6 +43,15 @@ namespace cbm::algo::qa
     {
     }
 
+    /// \brief  Constructor from a single histogram
+    /// \tparam Hist  Histogram class
+    /// \param  hist  Histogram object
+    /// \param  opt   Draw options for the histogram
+    template<class Hist>
+    PadConfig(const Hist* hist, std::string_view opt)
+    {
+      RegisterHistogram(hist, opt);
+    }
 
     /// \brief Copy constructor
     PadConfig(const PadConfig&) = default;
@@ -104,11 +113,11 @@ namespace cbm::algo::qa
     std::string ToString() const;
 
    private:
-    bool fbGridX = false;  ///< Grid flag for x-axis
-    bool fbGridY = false;  ///< Grid flag for y-axis
-    bool fbLogX  = false;  ///< Log flag for x-axis
-    bool fbLogY  = false;  ///< Log flag for y-axis
-    bool fbLogZ  = false;  ///< Log flag for z-axis
+    bool fbGridX{false};  ///< Grid flag for x-axis
+    bool fbGridY{false};  ///< Grid flag for y-axis
+    bool fbLogX{false};   ///< Log flag for x-axis
+    bool fbLogY{false};   ///< Log flag for y-axis
+    bool fbLogZ{false};   ///< Log flag for z-axis
 
     std::vector<std::pair<std::string, std::string>> fvObjectList;  ///< List of objects on the pad
   };
diff --git a/algo/qa/QaTaskHeader.h b/algo/qa/QaTaskHeader.h
index ca41383152..ceb03e5ad4 100644
--- a/algo/qa/QaTaskHeader.h
+++ b/algo/qa/QaTaskHeader.h
@@ -24,7 +24,8 @@ namespace cbm::algo::qa
     /// \param pManager a QA-manager
     /// \param name A name of the task (histograms directory)
     TaskHeader(const std::unique_ptr<Manager>& pManager, std::string_view name)
-      : fpData(pManager != nullptr ? pManager->GetData() : nullptr)
+      : fsName(name)
+      , fpData(pManager != nullptr ? pManager->GetData() : nullptr)
     {
       if (fpData != nullptr) {
         fpData->RegisterNewTask(name);
@@ -52,6 +53,9 @@ namespace cbm::algo::qa
     /// the fpData instance is not defined, and no actions on the task should be performed
     bool IsActive() const { return static_cast<bool>(fpData != nullptr); }
 
+    /// \brief Gets name of the task
+    const std::string& GetTaskName() { return fsName; }
+
    protected:
     /// \brief Adds a canvas configuration
     /// \param canvas  A CanvasConfig object
@@ -68,6 +72,7 @@ namespace cbm::algo::qa
     }
 
    private:
+    std::string fsName{};                   ///< Name of the task
     std::shared_ptr<Data> fpData{nullptr};  ///< An instance of the QA data (shared between different tasks)
   };
 }  // namespace cbm::algo::qa
diff --git a/algo/qa/hitfind/BmonHitfindQa.cxx b/algo/qa/hitfind/BmonHitfindQa.cxx
new file mode 100644
index 0000000000..0bf088900f
--- /dev/null
+++ b/algo/qa/hitfind/BmonHitfindQa.cxx
@@ -0,0 +1,112 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   BmonHitfindQa.cxx
+/// \brief  A BMON hitfinder QA (implementation)
+/// \since  09.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+#include "qa/hitfind/BmonHitfindQa.h"
+
+#include "CbmBmonDigi.h"
+#include "CbmTofAddress.h"
+#include "bmon/Hit.h"
+#include "qa/Histogram.h"
+
+#include <fmt/format.h>
+
+using cbm::algo::bmon::HitfindQa;
+
+// ---------------------------------------------------------------------------------------------------------------------
+//
+void HitfindQa::Init()
+try {
+  using fmt::format;
+  if (!IsActive()) {
+    return;
+  }
+
+  size_t nDiamonds = fParameters.diamonds.size();
+  if (nDiamonds < 1) {
+    throw std::runtime_error("parameters were not initialized. Please, provide the configuration using the function "
+                             "HitfindQa::InitParameters(calSetup, hitSetup)");
+  }
+
+  fvphDigiOccupVsChan.resize(nDiamonds, nullptr);
+  fvphDigiChargeVsChan.resize(nDiamonds, nullptr);
+  fvphHitNofChan.resize(nDiamonds, nullptr);
+  fvphHitTimeDiff.resize(nDiamonds, nullptr);
+
+  for (size_t iD = 0; iD < nDiamonds; ++iD) {
+    const auto& diamondPar = fParameters.diamonds[iD];
+    int nCh                = diamondPar.nChannels;
+    auto sDN               = format("_diamond_{:#08x}", diamondPar.address);  // diamond suffix
+
+    // Histograms initialisation
+    /* clang-format off */
+    fvphDigiOccupVsChan[iD] = MakeObj<qa::H1D>(
+      format("bmon_digi_occup_channel{}", sDN),
+      format("BMON-{} digi occupancy vs. channel;channel;counts", iD), 
+      nCh, -0.5, nCh - 0.5);
+    fvphDigiChargeVsChan[iD] = MakeObj<qa::H2D>(
+      format("bmon_digi_charge_channel{}", sDN),
+      format("BMON-{} digi charge vs. channel;channel;charge;counts", iD),
+      nCh, -0.5, nCh - 0.5, kChrgB, kChrgL, kChrgU);
+    fvphHitNofChan[iD]       = MakeObj<qa::H1D>(
+      format("bmon_hit_nChannels{}", sDN),
+      format("BMON-{} hit number of channels;N_{{chan}};counts", iD), 
+      2, 0.5, 2.5);
+    fvphHitTimeDiff[iD]      = MakeObj<qa::H1D>(
+      format("bmon_hit_time_diff{}", sDN),
+      format("BMON-{} digi time difference in a hit formed from two digis;#Delta t_{{digi}} [ns];counts", iD), 
+      kDtimeB, kDtimeL, kDtimeU);
+    /* clang-format on */
+
+    // Canvas initialization
+    auto cName = format("{}/bmon{}", GetTaskName(), sDN);
+    auto cTitl = format("BMON-{}", iD);
+    auto canv  = qa::CanvasConfig(cName, cTitl, 2, 2);
+    canv.AddPadConfig(qa::PadConfig(fvphDigiOccupVsChan[iD], "hist"));   // (0,0)
+    canv.AddPadConfig(qa::PadConfig(fvphDigiChargeVsChan[iD], "colz"));  // (1,0)
+    canv.AddPadConfig(qa::PadConfig(fvphHitNofChan[iD], "hist"));        // (0,1)
+    canv.AddPadConfig(qa::PadConfig(fvphHitTimeDiff[iD], "hist"));       // (1,1)
+    AddCanvasConfig(canv);
+  }
+}
+catch (const std::exception& err) {
+  L_(fatal) << "bmon::HitfindQa: initialization failed. Reason: " << err.what();
+  throw std::runtime_error("bmon::HitfindQa initialization failure");
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+//
+void HitfindQa::Exec()
+{
+  if (!IsActive()) {
+    return;
+  }
+
+  // Fill digi distributions
+  for (const auto& digi : *fpDigis) {
+    size_t iDiamond = fParameters.GetDiamondIndex(digi.GetAddress());
+    int32_t chan    = digi.GetChannel();
+    fvphDigiOccupVsChan[iDiamond]->Fill(chan);
+    fvphDigiChargeVsChan[iDiamond]->Fill(chan, digi.GetCharge());
+  }
+
+  // Fill hit distributions
+  const auto& hits = fpHits->Data();
+  for (size_t iH = 0; iH < hits.size(); ++iH) {
+    const auto& hit = hits[iH];
+    size_t iDiamond = fParameters.GetDiamondIndex(hit.GetAddress());
+    int nChannels   = hit.GetNofChannels();
+    fvphHitNofChan[iDiamond]->Fill(nChannels);
+    if (nChannels == 2) {
+      int32_t iDigi     = (*fpDigiIndices)[iH];
+      const auto& digiF = (*fpDigis)[iDigi];
+      const auto& digiS = (*fpDigis)[iDigi + 1];
+      fvphHitTimeDiff[iDiamond]->Fill(digiS.GetTime() - digiF.GetTime());
+    }
+  }
+}
diff --git a/algo/qa/hitfind/BmonHitfindQa.h b/algo/qa/hitfind/BmonHitfindQa.h
new file mode 100644
index 0000000000..020f62fa82
--- /dev/null
+++ b/algo/qa/hitfind/BmonHitfindQa.h
@@ -0,0 +1,112 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   BmonHitfindQa.h
+/// \brief  A BMON hitfinder QA
+/// \since  07.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+#pragma once
+
+#include "PODVector.h"
+#include "PartitionedVector.h"
+#include "qa/QaTaskHeader.h"
+#include "qa/hitfind/BmonHitfindQaParameters.h"
+
+class CbmBmonDigi;
+
+namespace cbm::algo
+{
+  namespace qa
+  {
+    class H1D;
+    class H2D;
+  }  // namespace qa
+
+  namespace bmon
+  {
+    class Hit;
+  }
+}  // namespace cbm::algo
+
+namespace cbm::algo::bmon
+{
+  /// \class HitfindQa
+  /// \brief A QA module for the BMON hit-finder
+  /// \param pManager  Pointer to the QA manager
+  /// \param name      Name of the QA (directory)
+  class HitfindQa : public qa::TaskHeader {
+   public:
+    /// \brief Constructor
+    /// \param pManager  Pointer to the QA manager
+    /// \param name      Name of the QA (directory)
+    HitfindQa(const std::unique_ptr<qa::Manager>& pManager, std::string_view name) : qa::TaskHeader(pManager, name) {}
+
+    /// \brief Constructor from the configuration object
+    /// \param config  QA configuration object
+    HitfindQa() = default;
+
+    /// \brief Copy constructor
+    HitfindQa(const HitfindQa&) = delete;
+
+    /// \brief Move constructor
+    HitfindQa(HitfindQa&&) = delete;
+
+    /// \brief Destructor
+    ~HitfindQa() = default;
+
+    /// \brief Copy assignment operator
+    HitfindQa& operator=(const HitfindQa&) = delete;
+
+    /// \brief Move assignment operator
+    HitfindQa& operator=(HitfindQa&&) = delete;
+
+    /// \brief Executes the task, fills the histograms
+    void Exec();
+
+    /// \brief Initialized the task
+    void Init();
+
+    /// \brief Initialisation of the parameters
+    void InitParameters(const CalibrateSetup& calSetup, const HitfindSetup& hitSetup)
+    {
+      fParameters = std::move(HitfindQaParameters(calSetup, hitSetup));
+    }
+
+    /// \brief Registers a sample of digis
+    /// \param pDigis  A pointer to a vector of digis
+    void RegisterDigis(const std::vector<CbmBmonDigi>* pDigis) { fpDigis = pDigis; }
+
+    /// \brief Registers a sample of hits
+    /// \param pHits  A pointer to a vector of hits
+    void RegisterHits(const PartitionedVector<bmon::Hit>* pHits) { fpHits = pHits; }
+
+    /// \brief Registers a sample of digi indices, used by hits
+    /// \param pDigiIndices  A pointer to a vector of digi indices
+    void RegisterDigiIndices(const PODVector<int32_t>* pDigiIndices) { fpDigiIndices = pDigiIndices; }
+
+   private:
+    //* Constants
+    static constexpr int kChrgB     = 150;   ///< charge scale: number of bins
+    static constexpr double kChrgL  = 0.;    ///< charge scale: lower bound
+    static constexpr double kChrgU  = 150.;  ///< charge scale: upper bound
+    static constexpr int kDtimeB    = 40;    ///< digi time difference: number of bins
+    static constexpr double kDtimeL = 0.;    ///< digi time difference: lower bound [ns]
+    static constexpr double kDtimeU = 2.0;   ///< digi time difference: upper bound [ns]
+
+    //* Parameters
+    HitfindQaParameters fParameters;  ///< Parameters of the hit finder QA
+
+    //* Data samples
+    const std::vector<CbmBmonDigi>* fpDigis{nullptr};     ///< Pointer to BMON digi sample
+    const PartitionedVector<bmon::Hit>* fpHits{nullptr};  ///< Pointer to BMON hit sample
+    const PODVector<int32_t>* fpDigiIndices{nullptr};     ///< Pointer to BMON digi indices, used by hits
+
+    //* Histograms
+    std::vector<qa::H1D*> fvphDigiOccupVsChan;   ///< Digi occupancy vs. channel [diamond]
+    std::vector<qa::H2D*> fvphDigiChargeVsChan;  ///< Digi charge vs channel [diamond]
+    std::vector<qa::H1D*> fvphHitNofChan;        ///< Hit  number of channels [diamond]
+    std::vector<qa::H1D*> fvphHitTimeDiff;       ///< Time difference of two digis in a hit [diamond]
+  };
+}  // namespace cbm::algo::bmon
diff --git a/algo/qa/hitfind/BmonHitfindQaParameters.cxx b/algo/qa/hitfind/BmonHitfindQaParameters.cxx
new file mode 100644
index 0000000000..aa7f4aadce
--- /dev/null
+++ b/algo/qa/hitfind/BmonHitfindQaParameters.cxx
@@ -0,0 +1,58 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   BmonHitfindQaParameters.cxx
+/// \brief  A BMON hitfinder QA parameter configuration (implementation)
+/// \since  10.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+#include "qa/hitfind/BmonHitfindQaParameters.h"
+
+#include "AlgoFairloggerCompat.h"
+#include "CbmTofAddress.h"
+
+using cbm::algo::bmon::CalibrateSetup;
+using cbm::algo::bmon::HitfindQaParameters;
+using cbm::algo::bmon::HitfindSetup;
+
+// ---------------------------------------------------------------------------------------------------------------------
+//
+HitfindQaParameters::HitfindQaParameters(const CalibrateSetup& calSetup, const HitfindSetup& hitSetup)
+{
+
+  this->selectionMask = calSetup.selectionMask;
+  if (this->selectionMask != hitSetup.selectionMask) {
+    throw std::runtime_error("Mismatch of the selection bitmask in the BMON CalibrateSetup and HitfindSetup configs");
+  }
+
+  auto nDiamonds = calSetup.diamonds.size();
+  if (nDiamonds != hitSetup.diamonds.size()) {
+    throw std::runtime_error("Mismatch of number of diamonds in the BMON CalibrateSetup and HitfindSetup configs");
+  }
+
+  if (nDiamonds > 1) {
+    while (!((this->selectionMask >> fSelectionBitsOffset) % 2)) {
+      ++fSelectionBitsOffset;
+    }
+  }
+
+  this->diamonds.resize(nDiamonds);
+  for (const auto& calDiamond : calSetup.diamonds) {
+    uint32_t address      = calDiamond.refAddress & ~CbmTofAddress::GetChannelIdBitmask();
+    auto& thisDiamond     = this->diamonds[GetDiamondIndex(address)];
+    thisDiamond.address   = address;
+    thisDiamond.nChannels = calDiamond.chanPar.size();
+  }
+
+  for (const auto& hitDiamond : hitSetup.diamonds) {
+    int32_t address   = hitDiamond.refAddress & ~CbmTofAddress::GetChannelIdBitmask();
+    auto& thisDiamond = this->diamonds[GetDiamondIndex(address)];
+    if (thisDiamond.address != address) {
+      throw std::runtime_error("Mismatch between diamond addresses in BMON CalibrateSetup and HitfindSetup configs");
+    }
+    thisDiamond.deadStrips  = hitDiamond.deadStrips;
+    thisDiamond.timeRes     = hitDiamond.timeRes;
+    thisDiamond.maxTimeDist = hitDiamond.maxTimeDist;
+  }
+}
diff --git a/algo/qa/hitfind/BmonHitfindQaParameters.h b/algo/qa/hitfind/BmonHitfindQaParameters.h
new file mode 100644
index 0000000000..25f524ba87
--- /dev/null
+++ b/algo/qa/hitfind/BmonHitfindQaParameters.h
@@ -0,0 +1,50 @@
+/* Copyright (C) 2025 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Sergei Zharko [committer] */
+
+/// \file   BmonHitfindQaParameters.h
+/// \brief  A BMON hitfinder QA parameter configuration
+/// \since  10.02.2025
+/// \author Sergei Zharko <s.zharko@gsi.de>
+
+#pragma once
+
+#include "bmon/CalibrateSetup.h"
+#include "bmon/HitfindSetup.h"
+
+#include <vector>
+
+namespace cbm::algo::bmon
+{
+  /// \struct HitfindQaParameters
+  /// \brief  A structure to handle BMON QA parameters
+  struct HitfindQaParameters {
+    /// \struct Diamond
+    /// \brief  A diamond representation
+    struct Diamond {
+      double timeRes{0.};      ///< Time resolution [ns]
+      double maxTimeDist{0.};  ///< Max time distance between digis in a hit [ns]
+      int32_t address{0};      ///< Address of a diamond
+      int32_t nChannels{0};    ///< Number of channels in a diamond
+      uint32_t deadStrips{0};  ///< A bit mask of dead strips
+    };
+
+    uint32_t selectionMask{0};  ///< A bitmask to distinguish different diamonds
+    std::vector<Diamond> diamonds{};
+
+    /// \brief Default constructor
+    HitfindQaParameters() = default;
+
+    /// \brief Constructor
+    /// \param calSetup  Calibration parameters
+    /// \param hitSetup  Hitfinder parameters
+    HitfindQaParameters(const CalibrateSetup& calSetup, const HitfindSetup& hitSetup);
+
+    /// \brief Returns an index of the diamond by the address
+    /// \param address  A hardware address of the digi
+    size_t GetDiamondIndex(uint32_t address) const { return ((selectionMask & address) >> fSelectionBitsOffset); }
+
+   private:
+    uint32_t fSelectionBitsOffset{0};  ///< Number of bits to the right from the first bit in the selection mask
+  };
+}  // namespace cbm::algo::bmon
-- 
GitLab