From f025ce83fc2aa691877b767024546cfaed29e098 Mon Sep 17 00:00:00 2001
From: Pierre-Alain Loizeau <p.-a.loizeau@gsi.de>
Date: Thu, 23 Nov 2023 12:49:15 +0000
Subject: [PATCH] DC Nov23 backports: cbmreco: Enable storage of STS clusters
 and digis and extend archive plotter.

---
 algo/base/Definitions.h                       |   4 +-
 algo/data/sts/Cluster.h                       |  16 +++
 algo/detectors/sts/HitfinderChain.cxx         |  46 ++++++-
 algo/detectors/sts/HitfinderChain.h           |   9 +-
 algo/global/Reco.cxx                          |  16 ++-
 algo/global/RecoResults.h                     |  11 +-
 algo/global/StorableRecoResults.h             |  11 ++
 reco/app/cbmreco/main.cxx                     |   3 +
 services/archive_explorer/CMakeLists.txt      |   2 +
 services/archive_explorer/app/Application.cxx |  23 +++-
 services/archive_explorer/app/Histograms.cxx  |  90 +++++++++++++
 services/archive_explorer/app/Histograms.h    |  46 +++++++
 services/archive_explorer/app/Options.cxx     |   8 ++
 services/archive_explorer/app/Options.h       |  30 +++--
 .../lib/HistogramCollection.cxx               |  54 ++++++++
 .../lib/HistogramCollection.h                 |  78 ++++++++++++
 services/archive_explorer/lib/Server.cxx      | 119 +++++++-----------
 services/archive_explorer/lib/Server.h        |  40 +++---
 18 files changed, 482 insertions(+), 124 deletions(-)
 create mode 100644 services/archive_explorer/app/Histograms.cxx
 create mode 100644 services/archive_explorer/app/Histograms.h
 create mode 100644 services/archive_explorer/lib/HistogramCollection.cxx
 create mode 100644 services/archive_explorer/lib/HistogramCollection.h

diff --git a/algo/base/Definitions.h b/algo/base/Definitions.h
index 490edc62a4..cf2742fcfe 100644
--- a/algo/base/Definitions.h
+++ b/algo/base/Definitions.h
@@ -41,7 +41,7 @@ namespace cbm::algo
 
   enum class RecoData
   {
-    Digi,       //< Raw output from unpackers
+    RawDigi,    //< Raw output from unpackers
     DigiEvent,  //< Digis after event building
     Cluster,
     Hit,
@@ -81,7 +81,7 @@ CBM_ENUM_DICT(cbm::algo::Step,
 );
 
 CBM_ENUM_DICT(cbm::algo::RecoData,
-  {"Digi", RecoData::Digi},
+  {"RawDigi", RecoData::RawDigi},
   {"DigiEvent", RecoData::DigiEvent},
   {"Cluster", RecoData::Cluster},
   {"Hit", RecoData::Hit},
diff --git a/algo/data/sts/Cluster.h b/algo/data/sts/Cluster.h
index 1e102b32e4..56b89092f0 100644
--- a/algo/data/sts/Cluster.h
+++ b/algo/data/sts/Cluster.h
@@ -4,6 +4,8 @@
 #ifndef CBM_ALGO_DATA_STS_CLUSTER_H
 #define CBM_ALGO_DATA_STS_CLUSTER_H
 
+#include <boost/serialization/access.hpp>
+
 #include "Definitions.h"
 
 namespace cbm::algo::sts
@@ -16,6 +18,20 @@ namespace cbm::algo::sts
     real fPositionError;  ///< Cluster centre error (r.m.s.) in channel number units
     u32 fTime;            ///< cluster time [ns]
     real fTimeError;      ///< Error of cluster time [ns]
+
+  private:  // serialization
+    friend class boost::serialization::access;
+
+    template<class Archive>
+    void serialize(Archive& ar, unsigned int /*version*/)
+    {
+      ar& fCharge;
+      ar& fSize;
+      ar& fPosition;
+      ar& fPositionError;
+      ar& fTime;
+      ar& fTimeError;
+    }
   };
 
 }  // namespace cbm::algo::sts
diff --git a/algo/detectors/sts/HitfinderChain.cxx b/algo/detectors/sts/HitfinderChain.cxx
index d0a2d5aa5c..65ade190e7 100644
--- a/algo/detectors/sts/HitfinderChain.cxx
+++ b/algo/detectors/sts/HitfinderChain.cxx
@@ -44,7 +44,7 @@ void sts::HitfinderChain::Finalize()
   xpu::set<TheHitfinder>(fHitfinder);
 }
 
