diff --git a/benchmarks/clustering/barrel_clusters.sh b/benchmarks/clustering/barrel_clusters.sh
deleted file mode 100644
index 28f924155d05bc87a9f52410d5ce6e018662fcb5..0000000000000000000000000000000000000000
--- a/benchmarks/clustering/barrel_clusters.sh
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/bin/bash
-
-print_env.sh
-
-## To run the reconstruction, we need the following global variables:
-## - JUGGLER_INSTALL_PREFIX:   Install prefix for Juggler (simu/recon)
-## - JUGGLER_DETECTOR:         the detector package we want to use for this benchmark
-## - JUGGLER_DETECTOR_VERSION: the detector package we want to use for this benchmark
-## - DETECTOR_PATH:            full path to the detector definitions
-##
-## You can ready options/env.sh for more in-depth explanations of the variables
-## and how they can be controlled.
-
-# deprecated
-export JUGGLER_DETECTOR_PATH=${DETECTOR_PATH}
-
-if [[ ! -n  "${JUGGLER_N_EVENTS}" ]] ; then 
-  export JUGGLER_N_EVENTS=1
-fi
-
-if [[ ! -n "${CB_EMCAL_SAMP_FRAC}" ]] ; then
-  export CB_EMCAL_SAMP_FRAC=1.0
-fi
-
-export JUGGLER_FILE_NAME_TAG="barrel_clusters"
-export JUGGLER_GEN_FILE="${JUGGLER_FILE_NAME_TAG}.hepmc"
-
-export JUGGLER_SIM_FILE="sim_${JUGGLER_FILE_NAME_TAG}.root"
-export JUGGLER_REC_FILE="rec_${JUGGLER_FILE_NAME_TAG}.root"
-
-echo "JUGGLER_N_EVENTS = ${JUGGLER_N_EVENTS}"
-echo "JUGGLER_DETECTOR = ${JUGGLER_DETECTOR}"
-
-root -b -q "benchmarks/clustering/scripts/gen_central_pions.cxx(${JUGGLER_N_EVENTS}, \"${JUGGLER_FILE_NAME_TAG}.hepmc\")"
-
-### run geant4 simulations
-npsim --runType batch \
-      --part.minimalKineticEnergy 1000*GeV  \
-      -v WARNING \
-      --numberOfEvents ${JUGGLER_N_EVENTS} \
-      --compactFile ${DETECTOR_PATH}/${JUGGLER_DETECTOR}.xml \
-      --inputFiles  ${JUGGLER_FILE_NAME_TAG}.hepmc \
-      --outputFile  ${JUGGLER_SIM_FILE}
-if [[ "$?" -ne "0" ]] ; then
-  echo "ERROR running npdet"
-  exit 1
-fi
-
-# Need to figure out how to pass file name to juggler from the commandline
-gaudirun.py benchmarks/clustering/options/fullcalo_clustering.py
-if [[ "$?" -ne "0" ]] ; then
-  echo "ERROR running juggler"
-  exit 1
-fi
-
-mkdir -p results/clustering
-
-#root -b -q "benchmarks/clustering/scripts/barrel_clusters.cxx(\"${JUGGLER_REC_FILE}\")"
-
-#root_filesize=$(stat --format=%s "${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}")
-#if [[ "${JUGGLER_N_EVENTS}" -lt "500" ]] ; then 
-#  # file must be less than 10 MB to upload
-#  if [[ "${root_filesize}" -lt "10000000" ]] ; then 
-#    cp ${JUGGLER_REC_FILE} results/clustering/.
-#  fi
-#fi
diff --git a/benchmarks/clustering/config.yml b/benchmarks/clustering/config.yml
index 76aab71dd4441954b566c91de26fbc1ab3dfbdec..0a244d41cbcfc724c30f44e1691fbc8895261d1e 100644
--- a/benchmarks/clustering/config.yml
+++ b/benchmarks/clustering/config.yml
@@ -3,7 +3,7 @@ clustering:process :
   stage: process
   timeout: 8 hour
   script:
-    - bash benchmarks/clustering/barrel_clusters.sh
+    - bash benchmarks/clustering/full_cal_clusters.sh -t fullcalo -n 1000 -p "pion-" --pmin 5 --pmax 5
 
 clustering:results:
   extends: .rec_benchmark
