From b18173f74b10b4dd976de42da653e0a8eaad63e6 Mon Sep 17 00:00:00 2001
From: Jihee Kim <jihee.kim@anl.gov>
Date: Sat, 3 Apr 2021 03:18:13 +0000
Subject: [PATCH] Simple Clustering

---
 Examples/options/sampling_cluster2n1.py       |  58 +++++++++
 Examples/sampling_calo.py                     |  54 ++++++++
 .../components/EcalTungstenSamplingDigi.cpp   |  23 +++-
 JugReco/CMakeLists.txt                        |   2 +
 .../components/CalorimeterIslandCluster.cpp   |  20 ++-
 JugReco/src/components/ClusterRecoCoG.cpp     |  12 +-
 .../EcalTungstenSamplingCluster.cpp           | 116 ++++++++++++++++++
 .../components/EcalTungstenSamplingReco.cpp   |  52 +++++---
 .../src/components/SamplingECalHitsMerger.cpp | 110 +++++++++++++++++
 9 files changed, 418 insertions(+), 29 deletions(-)
 create mode 100644 Examples/options/sampling_cluster2n1.py
 create mode 100644 Examples/sampling_calo.py
 create mode 100644 JugReco/src/components/EcalTungstenSamplingCluster.cpp
 create mode 100644 JugReco/src/components/SamplingECalHitsMerger.cpp