-sts::HitfinderChain::Result sts::HitfinderChain::operator()(gsl::span<const CbmStsDigi> digis)
+sts::HitfinderChain::Result sts::HitfinderChain::operator()(gsl::span<const CbmStsDigi> digis, bool storeClusters)
 {
   EnsureParameters();
 
@@ -169,7 +169,7 @@ sts::HitfinderChain::Result sts::HitfinderChain::operator()(gsl::span<const CbmS
   // xpu::run_kernel<FindClustersBasic>(xpu::grid::n_blocks(hfc.nModules * 2));
   // xpu::run_kernel<CalculateClustersBasic>(xpu::grid::n_blocks(hfc.nModules * 2));
   L_(debug) << "STS Hitfinder Chain: Sort Clusters...";
-  queue.launch<SortClusters>(xpu::n_blocks(nModuleSides));  // FIXME n_blocks(nModuleSides) once debugging is done
+  queue.launch<SortClusters>(xpu::n_blocks(nModuleSides));
 
   L_(debug) << "STS Hitfinder Chain: Find Hits...";
   queue.copy(hfc.nClustersPerModule, xpu::d2h);
@@ -231,6 +231,9 @@ sts::HitfinderChain::Result sts::HitfinderChain::operator()(gsl::span<const CbmS
     }
   }
 
+  PartitionedVector<sts::Cluster> clusters;
+  if (storeClusters) clusters = FlattenClusters(queue);
+
   size_t nHitsTotal = std::accumulate(nHits.begin(), nHits.end(), 0);
   L_(info) << "Timeslice contains " << nHitsTotal << " STS hits and " << nClustersTotal << " STS clusters";
 
@@ -241,6 +244,7 @@ sts::HitfinderChain::Result sts::HitfinderChain::operator()(gsl::span<const CbmS
 
   Result result;
   result.hits    = std::move(hits);
+  result.clusters = std::move(clusters);
   result.monitor = monitor[0];
   return result;
 }
@@ -578,6 +582,44 @@ PartitionedSpan<sts::Hit> sts::HitfinderChain::FlattenHits(xpu::queue queue)
   return partitionedHits;
 }
 
