From 265502c7b38439e6b5aec03dd890d881cf657943 Mon Sep 17 00:00:00 2001
From: Felix Riehn <friehn@lip.pt>
Date: Fri, 7 Mar 2025 12:49:32 +0000
Subject: [PATCH] Resolve "update pythia to v8.313"

---
 applications/c8_air_shower.cpp                |   6 +-
 .../detail/modules/pythia8/Interaction.inl    | 446 ------------------
 .../modules/pythia8/InteractionModel.inl      | 343 ++++++++++++++
 .../modules/qgsjetII/InteractionModel.inl     |   8 +
 .../modules/sibyll/InteractionModel.inl       |  13 +
 .../framework/utility/CrossSectionTable.hpp   |  89 ++++
 corsika/modules/Pythia8.hpp                   |  15 +
 corsika/modules/pythia8/Interaction.hpp       | 108 -----
 corsika/modules/pythia8/InteractionModel.hpp  | 153 ++++++
 corsika/modules/qgsjetII/InteractionModel.hpp |  17 +
 corsika/modules/sibyll/InteractionModel.hpp   |   3 +
 examples/CMakeLists.txt                       |   8 +
 examples/physics_examples/cross_section.cpp   | 163 +++++++
 .../physics_examples/had_interactions.cpp     | 267 +++++++++++
 modules/data                                  |   2 +-
 modules/pythia8/CMakeLists.txt                |   4 +-
 tests/modules/testPythia8.cpp                 |   4 +-
 tests/modules/testPythia8Interaction.inl      |  96 ++--
 18 files changed, 1148 insertions(+), 597 deletions(-)
 delete mode 100644 corsika/detail/modules/pythia8/Interaction.inl
 create mode 100644 corsika/detail/modules/pythia8/InteractionModel.inl
 create mode 100644 corsika/framework/utility/CrossSectionTable.hpp
 delete mode 100644 corsika/modules/pythia8/Interaction.hpp
 create mode 100644 corsika/modules/pythia8/InteractionModel.hpp
 create mode 100644 examples/physics_examples/cross_section.cpp
 create mode 100644 examples/physics_examples/had_interactions.cpp