diff --git a/Examples/options/sampling_cluster2n1.py b/Examples/options/sampling_cluster2n1.py
new file mode 100644
index 00000000..125984f2
--- /dev/null
+++ b/Examples/options/sampling_cluster2n1.py
@@ -0,0 +1,58 @@
+from Gaudi.Configuration import *
+from GaudiKernel import SystemOfUnits as units
+
+from GaudiKernel.DataObjectHandleBase import DataObjectHandleBase
+from Configurables import ApplicationMgr, EICDataSvc, PodioOutput, GeoSvc
+
+from Configurables import PodioInput
+from Configurables import Jug__Digi__EcalTungstenSamplingDigi as EcalTungstenSamplingDigi
+from Configurables import Jug__Reco__EcalTungstenSamplingReco as EcalTungstenSamplingReco
+from Configurables import Jug__Reco__SamplingECalHitsMerger as SamplingECalHitsMerger
+from Configurables import Jug__Reco__CalorimeterIslandCluster as IslandCluster
+from Configurables import Jug__Reco__ClusterRecoCoG as RecoCoG
+
+geo_service  = GeoSvc("GeoSvc", detectors=["../topside/test.xml"])
+podioevent = EICDataSvc("EventDataSvc", inputs=["barrel_electrons.root"], OutputLevel=DEBUG)
+
+podioinput = PodioInput("PodioReader", collections=["mcparticles", "EcalBarrelHits"], OutputLevel=DEBUG)
+emcaldigi = EcalTungstenSamplingDigi("ecal_digi",
+                                     inputHitCollection="EcalBarrelHits",
+                                     outputHitCollection="DigiEcalBarrelHits",
+                                     inputEnergyUnit=units.GeV,
+                                     inputTimeUnit=units.ns,
+                                     OutputLevel=DEBUG)
+emcalreco = EcalTungstenSamplingReco("ecal_reco",
+                                     inputHitCollection="DigiEcalBarrelHits",
+                                     outputHitCollection="RecoEcalBarrelHits",
+                                     OutputLevel=DEBUG)
+# readout id definition for barrel ecal
+# <id>system:8,barrel:3,module:4,layer:6,slice:5,x:32:-16,y:-16</id>
+# xy_merger sum layers/slices, masking (8+3+4, 8+3+4+5+6-1)
+xymerger = SamplingECalHitsMerger("ecal_xy_merger",
+                                   cellIDMaskRanges=[(15, 25)],
+                                   inputHitCollection="RecoEcalBarrelHits",
+                                   outputHitCollection="RecoEcalBarrelHitsXY")
+# xy_merger sum modules, masking (8+3+4+5+6, 8+3+4+5+6+32-1)
+zmerger = SamplingECalHitsMerger("ecal_z_merger",
+                                  cellIDMaskRanges=[(26, 57)],
+                                  inputHitCollection="RecoEcalBarrelHits",
+                                  outputHitCollection="RecoEcalBarrelHitsZ")
+emcalcluster = IslandCluster(inputHitCollection="RecoEcalBarrelHitsXY",
+                             outputClusterCollection="EcalBarrelClusters",
+                             minClusterCenterEdep=5.0*units.MeV,
+                             splitCluster=False,
+                             groupRange=5.0)
+clusterreco = RecoCoG(clusterCollection="EcalBarrelClusters", logWeightBase=6.2, OutputLevel=DEBUG)
+
+
+out = PodioOutput("out", filename="barrel_cluster.root")
+out.outputCommands = ["keep *"]
+
+ApplicationMgr(
+    TopAlg = [podioinput, emcaldigi, emcalreco, xymerger, zmerger, emcalcluster, clusterreco, out],
+    EvtSel = 'NONE',
+    EvtMax   = 1000,
+    ExtSvc = [podioevent],
+    OutputLevel=DEBUG
+ )
+
diff --git a/Examples/sampling_calo.py b/Examples/sampling_calo.py
new file mode 100644
index 00000000..08d45fb6
--- /dev/null
+++ b/Examples/sampling_calo.py
@@ -0,0 +1,54 @@
+import numpy as np
+import pandas as pd
+import ROOT
+from ROOT import gROOT
+import argparse
+from matplotlib import pyplot as plt
+from matplotlib.patches import Ellipse
+import matplotlib.transforms as transforms
+
+parser = argparse.ArgumentParser(description='sampling calorimeter performance')
+parser.add_argument('file', type=str, help='path to root file')
+args = parser.parse_args()
+
+gROOT.Macro('rootlogon.C')
+# root dataframe
+rdf = ROOT.RDataFrame('events', args.file)
+
+rdf = rdf.Define('fraction', 'EcalBarrelClusters.energy/5000')\
+         .Define('r', 'sqrt(EcalBarrelClusters.position.x*EcalBarrelClusters.position.x + EcalBarrelClusters.position.y*EcalBarrelClusters.position.y)')\
+         .Define('z', 'EcalBarrelClusters.position.z')\
+         .Define('angle', 'acos(z/sqrt(r*r + z*z))/M_PI*180.')
+
+hist = rdf.Histo1D(ROOT.RDF.TH1DModel('energy', 'Sampling Fraction;Fraction;Counts', 280, 0.04, 0.6), 'fraction')
+c1 = ROOT.TCanvas('c1', '', 2560, 1440)
+hist.Fit('gaus')
+hist.Draw()
+c1.SaveAs('sampling_energy.png')
+
+c1 = ROOT.TCanvas('c1', '', 2560, 1440)
+hist = rdf.Histo1D(ROOT.RDF.TH1DModel('angle', ';Angle (deg);Counts', 180, 0, 180), 'angle')
+hist.Draw()
+c1.SaveAs('sampling_angle.png')
+
+c1 = ROOT.TCanvas('c1', '', 2560, 1440)
+hist = rdf.Histo2D(ROOT.RDF.TH2DModel('2d', ';Fraction;Angle (deg)', 40, 0.1, 0.5, 60, 60, 120), 'fraction', 'angle')
+hist.Draw('colz')
+c1.SaveAs('sampling_2d.png')
+
+f = ROOT.TFile(args.file)
+events = f.events
+
+for iev in range(events.GetEntries()):
+    events.GetEntry(iev)
+    xedep = []
+    for hit in events.RecoEcalBarrelHitsZ:
+        xedep.append((np.sqrt(hit.position.x**2 + hit.position.y**2), hit.energy))
+    df = pd.DataFrame(data=xedep, columns=['x', 'edep']).set_index('x', drop=True)
+    df.sort_index(inplace=True)
+    df.index -= 890
+    print(df.cumsum())
+
+fig, ax = plt.subplots()
+ax.plot(df.index, df.cumsum()['edep'].values)
+fig.savefig('test.png')
diff --git a/JugDigi/src/components/EcalTungstenSamplingDigi.cpp b/JugDigi/src/components/EcalTungstenSamplingDigi.cpp
index b77f6a0d..e36deb95 100644
--- a/JugDigi/src/components/EcalTungstenSamplingDigi.cpp
+++ b/JugDigi/src/components/EcalTungstenSamplingDigi.cpp
@@ -1,3 +1,4 @@
+// Digitize the simulation outputs from Ecal Tungsten Sampling Calorimeter
 #include <algorithm>
 #include <cmath>
 