+PartitionedVector<sts::Cluster> sts::HitfinderChain::FlattenClusters(xpu::queue queue)
+{
+  auto& hfc = fHitfinder;
+
+  xpu::h_view nClusters(hfc.nClustersPerModule);
+  int maxClustersPerModule = hfc.maxClustersPerModule;
+  sts::Cluster* clusters   = hfc.clusterDataPerModule.get();
+
+  int nModuleSides = nClusters.size();
+
+  L_(debug) << "Copy clusters from " << nModuleSides << " modules sides";
+
+  std::vector<u32> addresses;
+  for (size_t i = 0; i < 2; i++) {  // Repeat twice for front and back
+    for (auto& m : fPars->setup.modules)
+      addresses.push_back(m.address);
+  }
+
+  size_t nClustersTotal = std::accumulate(nClusters.begin(), nClusters.end(), 0);
+
+  // TODO: copy could be made much more efficient by using pinned memory
+  // But clusters are stored for debugging only, so no effort is made to optimize this
+  std::vector<sts::Cluster> clustersFlat;
+  clustersFlat.resize(nClustersTotal);
+  size_t offset = 0;
+  for (int m = 0; m < nModuleSides; m++) {
+    size_t nClustersInModule = nClusters[m];
+    queue.copy(clusters + maxClustersPerModule * m, clustersFlat.data() + offset,
+               nClustersInModule * sizeof(sts::Cluster));
+    offset += nClustersInModule;
+  }
+  queue.wait();
+
+  std::vector<size_t> sizes(nClusters.begin(), nClusters.end());
+  PartitionedVector<sts::Cluster> out(std::move(clustersFlat), gsl::make_span(sizes), gsl::make_span(addresses));
+  return out;
+}
+
 size_t sts::HitfinderChain::GetNHits(xpu::h_view<int> nHitsPerModule, int module)
 {
   return std::min<size_t>(nHitsPerModule[module], fHitfinder.maxHitsPerModule);
diff --git a/algo/detectors/sts/HitfinderChain.h b/algo/detectors/sts/HitfinderChain.h
index ddcf955a65..891a3d6187 100644
--- a/algo/detectors/sts/HitfinderChain.h
+++ b/algo/detectors/sts/HitfinderChain.h
@@ -15,6 +15,7 @@
 #include <vector>
 
 #include "PartitionedSpan.h"
+#include "PartitionedVector.h"
 #include "SubChain.h"
 #include "gpu/xpu_legacy.h"
 #include "sts/Hitfinder.h"
@@ -40,6 +41,7 @@ namespace cbm::algo::sts
   public:
     struct Result {
       PartitionedSpan<sts::Hit> hits;
+      PartitionedVector<sts::Cluster> clusters;
       sts::HitfinderMonitor monitor;
     };
 
@@ -51,7 +53,7 @@ namespace cbm::algo::sts
      */
     void Finalize();
 
-    Result operator()(gsl::span<const CbmStsDigi>);
+    Result operator()(gsl::span<const CbmStsDigi>, bool storeClusters = false);
 
   private:
     static constexpr u16 InvalidModule = 0xFFFF;
@@ -104,6 +106,11 @@ namespace cbm::algo::sts
      */
     PartitionedSpan<sts::Hit> FlattenHits(xpu::queue);
 
+    /**
+     * Copy Clusters back to host into a flat array.
+     */
+    PartitionedVector<sts::Cluster> FlattenClusters(xpu::queue);
+
     /**
     * @brief Returns the number of hits of a module.
     *
diff --git a/algo/global/Reco.cxx b/algo/global/Reco.cxx
index 56be1fbbc7..41965fe098 100644
--- a/algo/global/Reco.cxx
+++ b/algo/global/Reco.cxx
@@ -27,9 +27,6 @@ void Reco::Validate(const Options& opts)
 {
   if (!fs::exists(opts.ParamsDir())) throw FatalError("ParamsDir does not exist: {}", opts.ParamsDir().string());
 
-  if (opts.HasOutput(RecoData::Digi)) throw FatalError("Storing raw digis via 'Digi' option not supported");
-  if (opts.HasOutput(RecoData::Cluster)) throw FatalError("Storing clusters via 'Cluster' option not supported");
-
   bool hasOutputFile = !opts.OutputFile().empty();
   bool hasOutputType = !opts.OutputTypes().empty();
 
@@ -154,9 +151,12 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
 
     sts::HitfinderMonitor stsHitfinderMonitor;
     PartitionedSpan<sts::Hit> stsHits;
+    PartitionedVector<sts::Cluster> stsClusters;
     if (Opts().HasStep(Step::LocalReco) && Opts().HasDetector(fles::Subsystem::STS)) {
-      auto stsResults     = fStsHitFinder(unpackResult.first.fSts);
+      bool storeClusters  = Opts().HasOutput(RecoData::Cluster);
+      auto stsResults     = fStsHitFinder(unpackResult.first.fSts, storeClusters);
       stsHits             = stsResults.hits;
+      stsClusters         = std::move(stsResults.clusters);
       stsHitfinderMonitor = stsResults.monitor;
       QueueStsRecoMetrics(stsHitfinderMonitor);
     }
@@ -185,9 +185,13 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
       QueueTrackingMetrics(trackingOutput.monitorData);
     }
 
+    if (Opts().HasOutput(RecoData::RawDigi)) results.stsDigis = std::move(unpackResult.first.fSts);
     if (Opts().HasOutput(RecoData::DigiEvent)) results.events = std::move(events);
-    if (Opts().HasOutput(RecoData::Hit)) results.stsHits = std::move(stsHits);
-    if (Opts().HasOutput(RecoData::Hit)) results.tofHits = std::move(tofHits);
+    if (Opts().HasOutput(RecoData::Cluster)) results.stsClusters = std::move(stsClusters);
+    if (Opts().HasOutput(RecoData::Hit)) {
+      results.stsHits = std::move(stsHits);
+      results.tofHits = std::move(tofHits);
+    }
   }
   PrintTimings(tsTimes);
 
diff --git a/algo/global/RecoResults.h b/algo/global/RecoResults.h
index 42f1aa04a0..ecc34dd934 100644
--- a/algo/global/RecoResults.h
+++ b/algo/global/RecoResults.h
@@ -1,6 +1,6 @@
 /* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
    SPDX-License-Identifier: GPL-3.0-only
-   Authors: Sergei Zharko [committer] */
+   Authors: Sergei Zharko [committer], Felix Weiglhofer */
 
 /// \file   RecoResults.h
 /// \date   22.10.2023
@@ -15,14 +15,17 @@
 #include "PartitionedSpan.h"
 #include "PartitionedVector.h"
 #include "ca/core/utils/CaVector.h"
-#include "data/sts/Hit.h"
+#include "sts/Cluster.h"
+#include "sts/Hit.h"
 
 namespace cbm::algo
 {
-  /// \struct RecoResults
-  /// \brief  Structure to keep reconstructed results: digi-events, hits and tracks
+  /// @name RecoResults
+  /// @brief  Structure to keep reconstructed results: digi-events, hits and tracks
   struct RecoResults {
     std::vector<DigiEvent> events;
+    PODVector<CbmStsDigi> stsDigis;
+    PartitionedVector<sts::Cluster> stsClusters;
     PartitionedSpan<sts::Hit> stsHits;
     PartitionedVector<tof::Hit> tofHits;
     ca::Vector<ca::Track> tracks;
diff --git a/algo/global/StorableRecoResults.h b/algo/global/StorableRecoResults.h
index 87d99fc8b3..8a11c4522b 100644
--- a/algo/global/StorableRecoResults.h
+++ b/algo/global/StorableRecoResults.h
@@ -17,6 +17,7 @@
 #include "PartitionedVector.h"
 #include "ca/core/data/CaTrack.h"
 #include "ca/core/utils/CaVector.h"
+#include "sts/Cluster.h"
 #include "sts/Hit.h"
 
 namespace cbm::algo
@@ -47,6 +48,12 @@ namespace cbm::algo
     std::vector<CbmDigiEvent>& DigiEvents() { return fDigiEvents; }
     const std::vector<CbmDigiEvent>& DigiEvents() const { return fDigiEvents; }
 
+    std::vector<CbmStsDigi>& StsDigis() { return fStsDigis; }
+    const std::vector<CbmStsDigi>& StsDigis() const { return fStsDigis; }
+
+    PartitionedVector<sts::Cluster>& StsClusters() { return fStsClusters; }
+    const PartitionedVector<sts::Cluster>& StsClusters() const { return fStsClusters; }
+
     PartitionedVector<sts::Hit>& StsHits() { return fStsHits; }
     const PartitionedVector<sts::Hit>& StsHits() const { return fStsHits; }
 
@@ -67,6 +74,8 @@ namespace cbm::algo
     uint64_t fTsStartTime = UINT64_MAX;
 
     std::vector<CbmDigiEvent> fDigiEvents;
+    std::vector<CbmStsDigi> fStsDigis;
+    PartitionedVector<sts::Cluster> fStsClusters;
     PartitionedVector<sts::Hit> fStsHits;
     PartitionedVector<tof::Hit> fTofHits;
 
@@ -90,6 +99,8 @@ namespace cbm::algo
       ar& fTsStartTime;
 
       ar& fDigiEvents;
+      ar& fStsDigis;
+      ar& fStsClusters;
       ar& fStsHits;
       ar& fTofHits;
       ar& fTracks;
diff --git a/reco/app/cbmreco/main.cxx b/reco/app/cbmreco/main.cxx
index f5e2f84d8d..296671a491 100644
--- a/reco/app/cbmreco/main.cxx
+++ b/reco/app/cbmreco/main.cxx
@@ -29,6 +29,9 @@ std::shared_ptr<StorableRecoResults> makeStorableRecoResults(const fles::Timesli
     storable->DigiEvents().emplace_back(digiEvent.ToStorable());
   }
 
+  // TODO: some of these copies can be avoided / made into moves
+  storable->StsDigis()           = ToStdVector(results.stsDigis);
+  storable->StsClusters()        = results.stsClusters;
   storable->StsHits()            = results.stsHits;
   storable->TofHits()            = results.tofHits;
   storable->Tracks()             = results.tracks;
diff --git a/services/archive_explorer/CMakeLists.txt b/services/archive_explorer/CMakeLists.txt
index 7631ccca32..9e48002750 100644
--- a/services/archive_explorer/CMakeLists.txt
+++ b/services/archive_explorer/CMakeLists.txt
@@ -5,6 +5,7 @@ set(APP cbm-archive-explorer)
 
 set(APP_SRCS
   app/Application.cxx
+  app/Histograms.cxx
   app/Options.cxx
   app/main.cxx
 )
@@ -30,6 +31,7 @@ set(INCLUDE_DIRECTORIES
 )
 
 set(SRCS
+  lib/HistogramCollection.cxx
   lib/Server.cxx
 )
 
diff --git a/services/archive_explorer/app/Application.cxx b/services/archive_explorer/app/Application.cxx
index c3cc8e17e2..31d86f4479 100644
--- a/services/archive_explorer/app/Application.cxx
+++ b/services/archive_explorer/app/Application.cxx
@@ -5,6 +5,7 @@
 
 #include <log.hpp>
 
+#include "Histograms.h"
 #include "RecoResultsInputArchive.h"
 #include "Server.h"
 
@@ -18,8 +19,26 @@ Application::Application(const Options& options) : fOpts(options)
   L_(info) << "Opening archive " << fOpts.Input();
 
   auto archive = std::make_shared<RecoResultsInputArchive>(fOpts.Input().string());
-
-  fServer = std::make_unique<Server>(fOpts.Port(), archive);
+  auto histograms = std::make_shared<Histograms>();
+
+  std::shared_ptr<RecoResultsInputArchive> archive2;
+  std::shared_ptr<Histograms> histograms2;
+  if (not fOpts.Input2().empty()) {
+    L_(info) << "Opening second archive " << fOpts.Input2();
+    archive2    = std::make_shared<RecoResultsInputArchive>(fOpts.Input2().string());
+    histograms2 = std::make_shared<Histograms>();
+  }
+
+  Server::Settings settings {
+    .port        = fOpts.Port(),
+    .sensor      = fOpts.Sensor(),
+    .archive     = archive,
+    .histograms  = histograms,
+    .archive2    = archive2,
+    .histograms2 = histograms2,
+  };
+
+  fServer = std::make_unique<Server>(settings);
 }
 
 Application::~Application() {}
diff --git a/services/archive_explorer/app/Histograms.cxx b/services/archive_explorer/app/Histograms.cxx
new file mode 100644
index 0000000000..df937e0518
--- /dev/null
+++ b/services/archive_explorer/app/Histograms.cxx
@@ -0,0 +1,90 @@
+/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer] */
+#include "Histograms.h"
+
+#include <TH1.h>
+
+#include <log.hpp>
+#include <unordered_map>
+
+#include "StorableRecoResults.h"
+
+using namespace cbm::explore;
+
+Histograms::Histograms()
+{
+  CreateFolder("/sts", "STS");
+  CreateFolder("/sts/digis", "Digis");
+  CreateHisto(fHStsDigisTime, "/sts/digis", "hStsDigisTime", "Sts Digis Time", 1000, 0, 128000000);
+
+  CreateFolder("/sts/clusters", "Clusters");
+  CreateHisto(fHStsClustersTime, "/sts/clusters", "hStsClustersTime", "Sts Clusters Time", 1000, 0, 128000000);
+
+  CreateFolder("/sts/hits", "Hits");
+  CreateHisto(fHStsHitsX, "/sts/hits", "hStsHitsX", "Sts Hits X", 100, -10, 10);
+  CreateHisto(fHStsHitsY, "/sts/hits", "hStsHitsY", "Sts Hits Y", 100, -10, 10);
+  CreateHisto(fHStsHitsZ, "/sts/hits", "hStsHitsZ", "Sts Hits Z", 100, 0, 50);
+  CreateHisto(fHStsHitsTime, "/sts/hits", "hStsHitsTime", "Sts Hits Time", 1000, 0, 128000000);
+  CreateHisto(fHStsHitsDx, "/sts/hits", "hStsHitsDx", "Sts Hits Dx", 100, 0, 0.1);
+  CreateHisto(fHStsHitsDy, "/sts/hits", "hStsHitsDy", "Sts Hits Dy", 100, 0, 0.1);
+  CreateHisto(fHStsHitsDz, "/sts/hits", "hStsHitsDz", "Sts Hits Dz", 100, 0, 0.1);
+  CreateHisto(fHStsHitsDxy, "/sts/hits", "hStsHitsDxy", "Sts Hits Dxy", 100, 0, 0.1);
+  CreateHisto(fHStsHitsTimeError, "/sts/hits", "hStsHitsTimeError", "Sts Hits Time Error", 100, 0, 0.1);
+  CreateHisto(fHStsHitsDu, "/sts/hits", "hStsHitsDu", "Sts Hits Du", 100, 0, 0.1);
+  CreateHisto(fHStsHitsDv, "/sts/hits", "hStsHitsDv", "Sts Hits Dv", 100, 0, 0.1);
+
+  CreateHisto(fHNHitsFromCluster, "/sts/hits", "hNHitsFromCluster", "Number of hits from cluster", 100, 0, 100);
+}
+
+void Histograms::FillHistos(HistoData fill)
+{
+  auto& stsDigis = fill.data->StsDigis();
+  L_(info) << "Filling histos with " << stsDigis.size() << " STS digis";
+  for (auto& digi : stsDigis) {
+    if (fill.Skip(digi.GetAddress())) continue;
+    fHStsDigisTime->Fill(digi.GetTimeU32());
+  }
+
+  auto& stsClusters = fill.data->StsClusters();
+  for (size_t p = 0; p < stsClusters.NPartitions(); p++) {
+    auto [sensor, address] = stsClusters.Partition(p);
+    if (fill.Skip(address)) continue;
+    L_(info) << "Filling STS sensor " << address << " with " << sensor.size() << " clusters";
+    for (auto& cluster : sensor) {
+      fHStsClustersTime->Fill(cluster.fTime);
+    }
+  }
+
+  auto& stsHits = fill.data->StsHits();
+  for (size_t p = 0; p < stsHits.NPartitions(); p++) {
+    auto [sensor, address] = stsHits.Partition(p);
+    if (fill.Skip(address)) continue;
+    L_(info) << "Filling STS sensor " << address << " with " << sensor.size() << " hits";
+
+    std::unordered_map<int, int> nHitsFromClusterF, nHitsFromClusterB;
+
+    for (auto& hit : sensor) {
+      fHStsHitsX->Fill(hit.X());
+      fHStsHitsY->Fill(hit.Y());
+      fHStsHitsZ->Fill(hit.Z());
+      fHStsHitsTime->Fill(hit.Time());
+      fHStsHitsDx->Fill(hit.Dx());
+      fHStsHitsDy->Fill(hit.Dy());
+      fHStsHitsDz->Fill(hit.fDz);
+      fHStsHitsDxy->Fill(hit.fDxy);
+      fHStsHitsTimeError->Fill(hit.TimeError());
+      fHStsHitsDu->Fill(hit.fDu);
+      fHStsHitsDv->Fill(hit.fDv);
+
+      nHitsFromClusterF[hit.fFrontClusterId]++;
+      nHitsFromClusterB[hit.fBackClusterId]++;
+    }
+
+    for (auto& [clusterId, nHits] : nHitsFromClusterF)
+      fHNHitsFromCluster->Fill(nHits);
+
+    for (auto& [clusterId, nHits] : nHitsFromClusterB)
+      fHNHitsFromCluster->Fill(nHits);
+  }
+}
diff --git a/services/archive_explorer/app/Histograms.h b/services/archive_explorer/app/Histograms.h
new file mode 100644
index 0000000000..1a4561fbd8
--- /dev/null
+++ b/services/archive_explorer/app/Histograms.h
@@ -0,0 +1,46 @@
+/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer] */
+#pragma once
+
+#include "HistogramCollection.h"
+
+class TH1F;
+class TH1I;
+
+namespace cbm::explore
+{
+
+  class Histograms : public HistogramCollection {
+
+  public:
+    Histograms();
+
+    virtual ~Histograms() {}
+
+    void FillHistos(HistoData fill) override;
+
+  private:
+    // Sts Digis
+    TH1I* fHStsDigisTime = nullptr;
+
+    // Sts Clusters
+    TH1I* fHStsClustersTime = nullptr;
+
+    // Sts Hits
+    TH1F* fHStsHitsX         = nullptr;
+    TH1F* fHStsHitsY         = nullptr;
+    TH1F* fHStsHitsZ         = nullptr;
+    TH1I* fHStsHitsTime      = nullptr;
+    TH1F* fHStsHitsDx        = nullptr;
+    TH1F* fHStsHitsDy        = nullptr;
+    TH1F* fHStsHitsDz        = nullptr;
+    TH1F* fHStsHitsDxy       = nullptr;
+    TH1F* fHStsHitsTimeError = nullptr;
+    TH1F* fHStsHitsDu        = nullptr;
+    TH1F* fHStsHitsDv        = nullptr;
+
+    TH1I* fHNHitsFromCluster = nullptr;
+  };
+
+}  // namespace cbm::explore
diff --git a/services/archive_explorer/app/Options.cxx b/services/archive_explorer/app/Options.cxx
index 9520cc81bd..50ad358a2c 100644
--- a/services/archive_explorer/app/Options.cxx
+++ b/services/archive_explorer/app/Options.cxx
@@ -9,6 +9,8 @@
 
 namespace po = boost::program_options;
 