diff --git a/applications/c8_air_shower.cpp b/applications/c8_air_shower.cpp
index f48c48c40..1630426d3 100644
--- a/applications/c8_air_shower.cpp
+++ b/applications/c8_air_shower.cpp
@@ -266,7 +266,7 @@ int main(int argc, char** argv) {
       ->group("Misc.");
   app.add_option("-M,--hadronModel", "High-energy hadronic interaction model")
       ->default_val("SIBYLL-2.3d")
-      ->check(CLI::IsMember({"SIBYLL-2.3d", "QGSJet-II.04", "EPOS-LHC"}))
+      ->check(CLI::IsMember({"SIBYLL-2.3d", "QGSJet-II.04", "EPOS-LHC", "Pythia8"}))
       ->group("Misc.");
   app.add_option("-T,--hadronModelTransitionEnergy",
                  "Transition between high-/low-energy hadronic interaction "
@@ -440,6 +440,10 @@ int main(int argc, char** argv) {
   } else if (modelStr == "EPOS-LHC") {
     heModel = DynamicInteractionProcess<StackType>{
         std::make_shared<corsika::epos::Interaction>(corsika::setup::C7trackedParticles)};
+  } else if (modelStr == "Pythia8") {
+    heModel = DynamicInteractionProcess<StackType>{
+        std::make_shared<corsika::pythia8::Interaction>(
+            corsika::setup::C7trackedParticles)};
   } else {
     CORSIKA_LOG_CRITICAL("invalid choice \"{}\"; also check argument parser", modelStr);
     return EXIT_FAILURE;
diff --git a/corsika/detail/modules/pythia8/Interaction.inl b/corsika/detail/modules/pythia8/Interaction.inl
deleted file mode 100644
index 6c19a6c5b..000000000
--- a/corsika/detail/modules/pythia8/Interaction.inl
+++ /dev/null
@@ -1,446 +0,0 @@
-/*
- * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
- *
- * This software is distributed under the terms of the 3-clause BSD license.
- * See file LICENSE for a full version of the license.
- */
-
-#pragma once
-
-#include <tuple>
-#include <stdexcept>
-
-#include <boost/filesystem/path.hpp>
-#include <fmt/core.h>
-
-#include <corsika/modules/pythia8/Interaction.hpp>
-
-#include <corsika/framework/geometry/FourVector.hpp>
-#include <corsika/framework/utility/COMBoost.hpp>
-#include <corsika/framework/core/EnergyMomentumOperations.hpp>
-#include <corsika/media/Environment.hpp>
-#include <corsika/media/NuclearComposition.hpp>
-
-namespace corsika::pythia8 {
-
-  inline Interaction::~Interaction() {
-    CORSIKA_LOG_INFO("Pythia::Interaction n= {}", count_);
-  }
-
-  inline Interaction::Interaction(boost::filesystem::path const& mpiInitFile,
-                                  bool const print_listing)
-      : print_listing_(print_listing)
-      , pythiaMain_{CORSIKA_Pythia8_XML_DIR, false}
-      , pythiaColl_{CORSIKA_Pythia8_XML_DIR, false} {
-    auto rndm = std::make_shared<corsika::pythia8::Random>();
-    pythiaColl_.setRndmEnginePtr(rndm);
-    pythiaMain_.setRndmEnginePtr(rndm);
-
-    CORSIKA_LOG_INFO("Pythia8 MPI init file: {}", mpiInitFile.native());
-    // Main Pythia object for managing the cascade evolution.
-    // Can also do decays, but no hard processes.
-
-    pythiaMain_.readString("ProcessLevel:all = off");
-
-    // Reduce statistics printout to relevant ones.
-    pythiaMain_.readString("Stat:showProcessLevel = off");
-    pythiaMain_.readString("Stat:showPartonLevel = off");
-
-    // Add Argon, since not in Particle data. id:all = name antiName
-    // spinType chargeType colType m0 mWidth mMin mMax tau0.
-    pythiaMain_.readString(
-        "1000180400:all = 40Ar 40Arbar 1 54 0 "
-        "37.22474 0. 0. 0. 0.");
-
-    // we can't test this block, LCOV_EXCL_START
-    if (!pythiaMain_.init())
-      throw std::runtime_error("Pythia::Interaction: Initialization failed!");
-    // LCOV_EXCL_STOP
-
-    // Secondary Pythia object for performing individual collisions.
-    // Variable incoming beam type and energy.
-    pythiaColl_.readString("Beams:allowVariableEnergy = on");
-    pythiaColl_.readString("Beams:allowIDAswitch = on");
-
-    // Set up for fixed-target collisions.
-    pythiaColl_.readString(
-        "Beams:frameType = 3"); // arbitrary frame, need to define full 4-momenta
-    pythiaColl_.settings.parm("Beams:pzA", eMaxLab_ / 1_GeV);
-    pythiaColl_.readString("Beams:pzB = 0.");
-
-    // Must use the soft and low-energy QCD processes.
-    pythiaColl_.readString("SoftQCD:all = on");
-    pythiaColl_.readString("LowEnergyQCD:all = on");
-
-    // Decays to be done by pythiaMain_.
-    pythiaColl_.readString("HadronLevel:Decay = off");
-
-    // Reduce printout and relax energy-momentum conservation.
-    pythiaColl_.readString("Print:quiet = on");
-    pythiaColl_.readString("Check:epTolErr = 0.1");
-    pythiaColl_.readString("Check:epTolWarn = 0.0001");
-    pythiaColl_.readString("Check:mTolErr = 0.01");
-
-    // Redure statistics printout to relevant ones.
-    pythiaColl_.readString("Stat:showProcessLevel = off");
-    pythiaColl_.readString("Stat:showPartonLevel = off");
-
-    bool const reuse = true; // could be made more flexible
-    // Reuse MPI initialization file if it exists; else create a new one.
-    if (reuse)
-      pythiaColl_.readString("MultipartonInteractions:reuseInit = 3");
-    else
-      pythiaColl_.readString("MultipartonInteractions:reuseInit = 1");
-    pythiaColl_.settings.word("MultipartonInteractions:initFile", mpiInitFile.native());
-
-    // initialize
-    // we can't test this block, LCOV_EXCL_START
-    if (!pythiaColl_.init())
-      throw std::runtime_error("Pythia::Interaction: Initialization failed!");
-    // LCOV_EXCL_STOP
-  }
-
-  inline bool Interaction::isValid(Code const projectileId, Code const targetId,
-                                   HEPEnergyType const sqrtS) const {
-    if (is_nucleus(projectileId)) // not yet possible with Pythia
-      return false;
-
-    HEPEnergyType const labE = calculate_lab_energy(
-        static_pow<2>(sqrtS), get_mass(projectileId), get_mass(targetId));
-    if (labE < eKinMinLab_) return false;
-
-    return std::find(validTargets_.begin(), validTargets_.end(), targetId) !=
-           validTargets_.end();
-  }
-
-  inline bool Interaction::canInteract(Code const pCode) const {
-    return is_hadron(pCode) && !is_nucleus(pCode); // should be sufficient
-  }
-
-  inline std::tuple<CrossSectionType, CrossSectionType>
-  Interaction::getCrossSectionInelEla(Code const projectileId, Code const targetId,
-                                      FourMomentum const& projectileP4,
-                                      FourMomentum const& targetP4) const {
-    HEPEnergyType const CoMenergy =
-        is_nucleus(targetId)
-            ? (projectileP4 + targetP4 / get_nucleus_A(targetId)).getNorm()
-            : (projectileP4 + targetP4).getNorm();
-
-    if (is_nucleus(targetId) || is_nucleus(projectileId)) {
-      CORSIKA_LOG_ERROR(
-          "Pythia8::Interaction::getCrossSectionInelEla() called with nuclear projectile "
-          "or target");
-      return std::make_tuple(CrossSectionType::zero(), CrossSectionType::zero());
-    }
-
-    if (!isValid(projectileId, targetId, CoMenergy)) {
-      return std::make_tuple(CrossSectionType::zero(), CrossSectionType::zero());
-    }
-
-    // input particle PDG
-    auto const pdgCodeBeam = static_cast<int>(get_PDG(projectileId));
-    auto const pdgCodeTarget = static_cast<int>(get_PDG(targetId));
-    double const ecm_GeV = CoMenergy * (1 / 1_GeV);
-
-    auto const sigTot =
-        pythiaColl_.getSigmaTotal(pdgCodeBeam, pdgCodeTarget, ecm_GeV) * 1_mb;
-    auto const sigEla =
-        pythiaColl_.getSigmaPartial(pdgCodeBeam, pdgCodeTarget, ecm_GeV, 2) * 1_mb;
-
-    return std::make_tuple(sigTot - sigEla, sigEla);
-  }
-
-  CrossSectionType Interaction::getCrossSection(Code const projectileId,
-                                                Code const targetId,
-                                                FourMomentum const& projectileP4,
-                                                FourMomentum const& targetP4) const {
-
-    if (!is_nucleus(targetId)) {
-      auto const [sigProd, sigEla] =
-          getCrossSectionInelEla(projectileId, targetId, projectileP4, targetP4);
-      return sigProd + sigEla;
-    } else if (get_nucleus_A(targetId) == 1) {
-      auto const [sigProd, sigEla] = getCrossSectionInelEla(
-          projectileId, get_nucleus_Z(targetId) == 1 ? Code::Proton : Code::Neutron,
-          projectileP4, targetP4);
-      return sigProd + sigEla;
-    } else {
-      auto const [sigProd, sigEla] =
-          getCrossSectionInelEla(projectileId, Code::Proton, projectileP4, targetP4);
-      auto const sigTot = sigProd + sigEla;
-      auto const nSubcoll = getAverageSubcollisions(targetId, sigTot);
-      if (nSubcoll == 0) { // no parameterization available -> we can't handle this
-        return CrossSectionType::zero();
-      } else {
-        return sigTot * get_nucleus_A(targetId) / nSubcoll;
-      }
-    }
-  }
-
-  double Interaction::getAverageSubcollisions(Code targetId,
-                                              CrossSectionType sigTot) const {
-    if (targetId == Code::Proton || targetId == Code::Neutron ||
-        targetId == Code::Hydrogen || targetId == Code::AntiProton ||
-        targetId == Code::AntiNeutron)
-      return 1;
-
-    auto const Z = get_nucleus_Z(targetId);
-    auto const A = get_nucleus_A(targetId);
-
-    double nCollAvg;
-
-    if (Z == 7 && A == 14)
-      nCollAvg = (sigTot < 31._mb) // this is from the paper
-                     ? 1. + 0.017 / 1_mb * sigTot
-                     : 1.2 + 0.0105 / 1_mb * sigTot;
-    else if (Z == 8 && A == 16)
-      nCollAvg = (sigTot < 16._mb) // provided by Torbjörn Sjöstrand
-                     ? 1. + 0.0245 / 1_mb * sigTot
-                     : 1.2 + 0.012 / 1_mb * sigTot;
-    else if (Z == 18 && A == 40)
-      nCollAvg =
-          (sigTot < 10._mb) ? 1. + 0.050 / 1_mb * sigTot : 1.28 + 0.022 / 1_mb * sigTot;
-    else {
-      CORSIKA_LOG_ERROR(
-          fmt::format("Pythia8 cross-sections not defined for ({},{}) nucleus", A, Z));
-      nCollAvg = 0;
-    }
-
-    return nCollAvg;
-  }
-
-  template <class TView>
-  inline void Interaction::doInteraction(TView& view, Code const projectileId,
-                                         Code const targetId,
-                                         FourMomentum const& projectileP4,
-                                         FourMomentum const& targetP4) {
-
-    CORSIKA_LOG_DEBUG(
-        "Pythia::Interaction: "
-        "doInteraction: {} interaction? ",
-        projectileId, corsika::pythia8::Interaction::canInteract(projectileId));
-
-    // define system
-    auto const sqrtS2 = (projectileP4 + targetP4).getNormSqr();
-    HEPEnergyType const sqrtS = sqrt(sqrtS2);
-    HEPEnergyType const eProjectileLab = (sqrtS2 - static_pow<2>(get_mass(projectileId)) -
-                                          static_pow<2>(get_mass(targetId))) /
-                                         (2 * get_mass(targetId));
-
-    if (!isValid(projectileId, targetId, sqrtS)) {
-      throw std::runtime_error("invalid target,projectile,energy combination.");
-    }
-
-    CORSIKA_LOG_DEBUG("Interaction: ebeam lab: {} GeV", eProjectileLab / 1_GeV);
-    CORSIKA_LOG_DEBUG(
-        "Interaction: "
-        " doInteraction: E(GeV): {}"
-        " Ecm(GeV): {}",
-        eProjectileLab / 1_GeV, sqrtS / 1_GeV);
-
-    count_++;
-
-    int const idNow = static_cast<int>(get_PDG(projectileId));
-
-    // References to the two event records. Clear main event record.
-    Pythia8::Event& eventMain = pythiaMain_.event;
-    Pythia8::Event& eventColl = pythiaColl_.event;
-
-    COMBoost const labFrameBoost{targetP4.getSpaceLikeComponents(), get_mass(targetId)};
-    auto const proj4MomLab = labFrameBoost.toCoM(projectileP4);
-    auto const& rotCS = labFrameBoost.getRotatedCS();
-    auto const pProjLab = proj4MomLab.getSpaceLikeComponents().getComponents(rotCS);
-
-    auto constexpr invGeV = 1 / 1_GeV;
-
-    Pythia8::Vec4 const pNow{pProjLab.getX() * invGeV, pProjLab.getY() * invGeV,
-                             pProjLab.getZ() * invGeV,
-                             proj4MomLab.getTimeLikeComponent() * invGeV};
-
-    // Insert incoming particle in cleared main event record.
-    int unsuccessful_iterations{0};
-    do { // retry event generation if Pythia gets stuck
-      eventMain.clear();
-      eventMain.append(90, -11, 0, 0, 1, 1, 0, 0, pNow, pNow.mCalc());
-      int const iHad = eventMain.append(idNow, 12, 0, 0, 0, 0, 0, 0, pNow,
-                                        get_mass(projectileId) * (1 / 1_GeV));
-
-      Pythia8::Vec4 const vNow{}; // production vertex
-
-      eventMain[iHad].vProd(vNow);
-
-      auto const [Anow, Znow] = std::invoke([targetId]() {
-        if (targetId == Code::Proton) {
-          return std::make_pair(1, 1);
-        } else if (targetId == Code::Neutron) {
-          return std::make_pair(1, 0);
-        } else if (is_nucleus(targetId)) {
-          return std::make_pair<int, int>(get_nucleus_A(targetId),
-                                          get_nucleus_Z(targetId));
-        } else {
-          // due to the earlier call to isValid(), we shouldn't end up here
-          // LCOV_EXCL_START
-          CORSIKA_LOG_ERROR("invalid target {}; you shouldn't have gotten this far!",
-                            targetId);
-          return std::make_pair(0, 0);
-          // LCOV_EXCL_STOP
-        }
-      });
-
-      // Set up for collisions on a nucleus.
-      int np = Znow;
-      int nn = Anow - Znow;
-      int sizeOld = 0;
-      int sizeNew = 0;
-      Pythia8::Vec4 const dirNow = pNow / pNow.pAbs();
-      Pythia8::Rndm& rndm = pythiaMain_.rndm;
-
-      double constexpr mp = get_mass(Code::Proton) / 1_GeV;
-      double const sqrtSNN_GeV = (pNow + Pythia8::Vec4{0, 0, 0, mp}).mCalc();
-
-      auto const nCollAvg = getAverageSubcollisions(
-          targetId, pythiaColl_.getSigmaTotal(idNow, 2212, sqrtSNN_GeV) * 1_mb);
-      double const probMore = 1. - 1. / nCollAvg;
-
-      // Loop over varying number of hit nucleons in target nucleus.
-      for (int iColl = 1; iColl <= Anow; ++iColl) {
-        if (iColl > 1 && rndm.flat() > probMore) break;
-
-        // Pick incoming projectile: trivial for first subcollision, else ...
-        int iProj = iHad;
-        int procType = 0;
-
-        // ... find highest-pLongitudinal particle from latest subcollision.
-        if (iColl > 1) {
-          iProj = 0;
-          double pMax = 0.;
-          for (int i = sizeOld; i < sizeNew; ++i)
-            if (eventMain[i].isFinal() && eventMain[i].isHadron()) {
-              if (double const pp = Pythia8::dot3(dirNow, eventMain[i].p()); pp > pMax) {
-                iProj = i;
-                pMax = pp;
-              }
-            }
-
-          // No further subcollision if no particle with enough energy.
-          // cannot be reliably provoked in tests
-          // LCOV_EXCL_START
-          if (iProj == 0 ||
-              eventMain[iProj].e() - eventMain[iProj].m() < eKinMinLab_ / 1_GeV)
-            break;
-          // LCOV_EXCL_STOP
-
-          // Choose process; only SD or ND at perturbative energies.
-          double const eCMSub =
-              (eventMain[iProj].p() + Pythia8::Vec4{0, 0, 0, mp}).mCalc();
-          if (eCMSub > 10.) procType = (rndm.flat() < probSD_) ? 4 : 1;
-        }
-
-        // Pick one p or n from target.
-        int const idProj = eventMain[iProj].id();
-        bool const doProton = rndm.flat() < (np / double(np + nn));
-        if (doProton)
-          np--;
-        else
-          nn--;
-        int const idNuc = doProton ? 2212 : 2112;
-
-        // Perform the projectile-nucleon subcollision.
-        pythiaColl_.setBeamIDs(idProj, idNuc);
-        pythiaColl_.setKinematics(eventMain[iProj].p(), Pythia8::Vec4());
-
-        if (!pythiaColl_.next(procType)) {
-          CORSIKA_LOG_WARN("Pythia collision next() failed {} {} {}",
-                           eventMain[iProj].p(), idProj, idNuc);
-          eventMain.clear();
-          ++unsuccessful_iterations;
-          goto event_repeat; // retry event, last remaining good use-case for goto
-        }
-
-        // Insert target nucleon. Mothers are (0,iProj) to mark who it
-        // interacted with. Always use proton mass for simplicity.
-        int const statusNuc = (iColl == 1) ? -181 : -182;
-        int const iNuc =
-            eventMain.append(idNuc, statusNuc, 0, iProj, 0, 0, 0, 0, 0., 0., 0., mp, mp);
-        eventMain[iNuc].vProdAdd(vNow);
-
-        // Update full energy of the event with the proton mass.
-        eventMain[0].e(eventMain[0].e() + mp);
-        eventMain[0].m(eventMain[0].p().mCalc());
-
-        // Insert secondary produced particles (but skip intermediate partons)
-        // into main event record and shift to correct production vertex.
-        sizeOld = eventMain.size();
-        for (int iSub = 3; iSub < eventColl.size(); ++iSub) {
-          if (!eventColl[iSub].isFinal()) continue;
-          int const iNew = eventMain.append(eventColl[iSub]);
-          eventMain[iNew].mothers(iNuc, iProj);
-          eventMain[iNew].vProdAdd(vNow);
-        }
-        sizeNew = eventMain.size();
-
-        // Update daughters of colliding hadrons and other history.
-        eventMain[iProj].daughters(sizeOld, sizeNew - 1);
-        eventMain[iNuc].daughters(sizeOld, sizeNew - 1);
-        eventMain[iProj].statusNeg();
-        double dTau = (iColl == 1) ? (vNow.e() - eventMain[iHad].tProd()) *
-                                         eventMain[iHad].m() / eventMain[iHad].e()
-                                   : 0.;
-        eventMain[iProj].tau(dTau);
-
-        // End of loop over interactions in a nucleus.
-      }
-      break; //
-
-    event_repeat:;
-    } while (unsuccessful_iterations < 100);
-
-    if (unsuccessful_iterations >= 100) {
-      CORSIKA_LOG_CRITICAL(
-          "Pythia event generation failed after 100 trials: projectile: {}, target: {}",
-          projectileId, targetId);
-      throw std::runtime_error{"Pythia event generation failed after 100 trials"};
-    }
-
-    MomentumVector Plab_final{labFrameBoost.getOriginalCS()};
-    auto Elab_final = HEPEnergyType::zero();
-
-    for (Pythia8::Particle const& p8p : eventMain) {
-      // skip particles that have decayed / are initial particles in pythia's event record
-      if (!p8p.isFinal()) continue;
-      try {
-        auto const volatile id = static_cast<PDGCode>(p8p.id());
-        auto const pyId = convert_from_PDG(id);
-
-        MomentumVector const pyPlab(
-            rotCS, {p8p.px() * 1_GeV, p8p.py() * 1_GeV, p8p.pz() * 1_GeV});
-        auto const pyP = labFrameBoost.fromCoM(FourVector{p8p.e() * 1_GeV, pyPlab});
-
-        HEPEnergyType const mass = get_mass(pyId);
-        HEPEnergyType const Ekin =
-            sqrt(pyP.getSpaceLikeComponents().getSquaredNorm() + mass * mass) - mass;
-
-        // add to corsika stack
-        auto pnew = view.addSecondary(std::make_tuple(pyId, Ekin, pyPlab.normalized()));
-
-        Plab_final += pnew.getMomentum();
-        Elab_final += pnew.getEnergy();
-      }
-      // irreproducible in tests, LCOV_EXCL_START
-      catch (std::out_of_range const& ex) {
-        CORSIKA_LOG_CRITICAL("Pythia ID {} unknown in C8", p8p.id());
-        throw ex;
-      }
-      // LCOV_EXCL_STOP
-    }
-
-    eventMain.clear();
-
-    CORSIKA_LOG_DEBUG(
-        "conservation (all GeV): "
-        "Elab_final= {}"
-        ", Plab_final= {}",
-        Elab_final / 1_GeV, (Plab_final / 1_GeV).getComponents());
-  }
-
-} // namespace corsika::pythia8
diff --git a/corsika/detail/modules/pythia8/InteractionModel.inl b/corsika/detail/modules/pythia8/InteractionModel.inl
new file mode 100644
index 000000000..01adc3c41
--- /dev/null
+++ b/corsika/detail/modules/pythia8/InteractionModel.inl
@@ -0,0 +1,343 @@
+/*
+ * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * This software is distributed under the terms of the 3-clause BSD license.
+ * See file LICENSE for a full version of the license.
+ */
+
+#pragma once
+
+#include <utility>
+#include <stdexcept>
+
+#include <boost/filesystem/path.hpp>
+#include <boost/iterator/transform_iterator.hpp>
+#include <fmt/core.h>
+#include <cnpy.hpp>
+
+#include <corsika/modules/pythia8/InteractionModel.hpp>
+
+#include <corsika/framework/geometry/FourVector.hpp>
+#include <corsika/framework/utility/COMBoost.hpp>
+#include <corsika/framework/utility/CrossSectionTable.hpp>
+#include <corsika/framework/core/EnergyMomentumOperations.hpp>
+#include <corsika/media/Environment.hpp>
+#include <corsika/media/NuclearComposition.hpp>
+
+namespace corsika::pythia8 {
+
+  inline InteractionModel::~InteractionModel() {}
+
+  inline InteractionModel::InteractionModel(std::set<Code> const& stableParticles,
+                                            boost::filesystem::path const& dataPath,
+                                            bool const printListing)
+      : crossSectionPPElastic_{loadPPTable(dataPath, "sig_el")}
+      , crossSectionPPInelastic_{loadPPTable(dataPath, "sig_inel")}
+      , print_listing_(printListing) {
+    auto rndm = std::make_shared<corsika::pythia8::Random>();
+    pythia_.setRndmEnginePtr(rndm);
+
+    CORSIKA_LOG_INFO("Pythia8 reuse files: {}", dataPath.native());
+
+    // projectile and target init to p Nitrogen to initialize pythia with angantyr
+    pythia_.readString("Beams:allowIDASwitch = on");
+    pythia_.settings.mode("Beams:idA",
+                          static_cast<PDGCodeIntType>(get_PDG(Code::Proton)));
+    pythia_.settings.mode("Beams:idB",
+                          static_cast<PDGCodeIntType>(get_PDG(Code::Oxygen)));
+
+    pythia_.readString("Beams:allowVariableEnergy = on");
+    pythia_.readString("Beams:frameType = 1"); // CoM
+    // initialize a maximum energy event
+    pythia_.settings.parm("Beams:eCM", 400_TeV / 1_GeV);
+
+    // needed when regular Pythia (not Angantyr) takes over for pp
+    pythia_.readString("SoftQCD:all = on");
+
+    /* reuse file settings
+    reuseInit = 2: initialization data is reuse file, but if the file is not found,
+    initialization fails reuseInit = 3: same, but if the file is not found, it will be
+    generated and saved after normal initialization */
+    pythia_.readString("MultipartonInteractions:reuseInit = 2");
+    pythia_.readFile(dataPath.native() + "/pythia8313.cmnd");
+    // switch on decays for all hadrons except the ones defined as tracked by C8
+    if (!stableParticles.empty()) {
+      pythia_.readString("HadronLevel:Decay = on");
+      for (auto pCode : stableParticles)
+        pythia_.particleData.mayDecay(static_cast<int>(get_PDG(pCode)), false);
+    } else {
+      // all hadrons stable
+      pythia_.readString("HadronLevel:Decay = on");
+    }
+    // Reduce printout and relax energy-momentum conservation.
+    pythia_.readString("Print:quiet = on");
+    pythia_.readString("Check:epTolErr = 0.1");
+    pythia_.readString("Check:epTolWarn = 0.0001");
+    pythia_.readString("Check:mTolErr = 0.01");
+
+    // Reduce statistics printout to relevant ones.
+    pythia_.readString("Stat:showProcessLevel = off");
+    pythia_.readString("Stat:showPartonLevel = off");
+
+    // initialize
+    // we can't test this block, LCOV_EXCL_START
+    if (!pythia_.init())
+      throw std::runtime_error("Pythia::InteractionModel: Initialization failed!");
+    // LCOV_EXCL_STOP
+
+    // create/load tables of cross sections
+    for (Code const projectile : validProjectiles_) {
+      for (Code const target : validTargets_) {
+        if (xs_map_.find(projectile) == xs_map_.end()) {
+          // projectile not mapped, load table directly
+          auto const tablePath =
+              dataPath / "pythia8_xs_npz" /
+              fmt::format("xs_{}_{}.npz",
+                          static_cast<PDGCodeIntType>(get_PDG(projectile)),
+                          static_cast<PDGCodeIntType>(get_PDG(target)));
+          auto const tables = cnpy::npz_load(tablePath.native());
+          // NOTE: tables are calculated for plab. In C8 we we assume elab. starting at
+          // plab=100GeV the difference is at most (1+m**2/p**2)=1.000088035
+          auto const energies = tables.at("plab").as_vec<float>();
+          auto const total_xs = tables.at("sig_tot").as_vec<float>();
+
+          if (auto const e_size = energies.size(), xs_size = total_xs.size();
+              xs_size != e_size) {
+            throw std::runtime_error{
+                fmt::format("Pythia8 table corrupt (plab size = {}; sig_tot size = {})",
+                            e_size, xs_size)};
+          }
+
+          auto xs_it = boost::make_transform_iterator(total_xs.cbegin(), millibarn_mult);
+          auto e_it_beg = boost::make_transform_iterator(energies.cbegin(), GeV_mult);
+          decltype(e_it_beg) e_it_end =
+              boost::make_transform_iterator(energies.cend(), GeV_mult);
+          crossSectionTables_.insert(
+              {std::pair{projectile, target},
+               CrossSectionTable<InterpolationTransforms::Log>{
+                   std::move(e_it_beg), std::move(e_it_end), std::move(xs_it)}});
+        }
+      }
+    }
+  }
+
+  inline CrossSectionTable<InterpolationTransforms::Log> InteractionModel::loadPPTable(
+      boost::filesystem::path const& dataPath, char const* key) {
+    auto const ppTablePath =
+        dataPath / "pythia8_xs_npz" /
+        fmt::format("xs_{}_{}.npz", static_cast<PDGCodeIntType>(get_PDG(Code::Proton)),
+                    static_cast<PDGCodeIntType>(get_PDG(Code::Proton)));
+    auto const tables = cnpy::npz_load(ppTablePath.native());
+    // NOTE: tables are calculated for plab. In C8 we we assume elab. starting at
+    // plab=100GeV the difference is at most (1+m**2/p**2)=1.000088035
+    auto const energies = tables.at("plab").as_vec<float>();
+    auto const xs = tables.at(key).as_vec<float>();
+
+    if (auto e_size = energies.size(), xs_size = xs.size(); xs_size != e_size) {
+      throw std::runtime_error{fmt::format(
+          "Pythia8 table corrupt (plab size = {}; xs size = {})", e_size, xs_size)};
+    }
+
+    auto xs_it = boost::make_transform_iterator(xs.cbegin(), millibarn_mult);
+    auto e_it_beg = boost::make_transform_iterator(energies.cbegin(), GeV_mult);
+    decltype(e_it_beg) e_it_end =
+        boost::make_transform_iterator(energies.cend(), GeV_mult);
+    return CrossSectionTable<InterpolationTransforms::Log>{
+        std::move(e_it_beg), std::move(e_it_end), std::move(xs_it)};
+  }
+
+  inline bool InteractionModel::isValid(Code const projectileId, Code const targetId,
+                                        HEPEnergyType const sqrtSNN) const {
+
+    CORSIKA_LOG_DEBUG("pythia isValid: {} + {} at sqrtSNN = {} GeV", projectileId,
+                      targetId, sqrtSNN / 1_GeV);
+    if (is_nucleus(targetId))
+      CORSIKA_LOG_DEBUG("nucleus = {}-{}", get_nucleus_A(targetId),
+                        get_nucleus_Z(targetId));
+    if (is_nucleus(projectileId)) // not yet possible with Pythia
+      return false;
+
+    if (!canInteract(projectileId)) return false;
+
+    auto const mass_target =
+        get_mass(targetId) / (is_nucleus(targetId) ? get_nucleus_A(targetId) : 1.);
+    HEPEnergyType const labE =
+        calculate_lab_energy(static_pow<2>(sqrtSNN), get_mass(projectileId), mass_target);
+    if (labE < eKinMinLab_) return false;
+
+    bool const validProjectile =
+        std::find(validProjectiles_.begin(), validProjectiles_.end(), projectileId) !=
+        validProjectiles_.end();
+    bool const validTarget = std::find(validTargets_.begin(), validTargets_.end(),
+                                       targetId) != validTargets_.end();
+    return validProjectile && validTarget;
+  }
+
+  inline bool InteractionModel::canInteract(Code const pCode) const {
+    return is_hadron(pCode) && !is_nucleus(pCode);
+  }
+
+  inline std::tuple<CrossSectionType, CrossSectionType>
+  InteractionModel::getCrossSectionInelEla(Code const projectileId, Code const targetId,
+                                           FourMomentum const& projectileP4,
+                                           FourMomentum const& targetP4) const {
+    // elastic cross sections only available for proton
+    if (projectileId != Code::Proton || targetId != Code::Proton) {
+      return std::tuple{CrossSectionType::zero(), CrossSectionType::zero()};
+    }
+
+    // NOTE: here we calculate SNN (per nucleon energy) AND S (per particle energy)
+    // because isValid expects sqrtSNN as input but the tables need Elab
+    auto const Aprojectile =
+        (is_nucleus(projectileId) ? get_nucleus_A(projectileId) : 1.);
+    auto const Atarget = (is_nucleus(targetId) ? get_nucleus_A(targetId) : 1.);
+    auto const SNN = (projectileP4 / Aprojectile + targetP4 / Atarget).getNormSqr();
+    HEPEnergyType const sqrtSNN = sqrt(SNN);
+    if (!isValid(projectileId, targetId, sqrtSNN))
+      return std::tuple{CrossSectionType::zero(), CrossSectionType::zero()};
+
+    // read cross section from table
+    // define system (the tables were created for lab. energies) since we cannot assume
+    // the input is in the lab. frame we calculate the lab. energy from SNN =
+    // (projectileP4 / Aprojectile + targetP4 / Atarget)**2 and Elab = S/(2. * mTarget /
+    // Atarget) where A* is the number of nucleons in a nucleus. A* is 1 if projectile or
+    // target are simple hadrons.
+    HEPEnergyType const Elab = calculate_lab_energy(
+        SNN, get_mass(projectileId) / Aprojectile, get_mass(targetId) / Atarget);
+
+    CrossSectionType const xsEl = crossSectionPPElastic_.interpolate(Elab);
+    CrossSectionType const xsInel = crossSectionPPInelastic_.interpolate(Elab);
+    return std::pair{xsInel, xsEl};
+  }
+
+  inline CrossSectionType InteractionModel::getCrossSection(
+      Code const projectileId, Code const targetId, FourMomentum const& projectileP4,
+      FourMomentum const& targetP4) const {
+
+    auto const Aprojectile =
+        (is_nucleus(projectileId) ? get_nucleus_A(projectileId) : 1.);
+    auto const Atarget = (is_nucleus(targetId) ? get_nucleus_A(targetId) : 1.);
+
+    auto const SNN = (projectileP4 / Aprojectile + targetP4 / Atarget).getNormSqr();
+    HEPEnergyType const sqrtSNN = sqrt(SNN);
+    if (!isValid(projectileId, targetId, sqrtSNN)) return CrossSectionType::zero();
+
+    // read cross section from table
+    // define system (the tables were created for lab. energies) since we cannot assume
+    // the input is in the lab. frame we calculate the lab. energy from SNN =
+    // (projectileP4 / Aprojectile + targetP4 / Atarget)**2 and Elab = S/(2. * mTarget /
+    // Atarget) where A* is the number of nucleons in a nucleus. A* is 1 if projectile or
+    // target are simple hadrons.
+    auto const it = xs_map_.find(projectileId);
+    auto const mapped_projectile = (it == xs_map_.end()) ? projectileId : it->second;
+    auto const& table = crossSectionTables_.at(std::pair{mapped_projectile, targetId});
+    HEPEnergyType const Elab = calculate_lab_energy(
+        SNN, get_mass(projectileId) / Aprojectile, get_mass(targetId) / Atarget);
+    CORSIKA_LOG_DEBUG("pythia getCrossSection: {}+{} at Elab= {} GeV, sqrtSNN = {} GeV",
+                      projectileId, get_name(targetId), Elab / 1_GeV, sqrtSNN / 1_GeV);
+    return table.interpolate(Elab);
+  }
+
+  template <class TView>
+  inline void InteractionModel::doInteraction(TView& view, Code const projectileId,
+                                              Code const targetId,
+                                              FourMomentum const& projectileP4,
+                                              FourMomentum const& targetP4) {
+
+    CORSIKA_LOG_DEBUG(
+        "Pythia::InteractionModel: "
+        "doInteraction: {} interaction? {} ",
+        projectileId, corsika::pythia8::InteractionModel::canInteract(projectileId));
+
+    // define system nucleon-nucleon
+    auto const projectileP4NN =
+        projectileP4 / (is_nucleus(projectileId) ? get_nucleus_A(projectileId) : 1);
+    auto const targetP4NN =
+        targetP4 / (is_nucleus(targetId) ? get_nucleus_A(targetId) : 1);
+    auto const SNN = (projectileP4NN + targetP4NN).getNormSqr();
+    auto const sqrtSNN = sqrt(SNN);
+    HEPEnergyType const eProjectileLab = calculate_lab_energy(
+        SNN, get_mass(projectileId), get_mass(targetId) / get_nucleus_A(targetId));
+
+    CORSIKA_LOG_DEBUG("InteractionModel: ebeam lab: {} GeV", eProjectileLab / 1_GeV);
+    CORSIKA_LOG_DEBUG(
+        "InteractionModel: "
+        " doInteraction: E(GeV): {}"
+        " Ecm_NN(GeV): {}",
+        eProjectileLab / 1_GeV, sqrtSNN / 1_GeV);
+
+    if (!isValid(projectileId, targetId, sqrtSNN)) {
+      throw std::runtime_error("invalid target,projectile,energy combination.");
+    }
+
+    COMBoost const boost{projectileP4NN, targetP4NN};
+    auto const& rotCS = boost.getRotatedCS();
+
+    // variables to talk to Pythia
+    double const eCM_pythia = sqrtSNN / 1_GeV; // CoM energy hadron-Nucleon
+    double const idA_pythia = static_cast<double>(get_PDG(projectileId));
+    double const idB_pythia = static_cast<double>(get_PDG(targetId));
+    CORSIKA_LOG_DEBUG("re-configuring pythia idA={}, idB={}, ecm={} GeV", idA_pythia,
+                      idB_pythia, eCM_pythia);
+    // configure event
+    pythia_.setBeamIDs(idA_pythia, idB_pythia);
+    pythia_.setKinematics(eCM_pythia);
+
+    // generate event (one chance only!)
+    if (!pythia_.next()) {
+      throw std::runtime_error("Pythia::InteractionModel: interaction failed!");
+    }
+
+    // LCOV_EXCL_START, we don't validate pythia8 internals
+    if (print_listing_) {
+      // print final state
+      pythia_.event.list();
+    }
+    // LCOV_EXCL_STOP
+
+    MomentumVector Plab_final{boost.getOriginalCS()};
+    auto Elab_final = HEPEnergyType::zero();
+
+    for (Pythia8::Particle const& p8p : pythia_.event) {
+      // skip particles that have decayed and initial particles
+      if (!p8p.isFinal()) continue;
+      try {
+        auto const volatile id = static_cast<PDGCode>(p8p.id());
+        auto const pyId = convert_from_PDG(id);
+
+        // skip nuclear remnants
+        if (is_nucleus(pyId)) continue;
+
+        MomentumVector const pyPcom(
+            rotCS, {p8p.px() * 1_GeV, p8p.py() * 1_GeV, p8p.pz() * 1_GeV});
+        auto const pyP = boost.fromCoM(FourVector{p8p.e() * 1_GeV, pyPcom});
+
+        HEPEnergyType const mass = get_mass(pyId);
+        HEPEnergyType const Ekin =
+            sqrt(pyP.getSpaceLikeComponents().getSquaredNorm() + mass * mass) - mass;
+
+        // add to corsika stack
+        auto pnew = view.addSecondary(
+            std::make_tuple(pyId, Ekin, pyP.getSpaceLikeComponents().normalized()));
+
+        Plab_final += pnew.getMomentum();
+        Elab_final += pnew.getEnergy();
+      }
+      // irreproducible in tests, LCOV_EXCL_START
+      catch (std::out_of_range const& ex) {
+        CORSIKA_LOG_CRITICAL("Pythia ID {} unknown in C8", p8p.id());
+        throw ex;
+      }
+      // LCOV_EXCL_STOP
+    }
+
+    pythia_.event.clear();
+
+    CORSIKA_LOG_DEBUG(
+        "conservation (all GeV): "
+        "Elab_final= {}"
+        ", Plab_final= {}",
+        Elab_final / 1_GeV, (Plab_final / 1_GeV).getComponents());
+  }
+
+} // namespace corsika::pythia8
diff --git a/corsika/detail/modules/qgsjetII/InteractionModel.inl b/corsika/detail/modules/qgsjetII/InteractionModel.inl
index 7ce791a07..232d0d392 100644
--- a/corsika/detail/modules/qgsjetII/InteractionModel.inl
+++ b/corsika/detail/modules/qgsjetII/InteractionModel.inl
@@ -101,6 +101,14 @@ namespace corsika::qgsjetII {
     return sigProd * 1_mb;
   }
 
+  inline std::tuple<CrossSectionType, CrossSectionType>
+  InteractionModel::getCrossSectionInelEla(Code projCode, Code targetCode,
+                                           FourMomentum const& proj4mom,
+                                           FourMomentum const& target4mom) const {
+    return {getCrossSection(projCode, targetCode, proj4mom, target4mom),
+            CrossSectionType::zero()};
+  }
+
   template <typename TSecondaries>
   inline void InteractionModel::doInteraction(TSecondaries& view, Code const projectileId,
                                               Code const targetId,
diff --git a/corsika/detail/modules/sibyll/InteractionModel.inl b/corsika/detail/modules/sibyll/InteractionModel.inl
index a3d8f3f06..979a97f29 100644
--- a/corsika/detail/modules/sibyll/InteractionModel.inl
+++ b/corsika/detail/modules/sibyll/InteractionModel.inl
@@ -50,6 +50,19 @@ namespace corsika::sibyll {
                                                          target4mom);
   }
 
+  inline std::tuple<CrossSectionType, CrossSectionType>
+  InteractionModel::getCrossSectionInelEla(Code projCode, Code targetCode,
+                                           FourMomentum const& proj4mom,
+                                           FourMomentum const& target4mom) const {
+    if (is_nucleus(projCode))
+      return {getNuclearInteractionModel().getCrossSection(projCode, targetCode, proj4mom,
+                                                           target4mom),
+              CrossSectionType::zero()};
+    else
+      return getHadronInteractionModel().getCrossSectionInelEla(projCode, targetCode,
+                                                                proj4mom, target4mom);
+  }
+
   template <typename TSecondaries>
   inline void InteractionModel::doInteraction(TSecondaries& view, Code projCode,
                                               Code targetCode,
diff --git a/corsika/framework/utility/CrossSectionTable.hpp b/corsika/framework/utility/CrossSectionTable.hpp
new file mode 100644
index 000000000..c32494429
--- /dev/null
+++ b/corsika/framework/utility/CrossSectionTable.hpp
@@ -0,0 +1,89 @@
+/*
+ * (c) Copyright 2024 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * This software is distributed under the terms of the 3-clause BSD license.
+ * See file LICENSE for a full version of the license.
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <cmath>
+#include <stdexcept>
+#include <utility>
+#include <vector>
+
+#include <boost/functional/identity.hpp>
+#include <boost/iterator/transform_iterator.hpp>
+
+#include <corsika/framework/core/PhysicalUnits.hpp>
+
+namespace corsika {
+  namespace InterpolationTransforms {
+    using Identity = boost::identity;
+
+    struct Log {
+      template <typename T>
+      auto operator()(T val) const {
+        if constexpr (is_quantity_v<T>) {
+          return std::log(val.magnitude());
+        } else {
+          return std::log(val);
+        }
+      }
+    };
+  } // namespace InterpolationTransforms
+
+  template <typename Transform>
+  class CrossSectionTable {
+  public:
+    template <typename InputIt1, typename InputIt2>
+    CrossSectionTable(InputIt1 energiesFirst, InputIt1 energiesLast,
+                      InputIt2 crosssectionsFirst) {
+      static_assert(std::is_same_v<typename InputIt1::value_type, HEPEnergyType>);
+      static_assert(std::is_same_v<typename InputIt2::value_type, CrossSectionType>);
+
+      table_.reserve(std::distance(energiesFirst, energiesLast));
+
+      auto itE = boost::make_transform_iterator(std::move(energiesFirst), transform_);
+      decltype(itE) const itEEnd =
+          boost::make_transform_iterator(std::move(energiesLast), transform_);
+
+      for (auto itXS = std::move(crosssectionsFirst); itE != itEEnd; ++itE, ++itXS) {
+        table_.emplace_back(*itE, *itXS);
+      }
+
+      std::sort(table_.begin(), table_.end(), less_datapoint);
+    }
+
+    CrossSectionType interpolate(HEPEnergyType energy) const {
+      auto const transformed_val = transform_(energy);
+      auto const lb_it =
+          std::lower_bound(table_.cbegin(), table_.cend(), transformed_val, less_x);
+      if (lb_it == table_.cbegin()) {
+        throw std::runtime_error{"CrossSectionTable: value out of bounds (lower limit)"};
+      }
+      if (lb_it == table_.cend()) {
+        throw std::runtime_error{"CrossSectionTable: value out of bounds (upper limit)"};
+      }
+
+      auto const prev_it = std::prev(lb_it);
+      auto const lambda =
+          (transformed_val - prev_it->first) / (lb_it->first - prev_it->first);
+      return lambda * lb_it->second + (1 - lambda) * prev_it->second;
+    }
+
+  private:
+    using key_type = std::invoke_result_t<Transform, HEPEnergyType>;
+    using datapoint_type = std::pair<key_type, CrossSectionType>;
+    std::vector<datapoint_type> table_;
+    Transform transform_{};
+
+    static bool less_datapoint(datapoint_type const& a, datapoint_type const& b) {
+      return a.first < b.first;
+    }
+    static bool less_x(datapoint_type const& a, typename datapoint_type::first_type b) {
+      return a.first < b;
+    }
+  };
+} // namespace corsika
diff --git a/corsika/modules/Pythia8.hpp b/corsika/modules/Pythia8.hpp
index fa5b65181..6973eca70 100644
--- a/corsika/modules/Pythia8.hpp
+++ b/corsika/modules/Pythia8.hpp
@@ -9,3 +9,18 @@
 
 #include <corsika/modules/pythia8/Decay.hpp>
 #include <corsika/modules/pythia8/NeutrinoInteraction.hpp>
+#include <corsika/modules/pythia8/InteractionModel.hpp>
+
+namespace corsika::pythia8 {
+  /**
+   * pythia8::Interaction is the process for ProcessSequence.
+   *
+   * The pythia8::InteractionModel is wrapped as an InteractionProcess here in order
+   * to provide all the functions for ProcessSequence.
+   */
+  class Interaction : public InteractionModel, public InteractionProcess<Interaction> {
+  public:
+    Interaction(std::set<Code> const& stableList = {})
+        : InteractionModel{stableList} {};
+  };
+} // namespace corsika::pythia8
diff --git a/corsika/modules/pythia8/Interaction.hpp b/corsika/modules/pythia8/Interaction.hpp
deleted file mode 100644
index 1187c550a..000000000
--- a/corsika/modules/pythia8/Interaction.hpp
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
- *
- * This software is distributed under the terms of the 3-clause BSD license.
- * See file LICENSE for a full version of the license.
- */
-
-#pragma once
-
-#include <tuple>
-#include <boost/filesystem/path.hpp>
-
-#include <corsika/framework/utility/CorsikaData.hpp>
-#include <corsika/framework/core/ParticleProperties.hpp>
-#include <corsika/framework/core/PhysicalUnits.hpp>
-#include <corsika/framework/random/RNGManager.hpp>
-#include <corsika/framework/process/InteractionProcess.hpp>
-#include <corsika/modules/pythia8/Pythia8.hpp>
-
-namespace corsika::pythia8 {
-  class Interaction : public InteractionProcess<Interaction> {
-
-  public:
-    Interaction(
-        boost::filesystem::path const& mpiInitFile = corsika_data("Pythia/main184.mpi"),
-        bool const print_listing = false);
-    ~Interaction();
-
-    bool canInteract(Code const) const;
-
-    bool isValid(Code const projectileId, Code const targetId,
-                 HEPEnergyType const sqrtS) const;
-    /**
-     * Returns inelastic AND elastic cross sections.
-     *
-     * These cross sections must correspond to the process described in doInteraction
-     * AND elastic scattering (sigma_tot = sigma_inel + sigma_el). Allowed targets are:
-     * nuclei or single nucleons (p,n,hydrogen). This "InelEla" method is used since
-     * Sibyll must be useful inside the NuclearInteraction model, which requires that.
-     *
-     * @param projectile is the Code of the projectile
-     * @param target is the Code of the target
-     * @param sqrtSnn is the center-of-mass energy (per nucleon pair)
-     * @param Aprojectil is the mass number of the projectils, if it is a nucleus
-     * @param Atarget is the mass number of the target, if it is a nucleus
-     *
-     * @return a tuple of: inelastic cross section, elastic cross section
-     */
-    std::tuple<CrossSectionType, CrossSectionType> getCrossSectionInelEla(
-        Code const projectile, Code const target, FourMomentum const& projectileP4,
-        FourMomentum const& targetP4) const;
-
-    /**
-     * Returns inelastic (production) cross section.
-     *
-     * This cross section must correspond to the process described in doInteraction.
-     * Allowed targets are: nuclei or single nucleons (p,n,hydrogen).
-     *
-     * @param projectile is the Code of the projectile
-     * @param target is the Code of the target
-     * @param sqrtSnn is the center-of-mass energy (per nucleon pair)
-     * @param Aprojectil is the mass number of the projectils, if it is a nucleus
-     * @param Atarget is the mass number of the target, if it is a nucleus
-     *
-     * @return inelastic cross section
-     * elastic cross section
-     */
-    CrossSectionType getCrossSection(
-        Code const projectile, Code const target, FourMomentum const& projectileP4,
-        FourMomentum const& targetP4) const; // non-const for now
-
-    /**
-     * In this function PYTHIA is called to produce one event. The
-     * event is copied (and boosted) into the shower lab frame.
-     */
-    template <typename TView>
-    void doInteraction(TView& output, Code const projectileId, Code const targetId,
-                       FourMomentum const& projectileP4, FourMomentum const& targetP4);
-
-    /**
-     * return average number of sub-collisions in a nucleus, using the parameterizations
-     * of Sjöstrand and Utheim, EPJ C 82 (2022) 1, 21, arXiv:2108.03481 [hep-ph]
-     *
-     * @param targetId target (nucleus)
-     * @param sigTot projectile-nucleon (=proton) cross-secion
-     */
-    double getAverageSubcollisions(Code targetId, CrossSectionType sigTot) const;
-
-    static std::array constexpr validTargets_{Code::Oxygen, Code::Nitrogen,
-                                              Code::Argon,  Code::Hydrogen,
-                                              Code::Proton, Code::Neutron};
-
-  private:
-    default_prng_type& RNG_ = RNGManager<>::getInstance().getRandomStream("pythia");
-    //~ bool const internalDecays_ = true;
-    int count_ = 0;
-    bool const print_listing_ = false;
-    Pythia8::Pythia pythiaMain_;
-    Pythia8::Pythia mutable pythiaColl_; // Pythia's getSigma...() are not marked const...
-    double const probSD_ =
-        0.3; // Fraction of single diffractive events beyond first collision in nucleus.
-    HEPEnergyType const eMaxLab_ = 1e18_eV;
-    HEPEnergyType const eKinMinLab_ = 0.2_GeV;
-  };
-
-} // namespace corsika::pythia8
-
-#include <corsika/detail/modules/pythia8/Interaction.inl>
diff --git a/corsika/modules/pythia8/InteractionModel.hpp b/corsika/modules/pythia8/InteractionModel.hpp
new file mode 100644
index 000000000..290969ea1
--- /dev/null
+++ b/corsika/modules/pythia8/InteractionModel.hpp
@@ -0,0 +1,153 @@
+/*
+ * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * This software is distributed under the terms of the 3-clause BSD license.
+ * See file LICENSE for a full version of the license.
+ */
+
+#pragma once
+
+#include <tuple>
+#include <unordered_map>
+#include <boost/filesystem/path.hpp>
+
+#include <corsika/framework/utility/CorsikaData.hpp>
+#include <corsika/framework/core/ParticleProperties.hpp>
+#include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/framework/random/RNGManager.hpp>
+#include <corsika/framework/utility/CrossSectionTable.hpp>
+#include <corsika/framework/process/InteractionProcess.hpp>
+#include <corsika/modules/pythia8/Pythia8.hpp>
+
+namespace corsika::pythia8 {
+
+  /**
+   * @brief Defines the interface to the PYTHIA8 interaction model. Configured for
+   * hadron-nucleus interactions using Angantyr.
+   *
+   * This is a TModel argument for InteractionProcess<TModel>.
+   */
+
+  class InteractionModel {
+
+  public:
+    /**
+     * Constructs the interface for PYTHIA8
+     *
+     * @param stableParticles - set of particle ids to be treated as stable inside
+     * pythia. if empty then all particles are treated as stable (no decays!)
+     * @param dataPath path to the pythia tables
+     * @param printListing switch on/off the pythia printout
+     */
+    InteractionModel(std::set<Code> const& stableParticles = {},
+                     boost::filesystem::path const& dataPath = corsika_data("Pythia"),
+                     bool const printListing = false);
+    ~InteractionModel();
+
+    bool canInteract(Code const) const;
+
+    /**
+     * Check if configuration of projectile and target is valid.
+     *
+     * @param projectileId is the Code of the projectile
+     * @param targetId is the Code of the target
+     * @param sqrtS is the squared sum of the 4-momenta of projectile and target
+     */
+    bool isValid(Code const projectileId, Code const targetId,
+                 HEPEnergyType const sqrtS) const;
+
+    /**
+     * Returns inelastic AND elastic cross sections.
+     *
+     * These cross sections must correspond to the process described in doInteraction
+     * (sigma_tot = sigma_inel + sigma_el). Allowed targets are:
+     * nuclei or single nucleons (p,n,hydrogen).
+     *
+     * @param projectile is the Code of the projectile
+     * @param target is the Code of the target
+     * @param projectileP4 is the 4-momentum of the projectile
+     * @param targetP4 is the 4-momentum of the target
+     *
+     * @return a tuple of: inelastic cross section, elastic cross section
+     */
+    std::tuple<CrossSectionType, CrossSectionType> getCrossSectionInelEla(
+        Code const projectile, Code const target, FourMomentum const& projectileP4,
+        FourMomentum const& targetP4) const;
+
+    /**
+     * Returns inelastic (production) cross section.
+     *
+     * This cross section must correspond to the process described in doInteraction.
+     * Allowed targets are: nuclei or single nucleons (p,n,hydrogen).
+     *
+     * @param projectile is the Code of the projectile
+     * @param target is the Code of the target
+     * @param projectileP4 is the 4-momentum of the projectile
+     * @param targetP4 is the 4-momentum of the target
+     *
+     * @return inelastic cross section
+     * elastic cross section
+     */
+    CrossSectionType getCrossSection(Code const projectile, Code const target,
+                                     FourMomentum const& projectileP4,
+                                     FourMomentum const& targetP4) const;
+
+    /**
+     * In this function PYTHIA8 is called to produce one event. The
+     * event is copied (and boosted) into the reference frame defined by the input
+     * 4-momenta.
+     */
+    template <typename TView>
+    void doInteraction(TView& output, Code const projectileId, Code const targetId,
+                       FourMomentum const& projectileP4, FourMomentum const& targetP4);
+
+    using key_type = std::pair<corsika::Code, corsika::Code>;
+
+  private:
+    static auto constexpr GeV_mult = [](float x) { return x * 1_GeV; };
+    static auto constexpr millibarn_mult = [](float x) { return x * 1_mb; };
+
+    static CrossSectionTable<InterpolationTransforms::Log> loadPPTable(
+        boost::filesystem::path const& dataPath, char const* key);
+
+    default_prng_type& RNG_ = RNGManager<>::getInstance().getRandomStream("pythia");
+
+    /**
+     * All particles that we enable as projectiles. Code::Nucleus is not listed as it is
+     * handled separately. The list is unlikely to be exhaustive, but should cover the
+     * mainstream use-cases sufficiently well.
+     */
+
+    static std::array constexpr validProjectiles_ = {
+        Code::PiPlus,       Code::PiMinus,   Code::Pi0,           Code::Proton,
+        Code::AntiProton,   Code::Neutron,   Code::AntiNeutron,   Code::KPlus,
+        Code::KMinus,       Code::K0Long,    Code::K0Short,       Code::SigmaMinus,
+        Code::SigmaPlusBar, Code::SigmaPlus, Code::SigmaMinusBar, Code::Xi0,
+        Code::Xi0Bar};
+
+    static std::array constexpr validTargets_ = {
+        Code::Proton, Code::Carbon, Code::Nitrogen, Code::Oxygen, Code::Argon};
+
+    std::unordered_map<corsika::Code, corsika::Code> const xs_map_ = {
+        {Code::SigmaMinus, Code::SigmaPlus},
+        {Code::SigmaMinusBar, Code::SigmaPlus},
+        {Code::SigmaPlusBar, Code::SigmaPlus},
+        {Code::Xi0Bar, Code::Xi0}};
+
+    Pythia8::Pythia pythia_;
+
+    std::unordered_map<key_type, CrossSectionTable<InterpolationTransforms::Log>>
+        crossSectionTables_;
+
+    CrossSectionTable<InterpolationTransforms::Log> crossSectionPPElastic_,
+        crossSectionPPInelastic_;
+
+    bool const print_listing_ = false;
+    HEPEnergyType const eMaxLab_ =
+        1e21_eV; // Cross-section tables tabulated up to 10^21 eV
+    HEPEnergyType const eKinMinLab_ = 100_GeV;
+  };
+
+} // namespace corsika::pythia8
+
+#include <corsika/detail/modules/pythia8/InteractionModel.inl>
diff --git a/corsika/modules/qgsjetII/InteractionModel.hpp b/corsika/modules/qgsjetII/InteractionModel.hpp
index 543545388..7734831a0 100644
--- a/corsika/modules/qgsjetII/InteractionModel.hpp
+++ b/corsika/modules/qgsjetII/InteractionModel.hpp
@@ -54,6 +54,23 @@ namespace corsika::qgsjetII {
                                      FourMomentum const& projectileP4,
                                      FourMomentum const& targetP4) const;
 
+    /**
+     * Returns inelastic AND elastic cross sections.
+     *
+     * These cross sections must correspond to the process described in doInteraction
+     * AND elastic scattering (sigma_tot = sigma_inel + sigma_el).
+     *
+     * @param projectile is the Code of the projectile
+     * @param target is the Code of the target
+     * @param projectileP4: four-momentum of projectile
+     * @param targetP4: four-momentum of target
+     *
+     * @return a tuple of: inelastic cross section, elastic cross section
+     */
+    std::tuple<CrossSectionType, CrossSectionType> getCrossSectionInelEla(
+        Code const projectile, Code const target, FourMomentum const& projectileP4,
+        FourMomentum const& targetP4) const;
+
     /**
      * In this function QGSJETII is called to produce one event.
      *
diff --git a/corsika/modules/sibyll/InteractionModel.hpp b/corsika/modules/sibyll/InteractionModel.hpp
index 5deda8362..55fdd5208 100644
--- a/corsika/modules/sibyll/InteractionModel.hpp
+++ b/corsika/modules/sibyll/InteractionModel.hpp
@@ -29,6 +29,9 @@ namespace corsika::sibyll {
     CrossSectionType getCrossSection(Code, Code, FourMomentum const&,
                                      FourMomentum const&) const;
 
+    std::tuple<CrossSectionType, CrossSectionType> getCrossSectionInelEla(
+        Code, Code, FourMomentum const&, FourMomentum const&) const;
+
     template <typename TSecondaries>
     void doInteraction(TSecondaries&, Code, Code, FourMomentum const&,
                        FourMomentum const&);
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index ae67a0d5b..228c9b0de 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -81,6 +81,14 @@ CORSIKA_REGISTER_EXAMPLE (static_sequence)
 ## physics_examples
 ###################
 
+add_executable (cross_section physics_examples/cross_section.cpp)
+target_link_libraries (cross_section PRIVATE CORSIKA8::CORSIKA8)
+CORSIKA_REGISTER_EXAMPLE (cross_section RUN_OPTIONS sibyll)
+
+add_executable (had_interactions physics_examples/had_interactions.cpp)
+target_link_libraries (had_interactions PRIVATE CORSIKA8::CORSIKA8)
+CORSIKA_REGISTER_EXAMPLE (had_interactions RUN_OPTIONS sibyll)
+
 add_executable (stopping_power physics_examples/stopping_power.cpp)
 target_link_libraries (stopping_power PRIVATE CORSIKA8::CORSIKA8)
 CORSIKA_REGISTER_EXAMPLE (stopping_power)
diff --git a/examples/physics_examples/cross_section.cpp b/examples/physics_examples/cross_section.cpp
new file mode 100644
index 000000000..36645851e
--- /dev/null
+++ b/examples/physics_examples/cross_section.cpp
@@ -0,0 +1,163 @@
+/*
+ * (c) Copyright 2019 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * This software is distributed under the terms of the 3-clause BSD license.
+ * See file LICENSE for a full version of the license.
+ */
+
+#include <corsika/modules/Epos.hpp>
+#include <corsika/modules/Sibyll.hpp>
+#include <corsika/modules/QGSJetII.hpp>
+#include <corsika/modules/Pythia8.hpp>
+
+#include <corsika/framework/core/ParticleProperties.hpp>
+#include <corsika/framework/core/PhysicalUnits.hpp>
+
+#include <corsika/framework/geometry/RootCoordinateSystem.hpp>
+#include <corsika/framework/geometry/Vector.hpp>
+
+#include <corsika/framework/random/RNGManager.hpp>
+
+#include <corsika/setup/SetupC7trackedParticles.hpp>
+#include <corsika/modules/Random.hpp>
+
+#include <fstream>
+#include <string>
+#include <tuple>
+
+using namespace corsika;
+
+/**
+ * Calculates the inelastic p-p cross section and the production cross section for
+ * p-Oxygen for a given interaction model using the C8 interface getCrossSection(ID1, ID2,
+ * P41, P42) where ID? and P4? are the particle Id and 4-momenta of projectile and target
+ * respectively. The input to getCrossSection can be ANY frame of reference. Internally
+ * the boost to the CM is calculated and the correct cross section is returned.
+ * calculate_cross_sections allows to switch between the CM and lab configurations.
+ *
+ * output is a table in ACSCII file cross-sections-<model-name>.txt
+ *
+ * Energy ranges from 100GeV to 10^12 GeV (10^11 eV to 10^21 eV) in the lab. frame
+ * table format is: "# i, E_lab (GeV), E_cm per nucleon (GeV), cross section p-p (mb),
+ * production "
+ *
+ * @param model the interaction model to use
+ * @param model_name string with model name
+ *
+ * @return a tuple of: inelastic cross section, elastic cross section
+ */
+
+template <typename TModel>
+void calculate_cross_sections(TModel& model, std::string const& model_name) {
+
+  std::stringstream fname;
+  fname << "cross-sections-" << model_name << ".txt";
+  std::ofstream out(fname.str());
+  out << "# cross section table for " << model_name << "\n";
+  out << "# i, P_lab (GeV), E_cm per nucleon (GeV), cross section p-p (mb), cross "
+         "section pi-p (mb), production "
+         "cross section p-O16 (mb), production cross section p-N14 (mb), production "
+         "cross section p-Ar40 (mb), cross section pi-O16 (mb), production cross "
+         "section, pi-N14 (mb), production, cross section pi-Ar40 (mb), cross section "
+         "He-O16 (mb), production cross section, He-N14 (mb), production, cross section "
+         "He-Ar40 (mb)\n";
+
+  CoordinateSystemPtr const& cs = get_root_CoordinateSystem();
+
+  float const amin = 2;
+  float const amax = 11;
+  int const nebins = 15;
+  float const da = (amax - amin) / (float)nebins;
+
+  for (int i = 0; i < nebins; ++i) {
+
+    corsika::units::si::HEPMomentumType const setP = pow(10, da * i + amin) * 1_GeV;
+    CrossSectionType xs_prod_hNuc[3][3] = {};
+    CrossSectionType xs_prod_hp[3] = {};
+    HEPEnergyType setE, comEnn;
+    int l = -1;
+    for (auto projId : {Code::Proton, Code::PiPlus, Code::Helium}) {
+      ++l;
+      setE = calculate_total_energy(setP, get_mass(projId));
+      comEnn = corsika::calculate_com_energy(setE, get_mass(projId),
+                                             corsika::constants::nucleonMass);
+
+      // four momenta in lab frame
+      corsika::FourMomentum p4Proj(setE, MomentumVector(cs, {0_eV, 0_eV, setP}));
+      corsika::FourMomentum p4protonTarg(Proton::mass,
+                                         MomentumVector(cs, {0_eV, 0_eV, 0_eV}));
+
+      // had-p
+      auto const [xs_prod, xs_ela] =
+          model->getCrossSectionInelEla(projId, Code::Proton, p4Proj, p4protonTarg);
+      xs_prod_hp[l] = xs_prod;
+
+      int k = -1;
+      for (auto targNuc : {Code::Oxygen, Code::Nitrogen, Code::Argon}) {
+        ++k;
+        FourMomentum const p4nucTarg = corsika::FourMomentum(
+            get_mass(targNuc), MomentumVector(cs, {0_eV, 0_eV, 0_eV}));
+        xs_prod_hNuc[l][k] = model->getCrossSection(projId, targNuc, p4Proj, p4nucTarg);
+      }
+    }
+    CORSIKA_LOG_INFO(
+        "Elab={:15.2f} GeV,Ecom={:15.2f} GeV, sig-p-p={:6.2f} mb, sig-pi-p={:6.2f} mb, "
+        "p-O={:6.2f} mb, p-N={:6.2f} mb, p-Ar={:6.2f} mb "
+        "pi-O={:6.2f} mb, pi-N={:6.2f} mb, pi-Ar={:6.2f} mb "
+        "He-O={:6.2f} mb, He-N={:6.2f} mb, He-Ar={:6.2f} mb",
+        setP / 1_GeV, comEnn / 1_GeV, xs_prod_hp[0] / 1_mb, xs_prod_hp[1] / 1_mb,
+        xs_prod_hNuc[0][0] / 1_mb, xs_prod_hNuc[0][1] / 1_mb, xs_prod_hNuc[0][2] / 1_mb,
+        xs_prod_hNuc[1][0] / 1_mb, xs_prod_hNuc[1][1] / 1_mb, xs_prod_hNuc[1][2] / 1_mb,
+        xs_prod_hNuc[2][0] / 1_mb, xs_prod_hNuc[2][1] / 1_mb, xs_prod_hNuc[2][2] / 1_mb);
+
+    out << i << " " << setP / 1_GeV << " " << comEnn / 1_GeV << " "
+        << xs_prod_hp[0] / 1_mb << " " << xs_prod_hp[1] / 1_mb << " "
+        << xs_prod_hNuc[0][0] / 1_mb << " " << xs_prod_hNuc[0][1] / 1_mb << " "
+        << xs_prod_hNuc[0][2] / 1_mb << " " << xs_prod_hNuc[1][0] / 1_mb << " "
+        << xs_prod_hNuc[1][1] / 1_mb << " " << xs_prod_hNuc[1][2] / 1_mb << " "
+        << " " << xs_prod_hNuc[2][0] / 1_mb << " " << xs_prod_hNuc[2][1] / 1_mb << " "
+        << xs_prod_hNuc[2][2] / 1_mb << "\n";
+  }
+  out.close();
+  std::cout << "wrote cross section table for " << model_name
+            << " in file: " << fname.str() << std::endl;
+}
+
+int main(int argc, char** argv) {
+
+  logging::set_level(logging::level::info);
+
+  if (argc != 2) {
+    std::cout << "usage: check <interaction model> \n valid models are: sibyll, "
+                 "epos, qgsjet, pythia8"
+              << std::endl;
+    return 1;
+  }
+  std::string int_model_name = std::string(argv[1]);
+
+  if (int_model_name == "sibyll") {
+    RNGManager<>::getInstance().registerRandomStream("sibyll");
+    auto model = std::make_shared<corsika::sibyll::InteractionModel>(
+        std::set<Code>{Code::Proton, Code::Oxygen, Code::Nitrogen},
+        corsika::setup::C7trackedParticles);
+    calculate_cross_sections(model, int_model_name);
+  } else if (int_model_name == "pythia8") {
+    RNGManager<>::getInstance().registerRandomStream("pythia");
+    auto model = std::make_shared<corsika::pythia8::InteractionModel>();
+    calculate_cross_sections(model, int_model_name);
+
+  } else if (int_model_name == "epos") {
+    RNGManager<>::getInstance().registerRandomStream("epos");
+    auto model = std::make_shared<corsika::epos::InteractionModel>(
+        corsika::setup::C7trackedParticles);
+    calculate_cross_sections(model, int_model_name);
+  } else if (int_model_name == "qgsjet") {
+    RNGManager<>::getInstance().registerRandomStream("qgsjet");
+    auto model = std::make_shared<corsika::qgsjetII::InteractionModel>();
+    calculate_cross_sections(model, int_model_name);
+  } else {
+    std::cout << "interaction model should be: sibyll, epos, qgsjet or pythia"
+              << std::endl;
+    return 1;
+  }
+}
diff --git a/examples/physics_examples/had_interactions.cpp b/examples/physics_examples/had_interactions.cpp
new file mode 100644
index 000000000..5e61c91ad
--- /dev/null
+++ b/examples/physics_examples/had_interactions.cpp
@@ -0,0 +1,267 @@
+/*
+ * (c) Copyright 2019 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * This software is distributed under the terms of the 3-clause BSD license.
+ * See file LICENSE for a full version of the license.
+ */
+
+#include <corsika/modules/Epos.hpp>
+#include <corsika/modules/QGSJetII.hpp>
+#include <corsika/modules/Sibyll.hpp>
+#include <corsika/modules/Pythia8.hpp>
+
+#include <corsika/framework/core/ParticleProperties.hpp>
+#include <corsika/framework/core/PhysicalUnits.hpp>
+
+#include <corsika/framework/geometry/Point.hpp>
+#include <corsika/framework/geometry/RootCoordinateSystem.hpp>
+#include <corsika/framework/geometry/Vector.hpp>
+
+#include <corsika/framework/random/RNGManager.hpp>
+
+#include <corsika/framework/core/EnergyMomentumOperations.hpp>
+
+#include <corsika/media/Environment.hpp>
+#include <corsika/media/HomogeneousMedium.hpp>
+#include <corsika/media/MediumPropertyModel.hpp>
+#include <corsika/media/NuclearComposition.hpp>
+#include <corsika/media/ShowerAxis.hpp>
+#include <corsika/media/UniformMagneticField.hpp>
+
+#include <corsika/setup/SetupStack.hpp>
+#include <corsika/setup/SetupC7trackedParticles.hpp>
+
+#include <corsika/modules/Random.hpp>
+
+#include <fstream>
+#include <iostream>
+#include <tuple>
+
+using namespace corsika;
+
+using EnvironmentInterface = IMediumPropertyModel<IMagneticFieldModel<IMediumModel>>;
+using EnvType = Environment<EnvironmentInterface>;
+
+template <typename TModel>
+void create_events(TModel& model, std::string const& model_name, Code const projectileId,
+                   FourMomentum projectileP4, Code const targetId, FourMomentum targetP4,
+                   int const nEvents) {
+
+  logging::set_level(logging::level::info);
+
+  // calculate Ecm per nucleon (sqrtSNN). Used only for output!
+  auto const projectileP4NN =
+      projectileP4 / (is_nucleus(projectileId) ? get_nucleus_A(projectileId) : 1);
+  auto const targetP4NN = targetP4 / (is_nucleus(targetId) ? get_nucleus_A(targetId) : 1);
+  auto const SNN = (projectileP4NN + targetP4NN).getNormSqr();
+  HEPEnergyType const sqrtSNN = sqrt(SNN);
+  auto const sqrtS = (projectileP4 + targetP4).getNorm();
+  CORSIKA_LOG_INFO("{} + {} interactions at sqrtSNN= {} GeV, sqrtS = {} GeV",
+                   projectileId, targetId, sqrtSNN / 1_GeV, sqrtS / 1_GeV);
+
+  // target material and environment (not used just there to create the C8 stack)
+  EnvType env;
+  auto& universe = *(env.getUniverse());
+  CoordinateSystemPtr const& rootCS = env.getCoordinateSystem();
+  auto world = EnvType::createNode<Sphere>(Point{rootCS, 0_m, 0_m, 0_m}, 150_km);
+  using MyHomogeneousModel =
+      MediumPropertyModel<UniformMagneticField<HomogeneousMedium<EnvironmentInterface>>>;
+  auto const props = world->setModelProperties<MyHomogeneousModel>(
+      Medium::AirDry1Atm, MagneticFieldVector(rootCS, 0_T, 0_T, 0_T),
+      1_kg / (1_m * 1_m * 1_m), NuclearComposition({targetId}, {1.}));
+  world->setModelProperties(props);
+  universe.addChild(std::move(world));
+
+  // projectile (dummy)
+  auto const plab3 = projectileP4.getSpaceLikeComponents();
+  auto p0 = Point(rootCS, 0_m, 0_m, 0_m);
+  setup::Stack<EnvType> stack;
+  stack.addParticle(std::make_tuple(
+      projectileId, calculate_kinetic_energy(plab3.getNorm(), get_mass(projectileId)),
+      plab3.normalized(), p0, 0_ns));
+  auto particle = stack.first();
+  particle.setNode(universe.getContainingNode(p0));
+
+  std::stringstream pname;
+  pname << "particles-" << model_name << "-" << projectileId << "-" << targetId << "-"
+        << sqrtSNN / 1_GeV << "GeV"
+        << ".txt";
+
+  unsigned int count_central_charged = 0;
+  HEPMomentumType total_pt = 0_GeV;
+
+  std::ofstream pout(pname.str());
+  // add header
+  pout << "# particle production in " << model_name << " for " << projectileId << " + "
+       << targetId << " interactions."
+       << "\n";
+  pout << "# lab. momentum of projectile: "
+       << projectileP4.getSpaceLikeComponents().getNorm() / 1_GeV << " (GeV)"
+       << "\n";
+  pout << "# CM energy per nucleon: " << sqrtSNN / 1_GeV << " (GeV)"
+       << "\n";
+  pout << "# i, j, particle name, PDG id, p.rap, rap., energy (GeV), px, py, pz (GeV), "
+          "pt (GeV), charge"
+       << "\n";
+  for (int i = 0; i < nEvents; ++i) {
+
+    // empty stack from previous interaction
+    stack.clear();
+    // add particle to get StackView to write secondaries to C8 stack
+    stack.addParticle(std::make_tuple(
+        projectileId, calculate_kinetic_energy(plab3.getNorm(), get_mass(projectileId)),
+        plab3.normalized(), p0, 0_ns)); // irrelevant
+    auto particle = stack.first();
+    particle.setNode(universe.getContainingNode(p0));
+    setup::Stack<EnvType>::stack_view_type output(particle);
+
+    // generate secondaries and fill output
+    model->doInteraction(output, projectileId, targetId, projectileP4, targetP4);
+
+    // loop through secondaries and calculate higher level variables (rapidity, pt, ...),
+    // write to file
+    int j = -1;
+    HEPMomentumType tpt = 0_GeV;
+    for (auto& secondary : output) {
+      ++j;
+      // pseudorapidity, rapidity
+      auto const p = secondary.getMomentum();
+      auto const pz = p.getZ(rootCS);
+      auto const px = p.getX(rootCS);
+      auto const py = p.getY(rootCS);
+      auto const pabs = p.getNorm();
+      auto const pt = sqrt(p.getSquaredNorm() - square(pz));
+      auto const en = secondary.getEnergy();
+      auto const mt = sqrt(square(pt) + square(get_mass(secondary.getPID())));
+      double prap, rap;
+      if (pz / 1_GeV <= 0) {
+        rap = log(mt / (en - pz));
+        prap = log((pt + 1.0_eV) / (pabs - pz));
+      } else {
+        rap = log((en + pz) / mt);
+        prap = log((pabs + pz) / (pt + 1.0_eV));
+      }
+      pout << i << " " << j << " " << secondary.getPID() << " "
+           << static_cast<int>(get_PDG(secondary.getPID())) << " " << prap << " " << rap
+           << " " << en / 1_GeV << " " << px / 1_GeV << " " << py / 1_GeV << " "
+           << pz / 1_GeV << " " << pt / 1_GeV << " "
+           << get_charge_number(secondary.getPID()) << "\n";
+      // calculate plateau (CMS experiment)
+      if (abs(prap) < 2.5 && get_charge_number(secondary.getPID()) != 0) {
+        count_central_charged++;
+      }
+      tpt += pt;
+    }
+    total_pt += tpt / float(j);
+  }
+  std::cout << "average plateau height: " << count_central_charged / float(nEvents)
+            << std::endl;
+  std::cout << "average pt (GeV): " << total_pt / float(nEvents) / 1_GeV << std::endl;
+  std::cout << "wrote hadronic events for " << model_name << " in file: " << pname.str()
+            << std::endl;
+};
+
+int main(int argc, char** argv) {
+
+  std::string int_model, projectile, target;
+  HEPMomentumType P0;
+  int Nevents = 10;
+  if (argc < 2 || argc > 7) {
+    std::cout << "usage: check <interaction model> \n valid models are: sibyll, "
+                 "epos, qgsjet, pythia8. Other options are (in that order): <projectile> "
+                 "<target> <projectile momentum (lab in GeV)> <no. of events>. Defaults "
+                 "are: proton, proton, 100, 10"
+              << std::endl;
+    return 1;
+  } else if (argc == 2) {
+    int_model = std::string(argv[1]);
+    projectile = "proton";
+    target = "proton";
+    P0 = 100_GeV;
+  } else if (argc == 3) {
+    int_model = std::string(argv[1]);
+    projectile = std::string(argv[2]);
+    target = "proton";
+    P0 = 100_GeV;
+  } else if (argc == 4) {
+    int_model = std::string(argv[1]);
+    projectile = std::string(argv[2]);
+    target = std::string(argv[3]);
+    P0 = 100_GeV;
+  } else if (argc == 5) {
+    int_model = std::string(argv[1]);
+    projectile = std::string(argv[2]);
+    target = std::string(argv[3]);
+    P0 = std::stoi(std::string(argv[4])) * 1_GeV;
+  } else if (argc == 6) {
+    int_model = std::string(argv[1]);
+    projectile = std::string(argv[2]);
+    target = std::string(argv[3]);
+    P0 = std::stoi(std::string(argv[4])) * 1_GeV;
+    Nevents = std::stoi(std::string(argv[5]));
+  }
+
+  Code projectileId;
+  if (projectile == "proton")
+    projectileId = Code::Proton;
+  else if (projectile == "antiproton")
+    projectileId = Code::AntiProton;
+  else if (projectile == "pion")
+    projectileId = Code::PiPlus;
+  else if (projectile == "oxygen")
+    projectileId = Code::Oxygen;
+  else {
+    std::cout << "unknown projectile. pick proton, antiproton, pion or oxygen"
+              << std::endl;
+    return 0;
+  }
+  Code targetId;
+  if (target == "proton")
+    targetId = Code::Proton;
+  else if (target == "nitrogen")
+    targetId = Code::Nitrogen;
+  else {
+    std::cout << "unknown target. pick proton or nitrogen" << std::endl;
+    return 0;
+  }
+
+  auto const rootCS = get_root_CoordinateSystem();
+  FourMomentum const P4proj{
+      calculate_total_energy(get_mass(projectileId), P0),
+      {rootCS, {HEPMomentumType::zero(), HEPMomentumType::zero(), P0}}};
+
+  FourMomentum const P4targ{
+      calculate_total_energy(get_mass(targetId), HEPMomentumType::zero()),
+      {rootCS,
+       {HEPMomentumType::zero(), HEPMomentumType::zero(), HEPMomentumType::zero()}}};
+
+  /*
+   the interface to hadronic models in C8 has two layers. The class InteractionModel
+   (HadronInteractionModel in the case of sibyll) implements the interface to the
+   interaction model. It provides the basic interface functions getCrossSection and
+   doInteraction. The class Interaction extends the interface in InteractionModel to an
+   element C8 process sequence. Here we use only the interface class.
+  */
+  if (int_model == "sibyll") {
+    RNGManager<>::getInstance().registerRandomStream("sibyll");
+    // this is the hadron model in sibyll. can only do hadron-nucleus interactions
+    auto model = std::make_shared<corsika::sibyll::HadronInteractionModel>(
+        corsika::setup::C7trackedParticles);
+    create_events(model, int_model, projectileId, P4proj, targetId, P4targ, Nevents);
+  } else if (int_model == "epos") {
+    RNGManager<>::getInstance().registerRandomStream("epos");
+    auto model = std::make_shared<corsika::epos::InteractionModel>(
+        corsika::setup::C7trackedParticles);
+    create_events(model, int_model, projectileId, P4proj, targetId, P4targ, Nevents);
+  } else if (int_model == "pythia8") {
+    RNGManager<>::getInstance().registerRandomStream("pythia");
+    auto model = std::make_shared<corsika::pythia8::InteractionModel>(
+        corsika::setup::C7trackedParticles);
+
+    create_events(model, int_model, projectileId, P4proj, targetId, P4targ, Nevents);
+  } else {
+    RNGManager<>::getInstance().registerRandomStream("qgsjet");
+    auto model = std::make_shared<corsika::qgsjetII::InteractionModel>();
+    create_events(model, int_model, projectileId, P4proj, targetId, P4targ, Nevents);
+  }
+}
diff --git a/modules/data b/modules/data
index d8ac240ed..73f5a85a0 160000
--- a/modules/data
+++ b/modules/data
@@ -1 +1 @@
-Subproject commit d8ac240ed196fd9f9e697326b15b64ed164c47a3
+Subproject commit 73f5a85a068c84ca67437a8014511df0b43b8365
diff --git a/modules/pythia8/CMakeLists.txt b/modules/pythia8/CMakeLists.txt
index d07add39c..5863b9891 100644
--- a/modules/pythia8/CMakeLists.txt
+++ b/modules/pythia8/CMakeLists.txt
@@ -51,14 +51,14 @@ if ("x_${USE_Pythia8_C8}" STREQUAL "x_SYSTEM")
 else ()
 
   set (_C8_Pythia8_Download_Dir "https://pythia.org/download/pythia83")
-  set (_C8_Pythia8_VERSION "8312")
+  set (_C8_Pythia8_VERSION "8313")
   message (STATUS "Building modules/pythia8 using pythia${_C8_Pythia8_VERSION}.tar.bz2")
   message (STATUS "This will take a bit.....")
   # this is not a full Pythia8 install, it is a bit simplified, e.g. no pythia8-config, no examples
   # and also, it is fitted into the normal CORSIKA8 install "include/corsika_modules/Pythia8, lib/corsika"
   ExternalProject_Add (pythia8
     URL ${_C8_Pythia8_Download_Dir}/pythia${_C8_Pythia8_VERSION}.tar.bz2
-    URL_MD5 0acde09714b5383ac807edb10f161dd9
+    URL_MD5 2bfcb4cefefca17605fab253016e6c6e
     SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/pythia8/source"
     INSTALL_DIR "${CMAKE_CURRENT_BINARY_DIR}/pythia8/install"
     CONFIGURE_COMMAND ./configure --prefix=${CMAKE_CURRENT_BINARY_DIR}/pythia8/install
diff --git a/tests/modules/testPythia8.cpp b/tests/modules/testPythia8.cpp
index dca606e5c..8b7edef7b 100644
--- a/tests/modules/testPythia8.cpp
+++ b/tests/modules/testPythia8.cpp
@@ -198,4 +198,6 @@ TEST_CASE("Pythia8Interface", "modules") {
                                       {Proton::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}) ==
             CrossSectionType::zero());
   }
-}
+
+#include <tests/modules/testPythia8Interaction.inl>
+}
\ No newline at end of file
diff --git a/tests/modules/testPythia8Interaction.inl b/tests/modules/testPythia8Interaction.inl
index 38d74c6dd..20860fadc 100644
--- a/tests/modules/testPythia8Interaction.inl
+++ b/tests/modules/testPythia8Interaction.inl
@@ -12,14 +12,23 @@
   when this is merged the interaction tests can be activated again by including this file
   in testPytha8.cpp eg #include "tests/modules/testPythia8Interaction.inl"
 */