@@ -15,6 +16,8 @@
 #include "eicd/RawCalorimeterHitCollection.h"
 #include "eicd/RawCalorimeterHitData.h"
 
+using namespace Gaudi::Units;
+
 namespace Jug {
   namespace Digi {
 
@@ -24,8 +27,15 @@ namespace Jug {
      */
     class EcalTungstenSamplingDigi : public GaudiAlgorithm {
     public:
-      Gaudi::Property<double>                      m_energyResolution{this, "energyResolution", 0.11}; // 11%sqrt(E)
-      Rndm::Numbers                                m_gaussDist;
+      Gaudi::Property<double>                      m_eRes{this, "energyResolution", 0.11}; // 11%sqrt(E)
+      Gaudi::Property<double>                      m_tRes{this, "timineResolution", 0.1*ns};
+      Gaudi::Property<double>                      m_eUnit{this, "inputEnergyUnit", GeV};
+      Gaudi::Property<double>                      m_tUnit{this, "inputTimeUnit", ns};
+      Gaudi::Property<int>                         m_capADC{this, "capacityADC", 8096};
+      Gaudi::Property<double>                      m_dyRangeADC{this, "DynamicRangeADC", 100*MeV};
+      Gaudi::Property<int>                         m_pedMeanADC{this, "pedestalMean", 400};
+      Gaudi::Property<double>                      m_pedSigmaADC{this, "pedestalSigma", 3.2};
+      Rndm::Numbers                                m_normDist;
       DataHandle<dd4pod::CalorimeterHitCollection> m_inputHitCollection{"inputHitCollection", Gaudi::DataHandle::Reader,
                                                                         this};
       DataHandle<eic::RawCalorimeterHitCollection> m_outputHitCollection{"outputHitCollection",
@@ -43,7 +53,7 @@ namespace Jug {
         if (GaudiAlgorithm::initialize().isFailure())
           return StatusCode::FAILURE;
         IRndmGenSvc* randSvc = svc<IRndmGenSvc>("RndmGenSvc", true);
-        StatusCode   sc      = m_gaussDist.initialize(randSvc, Rndm::Gauss(0.0, m_energyResolution.value()));
+        StatusCode   sc      = m_normDist.initialize(randSvc, Rndm::Gauss(0.0, 1.0));
         if (!sc.isSuccess()) {
           return StatusCode::FAILURE;
         }
@@ -59,11 +69,12 @@ namespace Jug {
         auto                              rawhits          = m_outputHitCollection.createAndPut();
         eic::RawCalorimeterHitCollection* rawHitCollection = new eic::RawCalorimeterHitCollection();
         for (const auto& ahit : *simhits) {
-          double                 res = m_gaussDist() / sqrt(ahit.energyDeposit());
+          double res = m_normDist()*m_eRes / sqrt(ahit.energyDeposit()*m_eUnit/GeV);
+          double ped = m_pedMeanADC + m_normDist()*m_pedSigmaADC;
           eic::RawCalorimeterHit rawhit(
               (long long)ahit.cellID(),
-              std::llround(ahit.energyDeposit() * (1. + res) * 1.0e6), // convert to keV integer
-              (double)ahit.truth().time * 1.0e6);
+              std::llround(ped + ahit.energyDeposit()*(1. + res) * m_eUnit/m_dyRangeADC*m_capADC), // convert to ADC Value
+              (double)ahit.truth().time*m_tUnit/ns + m_normDist()*m_tRes/ns);
           rawhits->push_back(rawhit);
         }
         return StatusCode::SUCCESS;
diff --git a/JugReco/CMakeLists.txt b/JugReco/CMakeLists.txt
index 184fdb12..675b92c6 100644
--- a/JugReco/CMakeLists.txt
+++ b/JugReco/CMakeLists.txt
@@ -46,11 +46,13 @@ gaudi_add_module(JugRecoPlugins
   src/components/CalorimeterIslandCluster.cpp 
   src/components/CrystalEndcapsReco.cpp 
   src/components/EcalTungstenSamplingReco.cpp
+  src/components/EcalTungstenSamplingCluster.cpp
   src/components/ClusterRecoCoG.cpp 
   src/components/PhotoMultiplierReco.cpp 
   src/components/PhotoRingClusters.cpp 
   src/components/FuzzyKClusters.cpp 
   src/components/EMCalReconstruction.cpp 
+  src/components/SamplingECalHitsMerger.cpp 
   LINK_LIBRARIES GaudiAlgLib GaudiKernel JugBase ROOT NPDet::DD4podIO EICD::eicd DDRec Acts)
 
 target_compile_options(JugRecoPlugins PRIVATE -Wno-suggest-override)
diff --git a/JugReco/src/components/CalorimeterIslandCluster.cpp b/JugReco/src/components/CalorimeterIslandCluster.cpp
index e13ab6ea..d8a6e204 100644
--- a/JugReco/src/components/CalorimeterIslandCluster.cpp
+++ b/JugReco/src/components/CalorimeterIslandCluster.cpp
@@ -36,6 +36,7 @@ namespace Jug::Reco {
 
   class CalorimeterIslandCluster : public GaudiAlgorithm {
   public:
+    Gaudi::Property<bool> m_splitCluster{this, "splitCluster", true};
     Gaudi::Property<double> m_groupRange{this, "groupRange", 1.8};
     Gaudi::Property<double> m_minClusterCenterEdep{this, "minClusterCenterEdep", 50.0*MeV};
     DataHandle<eic::CalorimeterHitCollection>
@@ -86,7 +87,7 @@ namespace Jug::Reco {
         debug() << "we have " << groups.size() << " groups of hits" << endmsg;
 
         for (auto &group : groups) {
-            auto maxima = find_local_maxima(group);
+            auto maxima = find_maxima(group, !m_splitCluster);
             split_group(group, maxima, clusters, split_hits);
             debug() << "hits in a group: " << group.size() <<  ", "
                     << "local maxima: " << maxima.hits_size() << endmsg;
@@ -120,9 +121,24 @@ private:
     }
 
     // find local maxima that above a certain threshold
-    eic::Cluster find_local_maxima(const std::vector<eic::CalorimeterHit> &group) const
+    eic::Cluster find_maxima(const std::vector<eic::CalorimeterHit> &group, bool global = false) const
     {
         eic::Cluster maxima;
+        if (group.empty()) {
+            return maxima;
+        }
+
+        if (global) {
+            int mpos = 0;
+            for (size_t i = 0; i < group.size(); ++i) {
+                if (group[mpos].energy() < group[i].energy()) {
+                    mpos = i;
+                }
+            }
+            maxima.addhits(group[mpos]);
+            return maxima;
+        }
+
         for(auto &hit : group)
         {
             // not a qualified center
diff --git a/JugReco/src/components/ClusterRecoCoG.cpp b/JugReco/src/components/ClusterRecoCoG.cpp
index 786b4795..45c530d7 100644
--- a/JugReco/src/components/ClusterRecoCoG.cpp
+++ b/JugReco/src/components/ClusterRecoCoG.cpp
@@ -75,8 +75,8 @@ public:
             auto hit = reconstruct(cl);
             cl.energy(hit.energy());
             cl.position(hit.position());
-            // info() << cl.energy()/GeV << " GeV, (" << cl.position().x/mm << ", "
-            //        << cl.position().y/mm << ", " << cl.position().z/mm << ")" << endmsg;
+            debug() << cl.hits_size() << " hits: " << cl.energy()/GeV << " GeV, (" << cl.position().x/mm << ", "
+                    << cl.position().y/mm << ", " << cl.position().z/mm << ")" << endmsg;
         }
 
         return StatusCode::SUCCESS;
@@ -95,6 +95,7 @@ private:
         float totalE = 0., maxE = 0.;
         auto centerID = cl.hits_begin()->cellID();
         for (auto &hit : cl.hits()) {
+            // info() << "hit energy = " << hit.energy() << endmsg;
             auto energy = hit.energy();
             totalE += energy;
             if (energy > maxE) {
@@ -109,11 +110,18 @@ private:
         float tw = 0., x = 0., y = 0., z = 0.;
         for (auto &hit : cl.hits()) {
             // suppress low energy contributions
+            // info() << std::log(hit.energy()/totalE) << endmsg;
             float w = std::max(0., m_logWeightBase + std::log(hit.energy()/totalE));
             tw += w;
             x += hit.local_x() * w;
             y += hit.local_y() * w;
             z += hit.local_z() * w;
+            /*
+            debug() << hit.cellID()
+                    << "(" << hit.local_x() << ", " << hit.local_y() << ", " << hit.local_z() << "), "
+                    << "(" << hit.x() << ", " << hit.y() << ", " << hit.z() << "), "
+                    << endmsg;
+            */
         }
         res.local({x/tw, y/tw, z/tw + m_depthCorrection});
 
diff --git a/JugReco/src/components/EcalTungstenSamplingCluster.cpp b/JugReco/src/components/EcalTungstenSamplingCluster.cpp
new file mode 100644
index 00000000..c3810dbb
--- /dev/null
+++ b/JugReco/src/components/EcalTungstenSamplingCluster.cpp
@@ -0,0 +1,116 @@
+#include <algorithm>
+
+#include "Gaudi/Property.h"
+#include "GaudiAlg/GaudiAlgorithm.h"
+#include "GaudiAlg/GaudiTool.h"
+#include "GaudiAlg/Transformer.h"
+#include "GaudiKernel/PhysicalConstants.h"
+#include "GaudiKernel/RndmGenerators.h"
+#include "GaudiKernel/ToolHandle.h"
+
+#include "DDRec/CellIDPositionConverter.h"
+#include "DDRec/Surface.h"
+#include "DDRec/SurfaceManager.h"
+
+// FCCSW
+#include "JugBase/DataHandle.h"
+#include "JugBase/IGeoSvc.h"
+
+// Event Model related classes
+#include "eicd/CalorimeterHitCollection.h"
+#include "eicd/ClusterCollection.h"
+
+using namespace Gaudi::Units;
+
+namespace Jug::Reco {
+
+  class EcalTungstenSamplingCluster : public GaudiAlgorithm {
+  public:
+    Gaudi::Property<double>                   m_minModuleEdep{this, "minModuleEdep", 0.5 * MeV};
+    DataHandle<eic::CalorimeterHitCollection> m_inputHitCollection{"inputHitCollection", Gaudi::DataHandle::Reader,
+                                                                   this};
+    DataHandle<eic::ClusterCollection> m_outputClusterCollection{"outputClusterCollection", Gaudi::DataHandle::Writer,
+                                                                 this};
+
+    /// Pointer to the geometry service
+    SmartIF<IGeoSvc> m_geoSvc;
+
+    // ill-formed: using GaudiAlgorithm::GaudiAlgorithm;
+    EcalTungstenSamplingCluster(const std::string& name, ISvcLocator* svcLoc) : GaudiAlgorithm(name, svcLoc)
+    {
+      declareProperty("inputHitCollection", m_inputHitCollection, "");
+      declareProperty("outputClusterCollection", m_outputClusterCollection, "");
+    }
+
+    StatusCode initialize() override
+    {
+      if (GaudiAlgorithm::initialize().isFailure()) {
+        return StatusCode::FAILURE;
+      }
+      m_geoSvc = service("GeoSvc");
+      if (!m_geoSvc) {
+        error() << "Unable to locate Geometry Service. "
+                << "Make sure you have GeoSvc and SimSvc in the right order in the configuration." << endmsg;
+        return StatusCode::FAILURE;
+      }
+      return StatusCode::SUCCESS;
+    }
+
+    StatusCode execute() override
+    {
+      // input collections
+      const auto& hits = *m_inputHitCollection.get();
+      // Create output collections
+      auto& clusters = *m_outputClusterCollection.createAndPut();
+
+      // Loop over hits
+      std::vector<bool> visits(hits.size(), false);
+      double ref_pos_z = 0.0;
+      for (size_t i = 0; i < hits.size(); i++)
+      {
+	// Ignore hits that already visited
+	if (visits[i]) {
+		continue;
+	}
+	// Select above minimum energy deposit
+      	if (hits[i].energy() > m_minModuleEdep/GeV) {
+          // Reference z position of hit
+	  ref_pos_z = hits[i].position().z;
+	  std::cout << "Before ref_pos_z: " << ref_pos_z << std::endl;
+	  // Call function to add up energy deposit in the same layer based on z position
+	  group_by_layer(i, ref_pos_z, hits, visits, clusters);
+	}
+      }
+      return StatusCode::SUCCESS;
+    }
+  private:
+    // Grouping hits by layers
+    void group_by_layer(int index, double ref_pos_z, const eic::CalorimeterHitCollection &hits, std::vector<bool> &visits, eic::ClusterCollection &clusters) const
+    {
+	visits[index] = true;
+	auto tot_edep = hits[index].energy();
+	double pos_x = hits[index].position().x;
+	double pos_y = hits[index].position().y;
+	double pos_z = hits[index].position().z;
+	double temp = ref_pos_z;
+	std::cout << "After ref_pos_z: " << temp << std::endl;
+	std::cout << "Reprint pos_z: " << pos_z << std::endl;
+	// Loop over hits to find hits on the same layer
+	for (size_t i = 0; i < hits.size(); i++) {
+		if(visits[i]) {
+			continue;
+		}
+		// Add up energy deposit based on the same z position and above energy threshold
+		if((double)hits[i].position().z == ref_pos_z && hits[i].energy() > m_minModuleEdep/GeV) {
+			tot_edep += hits[i].energy();
+			visits[i] = true;
+		}
+	}
+	// Save info as a cluster
+	// TODO: position x and y determination
+	clusters.push_back(eic::Cluster{tot_edep,{pos_x,pos_y,pos_z}, {0,0,0,0,0,0}, 0, 0});
+	return;
+    }
+  };
+  DECLARE_COMPONENT(EcalTungstenSamplingCluster)
+} // namespace Jug::Reco
diff --git a/JugReco/src/components/EcalTungstenSamplingReco.cpp b/JugReco/src/components/EcalTungstenSamplingReco.cpp
index 23e20b2a..5bedd82f 100644
--- a/JugReco/src/components/EcalTungstenSamplingReco.cpp
+++ b/JugReco/src/components/EcalTungstenSamplingReco.cpp
@@ -1,4 +1,8 @@
+// Reconstruct digitized outputs fof Ecal Tungsten Sampling Calorimeter
+// It is exactly the reverse step of JugDigi/src/components/EcalTungstenSamplingDigi.cpp
+
 #include <algorithm>
+#include <bitset>
 
 #include "Gaudi/Property.h"
 #include "GaudiAlg/GaudiAlgorithm.h"
@@ -25,8 +29,11 @@ using namespace Gaudi::Units;
 namespace Jug::Reco {
   class EcalTungstenSamplingReco : public GaudiAlgorithm {
   public:
-    Gaudi::Property<double>                      m_samplingFraction{this, "samplingFraction", 0.25};
-    Gaudi::Property<double>                      m_minModuleEdep{this, "minModuleEdep", 0.5 * MeV};
+    Gaudi::Property<int>                         m_capADC{this, "capacityADC", 8096};
+    Gaudi::Property<double>                      m_dyRangeADC{this, "DynamicRangeADC", 100*MeV};
+    Gaudi::Property<int>                         m_pedMeanADC{this, "pedestalMean", 400};
+    Gaudi::Property<double>                      m_pedSigmaADC{this, "pedestalSigma", 3.2};
+    Gaudi::Property<double>                      m_thresholdADC{this, "thresholdFactor", 3.0};
     DataHandle<eic::RawCalorimeterHitCollection> m_inputHitCollection{"inputHitCollection", Gaudi::DataHandle::Reader,
                                                                       this};
     DataHandle<eic::CalorimeterHitCollection>    m_outputHitCollection{"outputHitCollection", Gaudi::DataHandle::Writer,
@@ -64,24 +71,31 @@ namespace Jug::Reco {
 
       // energy time reconstruction
       for (auto& rh : rawhits) {
-        float energy = rh.amplitude() / 1.0e6; // convert keV -> GeV
-        if (energy >= (m_minModuleEdep / GeV)) {
-          float time = rh.timeStamp() / 1.0e6; // ns
-          auto  id   = rh.cellID();
-          // global positions
-          auto gpos = m_geoSvc->cellIDPositionConverter()->position(id);
-          // local positions
-          auto pos = m_geoSvc->cellIDPositionConverter()->findContext(id)->volumePlacement().position();
-          // cell dimension
-          auto dim = m_geoSvc->cellIDPositionConverter()->cellDimensions(id);
-          hits.push_back(eic::CalorimeterHit{id,
-                                             energy/m_samplingFraction,
-                                             time,
-                                             {gpos.x() / dd4hep::mm, gpos.y() / dd4hep::mm, gpos.z() / dd4hep::mm},
-                                             {pos.x() / dd4hep::mm, pos.y() / dd4hep::mm, pos.z() / dd4hep::mm},
-                                             {dim[0] / dd4hep::mm, dim[1] / dd4hep::mm, 0.0},
-                                             0});
+        // did not pass the threshold
+        if ((rh.amplitude() - m_pedMeanADC) < m_thresholdADC*m_pedSigmaADC) {
+          continue;
         }
+        float energy = (rh.amplitude() - m_pedMeanADC) / (float) m_capADC * m_dyRangeADC; // convert ADC -> energy
+        float time = rh.timeStamp(); // ns
+        auto id = rh.cellID();
+        // global positions
+        auto gpos = m_geoSvc->cellIDPositionConverter()->position(id);
+        // local positions
+        auto volman = m_geoSvc->detector()->volumeManager();
+        auto alignment = volman.lookupDetector(id).nominal();
+        auto pos = alignment.worldToLocal(dd4hep::Position(gpos.x(), gpos.y(), gpos.z()));
+        // auto pos = m_geoSvc->cellIDPositionConverter()->findContext(id)->volumePlacement().position();
+        // cell dimension
+        auto dim = m_geoSvc->cellIDPositionConverter()->cellDimensions(id);
+        // info() << std::bitset<64>(id) << "\n"
+        //        << m_geoSvc->cellIDPositionConverter()->findContext(id)->volumePlacement().volIDs().str() << endmsg;
+        hits.push_back(eic::CalorimeterHit{id,
+                                           energy,
+                                           time,
+                                           {gpos.x() / dd4hep::mm, gpos.y() / dd4hep::mm, gpos.z() / dd4hep::mm},
+                                           {pos.x() / dd4hep::mm, pos.y() / dd4hep::mm, pos.z() / dd4hep::mm},
+                                           {dim[0] / dd4hep::mm, dim[1] / dd4hep::mm, 0.0},
+                                           0});
       }
 
       return StatusCode::SUCCESS;
diff --git a/JugReco/src/components/SamplingECalHitsMerger.cpp b/JugReco/src/components/SamplingECalHitsMerger.cpp
new file mode 100644
index 00000000..c7926085
--- /dev/null
+++ b/JugReco/src/components/SamplingECalHitsMerger.cpp
@@ -0,0 +1,110 @@
+/*
+ *  A clustering algorithm to reduce 3D clustering to 2D (x, y) + 1D (depth)
+ *  2D clustering is formed by summing all layers in the same module
+ *  1D clustering is formed by summing all modules in the same layer
+ *
+ *  Author: Chao Peng (ANL), 03/31/2021
+ */
+#include <bitset>
+#include <algorithm>
+#include <unordered_map>
+
+#include "Gaudi/Property.h"
+#include "GaudiAlg/GaudiAlgorithm.h"
+#include "GaudiKernel/ToolHandle.h"
+#include "GaudiAlg/Transformer.h"
+#include "GaudiAlg/GaudiTool.h"
+#include "GaudiKernel/RndmGenerators.h"
+#include "GaudiKernel/PhysicalConstants.h"
+
+#include "DDRec/CellIDPositionConverter.h"
+#include "DDRec/SurfaceManager.h"
+#include "DDRec/Surface.h"
+
+// FCCSW
+#include "JugBase/DataHandle.h"
+#include "JugBase/IGeoSvc.h"
+
+// Event Model related classes
+#include "eicd/CalorimeterHitCollection.h"
+
+using namespace Gaudi::Units;
+
+namespace Jug::Reco {
+
+class SamplingECalHitsMerger : public GaudiAlgorithm {
+public:
+    Gaudi::Property<std::vector<std::pair<int, int>>>
+        u_cellIDMaskRanges{this, "cellIDMaskRanges", {{0, 31}}};
+    DataHandle<eic::CalorimeterHitCollection>
+        m_inputHitCollection{"inputHitCollection", Gaudi::DataHandle::Reader, this};
+    DataHandle<eic::CalorimeterHitCollection>
+        m_outputHitCollection{"outputHitCollection", Gaudi::DataHandle::Writer, this};
+    int64_t id_mask;
+
+    // ill-formed: using GaudiAlgorithm::GaudiAlgorithm;
+    SamplingECalHitsMerger(const std::string& name, ISvcLocator* svcLoc)
+        : GaudiAlgorithm(name, svcLoc)
+    {
+        declareProperty("inputHitCollection",       m_inputHitCollection,       "");
+        declareProperty("outputHitCollection",      m_outputHitCollection,    "");
+    }
+
+    StatusCode initialize() override
+    {
+        if (GaudiAlgorithm::initialize().isFailure()) {
+            return StatusCode::FAILURE;
+        }
+
+        // build masks from input
+        id_mask = 0;
+        for (auto &p : u_cellIDMaskRanges) {
+            debug() << "masking bit " << p.first << " - " << p.second << endmsg;
+            for (int64_t k = p.first; k <= p.second; ++k) {
+                id_mask |= (int64_t(1) << k);
+            }
+        }
+        id_mask = ~id_mask;
+        debug() << "cellID mask = " << std::bitset<64>(id_mask) << endmsg;
+
+        return StatusCode::SUCCESS;
+    }
+
+    StatusCode execute() override
+    {
+        // input collections
+	    const auto &hits = *m_inputHitCollection.get();
+        // Create output collections
+        auto &mhits = *m_outputHitCollection.createAndPut();
+
+        // sum energies that has the same id
+        std::unordered_map<long long, size_t> merge_map;
+        for (auto &h : hits) {
+            auto id = (h.cellID() & id_mask);
+            // debug() << h.cellID() << " - " << std::bitset<64>(h.cellID()) << endmsg;
+            auto it = merge_map.find(id);
+            if (it == merge_map.end()) {
+                merge_map[id] = mhits.size();
+                mhits.push_back(h.clone());
+                debug() << mhits[mhits.size() - 1].cellID() << " - " << std::bitset<64>(id) << endmsg;
+            } else {
+                mhits[it->second].energy(mhits[it->second].energy() + h.energy());
+            }
+        }
+
+        for (auto &h : mhits) {
+            debug() << h.cellID() << ": " << h.energy() << endmsg;
+        }
+
+        debug() << "Size before = " << hits.size() << ", after = " << mhits.size() << endmsg;
+
+        return StatusCode::SUCCESS;
+    }
+
+
+}; // class SamplingECalHitsMerger
+
+DECLARE_COMPONENT(SamplingECalHitsMerger)
+
+} // namespace Jug::Reco
+
-- 
GitLab