+using namespace cbm::explore;
+
 Options::Options(int argc, char** argv)
 {
   po::options_description options("Options");
@@ -17,8 +19,12 @@ Options::Options(int argc, char** argv)
   options.add_options()
     ("input,i", po::value(&fInput)->value_name("<input>")->required(),
       "input archive")
+    ("input2,j", po::value(&fInput2)->value_name("<input2>"),
+      "second input archive, used for comparison (optional)")
     ("port,p", po::value(&fPort)->value_name("<port>")->default_value(8080),
       "port to listen on")
+    ("sensor", po::value(&fSensor)->value_name("<sensor>"),
+      "sensor to filter on")
     ("help,h",
       "produce help message")
   ;
@@ -42,6 +48,8 @@ Options::Options(int argc, char** argv)
     std::exit(EXIT_SUCCESS);
   }
 
+  fFilterSensor = vm.count("sensor") > 0;
+
   try {
     po::notify(vm);
   }
diff --git a/services/archive_explorer/app/Options.h b/services/archive_explorer/app/Options.h
index 9ec407b889..d8a5f0734c 100644
--- a/services/archive_explorer/app/Options.h
+++ b/services/archive_explorer/app/Options.h
@@ -3,18 +3,28 @@
    Authors: Felix Weiglhofer [committer] */
 #pragma once
 