diff --git a/benchmarks/clustering/full_cal_clusters.sh b/benchmarks/clustering/full_cal_clusters.sh
new file mode 100644
index 0000000000000000000000000000000000000000..54498afd5e650003ca99f6ca40112553f96d0c9d
--- /dev/null
+++ b/benchmarks/clustering/full_cal_clusters.sh
@@ -0,0 +1,137 @@
+#!/bin/bash
+
+print_env.sh
+
+function print_the_help {
+  echo "USAGE: ${0} -n <nevents> -t <nametag> -p <particle> "
+  echo "  OPTIONS: "
+  echo "    -n,--nevents     Number of events"
+  echo "    -t,--nametag     name tag"
+  echo "    -p,--particle    particle type"
+  echo "    --pmin           minimum particle momentum (GeV)"
+  echo "    --pmax           maximum particle momentum (GeV)"
+  echo "                     allowed types: pion0, pion+, pion-, kaon0, kaon+, kaon-, proton, neutron, electron, positron, photon"
+  exit
+}
+
+POSITIONAL=()
+while [[ $# -gt 0 ]]
+do
+  key="$1"
+
+  case $key in
+    -h|--help)
+      shift # past argument
+      print_the_help
+      ;;
+    -t|--nametag)
+      nametag="$2"
+      shift # past argument
+      shift # past value
+      ;;
+    -p|--particle)
+      particle="$2"
+      shift # past argument
+      shift # past value
+      ;;
+    -n|--nevents)
+      export FULL_CAL_NUMEV="$2"
+      shift # past argument
+      shift # past value
+      ;;
+    --pmin)
+      export FULL_CAL_PMIN="$2"
+      shift
+      shift
+      ;;
+    --pmax)
+      export FULL_CAL_PMAX="$2"
+      shift
+      shift
+      ;;
+    *)    # unknown option
+      #POSITIONAL+=("$1") # save it in an array for later
+      echo "unknown option $1"
+      print_the_help
+      shift # past argument
+      ;;
+  esac
+done
+set -- "${POSITIONAL[@]}" # restore positional parameters
+
+export FULL_CAL_COMPACT_PATH=${DETECTOR_PATH}/${JUGGLER_DETECTOR}.xml
+
+if [[ ! -n  "${FULL_CAL_NUMEV}" ]] ; then
+  export FULL_CAL_NUMEV=1000
+fi
+
+if [[ ! -n  "${FULL_CAL_PMIN}" ]] ; then
+  export FULL_CAL_ENERGY=1.0
+fi
+
+if [[ ! -n  "${FULL_CAL_PMAX}" ]] ; then
+  export FULL_CAL_ENERGY=10.0
+fi
+
+export FULL_CAL_NAME_TAG="${nametag}"
+export FULL_CAL_GEN_FILE="${FULL_CAL_NAME_TAG}.hepmc"
+
+export FULL_CAL_SIM_FILE="sim_${FULL_CAL_NAME_TAG}.root"
+export FULL_CAL_REC_FILE="rec_${FULL_CAL_NAME_TAG}.root"
+
+echo "FULL_CAL_NUMEV = ${FULL_CAL_NUMEV}"
+echo "FULL_CAL_COMPACT_PATH = ${FULL_CAL_COMPACT_PATH}"
+
+# Generate the input events
+python benchmarks/imaging_ecal/scripts/gen_particles.py ${FULL_CAL_GEN_FILE} -n ${FULL_CAL_NUMEV}\
+    --angmin 5 --angmax 175 --phmin 0 --phmax 360 --pmin ${FULL_CAL_PMIN} --pmax ${FULL_CAL_PMAX}\
+    --particles="${particle}"
+
+if [[ "$?" -ne "0" ]] ; then
+  echo "ERROR running script: generating input events"
+  exit 1
+fi
+
+ls -lh ${FULL_CAL_GEN_FILE}
+
+# Run geant4 simulations
+npsim --runType batch \
+      -v WARNING \
+      --part.minimalKineticEnergy "1*TeV" \
+      --numberOfEvents ${FULL_CAL_NUMEV} \
+      --compactFile ${FULL_CAL_COMPACT_PATH} \
+      --inputFiles ${FULL_CAL_GEN_FILE} \
+      --outputFile ${FULL_CAL_SIM_FILE}
+
+if [[ "$?" -ne "0" ]] ; then
+  echo "ERROR running npdet"
+  exit 1
+fi
+
+rootls -t "${FULL_CAL_SIM_FILE}"
+
+# Directory for results
+mkdir -p results
+
+FULL_CAL_OPTION_DIR=benchmarks/clustering/options
+FULL_CAL_SCRIPT_DIR=benchmarks/clustering/scripts
+
+# Run Juggler
+xenv -x ${JUGGLER_INSTALL_PREFIX}/Juggler.xenv \
+    gaudirun.py ${FULL_CAL_OPTION_DIR}/full_cal_clustering.py
+if [[ "$?" -ne "0" ]] ; then
+  echo "ERROR running juggler"
+  exit 1
+fi
+
+# run analysis scripts
+python ${FULL_CAL_SCRIPT_DIR}/cluster_plots.py ${FULL_CAL_REC_FILE} -o results
+
+root_filesize=$(stat --format=%s "${FULL_CAL_REC_FILE}")
+if [[ "${FULL_CAL_NUMEV}" -lt "500" ]] ; then
+  # file must be less than 10 MB to upload
+  if [[ "${root_filesize}" -lt "10000000" ]] ; then
+    cp ${FULL_CAL_REC_FILE} results/.
+  fi
+fi
+
diff --git a/benchmarks/clustering/options/full_cal_clustering.py b/benchmarks/clustering/options/full_cal_clustering.py
new file mode 100644
index 0000000000000000000000000000000000000000..49d82f2860bdf6e9528f828c44a5918549b0be0d
--- /dev/null
+++ b/benchmarks/clustering/options/full_cal_clustering.py
@@ -0,0 +1,173 @@
+from Gaudi.Configuration import *
+import os
+import ROOT
+
+from GaudiKernel.DataObjectHandleBase import DataObjectHandleBase
+from Configurables import ApplicationMgr, EICDataSvc, PodioInput, PodioOutput, GeoSvc
+from GaudiKernel.SystemOfUnits import MeV, GeV, mm, cm, mrad
+
+detector_name = str(os.environ.get("JUGGLER_DETECTOR", "athena"))
+detector_path = str(os.environ.get("DETECTOR_PATH", "."))
+compact_path = os.path.join(detector_path, detector_name)
+
+# get sampling fractions from system environment variable, 1.0 by default
+cb_ecal_sf = float(os.environ.get("CB_EMCAL_SAMP_FRAC", 1.0))
+cb_hcal_sf = float(os.environ.get("CB_HCAL_SAMP_FRAC", 1.0))
+
+# input and output
+input_sims = [f.strip() for f in str.split(os.environ["FULL_CAL_SIM_FILE"], ",") if f.strip()]
+output_rec = str(os.environ["FULL_CAL_REC_FILE"])
+n_events = int(os.environ["FULL_CAL_NUMEV"])
+
+# geometry service
+geo_service = GeoSvc("GeoSvc", detectors=["{}.xml".format(compact_path)])
+# data service
+podioevent = EICDataSvc("EventDataSvc", inputs=input_sims)
+
+
+# juggler components
+from Configurables import Jug__Base__InputCopier_dd4pod__Geant4ParticleCollection_dd4pod__Geant4ParticleCollection_ as MCCopier
+from Configurables import Jug__Digi__CalorimeterHitDigi as CalHitDigi
+from Configurables import Jug__Reco__CalorimeterHitReco as CalHitReco
+from Configurables import Jug__Reco__CalorimeterHitsMerger as CalHitsMerger
+from Configurables import Jug__Reco__CalorimeterIslandCluster as IslandCluster
+
+from Configurables import Jug__Reco__ImagingPixelReco as ImCalPixelReco
+from Configurables import Jug__Reco__ImagingTopoCluster as ImagingCluster
+
+from Configurables import Jug__Reco__ClusterRecoCoG as RecoCoG
+from Configurables import Jug__Reco__ImagingClusterReco as ImagingClusterReco
+
+# branches needed from simulation root file
+sim_coll = [
+    "mcparticles",
+    "CrystalEcalHits",
+    "EcalBarrelHits",
+    "HcalBarrelHits",
+]
+
+# input and output
+podin = PodioInput("PodioReader", collections=sim_coll)
+podout = PodioOutput("out", filename=output_rec)
+# copier needed to get around input --> output copy bug. So truth (mcparticles) can be saved in output file
+copier = MCCopier("MCCopier", inputCollection="mcparticles", outputCollection="mcparticles2")
+
+
+# Crystal Endcap Ecal
+ce_ecal_digi = CalHitDigi("ce_ecal_digi",
+        inputHitCollection="CrystalEcalHits",
+        outputHitCollection="CrystalEcalHitsDigi",
+        energyResolutions=[0., 0.02, 0.],
+        dynamicRangeADC=5.*GeV,     # digi settings must match with reco
+        capacityADC=32768,
+        pedestalMean=400,
+        pedestalSigma=3)
+
+ce_ecal_reco = CalHitReco("ce_ecal_reco",
+        inputHitCollection="CrystalEcalHitsDigi",
+        outputHitCollection="CrystalEcalHitsReco",
+        dynamicRangeADC=5.*GeV,
+        capacityADC=32768,
+        pedestalMean=400,
+        pedestalSigma=3,
+        thresholdFactor=4,          # 4 sigma
+        minimumHitEdep=1.0*MeV)     # discard low energy hits
+
+ce_ecal_cl = IslandCluster("ce_ecal_cl",
+        # OutputLevel=DEBUG,
+        inputHitCollection="CrystalEcalHitsReco",
+        outputClusterCollection="CrystalEcalClusters",
+        splitHitCollection="CrystalEcalHitsSplit",
+        splitCluster=False,
+        minClusterCenterEdep=30*MeV,
+        groupRanges=[2.2*cm, 2.2*cm])
+
+ce_ecal_clreco = RecoCoG("ce_ecal_clreco",
+        clusterCollection="CrystalEcalClusters",
+        logWeightBase=4.6)
+
+
+# Central Barrel Ecal (Imaging Cal.)
+cb_ecal_digi = CalHitDigi("cb_ecal_digi",
+        inputHitCollection="EcalBarrelHits",
+        outputHitCollection="EcalBarrelHitsDigi",
+        energyResolutions=[0., 0.02, 0.],   # 2% flat resolution
+        dynamicRangeADC=3*MeV,
+        capacityADC=8192,
+        pedestalMean=400,
+        pedestalSigma=20)   # about 6 keV
+cb_ecal_reco = ImCalPixelReco("cb_ecal_reco",
+        inputHitCollection="EcalBarrelHitsDigi",
+        outputHitCollection="EcalBarrelHitsReco",
+        dynamicRangeADC=3.*MeV,
+        capacityADC=8192,
+        pedestalMean=400,
+        pedestalSigma=20,
+        thresholdFactor=3,  # about 20 keV
+        readoutClass="EcalBarrelHits",  # readout class
+        layerField="layer",             # field to get layer id
+        sectorField="module")           # field to get sector id
+cb_ecal_cl = ImagingCluster("cb_ecal_cl",
+        inputHitCollection="EcalBarrelHitsReco",
+        outputClusterCollection="EcalBarrelClusters",
+        localRanges=[2.*mm, 2*mm],              # same layer
+        adjLayerRanges=[10*mrad, 10*mrad],      # adjacent layer
+        adjLayerDiff=2,                         # id diff for adjacent layer
+        adjSectorDist=3.*cm)                    # different sector
+cb_ecal_clreco = ImagingClusterReco("cb_ecal_clreco",
+        inputClusterCollection="EcalBarrelClusters",
+        outputLayerCollection="EcalBarrelLayers")
+
+
+# Central Barrel Hcal
+cb_hcal_digi = CalHitDigi("cb_hcal_digi",
+         inputHitCollection="HcalBarrelHits",
+         outputHitCollection="HcalBarrelHitsDigi",
+         dynamicRangeADC=50.*MeV,
+         capacityADC=32768,
+         pedestalMean=400,
+         pedestalSigma=10)
+
+cb_hcal_reco = CalHitReco("cb_hcal_reco",
+        inputHitCollection="HcalBarrelHitsDigi",
+        outputHitCollection="HcalBarrelHitsReco",
+        dynamicRangeADC=50.*MeV,
+        capacityADC=32768,
+        pedestalMean=400,
+        pedestalSigma=10,
+        thresholdFactor=5.0)
+
+# merge hits in different layer (projection to local x-y plane)
+cb_hcal_merger = CalHitsMerger("cb_hcal_merger",
+          fields=["layer", "slice"],
+          fieldRefNumbers=[1, 0],
+          inputHitCollection="HcalBarrelHitsReco",
+          outputHitCollection="HcalBarrelHitsRecoXY")
+
+cb_hcal_cl = IslandCluster("cb_hcal_cl",
+        inputHitCollection="HcalBarrelHitsRecoXY",
+        outputClusterCollection="HcalBarrelClusters",
+        splitHitCollection="HcalBarrelHitsSplit",
+        splitCluster=False,
+        minClusterCenterEdep=30.*MeV,
+        groupRanges=[15.*cm, 15.*cm])
+
+cb_hcal_clreco = RecoCoG("cb_hcal_clreco",
+        clusterCollection="HcalBarrelClusters",
+        logWeightBase=6.2,
+        samplingFraction=cb_hcal_sf)
+
+podout.outputCommands = ["keep *"]
+
+ApplicationMgr(
+    TopAlg = [podin, copier,
+              ce_ecal_digi, ce_ecal_reco, ce_ecal_cl, ce_ecal_clreco,
+              cb_ecal_digi, cb_ecal_reco, cb_ecal_cl, cb_ecal_clreco,
+              cb_hcal_digi, cb_hcal_reco, cb_hcal_merger, cb_hcal_cl, cb_hcal_clreco,
+              podout],
+    EvtSel = 'NONE',
+    EvtMax   = n_events,
+    ExtSvc = [podioevent],
+    OutputLevel=DEBUG
+)
+
diff --git a/benchmarks/clustering/options/fullcalo_clustering.py b/benchmarks/clustering/options/fullcalo_clustering.py
deleted file mode 100644
index d2aa4e689f1676aad6972e6fa8a7dde87cf5b446..0000000000000000000000000000000000000000
--- a/benchmarks/clustering/options/fullcalo_clustering.py
+++ /dev/null
@@ -1,135 +0,0 @@
-from Gaudi.Configuration import *
-import os
-  
-from GaudiKernel.DataObjectHandleBase import DataObjectHandleBase
-from Configurables import ApplicationMgr, EICDataSvc, PodioOutput, GeoSvc
-from GaudiKernel import SystemOfUnits as units
-
-detector_name = "topside"
-if "JUGGLER_DETECTOR" in os.environ :
-  detector_name = str(os.environ["JUGGLER_DETECTOR"])
-
-if "JUGGLER_DETECTOR_PATH" in os.environ :
-  detector_name = str(os.environ["JUGGLER_DETECTOR_PATH"]) + "/" + detector_name
-
-# get sampling fraction from system environment variable, 1.0 by default
-sf = 1.0
-if "CB_EMCAL_SAMP_FRAC" in os.environ :
-  sf = str(os.environ["CB_EMCAL_SAMP_FRAC"])
-
-# todo add checks
-input_sim_file  = str(os.environ["JUGGLER_SIM_FILE"])
-output_rec_file = str(os.environ["JUGGLER_REC_FILE"])
-n_events = str(os.environ["JUGGLER_N_EVENTS"])
-
-geo_service  = GeoSvc("GeoSvc", detectors=["{}.xml".format(detector_name)])
-podioevent   = EICDataSvc("EventDataSvc", inputs=[input_sim_file])
-
-from Configurables import PodioInput
-from Configurables import Jug__Base__InputCopier_dd4pod__Geant4ParticleCollection_dd4pod__Geant4ParticleCollection_ as MCCopier
-from Configurables import Jug__Base__InputCopier_dd4pod__CalorimeterHitCollection_dd4pod__CalorimeterHitCollection_ as CalCopier
-
-from Configurables import Jug__Digi__HadronicCalDigi as HadCalorimeterDigi
-from Configurables import Jug__Digi__EMCalorimeterDigi as EMCalorimeterDigi
-from Configurables import Jug__Digi__CrystalEndcapsDigi as CrystalEndcapsDigi
-
-from Configurables import Jug__Reco__CrystalEndcapsReco as CrystalEndcapsReco
-from Configurables import Jug__Reco__EMCalReconstruction as EMCalReconstruction
-from Configurables import Jug__Reco__HCalReconstruction as HCalReconstruction
-
-from Configurables import Jug__Reco__SimpleClustering as SimpleClustering
-from Configurables import Jug__Reco__CalorimeterIslandCluster as IslandCluster
-from Configurables import Jug__Reco__ClusterRecoCoG as RecoCoG
-
-podioinput = PodioInput("PodioReader", collections=["mcparticles","CrystalEcalHits","EcalBarrelHits","HcalBarrelHits"], OutputLevel=DEBUG)
-
-## copiers to get around input --> output copy bug. Note the "2" appended to the output collection.
-copier = MCCopier("MCCopier", 
-        inputCollection="mcparticles", 
-        outputCollection="mcparticles2") 
-calcopier = CalCopier("CalCopier", 
-        inputCollection="CrystalEcalHits", 
-        outputCollection="CrystalEcalHits2") 
-
-
-##raw hits
-emcaldigi = CrystalEndcapsDigi("ecal_digi",
-        inputHitCollection="CrystalEcalHits", 
-        outputHitCollection="RawDigiEcalHits")
-ecdigi = EMCalorimeterDigi("ec_barrel_digi", 
-        inputHitCollection="EcalBarrelHits", 
-        outputHitCollection="RawEcalBarrelHits")
-
-hcaldigi = HadCalorimeterDigi("hcal_barrel_digi",
-         inputHitCollection="HcalBarrelHits",
-         outputHitCollection="RawHcalBarrelHits")
-
-
-
-#digitized hits
-crystal_ec_reco = CrystalEndcapsReco("crystal_ec_reco", 
-        inputHitCollection="RawDigiEcalHits", 
-        outputHitCollection="RecoEcalHits",
-        minModuleEdep=1.0*units.MeV)
-
-ecal_reco = EMCalReconstruction("ecal_reco", 
-        inputHitCollection="RawEcalBarrelHits", 
-        outputHitCollection="RecEcalBarrelHits",
-        samplingFraction=0.25,
-        minModuleEdep=0.0*units.MeV)
-
-hcal_reco = HCalReconstruction("hcal_reco",
-        inputHitCollection="RawHcalBarrelHits",
-        outputHitCollection="RecHcalBarrelHits",
-        samplingFraction=0.25,
-        minModuleEdep=0.0*units.MeV)
-#clusters
-ec_barrel_cluster = IslandCluster("ec_barrel_cluster", 
-        inputHitCollection="RecEcalBarrelHits", 
-        outputClusterCollection="EcalBarrelClusters",
-        splitHitCollection="splitEcalBarrelHitCollection",
-        minClusterCenterEdep=1*units.MeV, 
-        groupRange=2.0,
-        OutputLevel=DEBUG)
-
-crystal_ec_cluster = IslandCluster("crystal_ec_cluster", 
-        inputHitCollection="RecoEcalHits", 
-        outputClusterCollection="EcalClusters",
-        minClusterCenterEdep=30*units.MeV, 
-        groupRange=2.0,
-        OutputLevel=DEBUG)
-
-simple_cluster = SimpleClustering("simple_cluster", 
-        inputHitCollection="RecEcalBarrelHits", 
-        outputClusters="SimpleClusters",
-        OutputLevel=DEBUG)
-
-# finalizing clustering (center-of-gravity step)
-ec_barrel_clusterreco = RecoCoG("ec_barrel_clusterreco",
-        clusterCollection="EcalBarrelClusters", 
-        logWeightBase=6.2,
-        samplingFraction=sf) 
-
-clusterreco = RecoCoG("cluster_reco", 
-        clusterCollection="EcalClusters", 
-        logWeightBase=4.2, 
-        moduleDimZName="CrystalBox_z_length",
-        samplingFraction=sf, 
-        OutputLevel=DEBUG)
-
-out = PodioOutput("out", filename=output_rec_file)
-
-out.outputCommands = ["keep *"]
-
-ApplicationMgr(
-    TopAlg = [podioinput, copier, calcopier,
-              ecdigi, emcaldigi,hcaldigi,
-              crystal_ec_reco, ecal_reco, hcal_reco,  
-              #ec_barrel_cluster, crystal_ec_cluster, ec_barrel_clusterreco, clusterreco,
-              #simple_cluster,
-              out],
-    EvtSel = 'NONE',
-    EvtMax   = n_events,
-    ExtSvc = [podioevent],
-    OutputLevel=DEBUG
- )
diff --git a/benchmarks/clustering/scripts/barrel_clusters.cxx b/benchmarks/clustering/scripts/barrel_clusters.cxx
index c86c5eef6f205404538961029213cff63b0aa766..8968d48d635e042b5ba76b8e1871a74167c8e956 100644
--- a/benchmarks/clustering/scripts/barrel_clusters.cxx
+++ b/benchmarks/clustering/scripts/barrel_clusters.cxx
@@ -39,6 +39,13 @@ auto pt = [](std::vector<dd4pod::Geant4ParticleData> const& in) {
   }
   return result;
 };