-SECTION("pythia interaction") {
+#include "corsika/framework/core/EnergyMomentumOperations.hpp"
+#include "corsika/framework/core/Logging.hpp"
+#include "corsika/framework/core/PhysicalUnits.hpp"
 
-  // this will be a p-p collision at sqrts=3.5TeV -> no problem for pythia
-  auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
-      Code::Proton, 7_TeV, (DummyEnvironment::BaseNodeType* const)nodePtr, *csPtr);
-  auto& view = *secViewPtr;
+logging::set_level(logging::level::debug);
 
-  corsika::pythia8::Interaction collision;
+// this will be a p-p collision at sqrts=3.5TeV -> no problem for pythia
+auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
+    Code::Proton, 7_TeV, (DummyEnvironment::BaseNodeType* const)nodePtr, *csPtr);
+auto& view = *secViewPtr;
+
+std::set<Code> const tracked_hadrons = {Code::Proton, Code::Neutron, Code::Pi0,
+                                        Code::PiPlus};
+
+corsika::pythia8::InteractionModel collision(tracked_hadrons);
+
+SECTION("pythia interaction configurations") {
 
   REQUIRE(collision.canInteract(Code::Proton));
   REQUIRE(collision.canInteract(Code::AntiProton));
@@ -28,33 +37,42 @@ SECTION("pythia interaction") {
   REQUIRE(collision.canInteract(Code::PiMinus));
   REQUIRE(collision.canInteract(Code::PiPlus));
   REQUIRE_FALSE(collision.canInteract(Code::Electron));
+  REQUIRE_FALSE(collision.canInteract(Code::MuPlus));
+}
 
-  // pi+p
-  REQUIRE(collision.getCrossSection(
-              Code::PiPlus, Code::Proton,
-              {sqrt(static_pow<2>(PiPlus::mass) + static_pow<2>(100_GeV)),
-               {rootCS, {0_eV, 0_eV, 100_GeV}}},
-              {Proton::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}) > 0_mb);
+corsika::units::si::HEPMomentumType P0 = 10_TeV;
 
-  // pi+H
-  REQUIRE(collision.getCrossSection(
-              Code::PiPlus, Code::Hydrogen,
-              {sqrt(static_pow<2>(PiPlus::mass) + static_pow<2>(100_GeV)),
-               {rootCS, {0_eV, 0_eV, 100_GeV}}},
-              {Hydrogen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}) > 0_mb);
+SECTION("pythia interaction") {
+  // test some combinations of valid target and projectile particles
+  // so far only hadron-hadron and hadron-Nucleus is allowed
+  Code const target = GENERATE(Code::Nitrogen, Code::Oxygen, Code::Argon);
+  Code const projectile = GENERATE(Code::Proton, Code::PiPlus, Code::KPlus);
+
+  CORSIKA_LOG_INFO("testing: {} - {}", projectile, target);
+  REQUIRE(
+      collision.getCrossSection(
+          projectile, target,
+          {calculate_total_energy(P0, get_mass(projectile)), {rootCS, {0_eV, 0_eV, P0}}},
+          {get_mass(target), {rootCS, {0_eV, 0_eV, 0_eV}}}) > 0_mb);
+
+  collision.doInteraction(view, projectile, target,
+                          {corsika::calculate_total_energy(P0, get_mass(projectile)),
+                           {rootCS, {0_eV, 0_eV, P0}}},
+                          {get_mass(target), {rootCS, {0_eV, 0_eV, 0_eV}}});
+  REQUIRE(view.getSize() >= 2);
+}
 
-  // K+{p,n,N,O,Ar}
-  Code const target =
-      GENERATE(Code::Proton, Code::Neutron, Code::Nitrogen, Code::Oxygen, Code::Argon);
-  REQUIRE(collision.getCrossSection(
-              Code::KPlus, target,
-              {sqrt(static_pow<2>(KPlus::mass) + static_pow<2>(100_GeV)),
-               {rootCS, {0_eV, 0_eV, 100_GeV}}},
-              {get_mass(target), {rootCS, {0_eV, 0_eV, 0_eV}}}) > 0_mb);
+SECTION("pythia interaction: previous angantyr fail, now fixed") {
+  // when initializing pythia with angantyr, kaon-proton or pion-proton interactions do
+  // not seem to work
+  Code const target = Code::Proton;
+  Code const projectile = GENERATE(Code::KPlus, Code::PiPlus);
 
-  collision.doInteraction(view, Code::Proton, target,
-                          {sqrt(static_pow<2>(Proton::mass) + static_pow<2>(100_GeV)),
-                           {rootCS, {0_eV, 0_eV, 100_GeV}}},
+  CORSIKA_LOG_INFO("testing: {}-{}", projectile, target);
+
+  collision.doInteraction(view, projectile, target,
+                          {corsika::calculate_total_energy(P0, get_mass(projectile)),
+                           {rootCS, {0_eV, 0_eV, P0}}},
                           {get_mass(target), {rootCS, {0_eV, 0_eV, 0_eV}}});
   REQUIRE(view.getSize() >= 2);
 }
@@ -123,26 +141,28 @@ SECTION("pythia wrong projectile") {
       Code::Iron, 1_GeV, (DummyEnvironment::BaseNodeType* const)nodePtr, *csPtr);
   { [[maybe_unused]] auto const& dummy_StackPtr = stackPtr; }
 
+  HEPMomentumType const P0 = 100_GeV;
+
   corsika::pythia8::Interaction collision;
   REQUIRE(collision.getCrossSectionInelEla(
               Code::Electron, Code::Electron,
-              {sqrt(static_pow<2>(Electron::mass) + static_pow<2>(100_GeV)),
-               {rootCS, {0_eV, 0_eV, 100_GeV}}},
+              {sqrt(static_pow<2>(Electron::mass) + static_pow<2>(P0)),
+               {rootCS, {0_eV, 0_eV, P0}}},
               {Proton::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}) == std::tuple{0_mb, 0_mb});
 
-  REQUIRE_THROWS(
-      collision.doInteraction(*secViewPtr, Code::Helium, Code::Nitrogen,
-                              {sqrt(static_pow<2>(Helium::mass) + static_pow<2>(100_GeV)),
-                               {rootCS, {0_eV, 0_eV, 100_GeV}}},
-                              {Nitrogen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}));
+  REQUIRE_THROWS(collision.doInteraction(
+      *secViewPtr, Code::Helium, Code::Nitrogen,
+      {sqrt(static_pow<2>(Helium::mass) + static_pow<2>(P0)), {rootCS, {0_eV, 0_eV, P0}}},
+      {Nitrogen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}));
 
   // gamma+p not possible
   REQUIRE(collision.getCrossSection(
-              Code::Photon, Code::Proton, {100_GeV, {rootCS, {0_eV, 0_eV, 100_GeV}}},
+              Code::H0, Code::Proton,
+              {calculate_total_energy(P0, H0::mass), {rootCS, {0_eV, 0_eV, P0}}},
               {Proton::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}) == CrossSectionType::zero());
 
   REQUIRE(collision.getCrossSectionInelEla(
-              Code::Photon, Code::Proton, {100_GeV, {rootCS, {0_eV, 0_eV, 100_GeV}}},
+              Code::Photon, Code::Proton, {P0, {rootCS, {0_eV, 0_eV, P0}}},
               {Proton::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}) ==
           std::make_tuple(CrossSectionType::zero(), CrossSectionType::zero()));
 }
\ No newline at end of file
-- 
GitLab