-#include <compat/Filesystem.h>
+#include <optional>
 
-class Options {
-public:
-  Options(int argc, char** argv);
+#include "compat/Filesystem.h"
 
-  int Port() const { return fPort; }
+namespace cbm::explore
+{
+  class Options {
+  public:
+    Options(int argc, char** argv);
 
-  cbm::algo::fs::path Input() const { return fInput; }
+    int Port() const { return fPort; }
 
+    algo::fs::path Input() const { return fInput; }
+    algo::fs::path Input2() const { return fInput2; }
 
-private:
-  int fPort;
-  std::string fInput;
-};
+    std::optional<uint32_t> Sensor() const { return fFilterSensor ? std::make_optional(fSensor) : std::nullopt; }
+
+  private:
+    int fPort;
+    uint32_t fSensor;
+    bool fFilterSensor = false;
+    std::string fInput;
+    std::string fInput2;
+  };
+}  // namespace cbm::explore
diff --git a/services/archive_explorer/lib/HistogramCollection.cxx b/services/archive_explorer/lib/HistogramCollection.cxx
new file mode 100644
index 0000000000..527f51de88
--- /dev/null
+++ b/services/archive_explorer/lib/HistogramCollection.cxx
@@ -0,0 +1,54 @@
+/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer] */
+#include "HistogramCollection.h"
+
+#include <TH1.h>
+
+#include <fmt/format.h>
+
+using namespace cbm::explore;
+
+HistogramCollection::~HistogramCollection() {}
+
+void HistogramCollection::Reset()
+{
+  for (auto& histo : fHistos) {
+    histo.histo->Reset();
+  }
+}
+
+TH1* HistogramCollection::GetHisto(const std::string& name) const
+{
+  for (auto& histo : fHistos) {
+    if (histo.histo->GetName() == name) { return histo.histo; }
+  }
+  return nullptr;
+}
+
+std::string HistogramCollection::GetHistoPath(const std::string& name) const
+{
+  for (auto& histo : fHistos) {
+    if (histo.histo->GetName() == name) { return histo.path; }
+  }
+  return "";
+}
+
+void HistogramCollection::Div(const HistogramCollection& other)
+{
+  for (auto& histo : fHistos) {
+    auto otherHisto = other.GetHisto(histo.histo->GetName());
+    if (not otherHisto) {
+      throw std::runtime_error(
+        fmt::format("Cannot divide histograms, because they do not match: No histogram with name "
+                    "{} found in other collection.",
+                    histo.histo->GetName()));
+    }
+    histo.histo->Divide(otherHisto);
+  }
+}
+
+void HistogramCollection::CreateFolder(const char* path, const char* name)
+{
+  fFolders.push_back({.path = path, .name = name});
+}
diff --git a/services/archive_explorer/lib/HistogramCollection.h b/services/archive_explorer/lib/HistogramCollection.h
new file mode 100644
index 0000000000..8a5760c74d
--- /dev/null
+++ b/services/archive_explorer/lib/HistogramCollection.h
@@ -0,0 +1,78 @@
+/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer] */
+#pragma once
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+class TH1;
+
+namespace cbm::algo
+{
+  class StorableRecoResults;
+}
+
+namespace cbm::explore
+{
+
+  struct Folder {
+    std::string path;
+    std::string name;
+  };
+
+  struct Histo {
+    std::string path;
+    TH1* histo;
+  };
+
+  struct HistoData {
+    algo::StorableRecoResults* data = nullptr;
+    std::optional<uint32_t> sensor;
+
+    bool Skip(uint32_t address) const
+    {
+      if (not sensor) return false;
+      return address != *sensor;
+    }
+  };
+
+  class HistogramCollection {
+
+  public:
+    HistogramCollection() = default;
+    virtual ~HistogramCollection();
+
+    /// @brief Clear all histograms.
+    void Reset();
+
+    TH1* GetHisto(const std::string& name) const;
+    std::string GetHistoPath(const std::string& name) const;
+    std::vector<Histo> GetHistos() const { return fHistos; }
+
+    std::vector<Folder> GetFolders() const { return fFolders; }
+
+    void Div(const HistogramCollection& other);
+
+    virtual void FillHistos(HistoData fill) = 0;
+
+  protected:
+    template<typename Histo_t>
+    void CreateHisto(Histo_t*& histo, const char* folder, const char* name, const char* title, int nbins, double xlow,
+                     double xmax)
+    {
+      histo = new Histo_t(name, title, nbins, xlow, xmax);
+      histo->SetDirectory(nullptr);
+      fHistos.push_back({.path = folder, .histo = histo});
+    }
+
+    void CreateFolder(const char* path, const char* name);
+
+  private:
+    std::vector<Folder> fFolders;
+    std::vector<Histo> fHistos;
+  };
+
+}  // namespace cbm::explore
diff --git a/services/archive_explorer/lib/Server.cxx b/services/archive_explorer/lib/Server.cxx
index 331e4894db..cacdad97f3 100644
--- a/services/archive_explorer/lib/Server.cxx
+++ b/services/archive_explorer/lib/Server.cxx
@@ -15,23 +15,21 @@ using namespace cbm::explore;
 
 ClassImp(Server);
 
