diff --git a/Documentation/Examples/CMakeLists.txt b/Documentation/Examples/CMakeLists.txt
index 3cce0678741508b8de9453dd482e7626cd1cd7f9..936e54ed20ba87e53a25a1bc020fbadc382c773e 100644
--- a/Documentation/Examples/CMakeLists.txt
+++ b/Documentation/Examples/CMakeLists.txt
@@ -87,7 +87,7 @@ if (Pythia8_FOUND)
   install (TARGETS cascade_proton_example DESTINATION share/examples)
 endif()
 
-CORSIKA_ADD_TEST(vertical_EAS)
+add_executable(vertical_EAS vertical_EAS.cc)
 target_link_libraries (vertical_EAS
   SuperStupidStack
   CORSIKAunits
@@ -100,10 +100,10 @@ target_link_libraries (vertical_EAS
   CORSIKAcascade
   ProcessEnergyLoss
   ProcessObservationPlane
-  ProcessTrackWriter
   ProcessTrackingLine
   ProcessParticleCut
   ProcessStackInspector
+  ProcessInteractionCounter
   CORSIKAprocesses
   CORSIKAcascade
   CORSIKAparticles
diff --git a/Documentation/Examples/vertical_EAS.cc b/Documentation/Examples/vertical_EAS.cc
index 8dd6f827df46e28c98c2354c2e122734250d87e1..2cdb101b631f592111452c88738a43715916d7a9 100644
--- a/Documentation/Examples/vertical_EAS.cc
+++ b/Documentation/Examples/vertical_EAS.cc
@@ -9,45 +9,35 @@
  */
 
 #include <corsika/cascade/Cascade.h>
-#include <corsika/process/ProcessSequence.h>
-#include <corsika/process/StackProcess.h>
-#include <corsika/process/energy_loss/EnergyLoss.h>
-#include <corsika/process/observation_plane/ObservationPlane.h>
-#include <corsika/process/particle_cut/ParticleCut.h>
-#include <corsika/process/switch_process/SwitchProcess.h>
-#include <corsika/process/tracking_line/TrackingLine.h>
-
-#include <corsika/setup/SetupStack.h>
-#include <corsika/setup/SetupTrajectory.h>
-
 #include <corsika/environment/Environment.h>
 #include <corsika/environment/FlatExponential.h>
 #include <corsika/environment/LayeredSphericalAtmosphereBuilder.h>
 #include <corsika/environment/NuclearComposition.h>
-
 #include <corsika/geometry/Plane.h>
 #include <corsika/geometry/Sphere.h>
-
+#include <corsika/process/ProcessSequence.h>
+#include <corsika/process/StackProcess.h>
+#include <corsika/process/energy_loss/EnergyLoss.h>
+#include <corsika/process/interaction_counter/InteractionCounter.h>
+#include <corsika/process/observation_plane/ObservationPlane.h>
+#include <corsika/process/particle_cut/ParticleCut.h>
+#include <corsika/process/pythia/Decay.h>
 #include <corsika/process/sibyll/Decay.h>
 #include <corsika/process/sibyll/Interaction.h>
 #include <corsika/process/sibyll/NuclearInteraction.h>
-
-#include <corsika/process/pythia/Decay.h>
-
+#include <corsika/process/switch_process/SwitchProcess.h>
+#include <corsika/process/tracking_line/TrackingLine.h>
 #include <corsika/process/urqmd/UrQMD.h>
-
-#include <corsika/process/particle_cut/ParticleCut.h>
-#include <corsika/process/track_writer/TrackWriter.h>
-
-#include <corsika/units/PhysicalUnits.h>
-
 #include <corsika/random/RNGManager.h>
-
+#include <corsika/setup/SetupStack.h>
+#include <corsika/setup/SetupTrajectory.h>
+#include <corsika/units/PhysicalUnits.h>
 #include <corsika/utl/CorsikaFenv.h>
 
 #include <iomanip>
 #include <iostream>
 #include <limits>
+#include <string>
 #include <typeinfo>
 
 using namespace corsika;
@@ -64,6 +54,7 @@ using namespace corsika::units::si;
 
 void registerRandomStreams() {
   random::RNGManager::GetInstance().RegisterRandomStream("cascade");
+  random::RNGManager::GetInstance().RegisterRandomStream("qgran");
   random::RNGManager::GetInstance().RegisterRandomStream("s_rndm");
   random::RNGManager::GetInstance().RegisterRandomStream("pythia");
   random::RNGManager::GetInstance().RegisterRandomStream("UrQMD");
@@ -71,7 +62,11 @@ void registerRandomStreams() {
   random::RNGManager::GetInstance().SeedAll();
 }
 
-int main() {
+int main(int argc, char** argv) {
+  if (argc != 4) {
+    std::cerr << "usage: vertical_EAS <A> <Z> <energy/GeV>" << std::endl;
+    return 1;
+  }
   feenableexcept(FE_INVALID);
   // initialize random number sequence(s)
   registerRandomStreams();
@@ -97,15 +92,17 @@ int main() {
   // setup particle stack, and add primary particle
   setup::Stack stack;
   stack.Clear();
-  const Code beamCode = Code::Proton;
-  auto const mass = particles::GetMass(beamCode);
-  const HEPEnergyType E0 = 0.1_PeV;
+  const Code beamCode = Code::Nucleus;
+  unsigned short const A = std::stoi(std::string(argv[1]));
+  unsigned short Z = std::stoi(std::string(argv[2]));
+  auto const mass = particles::GetNucleusMass(A, Z);
+  const HEPEnergyType E0 = 1_GeV * std::stof(std::string(argv[3]));
   double theta = 0.;
   double phi = 0.;
 
   Point const injectionPos(
       rootCS, 0_m, 0_m,
-      112.8_km * 0.999 +
+      112.7_km +
           builder.earthRadius); // this is the CORSIKA 7 start of atmosphere/universe
 
   //  {
@@ -124,23 +121,33 @@ int main() {
   cout << "input angles: theta=" << theta << " phi=" << phi << endl;
   cout << "input momentum: " << plab.GetComponents() / 1_GeV << endl;
 
-  stack.AddParticle(
-      std::tuple<particles::Code, units::si::HEPEnergyType,
-                 corsika::stack::MomentumVector, geometry::Point, units::si::TimeType>{
-          beamCode, E0, plab, injectionPos, 0_ns});
-  //  }
+  if (A != 1) {
+    stack.AddParticle(std::tuple<particles::Code, units::si::HEPEnergyType,
+                                 corsika::stack::MomentumVector, geometry::Point,
+                                 units::si::TimeType, unsigned short, unsigned short>{
+        beamCode, E0, plab, injectionPos, 0_ns, A, Z});
+
+  } else {
+    stack.AddParticle(
+        std::tuple<particles::Code, units::si::HEPEnergyType,
+                   corsika::stack::MomentumVector, geometry::Point, units::si::TimeType>{
+            particles::Code::Proton, E0, plab, injectionPos, 0_ns});
+  }
 
   Line const line(injectionPos, plab.normalized() * 1_m * 1_Hz);
   auto const velocity = line.GetV0().norm();
 
-  auto const observationHeight = 1.425_km + builder.earthRadius;
+  auto const observationHeight = 1.4_km + builder.earthRadius;
 
-  setup::Trajectory const showerAxis(line, (112.8_km - observationHeight) / velocity);
+  setup::Trajectory const showerAxis(line, (112.7_km - observationHeight) / velocity);
 
   // setup processes, decays and interactions
 
   process::sibyll::Interaction sibyll;
+  process::interaction_counter::InteractionCounter sibyllCounted(sibyll);
+
   process::sibyll::NuclearInteraction sibyllNuc(sibyll, env);
+  process::interaction_counter::InteractionCounter sibyllNucCounted(sibyllNuc);
 
   process::pythia::Decay decayPythia;
 
@@ -166,9 +173,8 @@ int main() {
   });
   decaySibyll.PrintDecayConfig();
 
-  process::particle_cut::ParticleCut cut(5_GeV);
+  process::particle_cut::ParticleCut cut(100_GeV);
 
-  process::track_writer::TrackWriter trackWriter("tracks.dat");
   process::energy_loss::EnergyLoss eLoss(showerAxis);
 
   Plane const obsPlane(Point(rootCS, 0_m, 0_m, observationHeight),
@@ -180,16 +186,20 @@ int main() {
 
   process::UrQMD::UrQMD urqmd;
 
-  auto sibyllSequence = sibyll << sibyllNuc;
+  auto sibyllSequence = sibyllNucCounted << sibyllCounted;
   process::switch_process::SwitchProcess switchProcess(urqmd, sibyllSequence, 55_GeV);
   auto decaySequence = decayPythia << decaySibyll;
-  auto sequence = switchProcess << decaySequence << eLoss << cut << observationLevel
-                                << trackWriter;
+  auto sequence = switchProcess << decaySequence << eLoss << cut << observationLevel;
 
   // define air shower object, run simulation
   tracking_line::TrackingLine tracking;
   cascade::Cascade EAS(env, tracking, sequence, stack);
   EAS.Init();
+
+  // to fix the point of first interaction, uncomment the following two lines:
+  //  EAS.SetNodes();
+  //  EAS.forceInteraction();
+
   EAS.Run();
 
   eLoss.PrintProfile(); // print longitudinal profile
@@ -202,6 +212,10 @@ int main() {
   cout << "total dEdX energy (GeV): " << eLoss.GetTotal() / 1_GeV << endl
        << "relative difference (%): " << eLoss.GetTotal() / E0 * 100 << endl;
 
+  auto const hists = sibyllCounted.GetHistogram() + sibyllNucCounted.GetHistogram();
+  hists.saveLab("inthist_lab.txt");
+  hists.saveCMS("inthist_cms.txt");
+
   std::ofstream finish("finished");
   finish << "run completed without error" << std::endl;
 }
diff --git a/Processes/CMakeLists.txt b/Processes/CMakeLists.txt
index 9ca109dee4bbef7442de84944047730ffa0ba98c..6e93793eceed7a20cdaf7363dd585cc93b1afbac 100644
--- a/Processes/CMakeLists.txt
+++ b/Processes/CMakeLists.txt
@@ -10,7 +10,6 @@ if (PYTHIA8_FOUND)
 endif (PYTHIA8_FOUND)
 add_subdirectory (HadronicElasticModel)
 add_subdirectory (UrQMD)
-add_subdirectory (SwitchProcess)
 
 # continuous physics
 add_subdirectory (EnergyLoss)
@@ -22,6 +21,10 @@ add_subdirectory (StackInspector)
 # cuts, thinning, etc.
 add_subdirectory (ParticleCut)
 
+# meta-processes
+add_subdirectory (InteractionCounter)
+add_subdirectory (SwitchProcess)
+
 ##########################################
 # add_custom_target(CORSIKAprocesses)
 add_library (CORSIKAprocesses INTERFACE)
diff --git a/Processes/InteractionCounter/CMakeLists.txt b/Processes/InteractionCounter/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2323207a37e42a6723df1433553f80974606b732
--- /dev/null
+++ b/Processes/InteractionCounter/CMakeLists.txt
@@ -0,0 +1,58 @@
+set (
+  MODEL_HEADERS
+  InteractionCounter.h
+  InteractionHistogram.h
+  )
+
+set (
+  MODEL_SOURCES
+  InteractionHistogram.cc
+)
+
+set (
+  MODEL_NAMESPACE
+  corsika/process/interaction_counter
+  )
+
+add_library (ProcessInteractionCounter STATIC ${MODEL_SOURCES})
+CORSIKA_COPY_HEADERS_TO_NAMESPACE (ProcessInteractionCounter ${MODEL_NAMESPACE} ${MODEL_HEADERS})
+
+set_target_properties (
+  ProcessInteractionCounter
+  PROPERTIES
+  VERSION ${PROJECT_VERSION}
+  SOVERSION 1
+#  PUBLIC_HEADER "${MODEL_HEADERS}"
+  )
+
+# target dependencies on other libraries (also the header onlys)
+target_link_libraries (
+  ProcessInteractionCounter
+  CORSIKAunits
+  CORSIKAprocesssequence
+  CORSIKAthirdparty
+  )
+
+target_include_directories (
+  ProcessInteractionCounter 
+  INTERFACE 
+  $<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/include>
+  $<INSTALL_INTERFACE:include/include>
+  )
+
+install (FILES ${MODEL_HEADERS} DESTINATION include/${MODEL_NAMESPACE})
+
+# --------------------
+# code unit testing
+CORSIKA_ADD_TEST(testInteractionCounter SOURCES
+  testInteractionCounter.cc
+  ${MODEL_HEADERS}
+)
+
+target_link_libraries (
+  testInteractionCounter
+  ProcessInteractionCounter
+  CORSIKAunits
+  CORSIKAenvironment
+  CORSIKAtesting
+  )
diff --git a/Processes/InteractionCounter/InteractionCounter.h b/Processes/InteractionCounter/InteractionCounter.h
new file mode 100644
index 0000000000000000000000000000000000000000..a03c528881ce6e3a67f530c1fe170312cd90ce7f
--- /dev/null
+++ b/Processes/InteractionCounter/InteractionCounter.h
@@ -0,0 +1,67 @@
+/*
+ * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * See file AUTHORS for a list of contributors.
+ *
+ * This software is distributed under the terms of the GNU General Public
+ * Licence version 3 (GPL Version 3). See file LICENSE for a full version of
+ * the license.
+ */
+
+#ifndef _corsika_InteractionCounter_h
+#define _corsika_InteractionCounter_h
+
+#include <corsika/process/InteractionProcess.h>
+#include <corsika/process/ProcessSequence.h>
+#include <corsika/process/interaction_counter/InteractionHistogram.h>
+#include <corsika/setup/SetupStack.h>
+
+namespace corsika::process::interaction_counter {
+  /*!
+   * Wrapper around an InteractionProcess that fills histograms of the number
+   * of calls to DoInteraction() binned in projectile energy (both in
+   * lab and center-of-mass frame) and species
+   */
+  template <class TCountedProcess>
+  class InteractionCounter
+      : public InteractionProcess<InteractionCounter<TCountedProcess>> {
+
+    TCountedProcess& process_;
+    InteractionHistogram histogram_;
+
+  public:
+    InteractionCounter(TCountedProcess& process)
+        : process_(process) {}
+
+    template <typename TProjectile>
+    auto DoInteraction(TProjectile& projectile) {
+      auto const massNumber = projectile.GetNode()
+                                  ->GetModelProperties()
+                                  .GetNuclearComposition()
+                                  .GetAverageMassNumber();
+      auto const massTarget = massNumber * units::constants::nucleonMass;
+
+      if (auto const projectile_id = projectile.GetPID();
+          projectile_id == particles::Code::Nucleus) {
+        auto const A = projectile.GetNuclearA();
+        auto const Z = projectile.GetNuclearZ();
+        histogram_.fill(projectile_id, projectile.GetEnergy(), massTarget, A, Z);
+      } else {
+        histogram_.fill(projectile_id, projectile.GetEnergy(), massTarget);
+      }
+      return process_.DoInteraction(projectile);
+    }
+
+    void Init() { process_.Init(); }
+
+    template <typename TParticle>
+    auto GetInteractionLength(TParticle const& particle) const {
+      return process_.GetInteractionLength(particle);
+    }
+
+    auto const& GetHistogram() const { return histogram_; }
+  };
+
+} // namespace corsika::process::interaction_counter
+
+#endif
diff --git a/Processes/InteractionCounter/InteractionHistogram.cc b/Processes/InteractionCounter/InteractionHistogram.cc
new file mode 100644
index 0000000000000000000000000000000000000000..556b58f2788f1063a594bb41fb06a552ed5c563d
--- /dev/null
+++ b/Processes/InteractionCounter/InteractionHistogram.cc
@@ -0,0 +1,147 @@
+/*
+ * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * See file AUTHORS for a list of contributors.
+ *
+ * This software is distributed under the terms of the GNU General Public
+ * Licence version 3 (GPL Version 3). See file LICENSE for a full version of
+ * the license.
+ */
+
+#include <corsika/process/interaction_counter/InteractionHistogram.h>
+
+#include <fstream>
+#include <string>
+
+using namespace corsika::process::interaction_counter;
+
+void InteractionHistogram::fill(particles::Code projectile_id,
+                                units::si::HEPEnergyType lab_energy,
+                                units::si::HEPEnergyType mass_target, int A, int Z) {
+  using namespace units::si;
+
+  if (projectile_id == particles::Code::Nucleus) {
+    auto const sqrtS =
+        sqrt(A * A * (units::constants::nucleonMass * units::constants::nucleonMass) +
+             mass_target * mass_target + 2 * lab_energy * mass_target);
+
+    int32_t const pdg = 1'000'000'000l + Z * 10'000l + A * 10l;
+
+    if (nuclear_inthist_cms_.count(pdg) == 0) {
+      nuclear_inthist_lab_.emplace(
+          pdg,
+          boost::histogram::make_histogram(
+              boost::histogram::axis::regular<double,
+                                              boost::histogram::axis::transform::log>(
+                  num_bins_lab, lower_edge_lab, upper_edge_lab)));
+
+      nuclear_inthist_cms_.emplace(
+          pdg,
+          boost::histogram::make_histogram(
+              boost::histogram::axis::regular<double,
+                                              boost::histogram::axis::transform::log>(
+                  num_bins_cms, lower_edge_cms, upper_edge_cms)));
+    }
+
+    nuclear_inthist_lab_[pdg](lab_energy / 1_GeV);
+    nuclear_inthist_cms_[pdg](sqrtS / 1_GeV);
+  } else {
+    auto const projectile_mass = particles::GetMass(projectile_id);
+    auto const sqrtS = sqrt(projectile_mass * projectile_mass +
+                            mass_target * mass_target + 2 * lab_energy * mass_target);
+
+    interaction_histogram_cms_(
+        static_cast<corsika::particles::CodeIntType>(projectile_id), sqrtS / 1_GeV);
+
+    interaction_histogram_lab_(
+        static_cast<corsika::particles::CodeIntType>(projectile_id), lab_energy / 1_GeV);
+  }
+}
+
+void InteractionHistogram::saveLab(std::string const& filename) const {
+  save(interaction_histogram_lab_, nuclear_inthist_lab_, filename, "lab system");
+}
+
+void InteractionHistogram::saveCMS(std::string const& filename) const {
+  save(interaction_histogram_lab_, nuclear_inthist_cms_, filename,
+       "center-of-mass system");
+}
+
+InteractionHistogram& InteractionHistogram::operator+=(
+    InteractionHistogram const& other) {
+  interaction_histogram_lab_ += other.interaction_histogram_lab_;
+  interaction_histogram_cms_ += other.interaction_histogram_cms_;
+
+  nuclear_inthist_lab_ += other.nuclear_inthist_lab_;
+  nuclear_inthist_cms_ += other.nuclear_inthist_cms_;
+
+  return *this;
+}
+
+InteractionHistogram InteractionHistogram::operator+(InteractionHistogram other) const {
+  other.nuclear_inthist_lab_ += nuclear_inthist_lab_;
+  other.nuclear_inthist_cms_ += nuclear_inthist_cms_;
+
+  return other;
+}
+
+void InteractionHistogram::saveHist(hist_type const& hist, std::string const& filename,
+                                    std::string const& comment) {
+  std::ofstream file;
+  file.open(filename);
+  saveHist(hist, file, comment);
+}
+
+void InteractionHistogram::saveHist(hist_type const& hist, std::ofstream& file,
+                                    std::string const& comment) {
+  auto const& energy_axis = hist.axis(1);
+
+  file << "# interaction count histogram (" << comment << ")" << std::endl
+       << "# " << energy_axis.size() << " bins between " << energy_axis.bin(0).lower()
+       << " and " << energy_axis.bin(energy_axis.size() - 1).upper() << " GeV"
+       << std::endl;
+
+  for (particles::CodeIntType p = 0;
+       p < static_cast<particles::CodeIntType>(particles::Code::LastParticle); ++p) {
+
+    if (auto pdg = static_cast<particles::PDGCodeType>(
+            particles::GetPDG(static_cast<particles::Code>(p)));
+        pdg < 1'000'000'000l) {
+      file << "# " << static_cast<particles::Code>(p) << std::endl;
+      file << pdg;
+      for (int i = 0; i < energy_axis.size(); ++i) { file << ' ' << hist.at(p, i); }
+      file << std::endl;
+    }
+  }
+}
+
+void InteractionHistogram::saveHistMap(nuclear_hist_type const& histMap,
+                                       std::ofstream& file) {
+  file << "# nuclei" << std::endl;
+  for (auto const& [pdg, hist] : histMap) {
+    auto const num_ebins_nucl = hist.axis(0).size();
+
+    file << pdg << ' ';
+    for (int i = 0; i < num_ebins_nucl; ++i) { file << ' ' << hist.at(i); }
+    file << std::endl;
+  }
+}
+
+void InteractionHistogram::save(hist_type const& hist, nuclear_hist_type const& histMap,
+                                std::string const& filename, std::string const& comment) {
+  std::ofstream file;
+  file.open(filename);
+
+  saveHist(hist, file, comment);
+  saveHistMap(histMap, file);
+}
+
+InteractionHistogram::InteractionHistogram()
+    : interaction_histogram_cms_{boost::histogram::make_histogram(
+          boost::histogram::axis::integer<short>(0, numIds),
+          boost::histogram::axis::regular<double, boost::histogram::axis::transform::log>(
+              num_bins_cms, lower_edge_cms, upper_edge_cms))}
+    , interaction_histogram_lab_{boost::histogram::make_histogram(
+          boost::histogram::axis::integer<short>(0, numIds),
+          boost::histogram::axis::regular<double, boost::histogram::axis::transform::log>(
+              num_bins_lab, lower_edge_lab, upper_edge_lab))} {}
diff --git a/Processes/InteractionCounter/InteractionHistogram.h b/Processes/InteractionCounter/InteractionHistogram.h
new file mode 100644
index 0000000000000000000000000000000000000000..98fedae84ee5b101e3904b2d1d18e5dc26e29d32
--- /dev/null
+++ b/Processes/InteractionCounter/InteractionHistogram.h
@@ -0,0 +1,107 @@
+/*
+ * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * See file AUTHORS for a list of contributors.
+ *
+ * This software is distributed under the terms of the GNU General Public
+ * Licence version 3 (GPL Version 3). See file LICENSE for a full version of
+ * the license.
+ */
+
+#include <corsika/particles/ParticleProperties.h>
+#include <corsika/units/PhysicalUnits.h>
+
+#include <boost/histogram.hpp>
+#include <boost/histogram/ostream.hpp>
+#include <fstream>
+#include <functional>
+#include <map>
+#include <utility>
+
+namespace corsika::process::interaction_counter {
+  using hist_type = decltype(boost::histogram::make_histogram(
+      std::declval<boost::histogram::axis::integer<particles::CodeIntType>>(),
+      std::declval<boost::histogram::axis::regular<
+          double, boost::histogram::axis::transform::log>>()));
+
+  using nucl_hist_type = decltype(boost::histogram::make_histogram(
+      std::declval<boost::histogram::axis::regular<
+          double, boost::histogram::axis::transform::log>>()));
+
+  using nuclear_hist_type = std::map<int32_t, nucl_hist_type>;
+
+  template <typename TKey, typename TVal>
+  auto operator+=(std::map<TKey, TVal>& a, std::map<TKey, TVal> b) {
+    a.merge(b);
+    for (auto const& [id, hist] : b) { a[id] += hist; }
+    return a;
+  }
+
+  class InteractionHistogram {
+    static auto constexpr lower_edge_cms = 1., upper_edge_cms = 1e8;  // GeV sqrt s
+    static auto constexpr lower_edge_lab = 1., upper_edge_lab = 1e12; // GeV lab
+    static auto constexpr num_bins_lab = 120, num_bins_cms = 80;
+    static auto constexpr numIds =
+        static_cast<particles::CodeIntType>(particles::Code::LastParticle);
+
+    hist_type interaction_histogram_cms_;
+    hist_type interaction_histogram_lab_;
+
+    /*!
+     * These maps map PDG nuclei codes to their corresponding interaction histograms
+     */
+    nuclear_hist_type nuclear_inthist_lab_, nuclear_inthist_cms_;
+
+  public:
+    InteractionHistogram();
+
+    //! fill both CMS and lab histograms at the same time
+    void fill(particles::Code projectile_id, units::si::HEPEnergyType lab_energy,
+              units::si::HEPEnergyType mass_target, int A = 0, int Z = 0);
+
+    auto CMSHists() const {
+      return std::pair<decltype(interaction_histogram_cms_) const&,
+                       decltype(nuclear_inthist_cms_) const&>{interaction_histogram_cms_,
+                                                              nuclear_inthist_cms_};
+    }
+
+    auto labHists() const {
+      return std::pair<decltype(interaction_histogram_lab_) const&,
+                       decltype(nuclear_inthist_lab_) const&>(interaction_histogram_lab_,
+                                                              nuclear_inthist_lab_);
+    }
+
+    void saveLab(std::string const& filename) const;
+
+    void saveCMS(std::string const& filename) const;
+
+    InteractionHistogram& operator+=(InteractionHistogram const& other);
+
+    InteractionHistogram operator+(InteractionHistogram other) const;
+
+  private:
+    /*!
+     * Save a histogram into a text file. The \arg comment string is written
+     * into the header of the text file.
+     * This method is static so that you can sum the histograms of multiple
+     * InteractionProcesses before saving them into the same file.
+     */
+
+    static void saveHist(hist_type const& hist, std::string const& filename,
+                         std::string const& comment = "");
+
+    static void saveHist(hist_type const& hist, std::ofstream& file,
+                         std::string const& comment = "");
+
+    static void saveHistMap(nuclear_hist_type const& histMap, std::ofstream& file);
+
+    /*!
+     * save both the "normal" particle histograms as well as the "nuclear" histograms
+     * into the same file
+     */
+
+    static void save(hist_type const& hist, nuclear_hist_type const& histMap,
+                     std::string const& filename, std::string const& comment = "");
+  };
+
+} // namespace corsika::process::interaction_counter
diff --git a/Processes/InteractionCounter/testInteractionCounter.cc b/Processes/InteractionCounter/testInteractionCounter.cc
new file mode 100644
index 0000000000000000000000000000000000000000..797d0f87d1205fb0db28ae66b1ca21c50e709d3b
--- /dev/null
+++ b/Processes/InteractionCounter/testInteractionCounter.cc
@@ -0,0 +1,156 @@
+
+/*
+ * (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * See file AUTHORS for a list of contributors.
+ *
+ * This software is distributed under the terms of the GNU General Public
+ * Licence version 3 (GPL Version 3). See file LICENSE for a full version of
+ * the license.
+ */
+
+#include <corsika/process/interaction_counter/InteractionCounter.h>
+
+#include <corsika/environment/Environment.h>
+#include <corsika/environment/HomogeneousMedium.h>
+#include <corsika/environment/NuclearComposition.h>
+#include <corsika/geometry/Point.h>
+#include <corsika/geometry/RootCoordinateSystem.h>
+#include <corsika/geometry/Vector.h>
+#include <corsika/units/PhysicalUnits.h>
+
+#include <corsika/setup/SetupStack.h>
+
+#include <catch2/catch.hpp>
+
+#include <numeric>
+
+using namespace corsika;
+using namespace corsika::process::interaction_counter;
+using namespace corsika::units;
+using namespace corsika::units::si;
+
+auto setupEnvironment(particles::Code target_code) {
+  // setup environment, geometry
+  auto env = std::make_unique<environment::Environment<environment::IMediumModel>>();
+  auto& universe = *(env->GetUniverse());
+  const geometry::CoordinateSystem& cs = env->GetCoordinateSystem();
+
+  auto theMedium =
+      environment::Environment<environment::IMediumModel>::CreateNode<geometry::Sphere>(
+          geometry::Point{cs, 0_m, 0_m, 0_m},
+          1_km * std::numeric_limits<double>::infinity());
+
+  using MyHomogeneousModel = environment::HomogeneousMedium<environment::IMediumModel>;
+  theMedium->SetModelProperties<MyHomogeneousModel>(
+      1_kg / (1_m * 1_m * 1_m),
+      environment::NuclearComposition(std::vector<particles::Code>{target_code},
+                                      std::vector<float>{1.}));
+
+  auto const* nodePtr = theMedium.get();
+  universe.AddChild(std::move(theMedium));
+
+  return std::make_tuple(std::move(env), &cs, nodePtr);
+}
+
+template <typename TNodeType>
+auto setupStack(int vA, int vZ, HEPEnergyType vMomentum, TNodeType* vNodePtr,
+                geometry::CoordinateSystem const& cs) {
+  auto stack = std::make_unique<setup::Stack>();
+  auto constexpr mN = corsika::units::constants::nucleonMass;
+
+  geometry::Point const origin(cs, {0_m, 0_m, 0_m});
+  corsika::stack::MomentumVector const pLab(cs, {vMomentum, 0_GeV, 0_GeV});
+
+  HEPEnergyType const E0 =
+      sqrt(units::si::detail::static_pow<2>(mN * vA) + pLab.squaredNorm());
+  auto particle =
+      stack->AddParticle(std::tuple<particles::Code, units::si::HEPEnergyType,
+                                    corsika::stack::MomentumVector, geometry::Point,
+                                    units::si::TimeType, unsigned short, unsigned short>{
+          particles::Code::Nucleus, E0, pLab, origin, 0_ns, vA, vZ});
+
+  particle.SetNode(vNodePtr);
+  return std::make_tuple(
+      std::move(stack),
+      std::make_unique<decltype(corsika::stack::SecondaryView(particle))>(particle));
+}
+
+template <typename TNodeType>
+auto setupStack(particles::Code vProjectileType, HEPEnergyType vMomentum,
+                TNodeType* vNodePtr, geometry::CoordinateSystem const& cs) {
+  auto stack = std::make_unique<setup::Stack>();
+
+  geometry::Point const origin(cs, {0_m, 0_m, 0_m});
+  corsika::stack::MomentumVector const pLab(cs, {vMomentum, 0_GeV, 0_GeV});
+
+  HEPEnergyType const E0 =
+      sqrt(units::si::detail::static_pow<2>(particles::GetMass(vProjectileType)) +
+           pLab.squaredNorm());
+  auto particle = stack->AddParticle(
+      std::tuple<particles::Code, units::si::HEPEnergyType,
+                 corsika::stack::MomentumVector, geometry::Point, units::si::TimeType>{
+          vProjectileType, E0, pLab, origin, 0_ns});
+
+  particle.SetNode(vNodePtr);
+  return std::make_tuple(
+      std::move(stack),
+      std::make_unique<decltype(corsika::stack::SecondaryView(particle))>(particle));
+}
+
+struct DummyProcess {
+  template <typename TParticle>
+  GrammageType GetInteractionLength([[maybe_unused]] TParticle const& particle) {
+    return 100_g / 1_cm / 1_cm;
+  }
+
+  template <typename TParticle>
+  auto DoInteraction([[maybe_unused]] TParticle& projectile) {
+    return nullptr;
+  }
+};
+
+TEST_CASE("InteractionCounter") {
+  DummyProcess d;
+  InteractionCounter countedProcess(d);
+
+  SECTION("GetInteractionLength") {
+    REQUIRE(countedProcess.GetInteractionLength(nullptr) == 100_g / 1_cm / 1_cm);
+  }
+
+  auto [env, csPtr, nodePtr] = setupEnvironment(particles::Code::Oxygen);
+  SECTION("DoInteraction nucleus") {
+    unsigned short constexpr A = 14, Z = 7;
+    auto [stackPtr, secViewPtr] = setupStack(A, Z, 105_TeV, nodePtr, *csPtr);
+
+    auto projectile = secViewPtr->GetProjectile();
+    auto const ret = countedProcess.DoInteraction(projectile);
+    REQUIRE(ret == nullptr);
+
+    auto const& h = countedProcess.GetHistogram().labHists().second.at(1'000'070'140);
+    REQUIRE(h.at(50) == 1);
+    REQUIRE(std::accumulate(h.cbegin(), h.cend(), 0) == 1);
+
+    auto const& h2 = countedProcess.GetHistogram().CMSHists().second.at(1'000'070'140);
+    REQUIRE(h2.at(32) == 1); // bin 1.584 .. 1.995 TeV √s
+    REQUIRE(std::accumulate(h2.cbegin(), h2.cend(), 0) == 1);
+  }
+
+  SECTION("DoInteraction Lambda") {
+    auto constexpr code = particles::Code::Lambda0;
+    auto constexpr codeInt = static_cast<particles::CodeIntType>(code);
+    auto [stackPtr, secViewPtr] = setupStack(code, 105_TeV, nodePtr, *csPtr);
+
+    auto projectile = secViewPtr->GetProjectile();
+    auto const ret = countedProcess.DoInteraction(projectile);
+    REQUIRE(ret == nullptr);
+
+    auto const& h = countedProcess.GetHistogram().labHists().first;
+    REQUIRE(h.at(codeInt, 50) == 1);
+    REQUIRE(std::accumulate(h.cbegin(), h.cend(), 0) == 1);
+
+    auto const& h2 = countedProcess.GetHistogram().CMSHists().first;
+    REQUIRE(h2.at(codeInt, 32) == 1); // bin 1.584 .. 1.995 TeV √s
+    REQUIRE(std::accumulate(h2.cbegin(), h2.cend(), 0) == 1);
+  }
+}
diff --git a/Processes/Sibyll/NuclearInteraction.cc b/Processes/Sibyll/NuclearInteraction.cc
index a79fd80c2a5bc751bf247b4a01cc725986f9501f..d557f28662be5ea754074d2ebf3f25828146ca6b 100644
--- a/Processes/Sibyll/NuclearInteraction.cc
+++ b/Processes/Sibyll/NuclearInteraction.cc
@@ -177,7 +177,7 @@ namespace corsika::process::sibyll {
   template <>
   template <>
   tuple<units::si::CrossSectionType, units::si::CrossSectionType>
-  NuclearInteraction<SetupEnvironment>::GetCrossSection(Particle& vP,
+  NuclearInteraction<SetupEnvironment>::GetCrossSection(Particle const& vP,
                                                         const particles::Code TargetId) {
     using namespace units::si;
     if (vP.GetPID() != particles::Code::Nucleus)
@@ -216,7 +216,7 @@ namespace corsika::process::sibyll {
   template <>
   template <>
   units::si::GrammageType NuclearInteraction<SetupEnvironment>::GetInteractionLength(
-      Particle& vP) {
+      Particle const& vP) {
 
     using namespace units;
     using namespace units::si;
diff --git a/Processes/Sibyll/NuclearInteraction.h b/Processes/Sibyll/NuclearInteraction.h
index 199901cb8cd3b1061b6b2c2a30ea1f37e77d0e03..1be24afccb741d5f4c4d96dc59c1be9105c87507 100644
--- a/Processes/Sibyll/NuclearInteraction.h
+++ b/Processes/Sibyll/NuclearInteraction.h
@@ -50,10 +50,10 @@ namespace corsika::process::sibyll {
 
     template <typename Particle>
     std::tuple<corsika::units::si::CrossSectionType, corsika::units::si::CrossSectionType>
-    GetCrossSection(Particle& p, const corsika::particles::Code TargetId);
+    GetCrossSection(Particle const& p, const corsika::particles::Code TargetId);
 
     template <typename Particle>
-    corsika::units::si::GrammageType GetInteractionLength(Particle&);
+    corsika::units::si::GrammageType GetInteractionLength(Particle const&);
 
     template <typename Projectile>
     corsika::process::EProcessReturn DoInteraction(Projectile&);