+auto pid = [](ROOT::VecOps::RVec<dd4pod::Geant4ParticleData> const& in) {
+  std::vector<int> result;
+  for (size_t i = 0; i < in.size(); ++i) {
+    result.push_back(in[i].pdgID);
+  }
+  return result;
+};
 auto eta = [](ROOT::VecOps::RVec<dd4pod::Geant4ParticleData> const& in) {
   std::vector<float> result;
   ROOT::Math::PxPyPzMVector lv;
@@ -79,7 +86,7 @@ auto delta_E = [](std::vector<ROOT::Math::PxPyPzMVector> const& thrown, const st
 };
 
 
-void barrel_clusters(const char* in_fname = "topside/rec_barrel_clusters.root")
+int barrel_clusters(const char* in_fname = "topside/rec_barrel_clusters.root")
 {
   ROOT::EnableImplicitMT();
   ROOT::RDataFrame df("events", in_fname);
diff --git a/benchmarks/clustering/scripts/cluster_plots.py b/benchmarks/clustering/scripts/cluster_plots.py
new file mode 100644
index 0000000000000000000000000000000000000000..52ed607f53c734a93679db0f0ddcdc826d485c4d
--- /dev/null
+++ b/benchmarks/clustering/scripts/cluster_plots.py
@@ -0,0 +1,134 @@
+'''
+    A simple analysis script to extract some basic info of Monte-Carlo particles and clusters
+'''
+import os
+import ROOT
+import pandas as pd
+import numpy as np
+import argparse
+from matplotlib import pyplot as plt
+import matplotlib.ticker as ticker
+
+
+# load root macros, input is an argument string
+def load_root_macros(arg_macros):
+    for path in arg_macros.split(','):
+        path = path.strip()
+        if os.path.exists(path):
+            gROOT.Macro(path)
+            print('Loading root macro: \'{}\' loaded.'.format(path))
+        else:
+            print('Loading root macros: \'{}\' does not exist, skip loading it.'.format(path))
+
+
+def thrown_particles_figure(df, save):
+    # define truth particle info
+    df = df.Define('isThrown', 'mcparticles2.genStatus == 1')\
+           .Define('thrownParticles', 'mcparticles2[isThrown]')\
+           .Define('thrownP', 'fourvec(thrownParticles)')\
+           .Define('thrownPID', 'pid(thrownParticles)')\
+           .Define('thrownEta', 'eta(thrownParticles)')\
+           .Define('thrownTheta', 'theta(thrownP)')\
+           .Define('thrownMomentum', 'momentum(thrownP)')
+    # figure
+    fig, axs = plt.subplots(2, 2, figsize=(16, 12), dpi=120)
+
+    # pid info need some special treatment
+    pids = df.AsNumpy(['thrownPID'])['thrownPID']
+    # unpack vectors
+    pids = np.asarray([v for vec in pids for v in vec], dtype=int)
+    # build a dictionary for particle id and particle name
+    pdgbase = ROOT.TDatabasePDG()
+    pdict = {pid: pdgbase.GetParticle(int(pid)).GetName() for pid in np.unique(pids)}
+    pmap = {pid: i*2 for i, pid in enumerate(list(pdict))}
+    # convert pid to index in dictionary
+    pmaps = [pmap[i] for i in pids]
+    ax = axs.flat[0]
+    ax.hist(pmaps, bins=np.arange(-4, len(pmap)*2 + 4), align='left', ec='k')
+    ax.set_ylabel('Particle Counts', fontsize=24)
+    ax.tick_params(labelsize=22)
+    ax.set_axisbelow(True)
+    ax.grid(linestyle=':', which='both', axis='y')
+    ax.xaxis.set_major_locator(ticker.FixedLocator(list(pmap.values())))
+    ax.set_xticklabels(list(pdict.values()))
+
+    # kinematics
+    data = df.AsNumpy(['thrownMomentum', 'thrownTheta', 'thrownEta'])
+    labels = {
+        'thrownMomentum': (r'$p$ (GeV)', 'Counts'),
+        'thrownTheta': (r'$\theta$ (degree)', 'Counts'),
+        'thrownEta': (r'$\eta$', 'Counts'),
+    }
+    for ax, (name, vals) in zip(axs.flat[1:], data.items()):
+        hvals = [v for vec in vals for v in vec]
+        ax.hist(hvals, bins=50, ec='k')
+        ax.tick_params(labelsize=22, direction='in', which='both')
+        ax.grid(linestyle=':', which='both')
+        label = labels[name]
+        ax.set_axisbelow(True)
+        ax.set_xlabel(label[0], fontsize=24)
+        ax.set_ylabel(label[1], fontsize=24)
+
+    fig.text(0.5, 0.95, 'Thrown Particles', ha='center', fontsize=24)
+    fig.savefig(save)
+    plt.close(fig)
+
+
+def general_clusters_figure(df, collection, save):
+    data = df.AsNumpy([
+        '{}.nhits'.format(collection),
+        '{}.energy'.format(collection),
+        '{}.polar.theta'.format(collection),
+        '{}.polar.phi'.format(collection),
+        ])
+    # figure
+    fig, axs = plt.subplots(2, 2, figsize=(16, 12), dpi=120)
+    labels = [
+        ('Number of Hits', 'Counts'),
+        ('Energy (MeV)', 'Counts'),
+        ('Theta (rad)', 'Counts'),
+        ('Phi (rad)', 'Counts'),
+    ]
+    for ax, label, (name, vals) in zip(axs.flat, labels, data.items()):
+        hvals = [v for vec in vals for v in vec]
+        ax.hist(hvals, bins=50, ec='k')
+        ax.tick_params(labelsize=22, direction='in', which='both')
+        ax.grid(linestyle=':', which='both')
+        ax.set_axisbelow(True)
+        ax.set_xlabel(label[0], fontsize=24)
+        ax.set_ylabel(label[1], fontsize=24)
+
+    fig.text(0.5, 0.95, collection, ha='center', fontsize=24)
+    fig.savefig(save)
+    plt.close(fig)
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument('file', help='Path to reconstruction file.')
+    parser.add_argument('-o', dest='outdir', default='.', help='Output directory.')
+    parser.add_argument('-m', '--macros', dest='macros', default='rootlogon.C',
+            help='Macro files to be loaded by root, separated by \",\".')
+    args = parser.parse_args()
+
+    # multi-threading for RDataFrame
+    nthreads = os.cpu_count()//2
+    ROOT.ROOT.EnableImplicitMT(nthreads)
+    print('CPU available {}, using {}'.format(os.cpu_count(), nthreads))
+
+    # declare some functions from c++ script, needed for data frame processing
+    script_dir = os.path.dirname(os.path.realpath(__file__))
+    fcode = open(os.path.join(script_dir, 'barrel_clusters.cxx')).read()
+    ROOT.gInterpreter.Declare(fcode)
+
+    os.makedirs(args.outdir, exist_ok=True)
+    # load macros (add libraries/headers root cannot automatically found here)
+    load_root_macros(args.macros)
+
+    rdf = ROOT.RDataFrame('events', args.file)
+
+    thrown_particles_figure(rdf, save=os.path.join(args.outdir, 'thrown_particles.png'))
+    general_clusters_figure(rdf, collection='CrystalEcalClusters', save=os.path.join(args.outdir, 'crystal_ecal_clusters.png'))
+    general_clusters_figure(rdf, collection='EcalBarrelClusters', save=os.path.join(args.outdir, 'ecal_barrel_clusters.png'))
+    general_clusters_figure(rdf, collection='HcalBarrelClusters', save=os.path.join(args.outdir, 'hcal_barrel_clusters.png'))
+
diff --git a/benchmarks/clustering/scripts/gen_particles.py b/benchmarks/clustering/scripts/gen_particles.py
new file mode 100644
index 0000000000000000000000000000000000000000..2d0b81c8278d345cff479161f107f08f62c55bf1
--- /dev/null
+++ b/benchmarks/clustering/scripts/gen_particles.py
@@ -0,0 +1,104 @@
+import os
+from pyHepMC3 import HepMC3 as hm
+import numpy as np
+import argparse
+
+
+PARTICLES = {
+    "pion0": (111, 0.1349766),       # pi0
+    "pion+": (211, 0.13957018),      # pi+
+    "pion-": (-211, 0.13957018),     # pi-
+    "kaon0": (311, 0.497648),        # K0
+    "kaon+": (321, 0.493677),        # K+
+    "kaon-": (-321, 0.493677),       # K-
+    "proton": (2212, 0.938272),      # proton
+    "neutron": (2112, 0.939565),     # neutron
+    "electron": (11, 0.51099895e-3), # electron
+    "positron": (-11, 0.51099895e-3),# positron
+    "photon": (22, 0),               # photon
+}
+
+
+def gen_event(p, theta, phi, pid, mass):
+    evt = hm.GenEvent(momentum_unit=hm.Units.MomentumUnit.GEV, length_unit=hm.Units.LengthUnit.MM)
+    # final state
+    state = 1
+    e0 = np.sqrt(p*p + mass*mass)
+    px = np.cos(phi)*np.sin(theta)
+    py = np.sin(phi)*np.sin(theta)
+    pz = np.cos(theta)
+
+    # beam
+    pbeam = hm.GenParticle(hm.FourVector(0, 0, 0, 0.938272), 2212, 4)
+    ebeam = hm.GenParticle(hm.FourVector(0, 0, e0, np.sqrt(e0*e0 + 0.511e-3*0.511e-3)), 11, 4)
+
+    # out particle
+    hout = hm.GenParticle(hm.FourVector(px*p, py*p, pz*p, e0), pid, state)
+
+    # vertex
+    vert = hm.GenVertex()
+    vert.add_particle_in(ebeam)
+    vert.add_particle_in(pbeam)
+    vert.add_particle_out(hout)
+    evt.add_vertex(vert)
+    return evt
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+
+    parser.add_argument('output', help='path to the output file')
+    parser.add_argument('-n', type=int, default=1000, dest='nev', help='number of events to generate')
+    parser.add_argument('-s', type=int, default=-1, dest='seed', help='seed for random generator')
+    parser.add_argument('--parray', type=str, default="", dest='parray',
+                        help='an array of momenta in GeV, separated by \",\"')
+    parser.add_argument('--pmin', type=float, default=8.0, dest='pmin', help='minimum momentum in GeV')
+    parser.add_argument('--pmax', type=float, default=100.0, dest='pmax', help='maximum momentum in GeV')
+    parser.add_argument('--angmin', type=float, default=0.0, dest='angmin', help='minimum angle in degree')
+    parser.add_argument('--angmax', type=float, default=20.0, dest='angmax', help='maximum angle in degree')
+    parser.add_argument('--phmin', type=float, default=0.0, dest='phmin', help='minimum angle in degree')
+    parser.add_argument('--phmax', type=float, default=360.0, dest='phmax', help='maximum angle in degree')
+    parser.add_argument('--particles', type=str, default='electron', dest='particles',
+                        help='particle names, support {}'.format(list(PARTICLES.keys())))
+
+    args = parser.parse_args()
+
+    # random seed (< 0 will get it from enviroment variable 'SEED', or a system random number)
+    if args.seed < 0:
+        args.seed = os.environ.get('SEED', int.from_bytes(os.urandom(4), byteorder='big', signed=False))
+    print("Random seed is {}".format(args.seed))
+    np.random.seed(args.seed)
+
+    output = hm.WriterAscii(args.output);
+    if output.failed():
+        print("Cannot open file \"{}\"".format(args.output))
+        sys.exit(2)
+
+    # build particle info
+    parts = []
+    for pid in args.particles.split(','):
+        pid = pid.strip()
+        if pid not in PARTICLES.keys():
+            print('pid {:d} not found in dictionary, ignored.'.format(pid))
+            continue
+        parts.append(PARTICLES[pid])
+
+    # p values
+    pvals = np.random.uniform(args.pmin, args.pmax, args.nev) if not args.parray else \
+            np.random.choice([float(p.strip()) for p in args.parray.split(',')], args.nev)
+    thvals = np.random.uniform(args.angmin, args.angmax, args.nev)/180.*np.pi
+    phivals = np.random.uniform(args.phmin, args.phmax, args.nev)/180.*np.pi
+    partvals = [parts[i] for i in np.random.choice(len(parts), args.nev)]
+
+    count = 0
+    for p, theta, phi, (pid, mass) in zip(pvals, thvals, phivals, partvals):
+        if (count % 1000 == 0):
+            print("Generated {} events".format(count), end='\r')
+        evt = gen_event(p, theta, phi, pid, mass)
+        output.write_event(evt)
+        evt.clear()
+        count += 1
+
+    print("Generated {} events".format(args.nev))
+    output.close()
+