-Server::Server(int port, std::shared_ptr<algo::RecoResultsInputArchive> archive)
-  : TNamed("server", "server")
-  , fArchive(archive)
+Server::Server(Settings settings) : TNamed("server", "server"), fS(settings)
 {
-  L_(info) << "Starting Server with port " << port;
+  L_(info) << "Starting Server with port " << fS.port;
 
-  fServer = new THttpServer(fmt::format("http:{}", port).c_str());
+  if (fS.sensor) L_(info) << "Filtering on sensor " << *fS.sensor;
+  else {
+    L_(info) << "Not filtering on sensor";
+  }
+
+  fServer = new THttpServer(fmt::format("http:{}", fS.port).c_str());
   fServer->SetTimer(0, true);
 
   // FIXME: Why doesn't root use the local jsroot by default???
   fServer->SetJSROOT("https://jsroot.gsi.de/7.1.0/");
-}
-
-Server::~Server() {}
 
-int Server::Run()
-{
   // register this object in http server
   fServer->Register("/", this);
   fServer->Hide("/server");
@@ -46,7 +44,30 @@ int Server::Run()
   fServer->RegisterCommand("/NextTS", "/server/->RequestNextTS()");
   fServer->SetItemField("/NextTS", "_title", "Next Timeslice");
 
-  CreateHistos();
+  if (fS.archive == nullptr) throw std::runtime_error("Archive must not be null");
+  if (fS.histograms == nullptr) throw std::runtime_error("Histograms must not be null");
+
+  if (fS.archive2 != nullptr || fS.histograms2 != nullptr) {
+    if (fS.archive2 == nullptr) throw std::runtime_error("Archive2 must not be null");
+    if (fS.histograms2 == nullptr) throw std::runtime_error("Histograms2 must not be null");
+  }
+
+  // Setup folders
+  for (auto& folder : fS.histograms->GetFolders()) {
+    fServer->CreateItem(folder.path.c_str(), folder.name.c_str());
+    fServer->SetItemField(folder.path.c_str(), "_kind", "Folder");
+  }
+
+  // Setup histograms
+  for (auto& [path, histo] : fS.histograms->GetHistos()) {
+    fServer->Register(path.c_str(), histo);
+  }
+}
+
+Server::~Server() {}
+
+int Server::Run()
+{
   NextTS();
 
   L_(info) << "Starting event loop";
@@ -77,73 +98,27 @@ void Server::NextTS()
 {
   L_(info) << "Fetching next Timeslice...";
 
-  auto recoResults = fArchive->get();
-
-  if (fArchive->eos()) {
+  auto recoResults = fS.archive->get();
+  if (fS.archive->eos()) {
     L_(info) << "End of archive reached";
     return;
   }
 
-  ResetHistos();
-
-  auto& stsHits = recoResults->StsHits();
-  for (size_t p = 0; p < stsHits.NPartitions(); p++) {
-    auto [sensor, address] = stsHits.Partition(p);
-    L_(info) << "Filling sensor " << address << " with " << sensor.size() << " hits";
-    for (auto& hit : sensor) {
-      fHStsHitsX->Fill(hit.X());
-      fHStsHitsY->Fill(hit.Y());
-      fHStsHitsZ->Fill(hit.Z());
-      fHStsHitsTime->Fill(hit.Time());
-      fHStsHitsDx->Fill(hit.Dx());
-      fHStsHitsDy->Fill(hit.Dy());
-      fHStsHitsDz->Fill(hit.fDz);
-      fHStsHitsDxy->Fill(hit.fDxy);
-      fHStsHitsTimeError->Fill(hit.TimeError());
-      fHStsHitsDu->Fill(hit.fDu);
-      fHStsHitsDv->Fill(hit.fDv);
-    }
-  }
+  HistoData fill {.data = recoResults.get(), .sensor = fS.sensor};
 
-  L_(info) << "Finished Timeslice";
-}
+  fS.histograms->Reset();
+  fS.histograms->FillHistos(fill);
 
-void Server::ResetHistos()
-{
-  for (auto* histo : fHistos) {
-    histo->Reset();
+  bool divide = fS.archive2 != nullptr;
+  if (divide) {
+    recoResults = fS.archive2->get();
+    if (fS.archive2->eos()) throw std::runtime_error("End of archive2 reached too early");
+    fS.histograms2->Reset();
+    fill = {.data = recoResults.get(), .sensor = fS.sensor};
+    fS.histograms2->FillHistos(fill);
+    L_(info) << "Dividing histograms";
+    fS.histograms->Div(*fS.histograms2);
   }
-}
 
-void Server::CreateHistos()
-{
-  CreateFolder("/sts", "STS");
-  CreateFolder("/sts/hits", "Hits");
-  CreateHisto(fHStsHitsX, "/sts/hits", "hStsHitsX", "Sts Hits X", 100, -10, 10);
-  CreateHisto(fHStsHitsY, "/sts/hits", "hStsHitsY", "Sts Hits Y", 100, -10, 10);
-  CreateHisto(fHStsHitsZ, "/sts/hits", "hStsHitsZ", "Sts Hits Z", 100, 0, 50);
-  CreateHisto(fHStsHitsTime, "/sts/hits", "hStsHitsTime", "Sts Hits Time", 1000, 0, 128000000);
-  CreateHisto(fHStsHitsDx, "/sts/hits", "hStsHitsDx", "Sts Hits Dx", 100, 0, 0.1);
-  CreateHisto(fHStsHitsDy, "/sts/hits", "hStsHitsDy", "Sts Hits Dy", 100, 0, 0.1);
-  CreateHisto(fHStsHitsDz, "/sts/hits", "hStsHitsDz", "Sts Hits Dz", 100, 0, 0.1);
-  CreateHisto(fHStsHitsDxy, "/sts/hits", "hStsHitsDxy", "Sts Hits Dxy", 100, 0, 0.1);
-  CreateHisto(fHStsHitsTimeError, "/sts/hits", "hStsHitsTimeError", "Sts Hits Time Error", 100, 0, 0.1);
-  CreateHisto(fHStsHitsDu, "/sts/hits", "hStsHitsDu", "Sts Hits Du", 100, 0, 0.1);
-  CreateHisto(fHStsHitsDv, "/sts/hits", "hStsHitsDv", "Sts Hits Dv", 100, 0, 0.1);
-}
-
-template<typename Histo_t>
-void Server::CreateHisto(Histo_t*& histo, const char* folder, const char* name, const char* title, int nbins,
-                         double xlow, double xmax)
-{
-  histo = new Histo_t(name, title, nbins, xlow, xmax);
-  histo->SetDirectory(nullptr);
-  fServer->Register(folder, histo);
-  fHistos.push_back(histo);
-}
-
-void Server::CreateFolder(const char* path, const char* name)
-{
-  fServer->CreateItem(path, name);
-  fServer->SetItemField(path, "_kind", "Folder");
+  L_(info) << "Finished Timeslice";
 }
diff --git a/services/archive_explorer/lib/Server.h b/services/archive_explorer/lib/Server.h
index d20c7e3e8a..03f0cf2f5c 100644
--- a/services/archive_explorer/lib/Server.h
+++ b/services/archive_explorer/lib/Server.h
@@ -8,6 +8,7 @@
 
 #include <memory>
 
+#include "HistogramCollection.h"
 #include "RecoResultsInputArchive.h"
 
 class THttpServer;
@@ -21,7 +22,19 @@ namespace cbm::explore
   class Server : public TNamed {
 
   public:
-    Server(int port, std::shared_ptr<algo::RecoResultsInputArchive> archive);
+    struct Settings {
+      int port;
+      std::optional<uint32_t> sensor;  // optional sensor to filter on
+      std::shared_ptr<algo::RecoResultsInputArchive> archive;
+      std::shared_ptr<HistogramCollection> histograms;
+
+      // Optional second archive and histograms for comparison
+      // histograms will be divided by this one, if provided
+      std::shared_ptr<algo::RecoResultsInputArchive> archive2;
+      std::shared_ptr<HistogramCollection> histograms2;
+    };
+
+    Server(Settings settings);
 
     virtual ~Server();
 
@@ -35,35 +48,12 @@ namespace cbm::explore
 
     // Internal state
     THttpServer* fServer = nullptr;                           //!
-    std::shared_ptr<algo::RecoResultsInputArchive> fArchive;  //!
-    std::vector<TH1*> fHistos;                                //!
+    Settings fS;                                              //!
 
     // Server commands
     bool fRequestNextTS = false;
 
-    // Histograms
-    TH1F* fHStsHitsX         = nullptr;
-    TH1F* fHStsHitsY         = nullptr;
-    TH1F* fHStsHitsZ         = nullptr;
-    TH1I* fHStsHitsTime      = nullptr;
-    TH1F* fHStsHitsDx        = nullptr;
-    TH1F* fHStsHitsDy        = nullptr;
-    TH1F* fHStsHitsDz        = nullptr;
-    TH1F* fHStsHitsDxy       = nullptr;
-    TH1F* fHStsHitsTimeError = nullptr;
-    TH1F* fHStsHitsDu        = nullptr;
-    TH1F* fHStsHitsDv        = nullptr;
-
     void NextTS();
-    void ResetHistos();
-
-    void CreateHistos();
-
-    template<typename Histo_t>
-    void CreateHisto(Histo_t*& histo, const char* folder, const char* name, const char* title, int nbins, double xlow,
-                     double xmax);
-
-    void CreateFolder(const char* path, const char* name);
 
     ClassDef(Server, 1);
   };
-- 
GitLab