diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 144446321560e92191f4057aeedcaacf57b538c9..b632dda7765f723ffd79fc7dd1e14807fe4f57b5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -248,7 +248,7 @@ build_test_example-clang-8:
     - cmake --build . -- VERBOSE=1 -j2
     - cmake --build . --target run_examples -- -j2 
   rules:
-    - if: '$CI_MERGE_REQUEST_LABELS =~ /Ready for code review/' # run on merge requests, if label 'Ready for code review' is set
+    - if: '$CI_MERGE_REQUEST_LABELS =~ /Ready for Code Review/' # run on merge requests, if label 'Ready for Code Review' is set
     - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
       when: manual
       allow_failure: true
@@ -307,7 +307,7 @@ coverage:
     - tar czf coverage-report.tar.gz coverage-report
   coverage: '/^.*lines\.+:\s(.*\%)\s/'
   rules:
-    - if: '$CI_MERGE_REQUEST_LABELS =~ /Ready for code review/' # run on merge requests, if label 'Ready for code review' is set
+    - if: '$CI_MERGE_REQUEST_LABELS =~ /Ready for Code Review/' # run on merge requests, if label 'Ready for Code Review' is set
     - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
     - if: $CI_MERGE_REQUEST_ID
       when: manual
@@ -343,7 +343,7 @@ sanity:
     - cmake .. -DWITH_CORSIKA_SANITIZERS_ENABLED=ON -DCMAKE_BUILD_TYPE=Debug -DUSE_Pythia8_C8=C8
     - cmake --build . -- -j4
   rules:
-    - if: '$CI_MERGE_REQUEST_LABELS =~ /Ready for code review/' # run on merge requests, if label 'Ready for code review' is set
+    - if: '$CI_MERGE_REQUEST_LABELS =~ /Ready for Code Review/' # run on merge requests, if label 'Ready for Code Review' is set
     - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
       when: manual
       allow_failure: true
diff --git a/corsika/detail/framework/core/Cascade.inl b/corsika/detail/framework/core/Cascade.inl
index 64ca0a3aa5ff2f9fb0a95062d27ba4dcdba4807c..f11427148c96e927176168f6d9d8c1ee3dc9fc15 100644
--- a/corsika/detail/framework/core/Cascade.inl
+++ b/corsika/detail/framework/core/Cascade.inl
@@ -9,14 +9,21 @@
 #pragma once
 
 #include <corsika/framework/core/PhysicalUnits.hpp>
+
 #include <corsika/framework/process/ProcessReturn.hpp>
 #include <corsika/framework/process/ContinuousProcessStepLength.hpp>
 #include <corsika/framework/process/ContinuousProcessIndex.hpp>
+
 #include <corsika/framework/random/ExponentialDistribution.hpp>
 #include <corsika/framework/random/RNGManager.hpp>
 #include <corsika/framework/random/UniformRealDistribution.hpp>
+
 #include <corsika/framework/stack/SecondaryView.hpp>
+
+#include <corsika/framework/utility/COMBoost.hpp>
+
 #include <corsika/media/Environment.hpp>
+#include <corsika/media/NuclearComposition.hpp>
 
 #include <cassert>
 #include <cmath>
@@ -40,7 +47,7 @@ namespace corsika {
         auto pNext = stack_.getNextParticle();
 
         CORSIKA_LOG_TRACE(
-            "============== next particle : count={}, pid={}, "
+            "============== next particle : count={}, pid={}"
             ", stack entries={}"
             ", stack deleted={}",
             count_, pNext.getPID(), stack_.getEntries(), stack_.getErased());
@@ -59,32 +66,41 @@ namespace corsika {
   inline void Cascade<TTracking, TProcessList, TOutput, TStack>::forceInteraction() {
     CORSIKA_LOG_TRACE("forced interaction!");
     setNodes();
-    auto vParticle = stack_.getNextParticle();
-    stack_view_type secondaries(vParticle);
-    interaction(secondaries, sequence_.getInverseInteractionLength(vParticle));
+    auto particle = stack_.getNextParticle();
+    stack_view_type secondaries(particle);
+
+    auto const* currentLogicalNode = particle.getNode();
+    // assert that particle stays outside void Universe if it has no
+    // model properties set
+    assert((currentLogicalNode != &*environment_.getUniverse() ||
+            environment_.getUniverse()->hasModelProperties()) &&
+           "FATAL: The environment model has no valid properties set!");
+    NuclearComposition const& composition =
+        currentLogicalNode->getModelProperties().getNuclearComposition();
+
+    // determine projectile
+    HEPEnergyType const Elab = particle.getEnergy();
+    FourMomentum const projectileP4{Elab, particle.getMomentum()};
+    // determine cross section in material
+    CrossSectionType const sigma =
+        composition.getWeightedSum([=](Code const targetId) -> CrossSectionType {
+          FourMomentum const targetP4(
+              get_mass(targetId),
+              MomentumVector(particle.getMomentum().getCoordinateSystem(),
+                             {0_GeV, 0_GeV, 0_GeV}));
+          return sequence_.getCrossSection(particle, targetId, targetP4);
+        });
+    interaction(secondaries, projectileP4, composition, sigma);
     sequence_.doSecondaries(secondaries);
-    vParticle.erase(); // primary particle is done
+    particle.erase(); // primary particle is done
   }
 
   template <typename TTracking, typename TProcessList, typename TOutput, typename TStack>
   inline void Cascade<TTracking, TProcessList, TOutput, TStack>::step(
-      particle_type& vParticle) {
-
-    // determine combined total interaction length (inverse)
-    InverseGrammageType const total_inv_lambda =
-        sequence_.getInverseInteractionLength(vParticle);
-
-    // sample random exponential step length in grammage
-    ExponentialDistribution expDist(1 / total_inv_lambda);
-    GrammageType const next_interact = expDist(rng_);
+      particle_type& particle) {
 
-    CORSIKA_LOG_DEBUG(
-        "total_lambda={} g/cm2, "
-        ", next_interact={} g/cm2",
-        double((1. / total_inv_lambda) / 1_g * 1_cm * 1_cm),
-        double(next_interact / 1_g * 1_cm * 1_cm));
-
-    auto const* currentLogicalNode = vParticle.getNode();
+    // determine the volume where the particle is (last) known to be
+    auto const* currentLogicalNode = particle.getNode();
 
     // assert that particle stays outside void Universe if it has no
     // model properties set
@@ -92,24 +108,52 @@ namespace corsika {
             environment_.getUniverse()->hasModelProperties()) &&
            "FATAL: The environment model has no valid properties set!");
 
+    NuclearComposition const& composition =
+        currentLogicalNode->getModelProperties().getNuclearComposition();
+
+    // determine projectile
+    HEPEnergyType const Elab = particle.getEnergy();
+    FourMomentum const projectileP4{Elab, particle.getMomentum()};
+
+    // determine combined full inelastic cross section of the particles in the material
+
+    CrossSectionType const total_cx =
+        composition.getWeightedSum([=](Code const targetId) -> CrossSectionType {
+          FourMomentum const targetP4(
+              get_mass(targetId),
+              MomentumVector(particle.getMomentum().getCoordinateSystem(),
+                             {0_GeV, 0_GeV, 0_GeV}));
+          return sequence_.getCrossSection(particle, targetId, targetP4);
+        });
+
+    // calculate interaction length in medium
+    GrammageType const total_lambda =
+        (composition.getAverageMassNumber() * constants::u) / total_cx;
+
+    // sample random exponential step length in grammage
+    ExponentialDistribution expDist(total_lambda);
+    GrammageType const next_interact = expDist(rng_);
+
+    CORSIKA_LOG_DEBUG("total_lambda={} g/cm2, next_interact={} g/cm2",
+                      double(total_lambda / 1_g * 1_cm * 1_cm),
+                      double(next_interact / 1_g * 1_cm * 1_cm));
+
     // determine combined total inverse decay time
-    InverseTimeType const total_inv_lifetime = sequence_.getInverseLifetime(vParticle);
+    InverseTimeType const total_inv_lifetime = sequence_.getInverseLifetime(particle);
 
     // sample random exponential decay time
     ExponentialDistribution expDistDecay(1 / total_inv_lifetime);
     TimeType const next_decay = expDistDecay(rng_);
 
-    CORSIKA_LOG_DEBUG(
-        "total_lifetime={} s"
-        ", next_decay={} s",
-        (1 / total_inv_lifetime) / 1_s, next_decay / 1_s);
+    CORSIKA_LOG_DEBUG("total_lifetime={} ns, next_decay={} ns",
+                      (1 / total_inv_lifetime) / 1_ns, next_decay / 1_ns);
 
     // convert next_decay from time to length [m]
-    LengthType const distance_decay = next_decay * vParticle.getMomentum().getNorm() /
-                                      vParticle.getEnergy() * constants::c;
+    LengthType const distance_decay = next_decay * particle.getMomentum().getNorm() /
+                                      particle.getEnergy() * constants::c;
 
     // determine geometric tracking
-    auto [step, nextVol] = tracking_.getTrack(vParticle);
+    auto [step, nextVol] = tracking_.getTrack(particle);
     auto geomMaxLength = step.getLength(1);
 
     // convert next_step from grammage to length
@@ -119,7 +163,7 @@ namespace corsika {
 
     // determine the maximum geometric step length
     ContinuousProcessStepLength const continuousMaxStep =
-        sequence_.getMaxStepLength(vParticle, step);
+        sequence_.getMaxStepLength(particle, step);
     LengthType const continuous_max_dist = continuousMaxStep;
 
     // take minimum of geometry, interaction, decay for next step
@@ -150,26 +194,25 @@ namespace corsika {
     // move particle along the trajectory to new position
     // also update momentum/direction/time
     step.setLength(min_distance);
-    vParticle.setPosition(step.getPosition(1));
-    // assumption: tracking does not change absolute momentum (continuous physics can and
-    // will):
-    vParticle.setMomentum(step.getDirection(1) * vParticle.getMomentum().getNorm());
 
     // apply all continuous processes on particle + track
-    if (sequence_.doContinuous(vParticle, step, limitingId) ==
+    if (sequence_.doContinuous(particle, step, limitingId) ==
         ProcessReturn::ParticleAbsorbed) {
       CORSIKA_LOG_DEBUG("Cascade: delete absorbed particle PID={} E={} GeV",
-                        vParticle.getPID(), vParticle.getEnergy() / 1_GeV);
-      if (vParticle.isErased()) {
+                        particle.getPID(), particle.getEnergy() / 1_GeV);
+      if (particle.isErased()) {
         CORSIKA_LOG_WARN(
             "Particle marked as Absorbed in doContinuous, but prematurely erased. This "
             "may be bug. Check.");
       } else {
-        vParticle.erase();
+        particle.erase();
       }
-      return;
+      return; // particle is gone -> return
     }
-    vParticle.setTime(vParticle.getTime() + step.getDuration());
+    particle.setTime(particle.getTime() + step.getDuration());
+    particle.setPosition(step.getPosition(1));
+    particle.setMomentum(step.getDirection(1) * particle.getMomentum().getNorm());
+
     if (isContinuous) {
       return; // there is nothing further, step is finished
     }
@@ -188,28 +231,29 @@ namespace corsika {
         if (nextVol == environment_.getUniverse().get()) {
           CORSIKA_LOG_DEBUG(
               "particle left physics world, is now in unknown space -> delete");
-          vParticle.erase();
+          particle.erase();
         }
-        vParticle.setNode(nextVol);
+        particle.setNode(nextVol);
         /*
           doBoundary may delete the particle (or not)
 
-          caveat: any changes to vParticle, or even the production
+          caveat: any changes to particle, or even the production
           of new secondaries is currently not passed to ParticleCut,
           thus, particles outside the desired phase space may be produced.
 
           \todo: this must be fixed.
         */
 
-        sequence_.doBoundaryCrossing(vParticle, *currentLogicalNode, *nextVol);
+        sequence_.doBoundaryCrossing(particle, *currentLogicalNode, *nextVol);
         return; // step finished
       }
 
       CORSIKA_LOG_DEBUG("step limit reached (e.g. deflection). nothing further happens.");
 
+      // final sanity check, no actions
       {
         auto const* numericalNodeAfterStep =
-            environment_.getUniverse()->getContainingNode(vParticle.getPosition());
+            environment_.getUniverse()->getContainingNode(particle.getPosition());
         CORSIKA_LOG_TRACE(
             "Geometry check: numericalNodeAfterStep={} currentLogicalNode={}",
             fmt::ptr(numericalNodeAfterStep), fmt::ptr(currentLogicalNode));
@@ -231,23 +275,22 @@ namespace corsika {
     // secondaries, b) the projectile particle deleted (or
     // changed)
 
-    stack_view_type secondaries(vParticle);
+    stack_view_type secondaries(particle);
 
     /*
       Create SecondaryView object on Stack. The data container
       remains untouched and identical, and 'projectile' is identical
-      to 'vParticle' above this line. However,
-      projectile.AddSecondaries populate the SecondaryView, which can
+      to 'particle' above this line. However,
+      projectile.addSecondaries populate the SecondaryView, which can
       then be used afterwards for further processing. Thus: it is
-      important to use projectile/view (and not vParticle) for Interaction,
+      important to use projectile/view (and not particle) for Interaction,
       and Decay!
     */
-
-    [[maybe_unused]] auto projectile = secondaries.getProjectile();
-
     if (distance_interact < distance_decay) {
-      interaction(secondaries, total_inv_lambda);
+      interaction(secondaries, projectileP4, composition, total_cx);
     } else {
+      [[maybe_unused]] auto projectile = secondaries.getProjectile();
+
       if (decay(secondaries, total_inv_lifetime) == ProcessReturn::Decayed) {
         if (secondaries.getSize() == 1 &&
             projectile.getPID() == secondaries.getNextParticle().getPID()) {
@@ -258,7 +301,7 @@ namespace corsika {
     }
 
     sequence_.doSecondaries(secondaries);
-    vParticle.erase();
+    particle.erase();
   } // namespace corsika
 
   template <typename TTracking, typename TProcessList, typename TOutput, typename TStack>
@@ -266,19 +309,6 @@ namespace corsika {
       stack_view_type& view, InverseTimeType initial_inv_decay_time) {
     CORSIKA_LOG_DEBUG("decay");
 
-#ifdef DEBUG
-    InverseTimeType const actual_decay_time = sequence_.getInverseLifetime(view.parent());
-    if (actual_decay_time * 0.99 > initial_inv_decay_time) {
-      CORSIKA_LOG_WARN(
-          "Decay time decreased during step! This leads to un-physical step length. "
-          "delta_inverse_decay_time={}",
-          (actual_decay_time != InverseTimeType::zero() &&
-                   initial_inv_decay_time != InverseTimeType::zero()
-               ? 1 / initial_inv_decay_time - 1 / actual_decay_time
-               : TimeType::zero()));
-    }
-#endif
-
     // one option is that decay_time is now larger (less
     // probability for decay) than it was before the step, thus,
     // no decay might actually occur and is allowed
@@ -296,28 +326,20 @@ namespace corsika {
 
   template <typename TTracking, typename TProcessList, typename TOutput, typename TStack>
   inline ProcessReturn Cascade<TTracking, TProcessList, TOutput, TStack>::interaction(
-      stack_view_type& view, InverseGrammageType initial_inv_int_length) {
-    CORSIKA_LOG_DEBUG("collide");
+      stack_view_type& view, FourMomentum const& projectileP4,
+      NuclearComposition const& composition,
+      CrossSectionType const initial_cross_section) {
 
-#ifdef DEBUG
-    InverseGrammageType const actual_inv_length = sequence_.getInverseInteractionLength(
-        view.parent()); // 1/lambda_int after step, -dE/dX etc.
-
-    if (actual_inv_length * 0.99 > initial_inv_int_length) {
-      CORSIKA_LOG_WARN(
-          "Interaction length decreased during step! This leads to un-physical step "
-          "length. delta_inverse_interaction_length={}",
-          1 / initial_inv_int_length - 1 / actual_inv_length);
-    }
-#endif
+    CORSIKA_LOG_DEBUG("collide");
 
-    // one option is that interaction_length is now larger (less
+    // one option is that cross section is now smaller (less
     // probability for collision) than it was before the step, thus,
     // no interaction might actually occur and is allowed
 
-    UniformRealDistribution<InverseGrammageType> uniDist(initial_inv_int_length);
-    const auto sample_process = uniDist(rng_);
-    auto const returnCode = sequence_.selectInteraction(view, sample_process);
+    UniformRealDistribution<CrossSectionType> uniDist(initial_cross_section);
+    CrossSectionType const sample_process_by_cx = uniDist(rng_);
+    auto const returnCode = sequence_.selectInteraction(view, projectileP4, composition,
+                                                        rng_, sample_process_by_cx);
     if (returnCode != ProcessReturn::Interacted) {
       CORSIKA_LOG_DEBUG("Particle did not interact!");
     }
diff --git a/corsika/detail/framework/core/ParticleProperties.inl b/corsika/detail/framework/core/ParticleProperties.inl
index feb10651867f431b71afae8814353a11116cc4d8..0671a0db6453204feef2448055cc520b8f7c30a1 100644
--- a/corsika/detail/framework/core/ParticleProperties.inl
+++ b/corsika/detail/framework/core/ParticleProperties.inl
@@ -115,6 +115,11 @@ namespace corsika {
   inline HEPMassType constexpr get_nucleus_mass(Code const code) {
     unsigned int const A = get_nucleus_A(code);
     unsigned int const Z = get_nucleus_Z(code);
+    return get_nucleus_mass(A, Z);
+  }
+
+  inline HEPMassType constexpr get_nucleus_mass(unsigned int const A,
+                                                unsigned int const Z) {
     return get_mass(Code::Proton) * Z + (A - Z) * get_mass(Code::Neutron);
   }
 
diff --git a/corsika/detail/framework/process/InteractionCounter.inl b/corsika/detail/framework/process/InteractionCounter.inl
index 7a549d61da111e3a338b4b19d2b01cf7e5cc7ce2..33ff338618a030cf9c1f4825f0961a2b7e29865b 100644
--- a/corsika/detail/framework/process/InteractionCounter.inl
+++ b/corsika/detail/framework/process/InteractionCounter.inl
@@ -18,22 +18,20 @@ namespace corsika {
 
   template <class TCountedProcess>
   template <typename TSecondaryView>
-  inline void InteractionCounter<TCountedProcess>::doInteraction(TSecondaryView& view) {
-    auto const projectile = view.getProjectile();
-    auto const massNumber = projectile.getNode()
-                                ->getModelProperties()
-                                .getNuclearComposition()
-                                .getAverageMassNumber();
+  inline void InteractionCounter<TCountedProcess>::doInteraction(
+      TSecondaryView& view, Code const projectileId, Code const targetId,
+      FourMomentum const& projectileP4, FourMomentum const& targetP4) {
+    size_t const massNumber = is_nucleus(targetId) ? get_nucleus_A(targetId) : 1;
     auto const massTarget = massNumber * constants::nucleonMass;
-    histogram_.fill(projectile.getPID(), projectile.getEnergy(), massTarget);
-    process_.doInteraction(view);
+    histogram_.fill(projectileId, projectileP4.getTimeLikeComponent(), massTarget);
+    process_.doInteraction(view, projectileId, targetId, projectileP4, targetP4);
   }
 
   template <class TCountedProcess>
-  template <typename TParticle>
-  inline GrammageType InteractionCounter<TCountedProcess>::getInteractionLength(
-      TParticle const& particle) const {
-    return process_.getInteractionLength(particle);
+  inline CrossSectionType InteractionCounter<TCountedProcess>::getCrossSection(
+      Code const projectileId, Code const targetId, FourMomentum const& projectileP4,
+      FourMomentum const& targetP4) const {
+    return process_.getCrossSection(projectileId, targetId, projectileP4, targetP4);
   }
 
   template <class TCountedProcess>
diff --git a/corsika/detail/framework/process/InteractionProcess.hpp b/corsika/detail/framework/process/InteractionProcess.hpp
index 2446a385d7beec2424da598585dd89bd70466a5c..7319027194015b5caaf23946a1c12d2b4fd79c84 100644
--- a/corsika/detail/framework/process/InteractionProcess.hpp
+++ b/corsika/detail/framework/process/InteractionProcess.hpp
@@ -14,10 +14,12 @@
 namespace corsika {
 
   /**
-     traits test for InteractionProcess::doInteraction method
-  */
+   * @file InteractionProcess.hpp
+   *
+   * traits test for InteractionProcess::doInteraction methods etc.
+   */
 
-  template <class TProcess, typename TReturn, typename... TArgs>
+  template <class TProcess, typename TReturn, typename TTemplate, typename... TArgs>
   struct has_method_doInteract : public detail::has_method_signature<TReturn, TArgs...> {
 
     ///! method signature
@@ -29,7 +31,7 @@ namespace corsika {
 
     //! signature of templated method
     template <class T>
-    static decltype(testSignature(&T::template doInteraction<TArgs...>)) test(
+    static decltype(testSignature(&T::template doInteraction<TTemplate>)) test(
         std::nullptr_t);
 
     //! signature of non-templated method
@@ -38,26 +40,25 @@ namespace corsika {
 
   public:
     /**
-        @name traits results
-        @{
-    */
+     *  @name traits results
+     * @{
+     */
     using type = decltype(test<std::decay_t<TProcess>>(nullptr));
     static const bool value = type::value;
     //! @}
   };
 
-  //! @file InteractionProcess.hpp
   //! value traits type
-  template <class TProcess, typename TReturn, typename... TArgs>
+  template <class TProcess, typename TReturn, typename TTemplate, typename... TArgs>
   bool constexpr has_method_doInteract_v =
-      has_method_doInteract<TProcess, TReturn, TArgs...>::value;
+      has_method_doInteract<TProcess, TReturn, TTemplate, TArgs...>::value;
 
   /**
-     traits test for InteractionProcess::getInteractionLength method
-  */
+   *  traits test for TEMPLATED InteractionProcess::getCrossSection method (PROPOSAL).
+   */
 
-  template <class TProcess, typename TReturn, typename... TArgs>
-  struct has_method_getInteractionLength
+  template <class TProcess, typename TReturn, typename TTemplate, typename... TArgs>
+  struct has_method_getCrossSectionTemplate
       : public detail::has_method_signature<TReturn, TArgs...> {
 
     ///! method signature
@@ -69,28 +70,65 @@ namespace corsika {
 
     //! templated parameter option
     template <class T>
-    static decltype(testSignature(&T::template getInteractionLength<TArgs...>)) test(
+    static decltype(testSignature(&T::template getCrossSection<TTemplate>)) test(
         std::nullptr_t);
 
     //! non templated parameter option
     template <class T>
-    static decltype(testSignature(&T::getInteractionLength)) test(std::nullptr_t);
+    static decltype(testSignature(&T::getCrossSection)) test(std::nullptr_t);
 
   public:
     /**
-        @name traits results
-        @{
-    */
+     *  @name traits results
+     * @{
+     */
     using type = decltype(test<std::decay_t<TProcess>>(nullptr));
     static const bool value = type::value;
     //! @}
   };
 
-  //! @file InteractionProcess.hpp
-  //! value traits type
+  //! value traits type shortcut
+  template <class TProcess, typename TReturn, typename TTemplate, typename... TArgs>
+  bool constexpr has_method_getCrossSectionTemplate_v =
+      has_method_getCrossSectionTemplate<TProcess, TReturn, TTemplate, TArgs...>::value;
+
+  /**
+   *  traits test for InteractionProcess::getCrossSection method.
+   */
+
+  template <class TProcess, typename TReturn, typename... TArgs>
+  struct has_method_getCrossSection
+      : public detail::has_method_signature<TReturn, TArgs...> {
+
+    ///! method signature
+    using detail::has_method_signature<TReturn, TArgs...>::testSignature;
+
+    //! the default value
+    template <class T>
+    static std::false_type test(...);
+
+    //! templated parameter option
+    template <class T>
+    static decltype(testSignature(&T::template getCrossSection<TArgs...>)) test(
+        std::nullptr_t);
+
+    //! non templated parameter option
+    template <class T>
+    static decltype(testSignature(&T::getCrossSection)) test(std::nullptr_t);
+
+  public:
+    /**
+     *  @name traits results
+     * @{
+     */
+    using type = decltype(test<std::decay_t<TProcess>>(nullptr));
+    static const bool value = type::value;
+    //! @}
+  };
 
+  //! value traits type shortcut
   template <class TProcess, typename TReturn, typename... TArgs>
-  bool constexpr has_method_getInteractionLength_v =
-      has_method_getInteractionLength<TProcess, TReturn, TArgs...>::value;
+  bool constexpr has_method_getCrossSection_v =
+      has_method_getCrossSection<TProcess, TReturn, TArgs...>::value;
 
 } // namespace corsika
diff --git a/corsika/detail/framework/process/ProcessSequence.inl b/corsika/detail/framework/process/ProcessSequence.inl
index 4d381abb16af98729d6defdfb53bfc0503d420f9..25a592d52642c3b9586b867659578f46c9dc5657 100644
--- a/corsika/detail/framework/process/ProcessSequence.inl
+++ b/corsika/detail/framework/process/ProcessSequence.inl
@@ -9,6 +9,7 @@
 #pragma once
 
 #include <corsika/framework/core/PhysicalUnits.hpp>
+
 #include <corsika/framework/process/BaseProcess.hpp>
 #include <corsika/framework/process/BoundaryCrossingProcess.hpp>
 #include <corsika/framework/process/ContinuousProcess.hpp>
@@ -259,7 +260,8 @@ namespace corsika {
   template <typename TParticle, typename TTrack>
   inline ContinuousProcessStepLength
   ProcessSequence<TProcess1, TProcess2, IndexStart, IndexProcess1,
-                  IndexProcess2>::getMaxStepLength(TParticle& particle, TTrack& vTrack) {
+                  IndexProcess2>::getMaxStepLength(TParticle&& particle,
+                                                   TTrack&& vTrack) {
     // if no other process in the sequence implements it
     ContinuousProcessStepLength max_length(std::numeric_limits<double>::infinity() *
                                            meter);
@@ -309,24 +311,58 @@ namespace corsika {
   template <typename TProcess1, typename TProcess2, int IndexStart, int IndexProcess1,
             int IndexProcess2>
   template <typename TParticle>
-  inline InverseGrammageType
-  ProcessSequence<TProcess1, TProcess2, IndexStart, IndexProcess1,
-                  IndexProcess2>::getInverseInteractionLength(TParticle&& particle) {
+  inline CrossSectionType
+  ProcessSequence<TProcess1, TProcess2, IndexStart, IndexProcess1, IndexProcess2>::
+      getCrossSection([[maybe_unused]] TParticle const& projectile,
+                      [[maybe_unused]] Code const targetId,
+                      [[maybe_unused]] FourMomentum const& targetP4) const {
 
-    InverseGrammageType tot = 0 * meter * meter / gram; // default value
+    CrossSectionType tot = CrossSectionType::zero();
 
     if constexpr (is_process_v<process1_type>) { // to protect from further compiler
                                                  // errors if process1_type is invalid
-      if constexpr (is_interaction_process_v<process1_type> ||
-                    process1_type::is_process_sequence) {
-        tot += A_.getInverseInteractionLength(particle);
+      if constexpr (is_interaction_process_v<process1_type>) {
+
+        bool constexpr has_signature_cx1 =
+            has_method_getCrossSection_v<TProcess1,        // process object
+                                         CrossSectionType, // return type
+                                         Code, Code,       // parameters
+                                         FourMomentum const&, FourMomentum const&>;
+
+        if constexpr (has_signature_cx1) {
+          tot += A_.getCrossSection(projectile.getPID(), targetId,
+                                    {projectile.getEnergy(), projectile.getMomentum()},
+                                    targetP4);
+        } else { // for PROPOSAL
+          tot += A_.getCrossSection(projectile, projectile.getPID(),
+                                    {projectile.getEnergy(), projectile.getMomentum()});
+        }
+
+      } else if constexpr (process1_type::is_process_sequence) {
+        tot += A_.getCrossSection(projectile, targetId, targetP4);
       }
     }
     if constexpr (is_process_v<process2_type>) { // to protect from further compiler
                                                  // errors if process2_type is invalid
-      if constexpr (is_interaction_process_v<process2_type> ||
-                    process2_type::is_process_sequence) {
-        tot += B_.getInverseInteractionLength(particle);
+      if constexpr (is_interaction_process_v<process2_type>) {
+
+        bool constexpr has_signature_cx1 =
+            has_method_getCrossSection_v<TProcess2,        // process object
+                                         CrossSectionType, // return type
+                                         Code, Code,       // parameters
+                                         FourMomentum const&, FourMomentum const&>;
+
+        if constexpr (has_signature_cx1) {
+          tot += B_.getCrossSection(projectile.getPID(), targetId,
+                                    {projectile.getEnergy(), projectile.getMomentum()},
+                                    targetP4);
+        } else { // for PROPOSAL
+          tot += B_.getCrossSection(projectile, projectile.getPID(),
+                                    {projectile.getEnergy(), projectile.getMomentum()});
+        }
+
+      } else if constexpr (process2_type::is_process_sequence) {
+        tot += B_.getCrossSection(projectile, targetId, targetP4);
       }
     }
     return tot;
@@ -407,65 +443,190 @@ namespace corsika {
 
   template <typename TProcess1, typename TProcess2, int IndexStart, int IndexProcess1,
             int IndexProcess2>
-  template <typename TSecondaryView>
+  template <typename TSecondaryView, typename TRNG>
   inline ProcessReturn
   ProcessSequence<TProcess1, TProcess2, IndexStart, IndexProcess1, IndexProcess2>::
-      selectInteraction(TSecondaryView& view,
-                        [[maybe_unused]] InverseGrammageType lambda_inv_select,
-                        [[maybe_unused]] InverseGrammageType lambda_inv_sum) {
+      selectInteraction(TSecondaryView&& view, FourMomentum const& projectileP4,
+                        [[maybe_unused]] NuclearComposition const& composition,
+                        [[maybe_unused]] TRNG&& rng,
+                        [[maybe_unused]] CrossSectionType const cx_select,
+                        [[maybe_unused]] CrossSectionType cx_sum) {
 
-    // TODO: add check for lambda_inv_select > lambda_inv_tot
+    // TODO: add check for cx_select > cx_tot
 
     if constexpr (is_process_v<process1_type>) { // to protect from further compiler
                                                  // errors if process1_type is invalid
       if constexpr (process1_type::is_process_sequence) {
         // if A is a process sequence --> check inside
         ProcessReturn const ret =
-            A_.selectInteraction(view, lambda_inv_select, lambda_inv_sum);
+            A_.selectInteraction(view, projectileP4, composition, rng, cx_select, cx_sum);
         // if A_ did succeed, stop routine. Not checking other static branch B_.
         if (ret != ProcessReturn::Ok) { return ret; }
       } else if constexpr (is_interaction_process_v<process1_type>) {
-        // if this is not a ContinuousProcess --> evaluate probability
-        lambda_inv_sum += A_.getInverseInteractionLength(view.parent());
-        // check if we should execute THIS process and then EXIT
-        if (lambda_inv_select <= lambda_inv_sum) {
 
-          // interface checking on TProcess1
-          static_assert(has_method_doInteract_v<TProcess1, void, TSecondaryView&>,
-                        "TDerived has no method with correct signature \"void "
-                        "doInteraction(TSecondaryView&)\" required for "
-                        "InteractionProcess<TDerived>. ");
+        auto const& projectile = view.parent();
+        Code const projectileId = projectile.getPID();
+
+        // get cross section vector for all material components
+        // for selected process A
+
+        bool constexpr has_signature_cx1 =
+            has_method_getCrossSection_v<TProcess1,        // process object
+                                         CrossSectionType, // return type
+                                         Code, Code,       // parameters
+                                         FourMomentum const&, FourMomentum const&>;
+        bool constexpr has_signature_cx2 = // needed for PROPOSAL interface
+            has_method_getCrossSectionTemplate_v<
+                TProcess1,                   // process object
+                CrossSectionType,            // return type
+                decltype(projectile) const&, // template argument
+                decltype(projectile) const&, // parameters
+                Code, FourMomentum const&>;
+
+        static_assert((has_signature_cx1 || has_signature_cx2),
+                      "TProcess1 has no method with correct signature \"CrossSectionType "
+                      "getCrossSection(Code, Code, FourMomentum const&, FourMomentum "
+                      "const&)\" required by "
+                      "InteractionProcess<TProcess1>. ");
+
+        std::vector<CrossSectionType> weightedCrossSections;
+        if constexpr (has_signature_cx1) {
+          /*std::vector<CrossSectionType> const*/ weightedCrossSections =
+              composition.getWeighted([=](Code const targetId) -> CrossSectionType {
+                FourMomentum const targetP4(
+                    get_mass(targetId),
+                    MomentumVector(projectile.getMomentum().getCoordinateSystem(),
+                                   {0_GeV, 0_GeV, 0_GeV}));
+                return A_.getCrossSection(projectileId, targetId, projectileP4, targetP4);
+              });
+
+          cx_sum +=
+              std::accumulate(weightedCrossSections.cbegin(),
+                              weightedCrossSections.cend(), CrossSectionType::zero());
+
+        } else { // this is for PROPOSAL
+          cx_sum += A_.template getCrossSection(projectile, projectileId, projectileP4);
+        }
+
+        // check if we should execute THIS process and then EXIT
+        if (cx_select <= cx_sum) {
+
+          if constexpr (has_signature_cx1) {
+            // now also sample targetId from weighted cross sections
+            Code const targetId = composition.sampleTarget(weightedCrossSections, rng);
+            FourMomentum const targetP4(
+                get_mass(targetId),
+                MomentumVector(projectile.getMomentum().getCoordinateSystem(),
+                               {0_GeV, 0_GeV, 0_GeV}));
+
+            // interface checking on TProcess1
+            static_assert(
+                has_method_doInteract_v<TProcess1,       // process object
+                                        void,            // return type
+                                        TSecondaryView,  // template argument
+                                        TSecondaryView&, // method parameters
+                                        Code, Code, FourMomentum const&,
+                                        FourMomentum const&>,
+                "TProcess1 has no method with correct signature \"void "
+                "doInteraction<TSecondaryView>(TSecondaryView&, "
+                "Code, Code, FourMomentum const&, FourMomentum const&)\" required for "
+                "InteractionProcess<TProcess1>. ");
+
+            A_.template doInteraction(view, projectileId, targetId, projectileP4,
+                                      targetP4);
+
+          } else { // this is for PROPOSAL
+            A_.template doInteraction(view, projectileId, projectileP4);
+          }
 
-          A_.template doInteraction(view);
           return ProcessReturn::Interacted;
         }
-      } // end branch A
-    }
+      }
+    } // end branch A
 
     if constexpr (is_process_v<process2_type>) { // to protect from further compiler
                                                  // errors if process2_type is invalid
 
       if constexpr (process2_type::is_process_sequence) {
         // if B_ is a process sequence --> check inside
-        return B_.selectInteraction(view, lambda_inv_select, lambda_inv_sum);
+        return B_.selectInteraction(view, projectileP4, composition, rng, cx_select,
+                                    cx_sum);
       } else if constexpr (is_interaction_process_v<process2_type>) {
-        // if this is not a ContinuousProcess --> evaluate probability
-        lambda_inv_sum += B_.getInverseInteractionLength(view.parent());
-        // soon as SecondaryView::parent() is migrated!
-        // check if we should execute THIS process and then EXIT
-        if (lambda_inv_select <= lambda_inv_sum) {
 
-          // interface checking on TProcess1
-          static_assert(has_method_doInteract_v<TProcess2, void, TSecondaryView&>,
-                        "TDerived has no method with correct signature \"void "
-                        "doInteraction(TSecondaryView&)\" required for "
-                        "InteractionProcess<TDerived>. ");
+        auto const& projectile = view.parent();
+        Code const projectileId = projectile.getPID();
+
+        // get cross section vector for all material components, for selected process B
+        bool constexpr has_signature_cx1 =
+            has_method_getCrossSection_v<TProcess2,        // process object
+                                         CrossSectionType, // return type
+                                         Code, Code,       // parameters
+                                         FourMomentum const&, FourMomentum const&>;
+        bool constexpr has_signature_cx2 = // needed for PROPOSAL interface
+            has_method_getCrossSectionTemplate_v<
+                TProcess2,                    // process object
+                CrossSectionType,             // return type
+                decltype(*projectile) const&, // template argument
+                decltype(*projectile) const&, // parameters
+                Code,                         // parameters
+                FourMomentum const&>;
+        static_assert((has_signature_cx1 || has_signature_cx2),
+                      "TProcess2 has no method with correct signature \"CrossSectionType "
+                      "getCrossSection(Code, Code, FourMomentum const&, FourMomentum "
+                      "const&)\" required by "
+                      "InteractionProcess<TProcess1>. ");
+
+        std::vector<CrossSectionType> weightedCrossSections;
+        if constexpr (has_signature_cx1) {
+          /* std::vector<CrossSectionType> const*/ weightedCrossSections =
+              composition.getWeighted([=](Code const targetId) -> CrossSectionType {
+                FourMomentum const targetP4(
+                    get_mass(targetId),
+                    MomentumVector(projectile.getMomentum().getCoordinateSystem(),
+                                   {0_GeV, 0_GeV, 0_GeV}));
+                return B_.getCrossSection(projectileId, targetId, projectileP4, targetP4);
+              });
+
+          cx_sum +=
+              std::accumulate(weightedCrossSections.begin(), weightedCrossSections.end(),
+                              CrossSectionType::zero());
+        } else { // this is for PROPOSAL
+          cx_sum += B_.template getCrossSection(projectile, projectileId, projectileP4);
+        }
 
-          B_.doInteraction(view);
+        // check if we should execute THIS process and then EXIT
+        if (cx_select <= cx_sum) {
+
+          if constexpr (has_signature_cx1) {
+
+            // now also sample targetId from weighted cross sections
+            Code const targetId = composition.sampleTarget(weightedCrossSections, rng);
+            FourMomentum const targetP4(
+                get_mass(targetId),
+                MomentumVector(projectile.getMomentum().getCoordinateSystem(),
+                               {0_GeV, 0_GeV, 0_GeV}));
+
+            // interface checking on TProcess2
+            static_assert(
+                has_method_doInteract_v<TProcess2,       // process object
+                                        void,            // return type
+                                        TSecondaryView,  // template argument
+                                        TSecondaryView&, // method parameters
+                                        Code, Code, FourMomentum const&,
+                                        FourMomentum const&>,
+                "TProcess1 has no method with correct signature \"void "
+                "doInteraction<TSecondaryView>(TSecondaryView&, "
+                "Code, Code, FourMomentum const&, FourMomentum const&)\" required for "
+                "InteractionProcess<TProcess2>. ");
+
+            B_.doInteraction(view, projectileId, targetId, projectileP4, targetP4);
+          } else { // this is for PROPOSAL
+            B_.doInteraction(view, projectileId, projectileP4);
+          }
           return ProcessReturn::Interacted;
         }
-      } // end branch B_
-    }
+      }
+    } // end branch B_
     return ProcessReturn::Ok;
   }
 
@@ -501,7 +662,7 @@ namespace corsika {
   template <typename TSecondaryView>
   inline ProcessReturn ProcessSequence<
       TProcess1, TProcess2, IndexStart, IndexProcess1,
-      IndexProcess2>::selectDecay(TSecondaryView& view,
+      IndexProcess2>::selectDecay(TSecondaryView&& view,
                                   [[maybe_unused]] InverseTimeType decay_inv_select,
                                   [[maybe_unused]] InverseTimeType decay_inv_sum) {
 
diff --git a/corsika/detail/framework/process/SwitchProcessSequence.inl b/corsika/detail/framework/process/SwitchProcessSequence.inl
index 634395f55e0a59524263df2f9d72ebe80cac8be6..0d26014fda26eb3d7fbe57f4801b0ccd2f014824 100644
--- a/corsika/detail/framework/process/SwitchProcessSequence.inl
+++ b/corsika/detail/framework/process/SwitchProcessSequence.inl
@@ -210,82 +210,233 @@ namespace corsika {
   template <typename TCondition, typename TSequence, typename USequence, int IndexStart,
             int IndexProcess1, int IndexProcess2>
   template <typename TParticle>
-  inline InverseGrammageType SwitchProcessSequence<
+  CrossSectionType SwitchProcessSequence<
       TCondition, TSequence, USequence, IndexStart, IndexProcess1,
-      IndexProcess2>::getInverseInteractionLength(TParticle&& particle) {
-
-    if (select_(particle)) {
-      if constexpr (is_interaction_process_v<process1_type> ||
-                    process1_type::is_process_sequence) {
-        return A_.getInverseInteractionLength(particle);
+      IndexProcess2>::getCrossSection(TParticle const& projectile, Code const targetId,
+                                      FourMomentum const& targetP4) const {
+
+    if (select_(projectile)) {
+      if constexpr (is_interaction_process_v<process1_type>) {
+        bool constexpr has_signature_cx1 =
+            has_method_getCrossSection_v<TSequence,        // process object
+                                         CrossSectionType, // return type
+                                         Code, Code,       // parameters
+                                         FourMomentum const&, FourMomentum const&>;
+        if constexpr (has_signature_cx1) {
+
+          return A_.getCrossSection(projectile.getPID(), targetId,
+                                    {projectile.getEnergy(), projectile.getMomentum()},
+                                    targetP4);
+        } else {
+          return A_.getCrossSection(projectile, projectile.getPID(),
+                                    {projectile.getEnergy(), projectile.getMomentum()});
+        }
+      } else if (process1_type::is_process_sequence) {
+        return A_.getCrossSection(projectile, targetId, targetP4);
       }
 
     } else {
-
-      if constexpr (is_interaction_process_v<process2_type> ||
-                    process2_type::is_process_sequence) {
-        return B_.getInverseInteractionLength(particle);
+      if constexpr (is_interaction_process_v<process2_type>) {
+        bool constexpr has_signature_cx1 =
+            has_method_getCrossSection_v<USequence,        // process object
+                                         CrossSectionType, // return type
+                                         Code, Code,       // parameters
+                                         FourMomentum const&, FourMomentum const&>;
+        if constexpr (has_signature_cx1) {
+
+          return B_.getCrossSection(projectile.getPID(), targetId,
+                                    {projectile.getEnergy(), projectile.getMomentum()},
+                                    targetP4);
+        } else {
+          return B_.getCrossSection(projectile, targetId, targetP4);
+        }
+      } else if (process2_type::is_process_sequence) {
+        return B_.getCrossSection(projectile, targetId, targetP4);
       }
     }
-    return 0 * meter * meter / gram; // default value
+    return CrossSectionType::zero(); // default value
   }
 
   template <typename TCondition, typename TSequence, typename USequence, int IndexStart,
             int IndexProcess1, int IndexProcess2>
-  template <typename TSecondaryView>
-  inline ProcessReturn SwitchProcessSequence<TCondition, TSequence, USequence, IndexStart,
-                                             IndexProcess1, IndexProcess2>::
-      selectInteraction(TSecondaryView& view,
-                        [[maybe_unused]] InverseGrammageType lambda_inv_select,
-                        [[maybe_unused]] InverseGrammageType lambda_inv_sum) {
+  template <typename TSecondaryView, typename TRNG>
+  inline ProcessReturn SwitchProcessSequence<
+      TCondition, TSequence, USequence, IndexStart, IndexProcess1,
+      IndexProcess2>::selectInteraction(TSecondaryView& view,
+                                        FourMomentum const& projectileP4,
+                                        NuclearComposition const& composition, TRNG& rng,
+                                        [[maybe_unused]] CrossSectionType const cx_select,
+                                        [[maybe_unused]] CrossSectionType cx_sum) {
+
     if (select_(view.parent())) {
       if constexpr (process1_type::is_process_sequence) {
         // if A_ is a process sequence --> check inside
-        ProcessReturn const ret =
-            A_.selectInteraction(view, lambda_inv_select, lambda_inv_sum);
-        // if A_ did succeed, stop routine. Not checking other static branch B_.
-        if (ret != ProcessReturn::Ok) { return ret; }
+        return A_.selectInteraction(view, projectileP4, composition, rng, cx_select,
+                                    cx_sum);
       } else if constexpr (is_interaction_process_v<process1_type>) {
-        // if this is not a ContinuousProcess --> evaluate probability
-        lambda_inv_sum += A_.getInverseInteractionLength(view.parent());
-        // check if we should execute THIS process and then EXIT
-        if (lambda_inv_select < lambda_inv_sum) {
 
-          // interface checking on TSequence
-          static_assert(has_method_doInteract_v<TSequence, void, TSecondaryView&>,
-                        "TDerived has no method with correct signature \"void "
-                        "doInteraction(TSecondaryView&)\" required for "
-                        "InteractionProcess<TDerived>. ");
+        auto const& projectile = view.parent();
+        Code const projectileId = projectile.getPID();
+
+        // get cross section vector for all material components
+        // for selected process A
+        bool constexpr has_signature_cx1 =
+            has_method_getCrossSection_v<TSequence,        // process object
+                                         CrossSectionType, // return type
+                                         Code, Code,       // parameters
+                                         FourMomentum const&, FourMomentum const&>;
+        bool constexpr has_signature_cx2 = // needed for PROPOSAL interface
+            has_method_getCrossSectionTemplate_v<
+                TSequence,                   // process object
+                CrossSectionType,            // return type
+                decltype(projectile) const&, // template argument
+                decltype(projectile) const&, // parameters
+                Code, FourMomentum const&>;
+
+        static_assert((has_signature_cx1 || has_signature_cx2),
+                      "TSequence has no method with correct signature \"CrossSectionType "
+                      "getCrossSection(Code, Code, FourMomentum const&, FourMomentum "
+                      "const&)\" required by "
+                      "InteractionProcess<TSequence>. ");
+
+        std::vector<CrossSectionType> weightedCrossSections;
+        if constexpr (has_signature_cx1) {
+          /*std::vector<CrossSectionType> const*/ weightedCrossSections =
+              composition.getWeighted([=](Code const targetId) -> CrossSectionType {
+                FourMomentum const targetP4(
+                    get_mass(targetId),
+                    MomentumVector(projectile.getMomentum().getCoordinateSystem(),
+                                   {0_GeV, 0_GeV, 0_GeV}));
+                return A_.getCrossSection(projectileId, targetId, projectileP4, targetP4);
+              });
+
+          cx_sum +=
+              std::accumulate(weightedCrossSections.cbegin(),
+                              weightedCrossSections.cend(), CrossSectionType::zero());
+        } else { // this is for PROPOSAL
+          cx_sum += A_.template getCrossSection(projectile, projectileId, projectileP4);
+        }
+
+        if (cx_select < cx_sum) {
+
+          if constexpr (has_signature_cx1) {
+
+            // now also sample targetId from weighted cross sections
+            Code const targetId = composition.sampleTarget(weightedCrossSections, rng);
+            FourMomentum const targetP4(
+                get_mass(targetId),
+                MomentumVector(projectile.getMomentum().getCoordinateSystem(),
+                               {0_GeV, 0_GeV, 0_GeV}));
+
+            // interface checking on TProcess1
+            static_assert(
+                has_method_doInteract_v<TSequence,       // process object
+                                        void,            // return type
+                                        TSecondaryView,  // template argument
+                                        TSecondaryView&, // method parameters
+                                        Code, Code, FourMomentum const&,
+                                        FourMomentum const&>,
+                "TSequence has no method with correct signature \"void "
+                "doInteraction<TSecondaryView>(TSecondaryView&, "
+                "Code, Code, FourMomentum const&, FourMomentum const&)\" required for "
+                "InteractionProcess<TSequence>. ");
+
+            A_.template doInteraction(view, projectileId, targetId, projectileP4,
+                                      targetP4);
+          } else { // this is for PROPOSAL
+            A_.template doInteraction(view, projectileId, projectileP4);
+          }
 
-          A_.doInteraction(view);
           return ProcessReturn::Interacted;
-        }
-      } // end branch A_
+        } // end collision branch A
+      }
 
-    } else {
+    } else { // selection: end branch A, start branch B
 
       if constexpr (process2_type::is_process_sequence) {
         // if B_ is a process sequence --> check inside
-        return B_.selectInteraction(view, lambda_inv_select, lambda_inv_sum);
+        return B_.selectInteraction(view, projectileP4, composition, rng, cx_select,
+                                    cx_sum);
       } else if constexpr (is_interaction_process_v<process2_type>) {
-        // if this is not a ContinuousProcess --> evaluate probability
-        lambda_inv_sum += B_.getInverseInteractionLength(view.parent());
-        // check if we should execute THIS process and then EXIT
-        if (lambda_inv_select < lambda_inv_sum) {
 
-          // interface checking on TSequence
-          static_assert(has_method_doInteract_v<USequence, void, TSecondaryView&>,
-                        "TDerived has no method with correct signature \"void "
-                        "doInteraction(TSecondaryView&)\" required for "
-                        "InteractionProcess<TDerived>. ");
+        auto const& projectile = view.parent();
+        Code const projectileId = projectile.getPID();
+
+        // get cross section vector for all material components, for selected process B
+        bool constexpr has_signature_cx1 =
+            has_method_getCrossSection_v<USequence,        // process object
+                                         CrossSectionType, // return type
+                                         Code, Code,       // parameters
+                                         FourMomentum const&, FourMomentum const&>;
+        bool constexpr has_signature_cx2 = // needed for PROPOSAL interface
+            has_method_getCrossSectionTemplate_v<
+                USequence,                   // process object
+                CrossSectionType,            // return type
+                decltype(projectile) const&, // template argument
+                decltype(projectile) const&, // parameters
+                Code, FourMomentum const&>;
+
+        static_assert((has_signature_cx1 || has_signature_cx2),
+                      "USequence has no method with correct signature \"CrossSectionType "
+                      "getCrossSection(Code, Code, FourMomentum const&, FourMomentum "
+                      "const&)\" required by "
+                      "InteractionProcess<USequence>. ");
+
+        std::vector<CrossSectionType> weightedCrossSections;
+        if constexpr (has_signature_cx1) {
+          /* std::vector<CrossSectionType> const*/ weightedCrossSections =
+              composition.getWeighted([=](Code const targetId) -> CrossSectionType {
+                FourMomentum const targetP4(
+                    get_mass(targetId),
+                    MomentumVector(projectile.getMomentum().getCoordinateSystem(),
+                                   {0_GeV, 0_GeV, 0_GeV}));
+                return B_.getCrossSection(projectileId, targetId, projectileP4, targetP4);
+              });
+
+          cx_sum +=
+              std::accumulate(weightedCrossSections.begin(), weightedCrossSections.end(),
+                              CrossSectionType::zero());
+        } else { // this is for PROPOSAL
+          cx_sum += B_.template getCrossSection(projectile, projectileId, projectileP4);
+        }
+
+        // check if we should execute THIS process and then EXIT
+        if (cx_select <= cx_sum) {
+
+          if constexpr (has_signature_cx1) {
+
+            // now also sample targetId from weighted cross sections
+            Code const targetId = composition.sampleTarget(weightedCrossSections, rng);
+            FourMomentum const targetP4(
+                get_mass(targetId),
+                MomentumVector(projectile.getMomentum().getCoordinateSystem(),
+                               {0_GeV, 0_GeV, 0_GeV}));
+
+            // interface checking on TProcess2
+            static_assert(
+                has_method_doInteract_v<USequence,       // process object
+                                        void,            // return type
+                                        TSecondaryView,  // template argument
+                                        TSecondaryView&, // method parameters
+                                        Code, Code, FourMomentum const&,
+                                        FourMomentum const&>,
+                "USequence has no method with correct signature \"void "
+                "doInteraction<TSecondaryView>(TSecondaryView&, "
+                "Code, Code, FourMomentum const&, FourMomentum const&)\" required for "
+                "InteractionProcess<USequence>. ");
+
+            B_.doInteraction(view, projectileId, targetId, projectileP4, targetP4);
+          } else { // this is for PROPOSAL
+            B_.doInteraction(view, projectileId, projectileP4);
+          }
 
-          B_.doInteraction(view);
           return ProcessReturn::Interacted;
-        }
-      } // end branch B_
-    }
+        } // end collision in branch B
+      }
+    } // end branch B_
+
     return ProcessReturn::Ok;
-  }
+  } // namespace corsika
 
   template <typename TCondition, typename TSequence, typename USequence, int IndexStart,
             int IndexProcess1, int IndexProcess2>
diff --git a/corsika/detail/framework/utility/COMBoost.inl b/corsika/detail/framework/utility/COMBoost.inl
index 9b05bef0e5e5d0193d74b637416a0056b520c535..4905b940ba2ff53e2055ed68fc9ad6c5b4da6969 100644
--- a/corsika/detail/framework/utility/COMBoost.inl
+++ b/corsika/detail/framework/utility/COMBoost.inl
@@ -19,17 +19,15 @@
 
 namespace corsika {
 
-  inline COMBoost::COMBoost(FourVector<HEPEnergyType, MomentumVector> const& Pprojectile,
+  inline COMBoost::COMBoost(FourMomentum const& P4projectile,
                             HEPMassType const massTarget)
-      : originalCS_{Pprojectile.getSpaceLikeComponents().getCoordinateSystem()}
-      , rotatedCS_{
-            make_rotationToZ(Pprojectile.getSpaceLikeComponents().getCoordinateSystem(),
-                             Pprojectile.getSpaceLikeComponents())} {
-    auto const pProjectile = Pprojectile.getSpaceLikeComponents();
+      : originalCS_{P4projectile.getSpaceLikeComponents().getCoordinateSystem()}
+      , rotatedCS_{make_rotationToZ(originalCS_, P4projectile.getSpaceLikeComponents())} {
+    auto const pProjectile = P4projectile.getSpaceLikeComponents();
     auto const pProjNormSquared = pProjectile.getSquaredNorm();
     auto const pProjNorm = sqrt(pProjNormSquared);
 
-    auto const eProjectile = Pprojectile.getTimeLikeComponent();
+    auto const eProjectile = P4projectile.getTimeLikeComponent();
     auto const massProjectileSquared = eProjectile * eProjectile - pProjNormSquared;
     auto const s =
         massTarget * massTarget + massProjectileSquared + 2 * eProjectile * massTarget;
@@ -39,12 +37,38 @@ namespace corsika {
     auto const coshEta = sqrt(1 + pProjNormSquared / s);
 
     setBoost(coshEta, sinhEta);
+    CORSIKA_LOG_TRACE("COMBoost (1-beta)={}, gamma={}, det={}", 1 - sinhEta / coshEta,
+                      coshEta, boost_.determinant() - 1);
+  }
 
+  inline COMBoost::COMBoost(FourMomentum const& P4projectile,
+                            FourMomentum const& P4target)
+      : originalCS_{P4projectile.getSpaceLikeComponents().getCoordinateSystem()} {
+
+    // this is the center-of-momentum CM frame
+    auto const pCM =
+        P4projectile.getSpaceLikeComponents() + P4target.getSpaceLikeComponents();
+    auto const pCM2 = pCM.getSquaredNorm();
+    auto const pCMnorm = sqrt(pCM2);
+    if (pCMnorm == 0_eV) {
+      // CM is at reset
+      rotatedCS_ = originalCS_;
+    } else {
+      rotatedCS_ = make_rotationToZ(originalCS_, P4projectile.getSpaceLikeComponents() +
+                                                     P4target.getSpaceLikeComponents());
+    }
+
+    auto const s = (P4projectile + P4target).getNormSqr();
+    auto const sqrtS = sqrt(s);
+    auto const sinhEta = -pCMnorm / sqrtS;
+    auto const coshEta = sqrt(1 + pCM2 / s);
+
+    setBoost(coshEta, sinhEta);
     CORSIKA_LOG_TRACE("COMBoost (1-beta)={}, gamma={}, det={}", 1 - sinhEta / coshEta,
                       coshEta, boost_.determinant() - 1);
   }
 
-  inline COMBoost::COMBoost(MomentumVector const& momentum, HEPEnergyType mass)
+  inline COMBoost::COMBoost(MomentumVector const& momentum, HEPEnergyType const mass)
       : originalCS_{momentum.getCoordinateSystem()}
       , rotatedCS_{make_rotationToZ(momentum.getCoordinateSystem(), momentum)} {
     auto const squaredNorm = momentum.getSquaredNorm();
@@ -52,15 +76,17 @@ namespace corsika {
     auto const sinhEta = -norm / mass;
     auto const coshEta = sqrt(1 + squaredNorm / (mass * mass));
     setBoost(coshEta, sinhEta);
+    CORSIKA_LOG_TRACE("COMBoost (1-beta)={}, gamma={}, det={}", 1 - sinhEta / coshEta,
+                      coshEta, boost_.determinant() - 1);
   }
 
   template <typename FourVector>
-  inline FourVector COMBoost::toCoM(FourVector const& p) const {
-    auto pComponents = p.getSpaceLikeComponents().getComponents(rotatedCS_);
+  inline FourVector COMBoost::toCoM(FourVector const& p4) const {
+    auto pComponents = p4.getSpaceLikeComponents().getComponents(rotatedCS_);
     Eigen::Vector3d eVecRotated = pComponents.getEigenVector();
     Eigen::Vector2d lab;
 
-    lab << (p.getTimeLikeComponent() * (1 / 1_GeV)),
+    lab << (p4.getTimeLikeComponent() * (1 / 1_GeV)),
         (eVecRotated(2) * (1 / 1_GeV).magnitude());
 
     auto const boostedZ = boost_ * lab;
@@ -68,21 +94,23 @@ namespace corsika {
 
     eVecRotated(2) = boostedZ(1) * (1_GeV).magnitude();
 
+    CORSIKA_LOG_TRACE("E0={}, p={}, E0'={}, p'={}", p4.getTimeLikeComponent() / 1_GeV,
+                      eVecRotated(2) * (1 / 1_GeV).magnitude(), E_CoM / 1_GeV, boostedZ);
+
     return FourVector(E_CoM, MomentumVector(rotatedCS_, eVecRotated));
   }
 
   template <typename FourVector>
-  inline FourVector COMBoost::fromCoM(FourVector const& p) const {
-    auto pCM = p.getSpaceLikeComponents().getComponents(rotatedCS_);
-    auto const Ecm = p.getTimeLikeComponent();
+  inline FourVector COMBoost::fromCoM(FourVector const& p4) const {
+    auto pCM = p4.getSpaceLikeComponents().getComponents(rotatedCS_);
+    auto const Ecm = p4.getTimeLikeComponent();
 
     Eigen::Vector2d com;
     com << (Ecm * (1 / 1_GeV)), (pCM.getEigenVector()(2) * (1 / 1_GeV).magnitude());
 
-    CORSIKA_LOG_TRACE(
-        "COMBoost::fromCoM Ecm={} GeV"
-        " pcm={} GeV (norm = {} GeV), invariant mass={} GeV",
-        Ecm / 1_GeV, pCM / 1_GeV, pCM.getNorm() / 1_GeV, p.getNorm() / 1_GeV);
+    CORSIKA_LOG_TRACE("Ecm={} GeV, pcm={} GeV (norm = {} GeV), invariant mass={} GeV",
+                      Ecm / 1_GeV, pCM / 1_GeV, pCM.getNorm() / 1_GeV,
+                      p4.getNorm() / 1_GeV);
 
     auto const boostedZ = inverseBoost_ * com;
     auto const E_lab = boostedZ(0) * 1_GeV;
@@ -92,22 +120,22 @@ namespace corsika {
     Vector<typename decltype(pCM)::dimension_type> pLab{rotatedCS_, pCM};
     pLab.rebase(originalCS_);
 
-    FourVector f(E_lab, pLab);
-
     CORSIKA_LOG_TRACE("COMBoost::fromCoM --> Elab={} GeV",
                       " plab={} GeV (norm={} GeV) "
                       " GeV), invariant mass = {}",
-                      E_lab / 1_GeV, f.getNorm() / 1_GeV, pLab.getComponents(),
-                      pLab.getNorm() / 1_GeV);
+                      E_lab / 1_GeV, FourVector{E_lab, pLab}.getNorm() / 1_GeV,
+                      pLab.getComponents(), pLab.getNorm() / 1_GeV);
 
-    return f;
+    return FourVector{E_lab, pLab};
   }
 
-  inline void COMBoost::setBoost(double coshEta, double sinhEta) {
+  inline void COMBoost::setBoost(double const coshEta, double const sinhEta) {
     boost_ << coshEta, sinhEta, sinhEta, coshEta;
     inverseBoost_ << coshEta, -sinhEta, -sinhEta, coshEta;
   }
 
   inline CoordinateSystemPtr COMBoost::getRotatedCS() const { return rotatedCS_; }
 
+  inline CoordinateSystemPtr COMBoost::getOriginalCS() const { return originalCS_; }
+
 } // namespace corsika
diff --git a/corsika/detail/media/NuclearComposition.inl b/corsika/detail/media/NuclearComposition.inl
index 4995799d58ba35e30c09448946d67c50f8e29ade..c499a593d98f9ba064b0656ead63956fc3a78244 100644
--- a/corsika/detail/media/NuclearComposition.inl
+++ b/corsika/detail/media/NuclearComposition.inl
@@ -23,31 +23,52 @@
 namespace corsika {
 
   inline NuclearComposition::NuclearComposition(std::vector<Code> const& pComponents,
-                                                std::vector<float> const& pFractions)
+                                                std::vector<double> const& pFractions)
       : numberFractions_(pFractions)
       , components_(pComponents)
-      , avgMassNumber_(std::inner_product(
-            pComponents.cbegin(), pComponents.cend(), pFractions.cbegin(), 0.,
-            std::plus<double>(), [](auto const compID, auto const fraction) -> double {
-              if (is_nucleus(compID)) {
-                return get_nucleus_A(compID) * fraction;
-              } else {
-                return get_mass(compID) / convert_SI_to_HEP(constants::u) * fraction;
-              }
-            })) {
-    assert(pComponents.size() == pFractions.size());
-    auto const sumFractions =
-        std::accumulate(pFractions.cbegin(), pFractions.cend(), 0.f);
-
-    if (!(0.999f < sumFractions && sumFractions < 1.001f)) {
+      , avgMassNumber_(getWeightedSum([](Code const compID) -> double {
+        if (is_nucleus(compID)) {
+          return get_nucleus_A(compID);
+        } else {
+          return get_mass(compID) / convert_SI_to_HEP(constants::u);
+        }
+      })) {
+    if (pComponents.size() != pFractions.size()) {
+      throw std::runtime_error(
+          "Cannot construct NuclearComposition from vectors of different sizes.");
+    }
+    auto const sumFractions = std::accumulate(pFractions.cbegin(), pFractions.cend(), 0.);
+
+    if (!(0.999 < sumFractions && sumFractions < 1.001)) {
       throw std::runtime_error("element fractions do not add up to 1");
     }
     this->updateHash();
   }
 
   template <typename TFunction>
-  inline auto NuclearComposition::getWeightedSum(TFunction const& func) const {
-    using ResultQuantity = decltype(func(*components_.cbegin()));
+  inline auto NuclearComposition::getWeighted(TFunction const& func) const {
+    using ResultQuantity = decltype(func(std::declval<Code>()));
+    auto const product = [&](auto const compID, auto const fraction) {
+      return func(compID) * fraction;
+    };
+
+    if constexpr (phys::units::is_quantity_v<ResultQuantity>) {
+      std::vector<ResultQuantity> result(components_.size(), ResultQuantity::zero());
+      std::transform(components_.cbegin(), components_.cend(), numberFractions_.cbegin(),
+                     result.begin(), product);
+      return result;
+    } else {
+      std::vector<ResultQuantity> result(components_.size(), ResultQuantity(0));
+      std::transform(components_.cbegin(), components_.cend(), numberFractions_.cbegin(),
+                     result.begin(), product);
+      return result;
+    }
+  } // namespace corsika
+
+  template <typename TFunction>
+  inline auto NuclearComposition::getWeightedSum(TFunction const& func) const
+      -> decltype(func(std::declval<Code>())) {
+    using ResultQuantity = decltype(func(std::declval<Code>()));
 
     auto const prod = [&](auto const compID, auto const fraction) {
       return func(compID) * fraction;
@@ -68,7 +89,7 @@ namespace corsika {
 
   inline size_t NuclearComposition::getSize() const { return numberFractions_.size(); }
 
-  inline std::vector<float> const& NuclearComposition::getFractions() const {
+  inline std::vector<double> const& NuclearComposition::getFractions() const {
     return numberFractions_;
   }
 
@@ -82,16 +103,14 @@ namespace corsika {
 
   template <class TRNG>
   inline Code NuclearComposition::sampleTarget(std::vector<CrossSectionType> const& sigma,
-                                               TRNG& randomStream) const {
-
-    assert(sigma.size() == numberFractions_.size());
+                                               TRNG&& randomStream) const {
+    if (sigma.size() != numberFractions_.size()) {
+      throw std::runtime_error("incompatible vector sigma as input");
+    }
 
     std::discrete_distribution channelDist(
-        WeightProviderIterator<decltype(numberFractions_.begin()),
-                               decltype(sigma.begin())>(numberFractions_.begin(),
-                                                        sigma.begin()),
-        WeightProviderIterator<decltype(numberFractions_.begin()), decltype(sigma.end())>(
-            numberFractions_.end(), sigma.end()));
+        WeightProviderIterator(numberFractions_.begin(), sigma.begin()),
+        WeightProviderIterator(numberFractions_.end(), sigma.end()));
 
     auto const iChannel = channelDist(randomStream);
     return components_[iChannel];
@@ -99,6 +118,7 @@ namespace corsika {
 
   // Note: when this class ever modifies its internal data, the hash
   // must be updated, too!
+  // the hash value is important to find tables, etc.
   inline size_t NuclearComposition::getHash() const { return hash_; }
 
   inline bool NuclearComposition::operator==(NuclearComposition const& v) const {
@@ -107,7 +127,8 @@ namespace corsika {
 
   inline void NuclearComposition::updateHash() {
     std::vector<std::size_t> hashes;
-    for (float ifrac : this->getFractions()) hashes.push_back(std::hash<float>{}(ifrac));
+    for (double ifrac : this->getFractions())
+      hashes.push_back(std::hash<double>{}(ifrac));
     for (Code icode : this->getComponents())
       hashes.push_back(std::hash<int>{}(static_cast<int>(icode)));
     std::size_t h = std::hash<double>{}(this->getAverageMassNumber());
diff --git a/corsika/detail/modules/epos/Interaction.inl b/corsika/detail/modules/epos/Interaction.inl
deleted file mode 100644
index 1e69cff4eb6ec68037b9ae4fcb943cb4f45e5665..0000000000000000000000000000000000000000
--- a/corsika/detail/modules/epos/Interaction.inl
+++ /dev/null
@@ -1,584 +0,0 @@
-/*
- * (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu
- *
- * 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.
- */
-
-#pragma once
-
-#include <corsika/modules/epos/Interaction.hpp>
-#include <corsika/modules/epos/EposStack.hpp>
-
-#include <corsika/media/Environment.hpp>
-#include <corsika/media/NuclearComposition.hpp>
-
-#include <corsika/framework/utility/COMBoost.hpp>
-#include <corsika/framework/utility/CorsikaData.hpp>
-
-#include <corsika/setup/SetupStack.hpp>
-#include <corsika/setup/SetupTrajectory.hpp>
-
-#include <epos.hpp>
-
-#include <string>
-#include <tuple>
-
-using namespace corsika;
-using SetupParticle = setup::Stack::stack_iterator_type;
-
-namespace corsika::epos {
-
-  inline Interaction::Interaction(std::string const& dataPath,
-                                  bool const epos_printout_on)
-      : data_path_(dataPath)
-      , epos_listing_(epos_printout_on) {
-    if (dataPath == "") {
-      data_path_ = (std::string(corsika_data("EPOS").c_str()) + "/").c_str();
-    }
-    // initialize Eposlhc
-    static bool initialized = false;
-    if (!initialized) {
-      initialize();
-      initialized = true;
-    }
-    setParticlesStable();
-  }
-
-  inline void Interaction::setParticlesStable() const {
-    CORSIKA_LOGGER_DEBUG(logger_,
-                         "set all particles known to CORSIKA stable inside EPOS..");
-    for (auto& p : get_all_particles()) {
-      if (!is_hadron(p)) continue;
-      int const eid = convertToEposRaw(p);
-      if (eid != 0) {
-        ::epos::nodcy_.nrnody = ::epos::nodcy_.nrnody + 1;
-        ::epos::nodcy_.nody[::epos::nodcy_.nrnody - 1] = eid;
-      }
-    }
-  }
-
-  inline bool Interaction::isValidTarget(Code const TargetId) const {
-    return is_nucleus(TargetId) && (get_nucleus_A(TargetId) < maxTargetMassNumber_);
-  }
-
-  inline void Interaction::initialize() const {
-
-    CORSIKA_LOGGER_DEBUG(logger_, "initializing...");
-
-    // corsika7 ini
-    int iarg = 0;
-    ::epos::aaset_(iarg);
-
-    // debug output settings
-    ::epos::prnt1_.ish = 0;    // debug level in epos, 0: off, 6: medium output
-    ::epos::prnt3_.iwseed = 0; // 1: printout seeds, 0: off
-    ::epos::files_.ifch = 6;   // output unit, 6: screen
-
-    // dummy set seeds for random number generator in epos. need to fool epos checks...
-    // we will use external generator
-    ::epos::cseed_.seedi = 1;
-    ::epos::cseed_.seedj = 1;
-    ::epos::cseed_.seedc = 1;
-
-    ::epos::enrgy_.egymin = minEnergyCoM_ / 1_GeV; // 6.;
-    ::epos::enrgy_.egymax = maxEnergyCoM_ / 1_GeV; // 2.e6;
-
-    ::epos::lhcparameters_();
-
-    ::epos::hadr6_.isigma = 0; // do not show cross section
-    ::epos::hadr6_.isetcs = 3; /*  !option to obtain pomeron parameters
-      ! 0.....determine parameters but do not use Kfit
-      ! 1.....determine parameters and use Kfit
-      ! else..get from table
-      !         should be sufficiently detailed
-      !          say iclegy1=1,iclegy2=99
-      !         table is always done, more or less detailed!!!
-      !and option to use cross section tables
-      ! 2....tabulation
-      ! 3....simulation
-                               */
-    ::epos::cjinti_.ionudi =
-        1; // !include quasi elastic events but strict calculation of xs
-    ::epos::cjinti_.iorsce = 0; // !color exchange turned on(1) or off(0)
-    ::epos::cjinti_.iorsdf = 3; //  !droplet formation turned on(>0) or off(0)
-    ::epos::cjinti_.iorshh = 0; //    !other hadron-hadron int. turned on(1) or off(0)
-
-    ::epos::othe1_.istore = 0; // do not produce epos output file
-    ::epos::nucl6_.infragm =
-        2; // 0: keep free nucleons in fragmentation,1: one fragment, 2: fragmentation
-
-    ::epos::othe2_.iframe = 12; // lab frame, target at rest
-
-    // set paths to tables in corsika data
-    ::epos::datadir BASE(data_path_);
-    strcpy(::epos::fname_.fnnx, BASE.data);
-    ::epos::nfname_.nfnnx = BASE.length;
-
-    ::epos::datadir TL(data_path_ + "epos.initl");
-    strcpy(::epos::fname_.fnii, TL.data);
-    ::epos::nfname_.nfnii = TL.length;
-
-    ::epos::datadir EV(data_path_ + "epos.iniev");
-    strcpy(::epos::fname_.fnie, EV.data);
-    ::epos::nfname_.nfnie = EV.length;
-
-    ::epos::datadir RJ(data_path_ + "epos.inirj"); // lhcparameters adds ".lhc"
-    strcpy(::epos::fname_.fnrj, RJ.data);
-    ::epos::nfname_.nfnrj = RJ.length;
-
-    ::epos::datadir CS(data_path_ + "epos.inics"); // lhcparameters adds ".lhc"
-    strcpy(::epos::fname_.fncs, CS.data);
-    ::epos::nfname_.nfncs = CS.length;
-
-    // dummy event (prepare commons)
-    initializeEventLab(Code::Iron, Iron::nucleus_A, Iron::nucleus_Z, Code::Argon,
-                       Argon::nucleus_A, Argon::nucleus_Z, 100_GeV);
-  }
-
-  inline void Interaction::initializeEventCoM(Code const idBeam, int const iBeamA,
-                                              int const iBeamZ, Code const idTarget,
-                                              int const iTargetA, int const iTargetZ,
-                                              HEPEnergyType const Ecm) const {
-    CORSIKA_LOGGER_TRACE(logger_,
-                         "initialize event in CoM frame!"
-                         " Ecm={}",
-                         Ecm);
-    ::epos::lept1_.engy = -1.;
-    ::epos::enrgy_.ecms = -1.;
-    ::epos::enrgy_.elab = -1.;
-    ::epos::enrgy_.ekin = -1.;
-    ::epos::hadr1_.pnll = -1.;
-
-    ::epos::enrgy_.ecms = Ecm / 1_GeV; // -> c.m.s. frame
-
-    CORSIKA_LOGGER_TRACE(logger_,
-                         "inside EPOS: "
-                         "Ecm={}, "
-                         "Elab={}",
-                         ::epos::enrgy_.ecms, ::epos::enrgy_.elab);
-
-    configureParticles(idBeam, iBeamA, iBeamZ, idTarget, iTargetA, iTargetZ);
-    ::epos::ainit_();
-  }
-
-  inline void Interaction::initializeEventLab(Code const idBeam, int const iBeamA,
-                                              int const iBeamZ, Code const idTarget,
-                                              int const iTargetA, int const iTargetZ,
-                                              HEPEnergyType const Plab) const {
-    CORSIKA_LOGGER_TRACE(logger_,
-                         "initialize event in lab. frame!"
-                         " Plab per nuc={} GeV",
-                         Plab / 1_GeV);
-    ::epos::lept1_.engy = -1.;
-    ::epos::enrgy_.ecms = -1.;
-    ::epos::enrgy_.elab = -1.;
-    ::epos::enrgy_.ekin = -1.;
-    ::epos::hadr1_.pnll = -1.;
-
-    // hadron-nucleon momentum
-    ::epos::hadr1_.pnll = float(Plab / 1_GeV); // -> lab frame
-
-    CORSIKA_LOGGER_TRACE(logger_,
-                         "inside EPOS: "
-                         "Ecm={}, "
-                         "Elab={}, "
-                         "Pnll={}",
-                         ::epos::enrgy_.ecms, ::epos::enrgy_.elab, ::epos::hadr1_.pnll);
-
-    configureParticles(idBeam, iBeamA, iBeamZ, idTarget, iTargetA, iTargetZ);
-    ::epos::ainit_();
-  }
-
-  inline void Interaction::configureParticles(Code const idBeam, int const iBeamA,
-                                              int const iBeamZ, Code const idTarget,
-                                              int const iTargetA,
-                                              int const iTargetZ) const {
-    CORSIKA_LOGGER_TRACE(logger_,
-                         "setting "
-                         "Beam={}, "
-                         "BeamA={}, "
-                         "BeamZ={}, "
-                         "Target={}, "
-                         "TargetA={}, "
-                         "TargetZ={} ",
-                         idBeam, iBeamA, iBeamZ, idTarget, iTargetA, iTargetZ);
-
-    if (is_nucleus(idBeam)) {
-      ::epos::hadr25_.idprojin = convertToEposRaw(Code::Proton);
-      ::epos::nucl1_.laproj = iBeamZ;
-      ::epos::nucl1_.maproj = iBeamA;
-    } else {
-      ::epos::hadr25_.idprojin = convertToEposRaw(idBeam);
-      ::epos::nucl1_.laproj = -1;
-      ::epos::nucl1_.maproj = 1;
-    }
-
-    if (is_nucleus(idTarget)) {
-      ::epos::hadr25_.idtargin = convertToEposRaw(Code::Proton);
-      ::epos::nucl1_.matarg = iTargetA;
-      ::epos::nucl1_.latarg = iTargetZ;
-    } else if (idTarget == Code::Proton || idTarget == Code::Hydrogen) {
-      ::epos::hadr25_.idtargin = convertToEposRaw(Code::Proton);
-      ::epos::nucl1_.matarg = 1;
-      ::epos::nucl1_.latarg = -1;
-    } else if (idTarget == Code::Neutron) {
-      ::epos::hadr25_.idtargin = convertToEposRaw(Code::Neutron);
-      ::epos::nucl1_.matarg = 1;
-      ::epos::nucl1_.latarg = -1;
-    } else {
-      throw std::runtime_error("Epos: target outside range!");
-    }
-    CORSIKA_LOGGER_TRACE(logger_,
-                         "inside EPOS: "
-                         "Id beam={}, "
-                         "Z beam={}, "
-                         "A beam={}, "
-                         "XS beam={}, "
-                         "Id target={}, "
-                         "Z target={}, "
-                         "A target={}, "
-                         "XS target={} ",
-                         ::epos::hadr25_.idprojin, ::epos::nucl1_.laproj,
-                         ::epos::nucl1_.maproj, ::epos::had10_.iclpro,
-                         ::epos::hadr25_.idtargin, ::epos::nucl1_.latarg,
-                         ::epos::nucl1_.matarg, ::epos::had10_.icltar);
-  }
-
-  inline Interaction::~Interaction() { CORSIKA_LOGGER_DEBUG(logger_, "n={} ", count_); }
-
-  inline std::tuple<CrossSectionType, CrossSectionType> Interaction::calcCrossSectionCoM(
-      Code const BeamId, int const BeamA, int const BeamZ, Code const TargetId,
-      int const TargetA, int const TargetZ, const HEPEnergyType EnergyCOM) const {
-    CORSIKA_LOGGER_DEBUG(logger_,
-                         "calcCrossSection: input:"
-                         " beamId={}, beamA={}, beamZ={}"
-                         " target={}, targetA={}, targetZ={}"
-                         " Ecm={:4.3f} GeV,",
-                         BeamId, BeamA, BeamZ, TargetId, TargetA, TargetZ,
-                         EnergyCOM / 1_GeV);
-
-    const int iBeam = corsika::epos::getEposXSCode(
-        BeamId); // 0 (can not interact, 1: proton-like, 2: pion-like, 3:kaon-like)
-    if (!iBeam)
-      throw std::runtime_error(
-          "calcCrossSectionCoM: interaction of beam hadron not defined in "
-          "Epos!");
-
-    CORSIKA_LOGGER_TRACE(logger_,
-                         "projectile cross section type={} "
-                         "(0: cannot interact, 1:pion, 2:baryon, 3:kaon)",
-                         iBeam);
-    // reset beam particle // (1: pion-like, 2: proton-like, 3:kaon-like)
-    if (iBeam == 1)
-      initializeEventCoM(Code::PiPlus, BeamA, BeamZ, TargetId, TargetA, TargetZ,
-                         EnergyCOM);
-    else if (iBeam == 2)
-      initializeEventCoM(Code::Proton, BeamA, BeamZ, TargetId, TargetA, TargetZ,
-                         EnergyCOM);
-    else if (iBeam == 3)
-      initializeEventCoM(Code::KPlus, BeamA, BeamZ, TargetId, TargetA, TargetZ,
-                         EnergyCOM);
-    else
-      throw std::runtime_error(
-          "calcCrossSectionCoM: interaction of beam hadron not defined in "
-          "Epos!");
-
-    double sigProd, sigEla = 0;
-    float sigTot1, sigProd1, sigCut1 = 0;
-    if (!is_nucleus(TargetId) && !is_nucleus(BeamId)) {
-      sigProd = ::epos::hadr5_.sigine;
-      sigEla = ::epos::hadr5_.sigela;
-    } else {
-      // calculate from model, SLOW:
-      float sigQEla1 = 0; // target fragmentation/excitation
-      ::epos::crseaaepos_(sigTot1, sigProd1, sigCut1, sigQEla1);
-      sigProd = sigProd1;
-      // sigEla not properly defined here
-    }
-    CORSIKA_LOGGER_DEBUG(logger_,
-                         "calcCrossSectionCoM: output:"
-                         " sigProd={} mb,"
-                         " sigEla={} mb",
-                         sigProd, sigEla);
-
-    return std::make_tuple(sigProd * 1_mb, sigEla * 1_mb);
-  }
-
-  inline std::tuple<corsika::CrossSectionType, corsika::CrossSectionType>
-  Interaction::readCrossSectionTableLab(Code const BeamId, int const BeamA,
-                                        int const BeamZ, Code const TargetId,
-                                        HEPEnergyType const EnergyLab) const {
-    CORSIKA_LOGGER_DEBUG(logger_,
-                         "readCrossSectionTableLab: input: "
-                         "beamId={}, "
-                         "beamA={}, "
-                         "beamZ={} "
-                         "targetId={}, "
-                         "ELab={:4.3f} GeV,",
-                         BeamId, BeamA, BeamZ, TargetId, EnergyLab / 1_GeV);
-
-    // read cross section from epos internal tables
-    int Abeam = 0;
-    float Ekin = -1;
-
-    if (is_nucleus(BeamId)) {
-      Abeam = BeamA;
-      // kinetic energy per nucleon
-      Ekin = (EnergyLab / Abeam - constants::nucleonMass) / 1_GeV;
-    } else {
-      ::epos::hadr2_.idproj = convertToEposRaw(BeamId);
-      int const iBeam = corsika::epos::getEposXSCode(
-          BeamId); // 0 (can not interact, 1: pion-like, 2: proton-like, 3:kaon-like)
-      CORSIKA_LOGGER_TRACE(logger_,
-                           "projectile cross section type={} "
-                           "(0: cannot interact, 1:pion, 2:baryon, 3:kaon)",
-                           iBeam);
-
-      ::epos::had10_.iclpro = iBeam;
-      Abeam = 1;
-      Ekin = (EnergyLab - get_mass(BeamId)) / 1_GeV;
-    }
-    if (Ekin < 0) {
-      CORSIKA_LOGGER_ERROR(logger_,
-                           "Negative kinetic energy!"
-                           "Ekin={}",
-                           Ekin);
-      throw std::runtime_error("Epos cross section failed! Negative kinetic energy!");
-    }
-
-    int Atarget = 1;
-    if (is_nucleus(TargetId)) { Atarget = get_nucleus_A(TargetId); }
-
-    int iMode = 3; // 0: air, >0 not air
-
-    CORSIKA_LOGGER_DEBUG(logger_,
-                         "inside Epos "
-                         "beamId={}, beamXS={}",
-                         ::epos::hadr2_.idproj, ::epos::had10_.iclpro);
-
-    // cross section from table, FAST
-    float sigProdEpos = ::epos::eposcrse_(Ekin, Abeam, Atarget, iMode);
-    // sig-el from analytic calculation, no fast
-    float sigElaEpos = ::epos::eposelacrse_(Ekin, Abeam, Atarget, iMode);
-
-    return std::make_tuple(sigProdEpos * 1_mb, sigElaEpos * 1_mb);
-  }
-
-  inline std::tuple<corsika::CrossSectionType, corsika::CrossSectionType>
-  Interaction::getCrossSectionLab(corsika::Code const BeamId, int const BeamA,
-                                  int const BeamZ, corsika::Code const TargetId,
-                                  int const TargetA, int const TargetZ,
-                                  const corsika::HEPEnergyType EnergyLab) const {
-    CORSIKA_LOGGER_DEBUG(logger_,
-                         "getCrossSectionLab: input:"
-                         " beamId={}, beamA={}, beamZ={}"
-                         " target={}, targetA={}, targetZ={}"
-                         " ELab={:4.3f} GeV,",
-                         BeamId, BeamA, BeamZ, TargetId, TargetA, TargetZ,
-                         EnergyLab / 1_GeV);
-    return readCrossSectionTableLab(BeamId, BeamA, BeamZ, TargetId, EnergyLab);
-  }
-
-  template <>
-  inline corsika::GrammageType Interaction::getInteractionLength(
-      SetupParticle const& projectile) const {
-
-    const corsika::Code corsikaBeamId = projectile.getPID();
-    const bool kInteraction = corsika::epos::canInteract(corsikaBeamId);
-    CORSIKA_LOGGER_DEBUG(logger_,
-                         "InteractionLength: input: \n"
-                         " energy: {} GeV "
-                         " beam can interact: {} "
-                         " beam pid: {}",
-                         projectile.getEnergy() / 1_GeV, kInteraction,
-                         projectile.getPID());
-
-    if (kInteraction) {
-
-      // define projectile nuclei
-      int beamA = 1;
-      int beamZ = 1;
-      if (is_nucleus(corsikaBeamId)) {
-        beamA = get_nucleus_A(corsikaBeamId);
-        beamZ = get_nucleus_Z(corsikaBeamId);
-      }
-
-      // get target from environment
-      MomentumVector const& pLab = projectile.getMomentum();
-      CoordinateSystemPtr const& labCS = pLab.getCoordinateSystem();
-
-      // assume target is at rest!!
-      MomentumVector pTarget(labCS, {0_GeV, 0_GeV, 0_GeV});
-
-      // total momentum and energy
-      HEPEnergyType Elab = projectile.getEnergy() + constants::nucleonMass;
-
-      auto const* currentNode = projectile.getNode();
-      const auto& mediumComposition =
-          currentNode->getModelProperties().getNuclearComposition();
-
-      si::CrossSectionType weightedProdCrossSection = mediumComposition.getWeightedSum(
-          [=](corsika::Code targetID) -> si::CrossSectionType {
-            return std::get<0>(this->getCrossSectionLab(corsikaBeamId, beamA, beamZ,
-                                                        targetID, get_nucleus_A(targetID),
-                                                        get_nucleus_Z(targetID), Elab));
-          });
-
-      CORSIKA_LOGGER_DEBUG(logger_, "InteractionLength: weighted CrossSection (mb): {} ",
-                           weightedProdCrossSection / 1_mb);
-
-      // calculate interaction length in medium
-      GrammageType const int_length = mediumComposition.getAverageMassNumber() *
-                                      constants::u / weightedProdCrossSection;
-      CORSIKA_LOGGER_DEBUG(logger_, "interaction length (g/cm2): {} ",
-                           int_length / (0.001_kg) * 1_cm * 1_cm);
-
-      return int_length;
-    }
-
-    return std::numeric_limits<double>::infinity() * 1_g / (1_cm * 1_cm);
-  }
-
-  template <typename TSecondaryView>
-  inline void Interaction::doInteraction(TSecondaryView& view) {
-
-    auto const projectile = view.getProjectile();
-    auto const corsikaBeamId = projectile.getPID();
-
-    CORSIKA_LOGGER_DEBUG(logger_, "doInteraction: {} interaction, Elab={} ",
-                         corsikaBeamId, projectile.getEnergy());
-
-    if (corsika::epos::canInteract(corsikaBeamId)) {
-      count_ = count_ + 1;
-      // position and time of interaction, not used in Epos
-      Point const pOrig = projectile.getPosition();
-      TimeType const tOrig = projectile.getTime();
-
-      // define projectile
-      HEPEnergyType const eProjectileLab = projectile.getEnergy();
-      auto const pProjectileLab = projectile.getMomentum();
-      auto const projectileMomentum = pProjectileLab.getNorm();
-      CoordinateSystemPtr const& originalCS = pProjectileLab.getCoordinateSystem();
-
-      // epos frame with z along the projectile direction
-      CoordinateSystemPtr const zAxisFrame = make_rotationToZ(originalCS, pProjectileLab);
-
-      int beamA = 1;
-      int beamZ = 1;
-      if (is_nucleus(corsikaBeamId)) {
-        beamA = get_nucleus_A(corsikaBeamId);
-        beamZ = get_nucleus_Z(corsikaBeamId);
-        CORSIKA_LOGGER_DEBUG(logger_, "A={}, Z={} ", beamA, beamZ);
-      }
-
-      HEPEnergyType const projectileMomentumLabPerNucleon = projectileMomentum / beamA;
-
-      // define target
-
-      // sample target mass number
-      auto const* currentNode = projectile.getNode();
-      auto const& mediumComposition =
-          currentNode->getModelProperties().getNuclearComposition();
-      // get cross sections for target materials
-      /*
-        Here we read the cross section from the interaction model again,
-        should be passed from getInteractionLength if possible
-       */
-      //#warning reading interaction cross section again, should not be necessary
-      auto const& compVec = mediumComposition.getComponents();
-      std::vector<CrossSectionType> cross_section_of_components(compVec.size());
-
-      for (size_t i = 0; i < compVec.size(); ++i) {
-        auto const targetId = compVec[i];
-        [[maybe_unused]] auto const [sigProd, sigEla] = getCrossSectionLab(
-            corsikaBeamId, beamA, beamZ, targetId, get_nucleus_A(targetId),
-            get_nucleus_Z(targetId), eProjectileLab);
-        cross_section_of_components[i] = sigProd;
-      }
-
-      const auto targetCode =
-          mediumComposition.sampleTarget(cross_section_of_components, RNG_);
-      CORSIKA_LOGGER_DEBUG(logger_, "target selected: {} ", targetCode);
-
-      // // from corsika7 interface
-      // // NEXLNK-part
-      int targetA = 1;
-      int targetZ = 1;
-      if (is_nucleus(targetCode)) {
-        targetA = get_nucleus_A(targetCode);
-        targetZ = get_nucleus_Z(targetCode);
-      }
-      initializeEventLab(corsikaBeamId, beamA, beamZ, targetCode, targetA, targetZ,
-                         projectileMomentumLabPerNucleon);
-
-      // create event
-      int iarg = 1;
-      ::epos::aepos_(iarg);
-
-      ::epos::afinal_();
-
-      if (epos_listing_) {
-        char nam[9] = "EPOSLHC&";
-        ::epos::alistf_(nam, 9);
-      }
-
-      // NSTORE-part
-
-      MomentumVector Plab_final(originalCS, {0.0_GeV, 0.0_GeV, 0.0_GeV});
-      HEPEnergyType Elab_final = 0_GeV;
-
-      // secondaries
-      EposStack es;
-      CORSIKA_LOGGER_DEBUG(logger_, "number of particles: {}", es.getSize());
-      for (auto& psec : es) {
-        if (!psec.isFinal()) continue;
-
-        auto momentum = psec.getMomentum(zAxisFrame);
-
-        momentum.rebase(originalCS); // transform back into standard lab frame
-
-        EposCode const eposId = psec.getPID();
-        Code const pid = corsika::epos::convertFromEpos(eposId);
-        CORSIKA_LOGGER_TRACE(logger_,
-                             " id= {}"
-                             " p= {}",
-                             pid, momentum.getComponents() / 1_GeV);
-        if (!is_nucleus(pid)) {
-          auto pnew = view.addSecondary(std::make_tuple(pid, momentum, pOrig, tOrig));
-          Plab_final += pnew.getMomentum();
-          Elab_final += pnew.getEnergy();
-        } else {
-          unsigned int A = 0;
-          unsigned int Z = 0;
-          if (pid == Code::Deuterium) {
-            A = 2;
-            Z = 1;
-          } else if (pid == Code::Tritium) {
-            A = 3;
-            Z = 1;
-          } else if (pid == Code::Helium) {
-            A = 4;
-            Z = 2;
-          } else {
-            Z = get_nucleus_Z(eposId);
-            A = get_nucleus_Z(eposId);
-          }
-          auto pnew = view.addSecondary(
-              std::make_tuple(get_nucleus_code(A, Z), momentum, pOrig, tOrig));
-          Plab_final += pnew.getMomentum();
-          Elab_final += pnew.getEnergy();
-        }
-      }
-      CORSIKA_LOGGER_DEBUG(
-          logger_,
-          "conservation (all GeV): Ecm_final= n/a" /* << Ecm_final / 1_GeV*/
-          ", Elab_final={}"
-          ", Plab_final={}",
-          Elab_final / 1_GeV, (Plab_final / 1_GeV).getComponents());
-    } else
-      CORSIKA_LOGGER_WARN(
-          logger_, "Projectile not configured for interaction! This is likely an error!");
-  }
-} // namespace corsika::epos
diff --git a/corsika/detail/modules/epos/InteractionModel.inl b/corsika/detail/modules/epos/InteractionModel.inl
new file mode 100644
index 0000000000000000000000000000000000000000..039d9635de189d5c6a96309e6a96b4bd07365dd7
--- /dev/null
+++ b/corsika/detail/modules/epos/InteractionModel.inl
@@ -0,0 +1,493 @@
+/*
+ * (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <corsika/modules/epos/InteractionModel.hpp>
+#include <corsika/modules/epos/EposStack.hpp>
+
+#include <corsika/framework/geometry/Point.hpp>
+
+#include <corsika/framework/utility/COMBoost.hpp>
+#include <corsika/framework/utility/CorsikaData.hpp>
+
+#include <epos.hpp>
+
+#include <string>
+#include <tuple>
+
+namespace corsika::epos {
+
+  inline InteractionModel::InteractionModel(std::string const& dataPath,
+                                            bool const epos_printout_on)
+      : data_path_(dataPath)
+      , epos_listing_(epos_printout_on) {
+    // initialize Eposlhc
+    if (!isInitialized_) {
+      isInitialized_ = true;
+      if (dataPath == "") {
+        data_path_ = (std::string(corsika_data("EPOS").c_str()) + "/").c_str();
+      }
+      initialize();
+    }
+    setParticlesStable();
+  }
+
+  inline void InteractionModel::setParticlesStable() const {
+    CORSIKA_LOGGER_DEBUG(logger_,
+                         "set all particles known to CORSIKA stable inside EPOS..");
+    for (auto& p : get_all_particles()) {
+      if (!is_hadron(p)) continue;
+      int const eid = convertToEposRaw(p);
+      if (eid != 0) {
+        ::epos::nodcy_.nrnody = ::epos::nodcy_.nrnody + 1;
+        ::epos::nodcy_.nody[::epos::nodcy_.nrnody - 1] = eid;
+      }
+    }
+  }
+
+  inline bool InteractionModel::isValid(Code const projectileId, Code const targetId,
+                                        HEPEnergyType const sqrtS) const {
+    //! eposlhc only accepts nuclei with X<=A<=Y as targets, or protons aka Hydrogen or
+    //! neutrons (p,n == nucleon)
+    if (!is_nucleus(targetId) && targetId != Code::Neutron && targetId != Code::Proton) {
+      return false;
+    }
+    if (is_nucleus(targetId) && (get_nucleus_A(targetId) >= maxTargetMassNumber_)) {
+      return false;
+    }
+    if ((minEnergyCoM_ > sqrtS) || (sqrtS > maxEnergyCoM_)) { return false; }
+    if (!epos::canInteract(projectileId)) { return false; }
+    return true;
+  }
+
+  inline void InteractionModel::initialize() const {
+
+    CORSIKA_LOGGER_DEBUG(logger_, "initializing...");
+
+    // corsika7 ini
+    int iarg = 0;
+    ::epos::aaset_(iarg);
+
+    // debug output settings
+    ::epos::prnt1_.ish = 0;    // debug level in epos, 0: off, 6: medium output
+    ::epos::prnt3_.iwseed = 0; // 1: printout seeds, 0: off
+    ::epos::files_.ifch = 6;   // output unit, 6: screen
+
+    // dummy set seeds for random number generator in epos. need to fool epos checks...
+    // we will use external generator
+    ::epos::cseed_.seedi = 1;
+    ::epos::cseed_.seedj = 1;
+    ::epos::cseed_.seedc = 1;
+
+    ::epos::enrgy_.egymin = minEnergyCoM_ / 1_GeV; // 6.;
+    ::epos::enrgy_.egymax = maxEnergyCoM_ / 1_GeV; // 2.e6;
+
+    ::epos::lhcparameters_();
+
+    ::epos::hadr6_.isigma = 0; // do not show cross section
+    ::epos::hadr6_.isetcs = 3; /*  !option to obtain pomeron parameters
+      ! 0.....determine parameters but do not use Kfit
+      ! 1.....determine parameters and use Kfit
+      ! else..get from table
+      !         should be sufficiently detailed
+      !          say iclegy1=1,iclegy2=99
+      !         table is always done, more or less detailed!!!
+      !and option to use cross section tables
+      ! 2....tabulation
+      ! 3....simulation
+                               */
+    ::epos::cjinti_.ionudi =
+        1; // !include quasi elastic events but strict calculation of xs
+    ::epos::cjinti_.iorsce = 0; // !color exchange turned on(1) or off(0)
+    ::epos::cjinti_.iorsdf = 3; //  !droplet formation turned on(>0) or off(0)
+    ::epos::cjinti_.iorshh = 0; //    !other hadron-hadron int. turned on(1) or off(0)
+
+    ::epos::othe1_.istore = 0; // do not produce epos output file
+    ::epos::nucl6_.infragm =
+        2; // 0: keep free nucleons in fragmentation,1: one fragment, 2: fragmentation
+
+    ::epos::othe2_.iframe = 12; // lab frame, target at rest
+
+    // set paths to tables in corsika data
+    ::epos::datadir BASE(data_path_);
+    strcpy(::epos::fname_.fnnx, BASE.data);
+    ::epos::nfname_.nfnnx = BASE.length;
+
+    ::epos::datadir TL(data_path_ + "epos.initl");
+    strcpy(::epos::fname_.fnii, TL.data);
+    ::epos::nfname_.nfnii = TL.length;
+
+    ::epos::datadir EV(data_path_ + "epos.iniev");
+    strcpy(::epos::fname_.fnie, EV.data);
+    ::epos::nfname_.nfnie = EV.length;
+
+    ::epos::datadir RJ(data_path_ + "epos.inirj"); // lhcparameters adds ".lhc"
+    strcpy(::epos::fname_.fnrj, RJ.data);
+    ::epos::nfname_.nfnrj = RJ.length;
+
+    ::epos::datadir CS(data_path_ + "epos.inics"); // lhcparameters adds ".lhc"
+    strcpy(::epos::fname_.fncs, CS.data);
+    ::epos::nfname_.nfncs = CS.length;
+
+    // initialiazes maximum energy and mass
+    initializeEventLab(Code::Lead, Lead::nucleus_A, Lead::nucleus_Z, Code::Lead,
+                       Lead::nucleus_A, Lead::nucleus_Z, 1_TeV);
+  }
+
+  inline void InteractionModel::initializeEventCoM(Code const idBeam, int const iBeamA,
+                                                   int const iBeamZ, Code const idTarget,
+                                                   int const iTargetA, int const iTargetZ,
+                                                   HEPEnergyType const Ecm) const {
+    CORSIKA_LOGGER_TRACE(logger_,
+                         "initialize event in CoM frame!"
+                         " Ecm={}",
+                         Ecm);
+    ::epos::lept1_.engy = -1.;
+    ::epos::enrgy_.ecms = -1.;
+    ::epos::enrgy_.elab = -1.;
+    ::epos::enrgy_.ekin = -1.;
+    ::epos::hadr1_.pnll = -1.;
+
+    ::epos::enrgy_.ecms = Ecm / 1_GeV; // -> c.m.s. frame
+
+    CORSIKA_LOGGER_TRACE(logger_, "inside EPOS: Ecm={}, Elab={}", ::epos::enrgy_.ecms,
+                         ::epos::enrgy_.elab);
+
+    configureParticles(idBeam, iBeamA, iBeamZ, idTarget, iTargetA, iTargetZ);
+    ::epos::ainit_();
+  }
+
+  inline void InteractionModel::initializeEventLab(Code const idBeam, int const iBeamA,
+                                                   int const iBeamZ, Code const idTarget,
+                                                   int const iTargetA, int const iTargetZ,
+                                                   HEPEnergyType const Plab) const {
+    CORSIKA_LOGGER_TRACE(logger_,
+                         "initialize event in lab. frame!"
+                         " Plab per nuc={} GeV",
+                         Plab / 1_GeV);
+    ::epos::lept1_.engy = -1.;
+    ::epos::enrgy_.ecms = -1.;
+    ::epos::enrgy_.elab = -1.;
+    ::epos::enrgy_.ekin = -1.;
+    ::epos::hadr1_.pnll = -1.;
+
+    // hadron-nucleon momentum
+    ::epos::hadr1_.pnll = float(Plab / 1_GeV); // -> lab frame
+
+    CORSIKA_LOGGER_TRACE(logger_, "inside EPOS: Ecm={}, Elab={}, Pnll={}",
+                         ::epos::enrgy_.ecms, ::epos::enrgy_.elab, ::epos::hadr1_.pnll);
+
+    configureParticles(idBeam, iBeamA, iBeamZ, idTarget, iTargetA, iTargetZ);
+    ::epos::ainit_();
+  }
+
+  inline void InteractionModel::configureParticles(Code const idBeam, int const iBeamA,
+                                                   int const iBeamZ, Code const idTarget,
+                                                   int const iTargetA,
+                                                   int const iTargetZ) const {
+    CORSIKA_LOGGER_TRACE(logger_,
+                         "setting "
+                         "Beam={}, "
+                         "BeamA={}, "
+                         "BeamZ={}, "
+                         "Target={}, "
+                         "TargetA={}, "
+                         "TargetZ={} ",
+                         idBeam, iBeamA, iBeamZ, idTarget, iTargetA, iTargetZ);
+
+    if (is_nucleus(idBeam)) {
+      ::epos::hadr25_.idprojin = convertToEposRaw(Code::Proton);
+      ::epos::nucl1_.laproj = iBeamZ;
+      ::epos::nucl1_.maproj = iBeamA;
+    } else {
+      ::epos::hadr25_.idprojin = convertToEposRaw(idBeam);
+      ::epos::nucl1_.laproj = -1;
+      ::epos::nucl1_.maproj = 1;
+    }
+
+    if (is_nucleus(idTarget)) {
+      ::epos::hadr25_.idtargin = convertToEposRaw(Code::Proton);
+      ::epos::nucl1_.matarg = iTargetA;
+      ::epos::nucl1_.latarg = iTargetZ;
+    } else if (idTarget == Code::Proton || idTarget == Code::Hydrogen) {
+      ::epos::hadr25_.idtargin = convertToEposRaw(Code::Proton);
+      ::epos::nucl1_.matarg = 1;
+      ::epos::nucl1_.latarg = -1;
+    } else if (idTarget == Code::Neutron) {
+      ::epos::hadr25_.idtargin = convertToEposRaw(Code::Neutron);
+      ::epos::nucl1_.matarg = 1;
+      ::epos::nucl1_.latarg = -1;
+    }
+
+    CORSIKA_LOGGER_TRACE(logger_,
+                         "inside EPOS: "
+                         "Id beam={}, "
+                         "Z beam={}, "
+                         "A beam={}, "
+                         "XS beam={}, "
+                         "Id target={}, "
+                         "Z target={}, "
+                         "A target={}, "
+                         "XS target={} ",
+                         ::epos::hadr25_.idprojin, ::epos::nucl1_.laproj,
+                         ::epos::nucl1_.maproj, ::epos::had10_.iclpro,
+                         ::epos::hadr25_.idtargin, ::epos::nucl1_.latarg,
+                         ::epos::nucl1_.matarg, ::epos::had10_.icltar);
+  }
+
+  inline InteractionModel::~InteractionModel() {
+    CORSIKA_LOGGER_DEBUG(logger_, "n={} ", count_);
+  }
+
+  inline std::tuple<CrossSectionType, CrossSectionType>
+  InteractionModel::calcCrossSectionCoM(Code const BeamId, int const BeamA,
+                                        int const BeamZ, Code const TargetId,
+                                        int const TargetA, int const TargetZ,
+                                        const HEPEnergyType EnergyCOM) const {
+    CORSIKA_LOGGER_DEBUG(logger_,
+                         "calcCrossSection: input:"
+                         " beamId={}, beamA={}, beamZ={}"
+                         " target={}, targetA={}, targetZ={}"
+                         " Ecm={:4.3f} GeV,",
+                         BeamId, BeamA, BeamZ, TargetId, TargetA, TargetZ,
+                         EnergyCOM / 1_GeV);
+
+    const int iBeam = epos::getEposXSCode(
+        BeamId); // 0 (can not interact, 1: proton-like, 2: pion-like, 3:kaon-like)
+
+    CORSIKA_LOGGER_TRACE(logger_,
+                         "projectile cross section type={} "
+                         "(0: cannot interact, 1:pion, 2:baryon, 3:kaon)",
+                         iBeam);
+    // reset beam particle // (1: pion-like, 2: proton-like, 3:kaon-like)
+    if (iBeam == 1)
+      initializeEventCoM(Code::PiPlus, BeamA, BeamZ, TargetId, TargetA, TargetZ,
+                         EnergyCOM);
+    else if (iBeam == 2)
+      initializeEventCoM(Code::Proton, BeamA, BeamZ, TargetId, TargetA, TargetZ,
+                         EnergyCOM);
+    else if (iBeam == 3)
+      initializeEventCoM(Code::KPlus, BeamA, BeamZ, TargetId, TargetA, TargetZ,
+                         EnergyCOM);
+
+    double sigProd, sigEla = 0;
+    float sigTot1, sigProd1, sigCut1 = 0;
+    if (!is_nucleus(TargetId) && !is_nucleus(BeamId)) {
+      sigProd = ::epos::hadr5_.sigine;
+      sigEla = ::epos::hadr5_.sigela;
+    } else {
+      // calculate from model, SLOW:
+      float sigQEla1 = 0; // target fragmentation/excitation
+      ::epos::crseaaepos_(sigTot1, sigProd1, sigCut1, sigQEla1);
+      sigProd = sigProd1;
+      // sigEla not properly defined here
+    }
+    CORSIKA_LOGGER_DEBUG(logger_,
+                         "calcCrossSectionCoM: output:"
+                         " sigProd={} mb,"
+                         " sigEla={} mb",
+                         sigProd, sigEla);
+
+    return std::make_tuple(sigProd * 1_mb, sigEla * 1_mb);
+  }
+
+  inline std::tuple<CrossSectionType, CrossSectionType>
+  InteractionModel::readCrossSectionTableLab(Code const BeamId, int const BeamA,
+                                             int const BeamZ, Code const TargetId,
+                                             HEPEnergyType const EnergyLab) const {
+    CORSIKA_LOGGER_DEBUG(logger_,
+                         "readCrossSectionTableLab: input: "
+                         "beamId={}, "
+                         "beamA={}, "
+                         "beamZ={} "
+                         "targetId={}, "
+                         "ELab={:4.3f} GeV,",
+                         BeamId, BeamA, BeamZ, TargetId, EnergyLab / 1_GeV);
+
+    // read cross section from epos internal tables
+    int Abeam = 0;
+    float Ekin = -1;
+
+    if (is_nucleus(BeamId)) {
+      Abeam = BeamA;
+      // kinetic energy per nucleon
+      Ekin = (EnergyLab / Abeam - constants::nucleonMass) / 1_GeV;
+    } else {
+      ::epos::hadr2_.idproj = convertToEposRaw(BeamId);
+      int const iBeam = epos::getEposXSCode(
+          BeamId); // 0 (can not interact, 1: pion-like, 2: proton-like, 3:kaon-like)
+      CORSIKA_LOGGER_TRACE(logger_,
+                           "projectile cross section type={} "
+                           "(0: cannot interact, 1:pion, 2:baryon, 3:kaon)",
+                           iBeam);
+
+      ::epos::had10_.iclpro = iBeam;
+      Abeam = 1;
+      Ekin = (EnergyLab - get_mass(BeamId)) / 1_GeV;
+    }
+
+    int Atarget = 1;
+    if (is_nucleus(TargetId)) { Atarget = get_nucleus_A(TargetId); }
+
+    int iMode = 3; // 0: air, >0 not air
+
+    CORSIKA_LOGGER_DEBUG(logger_,
+                         "inside Epos "
+                         "beamId={}, beamXS={}",
+                         ::epos::hadr2_.idproj, ::epos::had10_.iclpro);
+
+    // cross section from table, FAST
+    float sigProdEpos = ::epos::eposcrse_(Ekin, Abeam, Atarget, iMode);
+    // sig-el from analytic calculation, no fast
+    float sigElaEpos = ::epos::eposelacrse_(Ekin, Abeam, Atarget, iMode);
+
+    return std::make_tuple(sigProdEpos * 1_mb, sigElaEpos * 1_mb);
+  }
+
+  inline std::tuple<CrossSectionType, CrossSectionType>
+  InteractionModel::getCrossSectionInelEla(Code const projectileId, Code const targetId,
+                                           FourMomentum const& projectileP4,
+                                           FourMomentum const& targetP4) const {
+    auto const sqrtS2 = (projectileP4 + targetP4).getNormSqr();
+    auto const sqrtS = sqrt(sqrtS2);
+
+    if (!isValid(projectileId, targetId, sqrtS)) {
+      return {CrossSectionType::zero(), CrossSectionType::zero()};
+    }
+    HEPEnergyType const Elab = (sqrtS2 - static_pow<2>(get_mass(projectileId)) -
+                                static_pow<2>(get_mass(targetId))) /
+                               (2 * get_mass(targetId));
+    int beamA = 1;
+    int beamZ = 1;
+    if (is_nucleus(projectileId)) {
+      beamA = get_nucleus_A(projectileId);
+      beamZ = get_nucleus_Z(projectileId);
+    }
+
+    CORSIKA_LOGGER_DEBUG(logger_,
+                         "getCrossSectionLab: input:"
+                         " beamId={}, beamA={}, beamZ={}"
+                         " target={}"
+                         " ELab={:4.3f} GeV, sqrtS={}",
+                         projectileId, beamA, beamZ, targetId, Elab / 1_GeV,
+                         sqrtS / 1_GeV);
+    return readCrossSectionTableLab(projectileId, beamA, beamZ, targetId, Elab);
+  }
+
+  template <typename TSecondaryView>
+  inline void InteractionModel::doInteraction(TSecondaryView& view,
+                                              Code const projectileId,
+                                              Code const targetId,
+                                              FourMomentum const& projectileP4,
+                                              FourMomentum const& targetP4) {
+
+    count_ = count_ + 1;
+
+    // define projectile
+    // define projectile, in lab frame
+    auto const sqrtS2 = (projectileP4 + targetP4).getNormSqr();
+    auto const sqrtS = sqrt(sqrtS2);
+    if (!isValid(projectileId, targetId, sqrtS)) {
+      throw std::runtime_error("invalid projectile/target/energy combination.");
+    }
+    HEPEnergyType const Elab = (sqrtS2 - static_pow<2>(get_mass(projectileId)) -
+                                static_pow<2>(get_mass(targetId))) /
+                               (2 * get_mass(targetId));
+
+    // system of initial-state
+    COMBoost const boost(projectileP4, targetP4);
+
+    auto const& originalCS = boost.getOriginalCS();
+    auto const& csPrime =
+        boost.getRotatedCS(); // z is along the CM motion (projectile, in Cascade)
+
+    HEPMomentumType const pLabMag =
+        sqrt((Elab - get_mass(projectileId)) * (Elab + get_mass(projectileId)));
+    MomentumVector pLab(csPrime, {0_eV, 0_eV, pLabMag});
+
+    // internal EPOS lab system
+    COMBoost const boostInternal({Elab, pLab}, get_mass(targetId));
+
+    CORSIKA_LOGGER_DEBUG(logger_, "doInteraction: {} interaction, Elab={} ", projectileId,
+                         Elab);
+
+    int beamA = 1;
+    int beamZ = 1;
+    if (is_nucleus(projectileId)) {
+      beamA = get_nucleus_A(projectileId);
+      beamZ = get_nucleus_Z(projectileId);
+      CORSIKA_LOGGER_DEBUG(logger_, "A={}, Z={} ", beamA, beamZ);
+    }
+
+    HEPMomentumType const projectileMomentumLabPerNucleon = pLabMag / beamA;
+
+    // // from corsika7 interface
+    // // NEXLNK-part
+    int targetA = 1;
+    int targetZ = 1;
+    if (is_nucleus(targetId)) {
+      targetA = get_nucleus_A(targetId);
+      targetZ = get_nucleus_Z(targetId);
+    }
+    initializeEventLab(projectileId, beamA, beamZ, targetId, targetA, targetZ,
+                       projectileMomentumLabPerNucleon);
+
+    // create event
+    int iarg = 1;
+    ::epos::aepos_(iarg);
+    ::epos::afinal_();
+
+    if (epos_listing_) { // LCOV_EXCL_START
+      char nam[9] = "EPOSLHC&";
+      ::epos::alistf_(nam, 9);
+    } // LCOV_EXCL_STOP
+
+    // NSTORE-part
+
+    MomentumVector Plab_final(originalCS, {0.0_GeV, 0.0_GeV, 0.0_GeV});
+    HEPEnergyType Elab_final = 0_GeV;
+
+    // position and time of interaction, not used in QgsjetII
+    auto const& projectile = view.getProjectile();
+    Point const& pOrig = projectile.getPosition();
+    TimeType const tOrig = projectile.getTime();
+
+    // secondaries
+    EposStack es;
+    CORSIKA_LOGGER_DEBUG(logger_, "number of particles: {}", es.getSize());
+    for (auto& psec : es) {
+      if (!psec.isFinal()) continue;
+
+      auto momentum = psec.getMomentum(csPrime);
+      // this is not "CoM" here, but rather the system defined by projectile+target,
+      // which in Cascade-mode is already lab
+      auto const P4com = boostInternal.toCoM(FourVector{psec.getEnergy(), momentum});
+      auto const P4output = boost.fromCoM(P4com);
+      auto p3output = P4output.getSpaceLikeComponents();
+      p3output.rebase(originalCS); // transform back into standard lab frame
+
+      EposCode const eposId = psec.getPID();
+      Code const pid = epos::convertFromEpos(eposId);
+      CORSIKA_LOGGER_TRACE(logger_,
+                           " id= {}"
+                           " p= {}",
+                           pid, p3output.getComponents() / 1_GeV);
+
+      auto pnew = view.addSecondary(std::make_tuple(pid, p3output, pOrig, tOrig));
+      Plab_final += pnew.getMomentum();
+      Elab_final += pnew.getEnergy();
+    }
+    CORSIKA_LOGGER_DEBUG(
+        logger_,
+        "conservation (all GeV): Ecm_final= n/a" /* << Ecm_final / 1_GeV*/
+        ", Elab_final={}"
+        ", Plab_final={}",
+        Elab_final / 1_GeV, (Plab_final / 1_GeV).getComponents());
+  }
+} // namespace corsika::epos
diff --git a/corsika/detail/modules/proposal/Interaction.inl b/corsika/detail/modules/proposal/Interaction.inl
index c450594146f13a7afaa809c99d0f3354d9427b02..e81f9984b7b880ddd208556b087df0d0302b3d59 100644
--- a/corsika/detail/modules/proposal/Interaction.inl
+++ b/corsika/detail/modules/proposal/Interaction.inl
@@ -39,24 +39,26 @@ namespace corsika::proposal {
     auto c = p_cross->second(media.at(comp.getHash()), emCut);
 
     // Look which interactions take place and build the corresponding
-    // interaction and secondarie builder. The interaction integral will
+    // interaction and secondary builder. The interaction integral will
     // interpolated too and saved in the calc map by a key build out of a hash
     // of composed of the component and particle code.
     auto inter_types = PROPOSAL::CrossSectionVector::GetInteractionTypes(c);
-    calc[std::make_pair(comp.getHash(), code)] = std::make_tuple(
+    calc_[std::make_pair(comp.getHash(), code)] = std::make_tuple(
         PROPOSAL::make_secondaries(inter_types, particle[code], media.at(comp.getHash())),
         PROPOSAL::make_interaction(c, true));
   }
 
   template <typename TStackView>
-  inline ProcessReturn Interaction::doInteraction(TStackView& view) {
+  inline ProcessReturn Interaction::doInteraction(TStackView& view,
+                                                  Code const projectileId,
+                                                  FourMomentum const& projectileP4) {
 
     auto const projectile = view.getProjectile();
 
-    if (canInteract(projectile.getPID())) {
+    if (canInteract(projectileId)) {
 
       // get or build corresponding calculators
-      auto c = getCalculator(projectile, calc);
+      auto c = getCalculator(projectile, calc_);
 
       // get the rates of the interaction types for every component.
       std::uniform_real_distribution<double> distr(0., 1.);
@@ -103,14 +105,27 @@ namespace corsika::proposal {
   }
 
   template <typename TParticle>
-  inline GrammageType Interaction::getInteractionLength(TParticle const& projectile) {
-
-    if (canInteract(projectile.getPID())) {
-      auto c = getCalculator(projectile, calc);
-      return std::get<eINTERACTION>(c->second)->MeanFreePath(projectile.getEnergy() /
-                                                             1_MeV) *
-             1_g / (1_cm * 1_cm);
+  inline CrossSectionType Interaction::getCrossSection(TParticle const& projectile,
+                                                       Code const projectileId,
+                                                       FourMomentum const& projectileP4) {
+
+    // ==============================================
+    // this block better diappears. RU 26.10.2021
+    //
+    // determine the volume where the particle is (last) known to be
+    auto const* currentLogicalNode = projectile.getNode();
+    NuclearComposition const& composition =
+        currentLogicalNode->getModelProperties().getNuclearComposition();
+    auto const meanMass = composition.getAverageMassNumber() * constants::u;
+    // ==============================================
+
+    if (canInteract(projectileId)) {
+      auto c = getCalculator(projectile, calc_);
+      return meanMass / (std::get<eINTERACTION>(c->second)->MeanFreePath(
+                             projectileP4.getTimeLikeComponent() / 1_MeV) *
+                         1_g / (1_cm * 1_cm));
     }
-    return std::numeric_limits<double>::infinity() * 1_g / (1_cm * 1_cm);
+
+    return CrossSectionType::zero();
   }
 } // namespace corsika::proposal
diff --git a/corsika/detail/modules/pythia8/Interaction.inl b/corsika/detail/modules/pythia8/Interaction.inl
index 9e13f4de82834de41ad14f411583169ec811a79f..fe55d587412537f86dff4ca4f0ca0f9dd5ef99ab 100644
--- a/corsika/detail/modules/pythia8/Interaction.inl
+++ b/corsika/detail/modules/pythia8/Interaction.inl
@@ -80,22 +80,38 @@ namespace corsika::pythia8 {
     Pythia8::Pythia::particleData.mayDecay(static_cast<int>(get_PDG(pCode)), false);
   }
 
-  inline void Interaction::configureLabFrameCollision(Code const BeamId,
-                                                      Code const TargetId,
+  inline bool Interaction::isValid(Code const projectileId, Code const targetId,
+                                   HEPEnergyType const sqrtS) const {
+
+    if ((10_GeV > sqrtS) || (sqrtS > 1_PeV)) { return false; }
+
+    if (targetId != Code::Hydrogen && targetId != Code::Neutron &&
+        targetId != Code::Proton) {
+      return false;
+    }
+
+    if (is_nucleus(projectileId)) { return false; }
+
+    if (!canInteract(projectileId)) { return false; }
+    return true;
+  }
+
+  inline void Interaction::configureLabFrameCollision(Code const projectileId,
+                                                      Code const targetId,
                                                       HEPEnergyType const BeamEnergy) {
     // Pythia configuration of the current event
     // very clumsy. I am sure this can be done better..
 
     // set beam
     // beam id for pythia
-    auto const pdgBeam = static_cast<int>(get_PDG(BeamId));
+    auto const pdgBeam = static_cast<int>(get_PDG(projectileId));
     std::stringstream stBeam;
     stBeam << "Beams:idA = " << pdgBeam;
     Pythia8::Pythia::readString(stBeam.str());
     // set target
-    auto pdgTarget = static_cast<int>(get_PDG(TargetId));
+    auto pdgTarget = static_cast<int>(get_PDG(targetId));
     // replace hydrogen with proton, otherwise pythia goes into heavy ion mode!
-    if (TargetId == Code::Hydrogen) pdgTarget = static_cast<int>(get_PDG(Code::Proton));
+    if (targetId == Code::Hydrogen) pdgTarget = static_cast<int>(get_PDG(Code::Proton));
     std::stringstream stTarget;
     stTarget << "Beams:idB = " << pdgTarget;
     Pythia8::Pythia::readString(stTarget.str());
@@ -116,276 +132,133 @@ namespace corsika::pythia8 {
     // LCOV_EXCL_STOP
   }
 
-  inline bool Interaction::canInteract(Code const pCode) {
+  inline bool Interaction::canInteract(Code const pCode) const {
     return pCode == Code::Proton || pCode == Code::Neutron || pCode == Code::AntiProton ||
            pCode == Code::AntiNeutron || pCode == Code::PiMinus || pCode == Code::PiPlus;
   }
 
-  inline std::tuple<CrossSectionType, CrossSectionType> Interaction::getCrossSection(
-      Code const BeamId, Code const TargetId, HEPEnergyType const CoMenergy) {
-    // interaction possible in pythia?
-    if (TargetId == Code::Proton || TargetId == Code::Hydrogen) {
-      if (canInteract(BeamId) && isValidCoMEnergy(CoMenergy)) {
-        // input particle PDG
-        auto const pdgCodeBeam = static_cast<int>(get_PDG(BeamId));
-        auto const pdgCodeTarget = static_cast<int>(get_PDG(TargetId));
-        double const ecm = CoMenergy / 1_GeV;
-
-        // calculate cross section
-        sigma_.calc(pdgCodeBeam, pdgCodeTarget, ecm);
-        if (sigma_.hasSigmaTot()) {
-          double const sigEla = sigma_.sigmaEl();
-          double const sigProd = sigma_.sigmaTot() - sigEla;
-
-          return std::make_tuple(sigProd * (1_fm * 1_fm), sigEla * (1_fm * 1_fm));
-
-        } else {
-          // we can't test pythia8 internals, LCOV_EXCL_START
-          throw std::runtime_error("pythia cross section init failed");
-          // we can't test pythia8 internals, LCOV_EXCL_STOP
-        }
-      } else {
-        return std::make_tuple(std::numeric_limits<double>::infinity() * 1_mb,
-                               std::numeric_limits<double>::infinity() * 1_mb);
-      }
-    } else {
-      throw std::runtime_error("invalid target for pythia");
-    }
-  }
+  inline std::tuple<CrossSectionType, CrossSectionType>
+  Interaction::getCrossSectionInelEla(Code const projectileId, Code const targetId,
+                                      FourMomentum const& projectileP4,
+                                      FourMomentum const& targetP4) const {
 
-  template <typename TParticle>
-  inline GrammageType Interaction::getInteractionLength(TParticle const& particle) {
+    HEPEnergyType const CoMenergy = (projectileP4 + targetP4).getNorm();
 
-    // coordinate system, get global frame of reference
-    MomentumVector const& pMomentum = particle.getMomentum();
-    CoordinateSystemPtr const& labCS = pMomentum.getCoordinateSystem();
+    if (!isValid(projectileId, targetId, CoMenergy)) {
+      return {CrossSectionType::zero(), CrossSectionType::zero()};
+    }
 
-    Code corsikaBeamId = particle.getPID();
+    // input particle PDG
+    auto const pdgCodeBeam = static_cast<int>(get_PDG(projectileId));
+    auto const pdgCodeTarget = static_cast<int>(get_PDG(targetId));
+    double const ecm = CoMenergy / 1_GeV;
 
-    // beam particles for pythia : 1, 2, 3 for p, pi, k
-    // read from cross section code table
-    bool const kInteraction = canInteract(corsikaBeamId);
+    //! @todo: remove this const_cast, when Pythia8 becomes const-correct! CHECK!
+    Pythia8::SigmaTotal& sigma = *const_cast<Pythia8::SigmaTotal*>(&sigma_);
 
-    // FOR NOW: assume target is at rest
-    MomentumVector pTarget(labCS, {0_GeV, 0_GeV, 0_GeV});
+    // calculate cross section
+    sigma.calc(pdgCodeBeam, pdgCodeTarget, ecm);
+    if (sigma.hasSigmaTot()) {
+      double const sigEla = sigma.sigmaEl();
+      double const sigProd = sigma.sigmaTot() - sigEla;
 
-    // total momentum and energy
-    HEPEnergyType Elab = particle.getEnergy() + constants::nucleonMass;
-    MomentumVector pTotLab(labCS, {0_GeV, 0_GeV, 0_GeV});
-    pTotLab += pMomentum;
-    pTotLab += pTarget;
-    auto const pTotLabNorm = pTotLab.getNorm();
-    // calculate cm. energy
-    HEPEnergyType const ECoM = sqrt(
-        (Elab + pTotLabNorm) * (Elab - pTotLabNorm)); // binomial for numerical accuracy
+      return std::make_tuple(sigProd * (1_fm * 1_fm), sigEla * (1_fm * 1_fm));
 
-    CORSIKA_LOG_DEBUG(
-        "Interaction: LambdaInt: \n"
-        " input energy: {} GeV"
-        " beam can interact: {}"
-        " beam pid: {}",
-        particle.getEnergy() / 1_GeV, kInteraction, particle.getPID());
-
-    // TODO: move limits into variables
-    if (kInteraction && Elab >= 8.5_GeV && isValidCoMEnergy(ECoM)) {
-
-      // get target from environment
-      /*
-        the target should be defined by the Environment,
-        ideally as full particle object so that the four momenta
-        and the boosts can be defined..
-      */
-      auto const* currentNode = particle.getNode();
-      auto const mediumComposition =
-          currentNode->getModelProperties().getNuclearComposition();
-      // determine average interaction length
-
-      auto const weightedProdCrossSection =
-          mediumComposition.getWeightedSum([=](auto vTargetID) {
-            return std::get<0>(this->getCrossSection(corsikaBeamId, vTargetID, ECoM));
-          });
-
-      CORSIKA_LOG_DEBUG(
-          "Interaction: IntLength: weighted CrossSection (mb): {} "
-          "Interaction: IntLength: average mass number: {} ",
-          weightedProdCrossSection / 1_mb, mediumComposition.getAverageMassNumber());
-
-      // calculate interaction length in medium
-      GrammageType const int_length = mediumComposition.getAverageMassNumber() *
-                                      constants::u / weightedProdCrossSection;
-      CORSIKA_LOG_DEBUG("Interaction: interaction length (g/cm2): {} ",
-                        int_length / (0.001_kg) * 1_cm * 1_cm);
-
-      return int_length;
+    } else {
+      // we can't test pythia8 internals, LCOV_EXCL_START
+      throw std::runtime_error("pythia cross section init failed");
+      // we can't test pythia8 internals, LCOV_EXCL_STOP
     }
-
-    return std::numeric_limits<double>::infinity() * 1_g / (1_cm * 1_cm);
   }
 
   template <class TView>
-  inline void Interaction::doInteraction(TView& view) {
+  inline void Interaction::doInteraction(TView& view, Code const projectileId,
+                                         Code const targetId,
+                                         FourMomentum const& projectileP4,
+                                         FourMomentum const& targetP4) {
 
     auto projectile = view.getProjectile();
 
-    const auto corsikaBeamId = projectile.getPID();
     CORSIKA_LOG_DEBUG(
         "Pythia::Interaction: "
-        "DoInteraction: {} interaction? ",
-        corsikaBeamId, corsika::pythia8::Interaction::canInteract(corsikaBeamId));
+        "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.");
+    }
+
+    // position and time of interaction
+    Point const& pOrig = projectile.getPosition();
+    TimeType const tOrig = projectile.getTime();
+
+    CORSIKA_LOG_DEBUG("Interaction: ebeam lab: {} GeV", eProjectileLab / 1_GeV);
 
-    if (is_nucleus(corsikaBeamId)) {
-      // nuclei handled by different process, this should not happen
-      throw std::runtime_error("Nuclear projectile are not handled by PYTHIA!");
+    // define target kinematics in lab frame
+    // define boost to and from CoM frame
+    // CoM frame definition in Pythia projectile: +z
+    COMBoost const boost(projectileP4, constants::nucleonMass);
+    auto const& labCS = boost.getOriginalCS();
+
+    CORSIKA_LOG_DEBUG("Interaction: position of interaction: ", pOrig.getCoordinates());
+    CORSIKA_LOG_DEBUG("Interaction: time: {}", tOrig);
+
+    CORSIKA_LOG_DEBUG(
+        "Interaction: "
+        " doInteraction: E(GeV): {}"
+        " Ecm(GeV): {}",
+        eProjectileLab / 1_GeV, sqrtS / 1_GeV);
+
+    count_++;
+
+    configureLabFrameCollision(projectileId, targetId, eProjectileLab);
+
+    // create event in pytia. LCOV_EXCL_START: we don't validate pythia8 internals
+    if (!Pythia8::Pythia::next())
+      throw std::runtime_error("Pythia::DoInteraction: failed!");
+    // LCOV_EXCL_STOP
+
+    // link to pythia stack
+    Pythia8::Event& event = Pythia8::Pythia::event;
+
+    // LCOV_EXCL_START, we don't validate pythia8 internals
+    if (print_listing_) {
+      // print final state
+      event.list();
     }
+    // LCOV_EXCL_STOP
+
+    MomentumVector Plab_final(labCS, {0.0_GeV, 0.0_GeV, 0.0_GeV});
+    HEPEnergyType Elab_final = 0_GeV;
+    for (int i = 0; i < event.size(); ++i) {
+      Pythia8::Particle const& p8p = event[i];
+      // skip particles that have decayed in pythia
+      if (!p8p.isFinal()) continue;
+
+      auto const pyId = convert_from_PDG(static_cast<PDGCode>(p8p.id()));
 
-    if (corsika::pythia8::Interaction::canInteract(corsikaBeamId)) {
-
-      // define projectile
-      HEPEnergyType const eProjectileLab = projectile.getEnergy();
-      auto const pProjectileLab = projectile.getMomentum();
-      CoordinateSystemPtr const& labCS = pProjectileLab.getCoordinateSystem();
-
-      // position and time of interaction, not used in Sibyll
-      Point pOrig = projectile.getPosition();
-      TimeType tOrig = projectile.getTime();
-
-      // define target
-      // FOR NOW: target is always at rest
-      auto const eTargetLab = 0_GeV + constants::nucleonMass;
-      auto const pTargetLab = MomentumVector(labCS, 0_GeV, 0_GeV, 0_GeV);
-      FourVector const PtargLab(eTargetLab, pTargetLab);
-
-      CORSIKA_LOG_DEBUG(
-          "Interaction: ebeam lab: {} GeV"
-          "Interaction: pbeam lab: {} GeV",
-          eProjectileLab / 1_GeV, pProjectileLab.getComponents() / 1_GeV);
-
-      CORSIKA_LOG_DEBUG(
-          "Interaction: etarget lab: {} GeV"
-          "Interaction: ptarget lab: {} GeV ",
-          eTargetLab / 1_GeV, pTargetLab.getComponents() / 1_GeV);
-
-      FourVector const PprojLab(eProjectileLab, pProjectileLab);
-
-      // define target kinematics in lab frame
-      // define boost to and from CoM frame
-      // CoM frame definition in Pythia projectile: +z
-      COMBoost const boost(PprojLab, constants::nucleonMass);
-
-      // just for show:
-      // boost projecticle
-      auto const PprojCoM = boost.toCoM(PprojLab);
-
-      // boost target
-      auto const PtargCoM = boost.toCoM(PtargLab);
-
-      CORSIKA_LOG_DEBUG(
-          "Interaction: ebeam CoM: {} GeV"
-          "Interaction: pbeam CoM: {} GeV",
-          PprojCoM.getTimeLikeComponent() / 1_GeV,
-          PprojCoM.getSpaceLikeComponents().getComponents() / 1_GeV);
-
-      CORSIKA_LOG_DEBUG(
-          "Interaction: etarget CoM: {} GeV"
-          "Interaction: ptarget CoM: {} GeV",
-          PtargCoM.getTimeLikeComponent() / 1_GeV,
-          PtargCoM.getSpaceLikeComponents().getComponents() / 1_GeV);
-
-      CORSIKA_LOG_DEBUG("Interaction: position of interaction: ", pOrig.getCoordinates());
-      CORSIKA_LOG_DEBUG("Interaction: time: {}", tOrig);
-
-      HEPEnergyType Etot = eProjectileLab + eTargetLab;
-      MomentumVector Ptot = projectile.getMomentum();
-      // invariant mass, i.e. cm. energy
-      HEPEnergyType Ecm = sqrt(Etot * Etot - Ptot.getSquaredNorm());
-
-      // sample target mass number
-      auto const* currentNode = projectile.getNode();
-      auto const& mediumComposition =
-          currentNode->getModelProperties().getNuclearComposition();
-      // get cross sections for target materials
-      /*
-        Here we read the cross section from the interaction model again,
-        should be passed from getInteractionLength if possible
-       */
-      //#warning reading interaction cross section again, should not be necessary
-      auto const& compVec = mediumComposition.getComponents();
-      std::vector<si::CrossSectionType> cross_section_of_components(compVec.size());
-
-      for (size_t i = 0; i < compVec.size(); ++i) {
-        auto const targetId = compVec[i];
-        auto const [sigProd, sigEla] = getCrossSection(corsikaBeamId, targetId, Ecm);
-        [[maybe_unused]] auto const& dummy_sigEla = sigEla;
-        cross_section_of_components[i] = sigProd;
-      }
-
-      auto const corsikaTargetId =
-          mediumComposition.sampleTarget(cross_section_of_components, RNG_);
-      CORSIKA_LOG_DEBUG("Interaction: target selected: {}", corsikaTargetId);
-
-      if (corsikaTargetId != Code::Hydrogen && corsikaTargetId != Code::Neutron &&
-          corsikaTargetId != Code::Proton)
-        throw std::runtime_error("DoInteraction: wrong target for PYTHIA");
-
-      CORSIKA_LOG_DEBUG(
-          "Interaction: "
-          " DoInteraction: E(GeV): {}"
-          " Ecm(GeV): {}",
-          eProjectileLab / 1_GeV, Ecm / 1_GeV);
-
-      if (eProjectileLab < 8.5_GeV || !isValidCoMEnergy(Ecm)) {
-        CORSIKA_LOG_DEBUG(
-            "Interaction: "
-            " DoInteraction: should have dropped particle.. "
-            "THIS IS AN ERROR");
-        throw std::runtime_error("energy too low for PYTHIA");
-
-      } else {
-        count_++;
-
-        configureLabFrameCollision(corsikaBeamId, corsikaTargetId, eProjectileLab);
-
-        // create event in pytia. LCOV_EXCL_START: we don't validate pythia8 internals
-        if (!Pythia8::Pythia::next())
-          throw std::runtime_error("Pythia::DoInteraction: failed!");
-        // LCOV_EXCL_STOP
-
-        // link to pythia stack
-        Pythia8::Event& event = Pythia8::Pythia::event;
-
-        // LCOV_EXCL_START, we don't validate pythia8 internals
-        if (print_listing_) {
-          // print final state
-          event.list();
-        }
-        // LCOV_EXCL_STOP
-
-        MomentumVector Plab_final(labCS, {0.0_GeV, 0.0_GeV, 0.0_GeV});
-        HEPEnergyType Elab_final = 0_GeV;
-        for (int i = 0; i < event.size(); ++i) {
-          Pythia8::Particle& p8p = event[i];
-          // skip particles that have decayed in pythia
-          if (!p8p.isFinal()) continue;
-
-          auto const pyId = convert_from_PDG(static_cast<PDGCode>(p8p.id()));
-
-          MomentumVector const pyPlab(
-              labCS, {p8p.px() * 1_GeV, p8p.py() * 1_GeV, p8p.pz() * 1_GeV});
-
-          // add to corsika stack
-          auto pnew =
-              projectile.addSecondary(std::make_tuple(pyId, pyPlab, pOrig, tOrig));
-
-          Plab_final += pnew.getMomentum();
-          Elab_final += pnew.getEnergy();
-        }
-        CORSIKA_LOG_DEBUG(
-            "conservation (all GeV): "
-            "Elab_final= {}"
-            ", Plab_final= {}",
-            Elab_final / 1_GeV, (Plab_final / 1_GeV).getComponents());
-      }
+      MomentumVector const pyPlab(labCS,
+                                  {p8p.px() * 1_GeV, p8p.py() * 1_GeV, p8p.pz() * 1_GeV});
+
+      // add to corsika stack
+      auto pnew = projectile.addSecondary(std::make_tuple(pyId, pyPlab, pOrig, tOrig));
+
+      Plab_final += pnew.getMomentum();
+      Elab_final += pnew.getEnergy();
     }
+
+    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/Interaction.inl b/corsika/detail/modules/qgsjetII/Interaction.inl
deleted file mode 100644
index c3147dbb83b00aff24e6f909220ff88586360ed4..0000000000000000000000000000000000000000
--- a/corsika/detail/modules/qgsjetII/Interaction.inl
+++ /dev/null
@@ -1,397 +0,0 @@
-/*
- * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
- *
- * 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/modules/qgsjetII/Interaction.hpp>
-
-#include <corsika/media/Environment.hpp>
-#include <corsika/media/NuclearComposition.hpp>
-#include <corsika/framework/geometry/QuantityVector.hpp>
-#include <corsika/framework/geometry/FourVector.hpp>
-#include <corsika/modules/qgsjetII/ParticleConversion.hpp>
-#include <corsika/modules/qgsjetII/QGSJetIIFragmentsStack.hpp>
-#include <corsika/modules/qgsjetII/QGSJetIIStack.hpp>
-#include <corsika/framework/utility/COMBoost.hpp>
-
-#include <sstream>
-#include <string>
-#include <tuple>
-
-#include <qgsjet-II-04.hpp>
-
-namespace corsika::qgsjetII {
-
-  inline Interaction::Interaction(boost::filesystem::path dataPath) {
-    CORSIKA_LOG_DEBUG("Reading QGSJetII data tables from {}", dataPath);
-
-    // initialize QgsjetII
-    static bool initialized = false;
-    if (!initialized) {
-      qgset_();
-      datadir DIR(dataPath.string() + "/");
-      qgaini_(DIR.data);
-      initialized = true;
-    }
-  }
-
-  inline Interaction::~Interaction() {
-    CORSIKA_LOG_DEBUG("QgsjetII::Interaction n= {}", count_);
-  }
-
-  inline CrossSectionType Interaction::getCrossSection(const Code beamId,
-                                                       const Code targetId,
-                                                       const HEPEnergyType Elab,
-                                                       const unsigned int Abeam,
-                                                       const unsigned int targetA) const {
-    double sigProd = std::numeric_limits<double>::infinity();
-
-    if (corsika::qgsjetII::canInteract(beamId)) {
-
-      int const iBeam = static_cast<QgsjetIIXSClassIntType>(
-          corsika::qgsjetII::getQgsjetIIXSCode(beamId));
-      int iTarget = 1;
-      if (is_nucleus(targetId)) {
-        iTarget = targetA;
-        if (iTarget > int(maxMassNumber_) || iTarget <= 0) {
-          std::ostringstream txt;
-          txt << "QgsjetII target outside range. Atarget=" << iTarget;
-          throw std::runtime_error(txt.str().c_str());
-        }
-      }
-      int iProjectile = 1;
-      if (is_nucleus(beamId)) {
-        iProjectile = Abeam;
-        if (iProjectile > int(maxMassNumber_) || iProjectile <= 0) {
-          std::ostringstream txt;
-          txt << "QgsjetII projectile outside range. Aprojectile=" << iProjectile;
-          throw std::runtime_error(txt.str().c_str());
-        }
-      }
-
-      CORSIKA_LOG_DEBUG(
-          "QgsjetII::getCrossSection Elab= {} GeV iBeam= {}"
-          " iProjectile= {} iTarget= {}",
-          Elab / 1_GeV, iBeam, iProjectile, iTarget);
-      sigProd = qgsect_(Elab / 1_GeV, iBeam, iProjectile, iTarget);
-      CORSIKA_LOG_DEBUG("QgsjetII::getCrossSection sigProd= {} mb", sigProd);
-    }
-
-    return sigProd * 1_mb;
-  }
-
-  template <typename TParticle>
-  inline GrammageType Interaction::getInteractionLength(const TParticle& particle) const {
-
-    // coordinate system, get global frame of reference
-    CoordinateSystemPtr const& rootCS = get_root_CoordinateSystem();
-
-    const Code corsikaBeamId = particle.getPID();
-
-    // beam particles for qgsjetII : 1, 2, 3 for p, pi, k
-    // read from cross section code table
-    const bool kInteraction = corsika::qgsjetII::canInteract(corsikaBeamId);
-
-    // FOR NOW: assume target is at rest
-    MomentumVector pTarget(rootCS, {0_GeV, 0_GeV, 0_GeV});
-
-    // total momentum and energy
-    HEPEnergyType const Elab = particle.getEnergy();
-
-    CORSIKA_LOG_DEBUG(
-        "Interaction: LambdaInt: \n"
-        " input energy: {} GeV"
-        " beam can interact: {}"
-        " beam pid: {}",
-        particle.getEnergy() / 1_GeV, kInteraction, corsikaBeamId);
-
-    if (kInteraction) {
-
-      int Abeam = 0;
-      if (is_nucleus(corsikaBeamId)) Abeam = get_nucleus_A(corsikaBeamId);
-
-      // get target from environment
-      /*
-        the target should be defined by the Environment,
-        ideally as full particle object so that the four momenta
-        and the boosts can be defined..
-      */
-
-      auto const* currentNode = particle.getNode();
-      const auto& mediumComposition =
-          currentNode->getModelProperties().getNuclearComposition();
-
-      CrossSectionType weightedProdCrossSection =
-          mediumComposition.getWeightedSum([=](Code targetID) -> CrossSectionType {
-            int targetA = 0;
-            if (is_nucleus(targetID)) targetA = get_nucleus_A(targetID);
-            return getCrossSection(corsikaBeamId, targetID, Elab, Abeam, targetA);
-          });
-
-      CORSIKA_LOG_DEBUG(
-          "Interaction: "
-          "IntLength: weighted CrossSection (mb): {}",
-          weightedProdCrossSection / 1_mb);
-
-      // calculate interaction length in medium
-      GrammageType const int_length = mediumComposition.getAverageMassNumber() *
-                                      constants::u / weightedProdCrossSection;
-      CORSIKA_LOG_DEBUG(
-          "Interaction: "
-          "interaction length (g/cm2): {}",
-          int_length / (0.001_kg) * 1_cm * 1_cm);
-
-      return int_length;
-    }
-
-    return std::numeric_limits<double>::infinity() * 1_g / (1_cm * 1_cm);
-  }
-
-  /**
-     In this function QGSJETII is called to produce one event. The
-     event is copied (and boosted) into the shower lab frame.
-   */
-
-  template <typename TView>
-  inline void Interaction::doInteraction(TView& view) {
-
-    auto const projectile = view.getProjectile();
-    auto const corsikaBeamId = projectile.getPID();
-    CORSIKA_LOG_DEBUG(
-        "ProcessQgsjetII: "
-        "doInteraction: {} interaction possible? {}",
-        corsikaBeamId, corsika::qgsjetII::canInteract(corsikaBeamId));
-
-    if (!corsika::qgsjetII::canInteract(corsikaBeamId)) return;
-
-    CoordinateSystemPtr const& rootCS = get_root_CoordinateSystem();
-
-    // position and time of interaction, not used in QgsjetII
-    Point const pOrig = projectile.getPosition();
-    TimeType const tOrig = projectile.getTime();
-
-    // define target
-    // for QgsjetII is always a single nucleon
-    // FOR NOW: target is always at rest
-    auto const targetEnergyLab = 0_GeV + constants::nucleonMass;
-    auto const targetMomentumLab = MomentumVector(rootCS, 0_GeV, 0_GeV, 0_GeV);
-    FourVector const PtargLab(targetEnergyLab, targetMomentumLab);
-
-    // define projectile
-    HEPEnergyType const projectileEnergyLab = projectile.getEnergy();
-    auto const projectileMomentumLab = projectile.getMomentum();
-
-    int beamA = 0;
-    if (is_nucleus(corsikaBeamId)) beamA = get_nucleus_A(corsikaBeamId);
-
-    HEPEnergyType const projectileEnergyLabPerNucleon = projectileEnergyLab / beamA;
-
-    CORSIKA_LOG_DEBUG(
-        "ebeam lab: {} GeV "
-        "pbeam lab: {} GeV ",
-        projectileEnergyLab / 1_GeV, projectileMomentumLab.getComponents() / 1_GeV);
-    CORSIKA_LOG_DEBUG(
-        "etarget lab: {} GeV "
-        "ptarget lab: {} GeV ",
-        targetEnergyLab / 1_GeV, targetMomentumLab.getComponents() / 1_GeV);
-    CORSIKA_LOG_DEBUG("position of interaction: {}", pOrig.getCoordinates());
-    CORSIKA_LOG_DEBUG("time: {} ", tOrig);
-
-    // sample target mass number
-    auto const* currentNode = projectile.getNode();
-    auto const& mediumComposition =
-        currentNode->getModelProperties().getNuclearComposition();
-    // get cross sections for target materials
-    /*
-      Here we read the cross section from the interaction model again,
-      should be passed from getInteractionLength if possible
-     */
-    auto const& compVec = mediumComposition.getComponents();
-    std::vector<CrossSectionType> cross_section_of_components(compVec.size());
-
-    for (size_t i = 0; i < compVec.size(); ++i) {
-      auto const targetId = compVec[i];
-      int targetA = 0;
-      if (is_nucleus(targetId)) targetA = get_nucleus_A(targetId);
-      const auto sigProd =
-          getCrossSection(corsikaBeamId, targetId, projectileEnergyLab, beamA, targetA);
-      cross_section_of_components[i] = sigProd;
-    }
-
-    const auto targetCode =
-        mediumComposition.sampleTarget(cross_section_of_components, rng_);
-
-    int targetMassNumber = 1;     // proton
-    if (is_nucleus(targetCode)) { // nucleus
-      targetMassNumber = get_nucleus_A(targetCode);
-      if (targetMassNumber > int(maxMassNumber_))
-        throw std::runtime_error(
-            "QgsjetII target mass outside range."); // LCOV_EXCL_LINE there is no
-                                                    // allowed path here
-    } else {
-      if (targetCode != Proton::code) // LCOV_EXCL_LINE there is no allowed path here
-        throw std::runtime_error(
-            "QgsjetII Taget not possible."); // LCOV_EXCL_LINE there is no allowed path
-                                             // here
-    }
-    CORSIKA_LOG_DEBUG("target: {}, qgsjetII code/A: {}", targetCode, targetMassNumber);
-
-    int projectileMassNumber = 1; // "1" means "hadron"
-    QgsjetIIHadronType qgsjet_hadron_type =
-        qgsjetII::getQgsjetIIHadronType(corsikaBeamId);
-    if (qgsjet_hadron_type == QgsjetIIHadronType::NucleusType) {
-      projectileMassNumber = get_nucleus_A(corsikaBeamId);
-      if (projectileMassNumber > int(maxMassNumber_))
-        throw std::runtime_error(
-            "QgsjetII projectile mass outside range."); // LCOV_EXCL_LINE there is no
-                                                        // allowed path here
-      std::array<QgsjetIIHadronType, 2> constexpr nucleons = {
-          QgsjetIIHadronType::ProtonType, QgsjetIIHadronType::NeutronType};
-      std::uniform_int_distribution select(0, 1);
-      qgsjet_hadron_type = nucleons[select(rng_)];
-    } else {
-      // from conex: replace pi0 or rho0 with pi+/pi- in alternating sequence
-      if (qgsjet_hadron_type == QgsjetIIHadronType::NeutralLightMesonType) {
-        qgsjet_hadron_type = alternate_;
-        alternate_ = (alternate_ == QgsjetIIHadronType::PiPlusType
-                          ? QgsjetIIHadronType::PiMinusType
-                          : QgsjetIIHadronType::PiPlusType);
-      }
-    }
-
-    // beam id for qgsjetII
-    int kBeam = 2; // default: proton Shouldn't we randomize neutron/proton for nuclei?
-    if (!is_nucleus(corsikaBeamId)) {
-      kBeam = corsika::qgsjetII::convertToQgsjetIIRaw(corsikaBeamId);
-      // from conex
-      if (kBeam == 0) { // replace pi0 or rho0 with pi+/pi-
-        static int select = 1;
-        kBeam = select;
-        select *= -1;
-      }
-      // replace lambda by neutron
-      if (kBeam == 6)
-        kBeam = 3;
-      else if (kBeam == -6)
-        kBeam = -3;
-      // else if (abs(kBeam)>6) -> throw
-    }
-
-    count_++;
-    int qgsjet_hadron_type_int = static_cast<QgsjetIICodeIntType>(qgsjet_hadron_type);
-    CORSIKA_LOG_DEBUG(
-        "qgsjet_hadron_type_int={} projectileMassNumber={} targetMassNumber={}",
-        qgsjet_hadron_type_int, projectileMassNumber, targetMassNumber);
-    qgini_(projectileEnergyLab / 1_GeV, qgsjet_hadron_type_int, projectileMassNumber,
-           targetMassNumber);
-    qgconf_();
-
-    // bookkeeping
-    MomentumVector Plab_final(rootCS, {0.0_GeV, 0.0_GeV, 0.0_GeV});
-    HEPEnergyType Elab_final = 0_GeV;
-
-    // to read the secondaries
-    // define rotation to and from CoM frame
-    // CoM frame definition in QgsjetII projectile: +z
-    auto const& originalCS = projectileMomentumLab.getCoordinateSystem();
-    CoordinateSystemPtr const zAxisFrame =
-        make_rotationToZ(originalCS, projectileMomentumLab);
-
-    // fragments
-    QGSJetIIFragmentsStack qfs;
-    for (auto& fragm : qfs) {
-      int const A = fragm.getFragmentSize();
-      if (A == 1) { // nucleon
-        std::uniform_real_distribution<double> select;
-        Code idFragm = Code::Proton;
-        if (select(rng_) > 0.5) { idFragm = Code::Neutron; }
-
-        const HEPMassType nucleonMass = get_mass(idFragm);
-        // no pT, frgments just go forward
-        auto momentum =
-            Vector(zAxisFrame, corsika::QuantityVector<hepmomentum_d>{
-                                   0.0_GeV, 0.0_GeV,
-                                   sqrt((projectileEnergyLabPerNucleon + nucleonMass) *
-                                        (projectileEnergyLabPerNucleon - nucleonMass))});
-
-        momentum.rebase(originalCS); // transform back into standard lab frame
-        CORSIKA_LOG_DEBUG(
-            "secondary fragment> id= {}"
-            " p={}",
-            idFragm, momentum.getComponents());
-        auto pnew = view.addSecondary(std::make_tuple(idFragm, momentum, pOrig, tOrig));
-        Plab_final += pnew.getMomentum();
-        Elab_final += pnew.getEnergy();
-
-      } else { // nucleus, A>1
-
-        int Z = 0;
-        switch (A) {
-          case 2: // deuterium
-            Z = 1;
-            break;
-          case 3: // tritium
-            Z = 1;
-            break;
-          case 4: // helium
-            Z = 2;
-            break;
-          default: // nucleus
-          {
-            Z = int(A / 2.15 + 0.7);
-          }
-        }
-
-        HEPMassType const nucleusMass = Proton::mass * Z + Neutron::mass * (A - Z);
-        // no pT, frgments just go forward
-        auto momentum = Vector(
-            zAxisFrame, QuantityVector<hepmomentum_d>{
-                            0.0_GeV, 0.0_GeV,
-                            sqrt((projectileEnergyLabPerNucleon * A + nucleusMass) *
-                                 (projectileEnergyLabPerNucleon * A - nucleusMass))});
-
-        momentum.rebase(originalCS); // transform back into standard lab frame
-        CORSIKA_LOG_DEBUG(
-            "secondary fragment> id={}"
-            " p={}"
-            " A={}"
-            " Z={}",
-            get_nucleus_code(A, Z), momentum.getComponents(), A, Z);
-
-        auto pnew = view.addSecondary(
-            std::make_tuple(get_nucleus_code(A, Z), momentum, pOrig, tOrig));
-        Plab_final += pnew.getMomentum();
-        Elab_final += pnew.getEnergy();
-      }
-    }
-
-    // secondaries
-    QGSJetIIStack qs;
-    for (auto& psec : qs) {
-
-      auto momentum = psec.getMomentum(zAxisFrame);
-
-      momentum.rebase(originalCS); // transform back into standard lab frame
-      CORSIKA_LOG_DEBUG("secondary> id= {}, p= {}",
-                        corsika::qgsjetII::convertFromQgsjetII(psec.getPID()),
-                        momentum.getComponents());
-      auto pnew = view.addSecondary(std::make_tuple(
-          corsika::qgsjetII::convertFromQgsjetII(psec.getPID()), momentum, pOrig, tOrig));
-      Plab_final += pnew.getMomentum();
-      Elab_final += pnew.getEnergy();
-    }
-    CORSIKA_LOG_DEBUG(
-        "conservation (all GeV): Ecm_final= n/a " /* << Ecm_final / 1_GeV*/
-        ", Elab_final={} "
-        ", Plab_final={}"
-        ", N_wounded,targ={}"
-        ", N_wounded,proj={}"
-        ", N_fragm,proj={}",
-        Elab_final / 1_GeV, (Plab_final / 1_GeV).getComponents(),
-        QGSJetIIFragmentsStackData::getWoundedNucleonsTarget(),
-        QGSJetIIFragmentsStackData::getWoundedNucleonsProjectile(), qfs.getSize());
-  }
-} // namespace corsika::qgsjetII
diff --git a/corsika/detail/modules/qgsjetII/InteractionModel.inl b/corsika/detail/modules/qgsjetII/InteractionModel.inl
new file mode 100644
index 0000000000000000000000000000000000000000..8203faa49d1c9c6c3fe16de453f01dcde138e3bb
--- /dev/null
+++ b/corsika/detail/modules/qgsjetII/InteractionModel.inl
@@ -0,0 +1,300 @@
+/*
+ * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * 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/modules/qgsjetII/InteractionModel.hpp>
+
+#include <corsika/framework/geometry/FourVector.hpp>
+#include <corsika/framework/geometry/Point.hpp>
+
+#include <corsika/modules/qgsjetII/ParticleConversion.hpp>
+#include <corsika/modules/qgsjetII/QGSJetIIFragmentsStack.hpp>
+#include <corsika/modules/qgsjetII/QGSJetIIStack.hpp>
+
+#include <corsika/framework/utility/COMBoost.hpp>
+
+#include <sstream>
+#include <tuple>
+
+#include <qgsjet-II-04.hpp>
+
+namespace corsika::qgsjetII {
+
+  inline InteractionModel::InteractionModel(boost::filesystem::path const dataPath) {
+    CORSIKA_LOG_DEBUG("Reading QGSJetII data tables from {}", dataPath);
+
+    // initialize QgsjetII
+    static bool initialized = false;
+    if (!initialized) {
+      qgset_();
+      datadir DIR(dataPath.string() + "/");
+      qgaini_(DIR.data);
+      initialized = true;
+    }
+  }
+
+  inline InteractionModel::~InteractionModel() {
+    CORSIKA_LOG_DEBUG("QgsjetII::InteractionModel n= {}", count_);
+  }
+
+  inline bool InteractionModel::isValid(Code const projectileId, Code const targetId,
+                                        HEPEnergyType const sqrtS) const {
+
+    if (sqrtS < sqrtSmin_) { return false; }
+    if (is_nucleus(targetId)) {
+      size_t iTarget = get_nucleus_A(targetId);
+      if (iTarget > int(maxMassNumber_) || iTarget <= 0) { return false; }
+    } else if (targetId != Proton::code) {
+      return false;
+    }
+
+    if (is_nucleus(projectileId)) {
+      size_t iProjectile = get_nucleus_A(projectileId);
+      if (iProjectile > int(maxMassNumber_) || iProjectile <= 0) { return false; }
+    } else if (!is_hadron(projectileId)) {
+      return false;
+    }
+    return true;
+  }
+
+  inline CrossSectionType InteractionModel::getCrossSection(
+      Code const projectileId, Code const targetId, FourMomentum const& projectileP4,
+      FourMomentum const& targetP4) const {
+
+    if (!corsika::qgsjetII::canInteract(projectileId)) {
+      return CrossSectionType::zero();
+    }
+
+    // define projectile, in lab frame
+    auto const sqrtS2 = (projectileP4 + targetP4).getNormSqr();
+    auto const sqrtS = sqrt(sqrtS2);
+    if (!isValid(projectileId, targetId, sqrtS)) { return CrossSectionType::zero(); }
+    HEPEnergyType const Elab = (sqrtS2 - static_pow<2>(get_mass(projectileId)) -
+                                static_pow<2>(get_mass(targetId))) /
+                               (2 * get_mass(targetId));
+
+    int const iBeam = static_cast<QgsjetIIXSClassIntType>(
+        corsika::qgsjetII::getQgsjetIIXSCode(projectileId));
+    int iTarget = 1;
+    if (is_nucleus(targetId)) { iTarget = get_nucleus_A(targetId); }
+    int iProjectile = 1;
+    if (is_nucleus(projectileId)) { iProjectile = get_nucleus_A(projectileId); }
+
+    CORSIKA_LOG_DEBUG(
+        "QgsjetII::getCrossSection Elab= {} GeV iBeam= {}"
+        " iProjectile= {} iTarget= {}",
+        Elab / 1_GeV, iBeam, iProjectile, iTarget);
+    double sigProd = qgsect_(Elab / 1_GeV, iBeam, iProjectile, iTarget);
+    CORSIKA_LOG_DEBUG("QgsjetII::getCrossSection sigProd= {} mb", sigProd);
+    return sigProd * 1_mb;
+  }
+
+  template <typename TSecondaries>
+  inline void InteractionModel::doInteraction(TSecondaries& view, Code const projectileId,
+                                              Code const targetId,
+                                              FourMomentum const& projectileP4,
+                                              FourMomentum const& targetP4) {
+
+    CORSIKA_LOG_DEBUG(
+        "ProcessQgsjetII: "
+        "doInteraction: {} interaction possible? {}",
+        projectileId, corsika::qgsjetII::canInteract(projectileId));
+
+    // define projectile, in lab frame
+    auto const sqrtS2 = (projectileP4 + targetP4).getNormSqr();
+    auto const sqrtS = sqrt(sqrtS2);
+    if (!corsika::qgsjetII::canInteract(projectileId) ||
+        !isValid(projectileId, targetId, sqrtS)) {
+      throw std::runtime_error("invalid target/projectile/energy combination.");
+    }
+    HEPEnergyType const Elab = (sqrtS2 - static_pow<2>(get_mass(projectileId)) -
+                                static_pow<2>(get_mass(targetId))) /
+                               (2 * get_mass(targetId));
+
+    int beamA = 0;
+    if (is_nucleus(projectileId)) { beamA = get_nucleus_A(projectileId); }
+
+    CORSIKA_LOG_DEBUG("ebeam lab: {} GeV ", Elab / 1_GeV);
+
+    int targetMassNumber = 1;   // proton
+    if (is_nucleus(targetId)) { // nucleus
+      targetMassNumber = get_nucleus_A(targetId);
+    }
+    CORSIKA_LOG_DEBUG("target: {}, qgsjetII code/A: {}", targetId, targetMassNumber);
+
+    // select QGSJetII internal projectile type
+    int projectileMassNumber = 1; // "1" means "hadron"
+    QgsjetIIHadronType qgsjet_hadron_type = qgsjetII::getQgsjetIIHadronType(projectileId);
+    if (qgsjet_hadron_type == QgsjetIIHadronType::NucleusType) {
+      projectileMassNumber = get_nucleus_A(projectileId);
+      std::array<QgsjetIIHadronType, 2> constexpr nucleons = {
+          QgsjetIIHadronType::ProtonType, QgsjetIIHadronType::NeutronType};
+      std::uniform_int_distribution select(0, 1);
+      qgsjet_hadron_type = nucleons[select(rng_)];
+    } else if (qgsjet_hadron_type == QgsjetIIHadronType::NeutralLightMesonType) {
+      // from conex: replace pi0 or rho0 with pi+/pi- in alternating sequence
+      qgsjet_hadron_type = alternate_;
+      alternate_ =
+          (alternate_ == QgsjetIIHadronType::PiPlusType ? QgsjetIIHadronType::PiMinusType
+                                                        : QgsjetIIHadronType::PiPlusType);
+    }
+
+    count_++;
+    int qgsjet_hadron_type_int = static_cast<QgsjetIICodeIntType>(qgsjet_hadron_type);
+    CORSIKA_LOG_DEBUG(
+        "qgsjet_hadron_type_int={} projectileMassNumber={} targetMassNumber={}",
+        qgsjet_hadron_type_int, projectileMassNumber, targetMassNumber);
+    qgini_(Elab / 1_GeV, qgsjet_hadron_type_int, projectileMassNumber, targetMassNumber);
+    qgconf_();
+
+    CoordinateSystemPtr const& rootCS = get_root_CoordinateSystem();
+
+    // bookkeeping
+    MomentumVector Plab_final(rootCS, {0.0_GeV, 0.0_GeV, 0.0_GeV});
+    HEPEnergyType Elab_final = 0_GeV;
+
+    // to read the secondaries
+    // define rotation to and from CoM frame
+    // CoM frame definition in QgsjetII projectile: +z
+
+    // QGSJetII, both, in input and output only considers the lab frame with a target at
+    // rest.
+
+    // system of initial-state
+    COMBoost boost(projectileP4, targetP4);
+
+    auto const& originalCS = boost.getOriginalCS();
+    auto const& csPrime =
+        boost.getRotatedCS(); // z is along the CM motion (projectile, in Cascade)
+
+    HEPMomentumType const pLabMag =
+        sqrt((Elab - get_mass(projectileId)) * (Elab + get_mass(projectileId)));
+    MomentumVector pLab(csPrime, {0_eV, 0_eV, pLabMag});
+
+    // internal QGSJetII system
+    COMBoost boostInternal({Elab, pLab}, get_mass(targetId));
+
+    // position and time of interaction, not used in QgsjetII
+    auto const projectile = view.getProjectile();
+    Point const pOrig = projectile.getPosition();
+    TimeType const tOrig = projectile.getTime();
+
+    // fragments
+    QGSJetIIFragmentsStack qfs;
+    for (auto& fragm : qfs) {
+      int const A = fragm.getFragmentSize();
+      if (A == 1) { // nucleon
+        std::uniform_real_distribution<double> select;
+        Code idFragm = Code::Proton;
+        if (select(rng_) > 0.5) { idFragm = Code::Neutron; }
+
+        const HEPMassType nucleonMass = get_mass(idFragm);
+        // no pT, fragments just go forward
+        HEPEnergyType const projectileEnergyLabPerNucleon = Elab / beamA;
+        MomentumVector momentum{csPrime,
+                                {0.0_GeV, 0.0_GeV,
+                                 sqrt((projectileEnergyLabPerNucleon + nucleonMass) *
+                                      (projectileEnergyLabPerNucleon - nucleonMass))}};
+
+        // this is not "CoM" here, but rather the system defined by projectile+target,
+        // which in Cascade-mode is already lab
+        auto const P4com =
+            boostInternal.toCoM(FourVector{projectileEnergyLabPerNucleon, momentum});
+        auto const P4output = boost.fromCoM(P4com);
+        auto p3output = P4output.getSpaceLikeComponents();
+        p3output.rebase(originalCS); // transform back into standard lab frame
+
+        CORSIKA_LOG_DEBUG(
+            "secondary fragment> id= {}"
+            " p={}",
+            idFragm, p3output.getComponents());
+        auto pnew = view.addSecondary(std::make_tuple(idFragm, p3output, pOrig, tOrig));
+        Plab_final += pnew.getMomentum();
+        Elab_final += pnew.getEnergy();
+
+      } else { // nucleus, A>1
+
+        int Z = 0;
+        switch (A) {
+          case 2: // deuterium
+            Z = 1;
+            break;
+          case 3: // tritium
+            Z = 1;
+            break;
+          case 4: // helium
+            Z = 2;
+            break;
+          default: // nucleus
+          {
+            Z = int(A / 2.15 + 0.7);
+          }
+        }
+
+        HEPMassType const nucleusMass = Proton::mass * Z + Neutron::mass * (A - Z);
+        // no pT, frgments just go forward
+        HEPEnergyType const projectileEnergyLabPerNucleon = Elab / beamA;
+        MomentumVector momentum{
+            csPrime,
+            {0.0_GeV, 0.0_GeV,
+             sqrt((projectileEnergyLabPerNucleon * A + nucleusMass) *
+                  (projectileEnergyLabPerNucleon * A - nucleusMass))}};
+
+        // this is not "CoM" here, but rather the system defined by projectile+target,
+        // which in Cascade-mode is already lab
+        auto const P4com =
+            boostInternal.toCoM(FourVector{projectileEnergyLabPerNucleon * A, momentum});
+        auto const P4output = boost.fromCoM(P4com);
+        auto p3output = P4output.getSpaceLikeComponents();
+        p3output.rebase(originalCS); // transform back into standard lab frame
+
+        CORSIKA_LOG_DEBUG(
+            "secondary fragment> id={}"
+            " p={}"
+            " A={}"
+            " Z={}",
+            get_nucleus_code(A, Z), p3output.getComponents(), A, Z);
+
+        auto pnew = view.addSecondary(
+            std::make_tuple(get_nucleus_code(A, Z), p3output, pOrig, tOrig));
+        Plab_final += pnew.getMomentum();
+        Elab_final += pnew.getEnergy();
+      }
+    }
+
+    // secondaries
+    QGSJetIIStack qs;
+    for (auto& psec : qs) {
+
+      auto momentum = psec.getMomentum(csPrime);
+      // this is not "CoM" here, but rather the system defined by projectile+target,
+      // which in Cascade-mode is already lab
+      auto const P4com = boostInternal.toCoM(FourVector{psec.getEnergy(), momentum});
+      auto const P4output = boost.fromCoM(P4com);
+      auto p3output = P4output.getSpaceLikeComponents();
+      p3output.rebase(originalCS); // transform back into standard lab frame
+
+      CORSIKA_LOG_DEBUG("secondary> id= {}, p= {}",
+                        corsika::qgsjetII::convertFromQgsjetII(psec.getPID()),
+                        p3output.getComponents());
+      auto pnew = view.addSecondary(std::make_tuple(
+          corsika::qgsjetII::convertFromQgsjetII(psec.getPID()), p3output, pOrig, tOrig));
+      Plab_final += pnew.getMomentum();
+      Elab_final += pnew.getEnergy();
+    }
+    CORSIKA_LOG_DEBUG(
+        "conservation (all GeV): Ecm_final= n/a " /* << Ecm_final / 1_GeV*/
+        ", Elab_final={} "
+        ", Plab_final={}"
+        ", N_wounded,targ={}"
+        ", N_wounded,proj={}"
+        ", N_fragm,proj={}",
+        Elab_final / 1_GeV, (Plab_final / 1_GeV).getComponents(),
+        QGSJetIIFragmentsStackData::getWoundedNucleonsTarget(),
+        QGSJetIIFragmentsStackData::getWoundedNucleonsProjectile(), qfs.getSize());
+  }
+} // namespace corsika::qgsjetII
diff --git a/corsika/detail/modules/sibyll/Decay.inl b/corsika/detail/modules/sibyll/Decay.inl
index 6b8ae281d4a4be07408785e1a846c376c8b4bc0f..ab0114182b41f5db321f19ec6d91458cc9b5000d 100644
--- a/corsika/detail/modules/sibyll/Decay.inl
+++ b/corsika/detail/modules/sibyll/Decay.inl
@@ -174,7 +174,7 @@ namespace corsika::sibyll {
 
     count_++;
     // remember position
-    Point const decayPoint = projectile.getPosition();
+    Point const& decayPoint = projectile.getPosition();
     TimeType const t0 = projectile.getTime();
     // switch on decay for this particle
     setUnstable(pCode);
diff --git a/corsika/detail/modules/sibyll/Interaction.inl b/corsika/detail/modules/sibyll/Interaction.inl
deleted file mode 100644
index 07f390b7385a6be73b155b8fe8dd8a64b4308c7a..0000000000000000000000000000000000000000
--- a/corsika/detail/modules/sibyll/Interaction.inl
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- * (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu
- *
- * 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.
- */
-
-#pragma once
-
-#include <corsika/modules/sibyll/Interaction.hpp>
-
-#include <corsika/media/Environment.hpp>
-#include <corsika/media/NuclearComposition.hpp>
-#include <corsika/framework/geometry/FourVector.hpp>
-#include <corsika/modules/sibyll/ParticleConversion.hpp>
-#include <corsika/modules/sibyll/SibStack.hpp>
-#include <corsika/framework/utility/COMBoost.hpp>
-
-#include <sibyll2.3d.hpp>
-
-#include <tuple>
-
-namespace corsika::sibyll {
-
-  inline Interaction::Interaction(const bool sibyll_printout_on)
-      : sibyll_listing_(sibyll_printout_on) {
-    // initialize Sibyll
-    static bool initialized = false;
-    if (!initialized) {
-      sibyll_ini_();
-      initialized = true;
-    }
-  }
-
-  inline Interaction::~Interaction() {
-    CORSIKA_LOG_DEBUG("Sibyll::Interaction n={}, Nnuc={}", count_, nucCount_);
-  }
-
-  inline std::tuple<corsika::CrossSectionType, corsika::CrossSectionType>
-  Interaction::getCrossSection(const corsika::Code BeamId, const corsika::Code TargetId,
-                               const corsika::HEPEnergyType CoMenergy) const {
-    double sigProd, sigEla, dummy, dum1, dum3, dum4;
-    double dumdif[3];
-    const int iBeam = corsika::sibyll::getSibyllXSCode(
-        BeamId); // 0 (can not interact, 1: proton-like, 2: pion-like, 3:kaon-like)
-    if (!iBeam)
-      throw std::runtime_error(
-          fmt::format("Interaction of beam {} not defined in "
-                      "Sibyll!",
-                      BeamId));
-    if (!isValidCoMEnergy(CoMenergy)) {
-      throw std::runtime_error(
-          "Interaction: getCrossSection: CoM energy outside range for Sibyll!");
-    }
-    const double dEcm = CoMenergy / 1_GeV;
-    // single nucleon target (p,n, hydrogen) or 4<=A<=18
-    if (isValidTarget(TargetId)) {
-      // single nucleon target
-      if (TargetId == corsika::Code::Proton || TargetId == Code::Hydrogen ||
-          TargetId == Code::Neutron) {
-        sib_sigma_hp_(iBeam, dEcm, dum1, sigEla, sigProd, dumdif, dum3, dum4);
-      } else {
-        // nuclear target
-        const int iTarget = corsika::get_nucleus_A(TargetId);
-        sib_sigma_hnuc_(iBeam, iTarget, dEcm, sigProd, dummy, sigEla);
-      }
-    } else {
-      //         throw std::runtime_error(
-      //            "Sibyll nuclear target outside range. Only nuclei with 4<=A<18 are
-      //            allowed.");
-
-      // no interaction in sibyll possible, return infinite cross section? or throw?
-      sigProd = std::numeric_limits<double>::infinity();
-      sigEla = std::numeric_limits<double>::infinity();
-    }
-    return std::make_tuple(sigProd * 1_mb, sigEla * 1_mb);
-  }
-
-  template <typename TParticle>
-  inline corsika::GrammageType Interaction::getInteractionLength(
-      TParticle const& projectile) const {
-
-    const corsika::Code corsikaBeamId = projectile.getPID();
-
-    // beam corsika for sibyll : 1, 2, 3 for p, pi, k
-    // read from cross section code table
-    const bool kInteraction = corsika::sibyll::canInteract(corsikaBeamId);
-
-    MomentumVector const& pLab = projectile.getMomentum();
-    CoordinateSystemPtr const& labCS = pLab.getCoordinateSystem();
-
-    // FOR NOW: assume target is at rest
-    MomentumVector pTarget(labCS, {0_GeV, 0_GeV, 0_GeV});
-
-    // total momentum and energy
-    HEPEnergyType Elab = projectile.getEnergy() + constants::nucleonMass;
-    MomentumVector pTotLab(labCS, {0_GeV, 0_GeV, 0_GeV});
-    pTotLab += pLab;
-    pTotLab += pTarget;
-    auto const pTotLabNorm = pTotLab.getNorm();
-    // calculate cm. energy
-    const HEPEnergyType ECoM = sqrt(
-        (Elab + pTotLabNorm) * (Elab - pTotLabNorm)); // binomial for numerical accuracy
-
-    CORSIKA_LOG_DEBUG(
-        "Interaction: LambdaInt: \n"
-        " input energy: {} GeV "
-        " beam can interact: {} "
-        " beam pid: {}",
-        projectile.getEnergy() / 1_GeV, kInteraction, projectile.getPID());
-
-    // TODO: move limits into variables
-    // FR: removed && Elab >= 8.5_GeV
-    if (kInteraction && isValidCoMEnergy(ECoM)) {
-
-      // get target from environment
-      /*
-        the target should be defined by the Environment,
-        ideally as full particle object so that the four momenta
-        and the boosts can be defined..
-      */
-
-      auto const* currentNode = projectile.getNode();
-      const auto& mediumComposition =
-          currentNode->getModelProperties().getNuclearComposition();
-
-      si::CrossSectionType weightedProdCrossSection = mediumComposition.getWeightedSum(
-          [=](corsika::Code targetID) -> si::CrossSectionType {
-            // Argon needs special handling ....
-            return targetID == Code::Argon ? CrossSectionType::zero()
-                                           : std::get<0>(this->getCrossSection(
-                                                 corsikaBeamId, targetID, ECoM));
-          });
-
-      CORSIKA_LOG_DEBUG(
-          "Interaction: "
-          "IntLength: weighted CrossSection (mb): {} ",
-          weightedProdCrossSection / 1_mb);
-
-      // calculate interaction length in medium
-      GrammageType const int_length = mediumComposition.getAverageMassNumber() *
-                                      constants::u / weightedProdCrossSection;
-      CORSIKA_LOG_DEBUG(
-          "Interaction: "
-          "interaction length (g/cm2): {} ",
-          int_length / (0.001_kg) * 1_cm * 1_cm);
-
-      return int_length;
-    }
-
-    return std::numeric_limits<double>::infinity() * 1_g / (1_cm * 1_cm);
-  }
-
-  /**
-     In this function SIBYLL is called to produce one event. The
-     event is copied (and boosted) into the shower lab frame.
-   */
-
-  template <typename TSecondaryView>
-  inline void Interaction::doInteraction(TSecondaryView& view) {
-
-    auto const projectile = view.getProjectile();
-    const auto corsikaBeamId = projectile.getPID();
-
-    if (corsika::is_nucleus(corsikaBeamId)) {
-      // nuclei handled by different process, this should not happen
-      throw std::runtime_error("Nuclear projectile are not handled by SIBYLL!");
-    }
-
-    // position and time of interaction, not used in Sibyll
-    Point const pOrig = projectile.getPosition();
-    TimeType const tOrig = projectile.getTime();
-
-    // define projectile
-    HEPEnergyType const eProjectileLab = projectile.getEnergy();
-    auto const pProjectileLab = projectile.getMomentum();
-    CoordinateSystemPtr const& originalCS = pProjectileLab.getCoordinateSystem();
-
-    CORSIKA_LOG_DEBUG(
-        "ProcessSibyll: "
-        "DoInteraction: pid {} interaction ",
-        corsikaBeamId);
-
-    // define target
-    // for Sibyll is always a single nucleon
-    // FOR NOW: target is always at rest
-    const auto eTargetLab = 0_GeV + constants::nucleonMass;
-    const auto pTargetLab = MomentumVector(originalCS, 0_GeV, 0_GeV, 0_GeV);
-    const FourVector PtargLab(eTargetLab, pTargetLab);
-
-    CORSIKA_LOG_DEBUG(
-        "Interaction: ebeam lab: {} GeV"
-        "Interaction: pbeam lab: {} GeV",
-        eProjectileLab / 1_GeV, pProjectileLab.getComponents());
-    CORSIKA_LOG_DEBUG(
-        "Interaction: etarget lab: {} GeV "
-        "Interaction: ptarget lab: {} GeV",
-        eTargetLab / 1_GeV, pTargetLab.getComponents() / 1_GeV);
-
-    const FourVector PprojLab(eProjectileLab, pProjectileLab);
-
-    // define target kinematics in lab frame
-    // define boost to and from CoM frame
-    // CoM frame definition in Sibyll projectile: +z
-    COMBoost const boost(PprojLab, constants::nucleonMass);
-    auto const& csPrime = boost.getRotatedCS();
-
-    // just for show:
-    // boost projecticle
-    [[maybe_unused]] auto const PprojCoM = boost.toCoM(PprojLab);
-    // boost target
-    [[maybe_unused]] auto const PtargCoM = boost.toCoM(PtargLab);
-    CORSIKA_LOG_DEBUG(
-        "Interaction: ebeam CoM: {} GeV "
-        "Interaction: pbeam CoM: {} GeV ",
-        PprojCoM.getTimeLikeComponent() / 1_GeV,
-        PprojCoM.getSpaceLikeComponents().getComponents(csPrime) / 1_GeV);
-    CORSIKA_LOG_DEBUG(
-        "Interaction: etarget CoM: {} GeV "
-        "Interaction: ptarget CoM: {} GeV ",
-        PtargCoM.getTimeLikeComponent() / 1_GeV,
-        PtargCoM.getSpaceLikeComponents().getComponents(csPrime) / 1_GeV);
-
-    CORSIKA_LOG_DEBUG("Interaction: position of interaction: {} ",
-                      pOrig.getCoordinates());
-    CORSIKA_LOG_DEBUG("Interaction: time: {} ", tOrig);
-
-    HEPEnergyType Etot = eProjectileLab + eTargetLab;
-    MomentumVector Ptot = projectile.getMomentum();
-    // invariant mass, i.e. cm. energy
-    HEPEnergyType Ecm = sqrt(Etot * Etot - Ptot.getSquaredNorm());
-
-    // sample target mass number
-    auto const* currentNode = projectile.getNode();
-    auto const& mediumComposition =
-        currentNode->getModelProperties().getNuclearComposition();
-    // get cross sections for target materials
-    /*
-      Here we read the cross section from the interaction model again,
-      should be passed from getInteractionLength if possible
-     */
-    //#warning reading interaction cross section again, should not be necessary
-    auto const& compVec = mediumComposition.getComponents();
-    std::vector<CrossSectionType> cross_section_of_components(compVec.size());
-
-    for (size_t i = 0; i < compVec.size(); ++i) {
-      auto const targetId = compVec[i];
-      if (targetId == Code::Argon) continue; // skip Argon ....
-      const auto [sigProd, sigEla] = getCrossSection(corsikaBeamId, targetId, Ecm);
-      [[maybe_unused]] const auto& dummy_sigEla = sigEla;
-      cross_section_of_components[i] = sigProd;
-    }
-
-    const auto targetCode =
-        mediumComposition.sampleTarget(cross_section_of_components, RNG_);
-    CORSIKA_LOG_DEBUG("Interaction: target selected: {} ", targetCode);
-    /*
-      FOR NOW: allow nuclei with A<18 or protons only.
-      when medium composition becomes more complex, approximations will have to be
-      allowed air in atmosphere also contains some Argon.
-    */
-    int targetSibCode = -1;
-    if (is_nucleus(targetCode)) targetSibCode = get_nucleus_A(targetCode);
-    if (targetCode == Proton::code) targetSibCode = 1;
-    CORSIKA_LOG_DEBUG("Interaction: sibyll code: {}", targetSibCode);
-    if (targetSibCode > int(maxTargetMassNumber_) || targetSibCode < 1)
-      throw std::runtime_error(
-          "Sibyll target outside range. Only nuclei with A<18 or protons are "
-          "allowed.");
-
-    // beam id for sibyll
-    const int kBeam = corsika::sibyll::convertToSibyllRaw(corsikaBeamId);
-
-    CORSIKA_LOG_DEBUG(
-        "Interaction: "
-        " DoInteraction: E(GeV): {} "
-        " Ecm(GeV): {} ",
-        eProjectileLab / 1_GeV, Ecm / 1_GeV);
-    if (Ecm > getMaxEnergyCoM())
-      throw std::runtime_error("Interaction::DoInteraction: CoM energy too high!");
-    // FR: removed eProjectileLab < 8.5_GeV ||
-    if (Ecm < getMinEnergyCoM()) {
-      CORSIKA_LOG_DEBUG(
-          "Interaction: "
-          " DoInteraction: should have dropped particle.. "
-          "THIS IS AN ERROR");
-      throw std::runtime_error("energy too low for SIBYLL");
-    } else {
-      count_++;
-      // Sibyll does not know about units..
-      const double sqs = Ecm / 1_GeV;
-      // running sibyll, filling stack
-      sibyll_(kBeam, targetSibCode, sqs);
-
-      if (sibyll_listing_) {
-        // print final state
-        int print_unit = 6;
-        sib_list_(print_unit);
-        nucCount_ += get_nwounded() - 1;
-      }
-
-      // add particles from sibyll to stack
-      // link to sibyll stack
-      SibStack ss;
-
-      MomentumVector Plab_final(originalCS, {0.0_GeV, 0.0_GeV, 0.0_GeV});
-      HEPEnergyType Elab_final = 0_GeV, Ecm_final = 0_GeV;
-      for (auto& psib : ss) {
-
-        // abort on particles that have decayed in Sibyll. Should not happen!
-        if (psib.hasDecayed())
-          throw std::runtime_error("found particle that decayed in SIBYLL!");
-
-        // transform 4-momentum to lab. frame
-        // note that the momentum needs to be rotated back
-        auto const tmp = psib.getMomentum().getComponents();
-        auto const pCoM = Vector<hepmomentum_d>(csPrime, tmp);
-        HEPEnergyType const eCoM = psib.getEnergy();
-        auto const Plab = boost.fromCoM(FourVector(eCoM, pCoM));
-        auto const p3lab = Plab.getSpaceLikeComponents();
-        assert(p3lab.getCoordinateSystem() == originalCS); // just to be sure!
-
-        // add to corsika stack
-        auto pnew = view.addSecondary(std::make_tuple(
-            corsika::sibyll::convertFromSibyll(psib.getPID()), p3lab, pOrig, tOrig));
-
-        Plab_final += pnew.getMomentum();
-        Elab_final += pnew.getEnergy();
-        Ecm_final += psib.getEnergy();
-      }
-      CORSIKA_LOG_DEBUG(
-          "conservation (all GeV): "
-          "Ecm_initial(per nucleon)={:.2f}, Ecm_final(per nucleon)={:.2f}, "
-          "Elab_initial={:.2f}, Elab_final={:.2f}, "
-          "Elab-diff (%)={:.2f}, "
-          "m in target nucleons={:.2f}, "
-          "Plab_initial={:.2f}, "
-          "Plab_final={:.2f} ",
-          Ecm / 1_GeV, Ecm_final * 2. / (get_nwounded() + 1) / 1_GeV, Etot / 1_GeV,
-          Elab_final / 1_GeV,
-          (Elab_final / (Etot + get_nwounded() * constants::nucleonMass) - 1) * 100,
-          constants::nucleonMass * get_nwounded() / 1_GeV,
-          (pProjectileLab / 1_GeV).getComponents(), (Plab_final / 1_GeV).getComponents());
-    }
-  }
-
-} // namespace corsika::sibyll
diff --git a/corsika/detail/modules/sibyll/InteractionModel.inl b/corsika/detail/modules/sibyll/InteractionModel.inl
new file mode 100644
index 0000000000000000000000000000000000000000..99f078673c2d80df3c6251bd6d22eed25a077c4f
--- /dev/null
+++ b/corsika/detail/modules/sibyll/InteractionModel.inl
@@ -0,0 +1,190 @@
+/*
+ * (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <corsika/framework/geometry/Point.hpp>
+
+#include <corsika/modules/sibyll/ParticleConversion.hpp>
+#include <corsika/framework/utility/COMBoost.hpp>
+#include <corsika/modules/sibyll/SibStack.hpp>
+
+#include <sibyll2.3d.hpp>
+
+#include <tuple>
+
+namespace corsika::sibyll {
+
+  inline void InteractionModel::setVerbose(bool const flag) { sibyll_listing_ = flag; }
+
+  inline InteractionModel::InteractionModel()
+      : sibyll_listing_(false) {
+    // initialize Sibyll
+    static bool initialized = false;
+    if (!initialized) {
+      sibyll_ini_();
+      initialized = true;
+    }
+  }
+
+  inline InteractionModel::~InteractionModel() {
+    CORSIKA_LOG_DEBUG("Sibyll::Model n={}, Nnuc={}", count_, nucCount_);
+  }
+
+  inline bool constexpr InteractionModel::isValid(Code const projectileId,
+                                                  Code const targetId,
+                                                  HEPEnergyType const sqrtSnn) const {
+    if ((minEnergyCoM_ > sqrtSnn) || (sqrtSnn > maxEnergyCoM_)) { return false; }
+
+    if (is_nucleus(targetId)) {
+      size_t const targA = get_nucleus_A(targetId);
+      if (targA != 1 && (targA < minNuclearTargetA_ || targA >= maxTargetMassNumber_)) {
+        return false;
+      }
+    } else if (targetId != Code::Proton && targetId != Code::Neutron &&
+               targetId != Code::Hydrogen) {
+      return false;
+    }
+    if (is_nucleus(projectileId) || !corsika::sibyll::canInteract(projectileId)) {
+      return false;
+    }
+    return true;
+  }
+
+  inline std::tuple<CrossSectionType, CrossSectionType>
+  InteractionModel::getCrossSectionInelEla(Code const projectileId, Code const targetId,
+                                           FourMomentum const& projectileP4,
+                                           FourMomentum const& targetP4) const {
+
+    int targetSibCode = 1; // nucleon or particle count
+    if (is_nucleus(targetId)) { targetSibCode = get_nucleus_A(targetId); }
+    // sqrtS per target nucleon
+    HEPEnergyType const sqrtSnn = (projectileP4 + targetP4 / targetSibCode).getNorm();
+
+    if (!isValid(projectileId, targetId, sqrtSnn)) {
+      return {CrossSectionType::zero(), CrossSectionType::zero()};
+    }
+
+    double dummy, dum1, dum3, dum4, dumdif[3]; // dummies needed for fortran call
+    int const iBeam = corsika::sibyll::getSibyllXSCode(
+        projectileId); // 0 (can not interact, 1: proton-like, 2: pion-like,
+                       // 3:kaon-like)
+
+    double const dEcm = sqrtSnn / 1_GeV;
+    // single nucleon target (p,n, hydrogen) or 4<=A<=18
+    double sigProd = 0;
+    double sigEla = 0;
+    if (targetId == Code::Proton || targetId == Code::Hydrogen ||
+        targetId == Code::Neutron) {
+      // single nucleon target
+      sib_sigma_hp_(iBeam, dEcm, dum1, sigEla, sigProd, dumdif, dum3, dum4);
+    } else {
+      // nuclear target
+      int const iTarget = get_nucleus_A(targetId);
+      sib_sigma_hnuc_(iBeam, iTarget, dEcm, sigProd, dummy, sigEla);
+    }
+    return {sigProd * 1_mb, sigEla * 1_mb};
+  } // namespace corsika::sibyll
+
+  /**
+   * In this function SIBYLL is called to produce one event. The
+   * event is copied (and boosted) into the shower lab frame.
+   */
+
+  template <typename TSecondaryView>
+  inline void InteractionModel::doInteraction(TSecondaryView& secondaries,
+                                              Code const projectileId,
+                                              Code const targetId,
+                                              FourMomentum const& projectileP4,
+                                              FourMomentum const& targetP4) {
+
+    int targetSibCode = 1; // nucleon or particle count
+    if (is_nucleus(targetId)) { targetSibCode = get_nucleus_A(targetId); }
+    CORSIKA_LOG_DEBUG("sibyll code: {} (nucleon/particle count)", targetSibCode);
+
+    // sqrtS per target nucleon
+    HEPEnergyType const sqrtSnn = (projectileP4 + targetP4 / targetSibCode).getNorm();
+    COMBoost const boost(projectileP4, targetP4 / targetSibCode);
+
+    if (!isValid(projectileId, targetId, sqrtSnn)) {
+      throw std::runtime_error("Invalid target/projectile/energy combination");
+    }
+
+    CORSIKA_LOG_DEBUG("pId={} tId={} sqrtSnn={}GeV", projectileId, targetId, sqrtSnn);
+
+    // beam id for sibyll
+    int const projectileSibyllCode = corsika::sibyll::convertToSibyllRaw(projectileId);
+
+    count_++;
+    // Sibyll does not know about units..
+    double const sqs = sqrtSnn / 1_GeV;
+    // running sibyll, filling stack
+    sibyll_(projectileSibyllCode, targetSibCode, sqs);
+
+    if (sibyll_listing_) {
+      // print final state
+      int print_unit = 6;
+      sib_list_(print_unit);
+      nucCount_ += get_nwounded() - 1;
+    }
+
+    // ------ output and particle readout -----
+    auto const& csPrime = boost.getRotatedCS();
+
+    // add particles from sibyll to stack
+
+    // position and time of interaction, not used in Sibyll
+    auto const& projectile = secondaries.parent();
+    Point const& pOrig = projectile.getPosition();
+    TimeType const tOrig = projectile.getTime(); // no time in sibyll
+
+    // link to sibyll stack
+    SibStack ss;
+
+    auto const& originalCS = boost.getOriginalCS();
+    MomentumVector Plab_final(originalCS, {0.0_GeV, 0.0_GeV, 0.0_GeV});
+    HEPEnergyType Elab_final = 0_GeV, Ecm_final = 0_GeV;
+    for (auto& psib : ss) {
+      // abort on particles that have decayed in Sibyll. Should not happen!
+      if (psib.hasDecayed()) { // LCOV_EXCL_START
+        throw std::runtime_error("found particle that decayed in SIBYLL!");
+      } // LCOV_EXCL_STOP
+
+      // transform 4-momentum to lab. frame
+      // note that the momentum needs to be rotated back
+      auto const tmp = psib.getMomentum().getComponents();
+      auto const pCoM = MomentumVector(csPrime, tmp);
+      HEPEnergyType const eCoM = psib.getEnergy();
+      auto const P4lab = boost.fromCoM(FourVector{eCoM, pCoM});
+      auto const p3lab = P4lab.getSpaceLikeComponents();
+
+      // add to corsika stack
+      auto pnew = secondaries.addSecondary(std::make_tuple(
+          corsika::sibyll::convertFromSibyll(psib.getPID()), p3lab, pOrig, tOrig));
+
+      Plab_final += pnew.getMomentum();
+      Elab_final += pnew.getEnergy();
+      Ecm_final += psib.getEnergy();
+    }
+    { // just output
+      HEPEnergyType const Elab_initial =
+          static_pow<2>(sqrtSnn) / (2 * constants::nucleonMass);
+      CORSIKA_LOG_DEBUG(
+          "conservation (all GeV): "
+          "sqrtSnn={}, sqrtSnn_final={}, "
+          "Elab_initial={}, Elab_final={}, "
+          "diff(%)={}, "
+          "E in nucleons={}, "
+          "Plab_final={} ",
+          sqrtSnn / 1_GeV, Ecm_final * 2. / (get_nwounded() + 1) / 1_GeV, Elab_initial,
+          Elab_final / 1_GeV, (Elab_final - Elab_initial) / Elab_initial * 100,
+          constants::nucleonMass * get_nwounded() / 1_GeV,
+          (Plab_final / 1_GeV).getComponents());
+    }
+  }
+} // namespace corsika::sibyll
diff --git a/corsika/detail/modules/sibyll/NuclearInteraction.inl b/corsika/detail/modules/sibyll/NuclearInteraction.inl
deleted file mode 100644
index 1460b1133616c1347cdd2eaa156bbcd06ac307b1..0000000000000000000000000000000000000000
--- a/corsika/detail/modules/sibyll/NuclearInteraction.inl
+++ /dev/null
@@ -1,585 +0,0 @@
-/*
- * (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu
- *
- * 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.
- */
-
-#pragma once
-
-#include <corsika/modules/sibyll/Interaction.hpp>
-#include <corsika/modules/sibyll/NuclearInteraction.hpp>
-
-#include <corsika/media/Environment.hpp>
-#include <corsika/media/NuclearComposition.hpp>
-#include <corsika/framework/geometry/FourVector.hpp>
-#include <corsika/framework/core/PhysicalUnits.hpp>
-#include <corsika/framework/utility/COMBoost.hpp>
-#include <corsika/framework/core/Logging.hpp>
-
-#include <nuclib.hpp>
-
-namespace corsika::sibyll {
-
-  template <typename TEnvironment>
-  inline NuclearInteraction<TEnvironment>::NuclearInteraction(sibyll::Interaction& hadint,
-                                                              TEnvironment const& env)
-      : environment_(env)
-      , hadronicInteraction_(hadint) {
-
-    // initialize hadronic interaction module
-
-    // check compatibility of energy ranges, someone could try to use low-energy model..
-    if (!hadronicInteraction_.isValidCoMEnergy(getMinEnergyPerNucleonCoM()) ||
-        !hadronicInteraction_.isValidCoMEnergy(getMaxEnergyPerNucleonCoM()))
-      throw std::runtime_error(
-          "NuclearInteraction: hadronic interaction model incompatible!");
-
-    // initialize nuclib
-    // TODO: make sure this does not overlap with sibyll
-    nuc_nuc_ini_();
-
-    // initialize cross sections
-    initializeNuclearCrossSections();
-  }
-
-  template <typename TEnvironment>
-  inline NuclearInteraction<TEnvironment>::~NuclearInteraction() {
-    CORSIKA_LOG_DEBUG("Nuclib::NuclearInteraction n={} Nnuc={}", count_, nucCount_);
-  }
-
-  template <typename TEnvironment>
-  inline void NuclearInteraction<TEnvironment>::printCrossSectionTable(Code pCode) {
-    if (pCode == Code::Argon) {
-      CORSIKA_LOG_WARN("SIBYLL cannot handle Argon as target!");
-      return;
-    }
-    const int k = targetComponentsIndex_.at(pCode);
-    Code pNuclei[] = {Code::Helium, Code::Lithium7, Code::Oxygen,
-                      Code::Neon,   Code::Argon,    Code::Iron};
-
-    std::ostringstream table;
-    table << "Nuclear CrossSectionTable pCode=" << pCode << " :\n en/A ";
-    for (auto& j : pNuclei) table << std::setw(9) << j;
-    table << "\n";
-
-    // loop over energy bins
-    for (unsigned int i = 0; i < getNEnergyBins(); ++i) {
-      table << " " << i << "  ";
-
-      for (auto& n : pNuclei) {
-        auto const j = get_nucleus_A(n);
-        table << " " << std::setprecision(5) << std::setw(8)
-              << cnucsignuc_.sigma[j - 1][k][i];
-      }
-      table << "\n";
-    }
-    CORSIKA_LOG_DEBUG(table.str());
-  }
-
-  template <typename TEnvironment>
-  inline void NuclearInteraction<TEnvironment>::initializeNuclearCrossSections() {
-
-    auto& universe = *(environment_.getUniverse());
-
-    auto const allElementsInUniverse = std::invoke([&]() {
-      std::set<Code> allElementsInUniverse;
-      auto collectElements = [&](auto& vtn) {
-        if (vtn.hasModelProperties()) {
-          auto const& comp =
-              vtn.getModelProperties().getNuclearComposition().getComponents();
-          for (auto const c : comp) allElementsInUniverse.insert(c);
-        }
-      };
-      universe.walk(collectElements);
-      return allElementsInUniverse;
-    });
-
-    CORSIKA_LOG_DEBUG("NuclearInteraction: initializing nuclear cross sections...");
-
-    // loop over target components, at most 4!!
-    int k = -1;
-    for (auto& ptarg : allElementsInUniverse) {
-      if (ptarg == Code::Argon) continue; // NEED TO IGNORE Argon ....
-      ++k;
-      CORSIKA_LOG_DEBUG("NuclearInteraction: init target component: {}", ptarg);
-      const int ib = get_nucleus_A(ptarg);
-      if (!hadronicInteraction_.isValidTarget(ptarg)) {
-        CORSIKA_LOG_DEBUG(
-            "NuclearInteraction::InitializeNuclearCrossSections: target nucleus? id={}",
-            ptarg);
-        throw std::runtime_error(
-            " target can not be handled by hadronic interaction model! ");
-      }
-      targetComponentsIndex_.insert(std::pair<Code, int>(ptarg, k));
-      // loop over energies, fNEnBins log. energy bins
-      for (unsigned int i = 0; i < getNEnergyBins(); ++i) {
-        // hard coded energy grid, has to be aligned to definition in signuc2!!, no
-        // comment..
-        const HEPEnergyType Ecm = pow(10., 1. + 1. * i) * 1_GeV;
-        // get p-p cross sections
-        auto const protonId = Code::Proton;
-        auto const [siginel, sigela] =
-            hadronicInteraction_.getCrossSection(protonId, protonId, Ecm);
-        const double dsig = siginel / 1_mb;
-        const double dsigela = sigela / 1_mb;
-        // loop over projectiles, mass numbers from 2 to fMaxNucleusAProjectile
-        for (unsigned int j = 1; j < gMaxNucleusAProjectile_; ++j) {
-          const int jj = j + 1;
-          double sig_out, dsig_out, sigqe_out, dsigqe_out;
-          sigma_mc_(jj, ib, dsig, dsigela, gNSample_, sig_out, dsig_out, sigqe_out,
-                    dsigqe_out);
-          // write to table
-          cnucsignuc_.sigma[j][k][i] = sig_out;
-          cnucsignuc_.sigqe[j][k][i] = sigqe_out;
-        }
-      }
-    }
-    CORSIKA_LOG_DEBUG(
-        "NuclearInteraction: cross sections for {} "
-        " components initialized!",
-        targetComponentsIndex_.size());
-    for (auto& ptarg : allElementsInUniverse) { printCrossSectionTable(ptarg); }
-  }
-
-  template <typename TEnvironment>
-  inline CrossSectionType NuclearInteraction<TEnvironment>::readCrossSectionTable(
-      const int ia, Code pTarget, HEPEnergyType elabnuc) {
-
-    const int ib = targetComponentsIndex_.at(pTarget) + 1; // table index in fortran
-    auto const ECoMNuc = sqrt(2. * constants::nucleonMass * elabnuc);
-    if (ECoMNuc < getMinEnergyPerNucleonCoM() || ECoMNuc > getMaxEnergyPerNucleonCoM())
-      throw std::runtime_error("NuclearInteraction: energy outside tabulated range!");
-    const double e0 = elabnuc / 1_GeV;
-    double sig;
-    CORSIKA_LOG_DEBUG("ReadCrossSectionTable: {} {} {}", ia, ib, e0);
-    signuc2_(ia, ib, e0, sig);
-    CORSIKA_LOG_DEBUG("ReadCrossSectionTable: sig={}", sig);
-    return sig * 1_mb;
-  }
-
-  // TODO: remove elastic cross section?
-  template <typename TEnvironment>
-  template <typename TParticle>
-  std::tuple<CrossSectionType, CrossSectionType> inline NuclearInteraction<
-      TEnvironment>::getCrossSection(TParticle const& projectile, Code const TargetId) {
-
-    if (!is_nucleus(projectile.getPID())) {
-      throw std::runtime_error(
-          "NuclearInteraction: getCrossSection: particle not a nucleus!");
-    }
-
-    unsigned int const iBeamA = get_nucleus_A(projectile.getPID());
-    HEPEnergyType LabEnergyPerNuc = projectile.getEnergy() / iBeamA;
-    CORSIKA_LOG_DEBUG(
-        "NuclearInteraction: getCrossSection: called with: beamNuclA={} "
-        " TargetId={} LabEnergyPerNuc={}GeV ",
-        iBeamA, TargetId, LabEnergyPerNuc / 1_GeV);
-
-    // use nuclib to calc. nuclear cross sections
-    // TODO: for now assumes air with hard coded composition
-    // extend to arbitrary mixtures, requires smarter initialization
-    // get nuclib projectile code: nucleon number
-    if (iBeamA > getMaxNucleusAProjectile() || iBeamA < 2) {
-      CORSIKA_LOG_DEBUG(
-          "NuclearInteraction: beam nucleus outside allowed range for NUCLIB!"
-          "A=" +
-          std::to_string(iBeamA));
-      throw std::runtime_error(
-          "NuclearInteraction: getCrossSection: beam nucleus outside allowed range for "
-          "NUCLIB!");
-    }
-
-    if (hadronicInteraction_.isValidTarget(TargetId)) {
-      auto const sigProd = readCrossSectionTable(iBeamA, TargetId, LabEnergyPerNuc);
-      CORSIKA_LOG_DEBUG("cross section (mb): " + std::to_string(sigProd / 1_mb));
-      return std::make_tuple(sigProd, 0_mb);
-    } else {
-      throw std::runtime_error("target outside range.");
-    }
-    return std::make_tuple(std::numeric_limits<double>::infinity() * 1_mb,
-                           std::numeric_limits<double>::infinity() * 1_mb);
-  }
-
-  template <typename TEnvironment>
-  template <typename TParticle>
-  inline GrammageType NuclearInteraction<TEnvironment>::getInteractionLength(
-      TParticle const& projectile) {
-
-    // coordinate system, get global frame of reference
-
-    const Code corsikaBeamId = projectile.getPID();
-
-    if (!is_nucleus(corsikaBeamId)) {
-      // no nuclear interaction
-      return std::numeric_limits<double>::infinity() * 1_g / (1_cm * 1_cm);
-    }
-
-    // read from cross section code table
-
-    MomentumVector pLab = projectile.getMomentum();
-    CoordinateSystemPtr const& labCS = pLab.getCoordinateSystem();
-
-    // FOR NOW: assume target is at rest
-    MomentumVector pTarget(labCS, {0.0_GeV, 0.0_GeV, 0.0_GeV});
-
-    // total momentum and energy
-    HEPEnergyType Elab = projectile.getEnergy() + constants::nucleonMass;
-    int const nuclA = get_nucleus_A(corsikaBeamId);
-    auto const ElabNuc = projectile.getEnergy() / nuclA;
-
-    MomentumVector pTotLab(labCS, {0.0_GeV, 0.0_GeV, 0.0_GeV});
-    pTotLab += pLab;
-    pTotLab += pTarget;
-    auto const pTotLabNorm = pTotLab.getNorm();
-    // calculate cm. energy
-    [[maybe_unused]] HEPEnergyType const ECoM = sqrt(
-        (Elab + pTotLabNorm) * (Elab - pTotLabNorm)); // binomial for numerical accuracy
-    auto const ECoMNN = sqrt(2. * ElabNuc * constants::nucleonMass);
-    CORSIKA_LOG_DEBUG(
-        "NuclearInteraction: LambdaInt: \n"
-        " input energy: {}GeV\n"
-        " input energy CoM: {}GeV\n"
-        " beam pid: {}\n"
-        " beam A: {}\n"
-        " input energy per nucleon: {}GeV\n"
-        " input energy CoM per nucleon: {}GeV ",
-        Elab / 1_GeV, ECoM / 1_GeV, get_name(corsikaBeamId), nuclA, ElabNuc / 1_GeV,
-        ECoMNN / 1_GeV);
-
-    // energy limits
-    // TODO: values depend on hadronic interaction model !! this is sibyll specific
-    if (ElabNuc >= 8.5_GeV && ECoMNN >= gMinEnergyPerNucleonCoM_ &&
-        ECoMNN < gMaxEnergyPerNucleonCoM_) {
-
-      // get target from environment
-      /*
-        the target should be defined by the Environment,
-        ideally as full particle object so that the four momenta
-        and the boosts can be defined..
-      */
-      auto const* const currentNode = projectile.getNode();
-      auto const& mediumComposition =
-          currentNode->getModelProperties().getNuclearComposition();
-      // determine average interaction length
-      // weighted sum
-      int i = -1;
-      CrossSectionType weightedProdCrossSection = 0_mb;
-      // get weights of components from environment/medium
-      const auto& w = mediumComposition.getFractions();
-      // loop over components in medium
-      for (auto const targetId : mediumComposition.getComponents()) {
-        if (targetId == Code::Argon) continue; // NEED TO IGNORE Argon ....
-        i++;
-        CORSIKA_LOG_DEBUG("NuclearInteraction: get interaction length for target: {}",
-                          get_name(targetId));
-        auto const [productionCrossSection, elaCrossSection] =
-            getCrossSection(projectile, targetId);
-        [[maybe_unused]] auto& dummy_elaCrossSection = elaCrossSection;
-
-        CORSIKA_LOG_DEBUG(
-            "NuclearInteraction: "
-            "IntLength: nuclib return (mb): " +
-            std::to_string(productionCrossSection / 1_mb));
-        weightedProdCrossSection += w[i] * productionCrossSection;
-      }
-      CORSIKA_LOG_DEBUG(
-          "NuclearInteraction: "
-          "IntLength: weighted CrossSection (mb): {} ",
-          weightedProdCrossSection / 1_mb);
-
-      // calculate interaction length in medium
-      GrammageType const int_length = mediumComposition.getAverageMassNumber() *
-                                      constants::u / weightedProdCrossSection;
-      CORSIKA_LOG_DEBUG(
-          "NuclearInteraction: "
-          "interaction length (g/cm2): {} ",
-          int_length * (1_cm * 1_cm / (0.001_kg)));
-
-      return int_length;
-    } else {
-      return std::numeric_limits<double>::infinity() * 1_g / (1_cm * 1_cm);
-    }
-  }
-
-  template <typename TEnvironment>
-  template <typename TSecondaryView>
-  inline void NuclearInteraction<TEnvironment>::doInteraction(TSecondaryView& view) {
-
-    auto projectile = view.getProjectile();
-
-    // this routine superimposes different nucleon-nucleon interactions
-    // in a nucleus-nucleus interaction, based the SIBYLL routine SIBNUC
-
-    const auto ProjId = projectile.getPID();
-    // TODO: calculate projectile mass in nuclearStackExtension
-    //      const auto ProjMass = projectile.getMass();
-
-    CORSIKA_LOG_DEBUG("NuclearInteraction: DoInteraction: called with: {}",
-                      get_name(ProjId));
-
-    // check if target-style nucleus (enum)
-    if (!is_nucleus(ProjId)) {
-      throw std::runtime_error(
-          "NuclearInteraction: DoInteraction: Wrong nucleus type. Nuclear projectiles "
-          "should use NuclearStackExtension!");
-    }
-
-    auto const ProjMass = get_mass(ProjId);
-    CORSIKA_LOG_DEBUG("NuclearInteraction: projectile mass: {} ", ProjMass / 1_GeV);
-
-    count_++;
-
-    // position and time of interaction, not used in NUCLIB
-    Point pOrig = projectile.getPosition();
-    TimeType tOrig = projectile.getTime();
-
-    CORSIKA_LOG_DEBUG("Interaction: position of interaction: {}", pOrig.getCoordinates());
-    CORSIKA_LOG_DEBUG("Interaction: time: {} ", tOrig / 1_s);
-
-    // projectile nucleon number
-    const unsigned int kAProj = get_nucleus_A(ProjId);
-    if (kAProj > getMaxNucleusAProjectile())
-      throw std::runtime_error("Projectile nucleus too large for NUCLIB!");
-
-    // kinematics
-    // define projectile nucleus
-    HEPEnergyType const eProjectileLab = projectile.getEnergy();
-    MomentumVector const pProjectileLab = projectile.getMomentum();
-    FourVector const PprojLab(eProjectileLab, pProjectileLab);
-    CoordinateSystemPtr const& labCS = pProjectileLab.getCoordinateSystem();
-
-    CORSIKA_LOG_DEBUG(
-        "NuclearInteraction: eProj lab: {} "
-        "pProj lab: {} ",
-        eProjectileLab / 1_GeV, pProjectileLab.getComponents() / 1_GeV);
-
-    // define projectile nucleon
-    HEPEnergyType const eProjectileNucLab = eProjectileLab / kAProj;
-    MomentumVector const pProjectileNucLab = pProjectileLab / kAProj;
-    FourVector const PprojNucLab(eProjectileNucLab, pProjectileNucLab);
-
-    CORSIKA_LOG_DEBUG(
-        "NuclearInteraction: eProjNucleon lab (GeV): {} "
-        "pProjNucleon lab (GeV): {} ",
-        eProjectileNucLab / 1_GeV, pProjectileNucLab.getComponents() / 1_GeV);
-
-    // define target
-    // always a nucleon
-    // target is always at rest
-    auto const eTargetNucLab = 0_GeV + constants::nucleonMass;
-    auto const pTargetNucLab = MomentumVector(labCS, 0_GeV, 0_GeV, 0_GeV);
-    FourVector const PtargNucLab(eTargetNucLab, pTargetNucLab);
-
-    CORSIKA_LOG_DEBUG(
-        "NuclearInteraction: etarget lab(GeV): {} "
-        "NuclearInteraction: ptarget lab(GeV): {} ",
-        eTargetNucLab / 1_GeV, pTargetNucLab.getComponents() / 1_GeV);
-
-    // center-of-mass energy in nucleon-nucleon frame
-    auto const PtotNN4 = PtargNucLab + PprojNucLab;
-    HEPEnergyType EcmNN = PtotNN4.getNorm();
-
-    CORSIKA_LOG_DEBUG("NuclearInteraction: nuc-nuc cm energy: {}", EcmNN / 1_GeV);
-
-    if (!hadronicInteraction_.isValidCoMEnergy(EcmNN)) {
-      CORSIKA_LOG_DEBUG(
-          "NuclearInteraction: nuc-nuc. CoM energy too low for hadronic "
-          "interaction model!");
-      throw std::runtime_error("NuclearInteraction: DoInteraction: energy too low!");
-    }
-
-    // define boost to NUCLEON-NUCLEON frame
-    COMBoost const boost(PprojNucLab, constants::nucleonMass);
-    // boost projecticle
-    auto const PprojNucCoM = boost.toCoM(PprojNucLab);
-
-    // boost target
-    auto const PtargNucCoM = boost.toCoM(PtargNucLab);
-
-    CORSIKA_LOG_DEBUG(
-        "Interaction: ebeam CoM: {} "
-        ", pbeam CoM: {} ",
-        PprojNucCoM.getTimeLikeComponent() / 1_GeV,
-        PprojNucCoM.getSpaceLikeComponents().getComponents() / 1_GeV);
-    CORSIKA_LOG_DEBUG(
-        "Interaction: etarget CoM: {}"
-        ", ptarget CoM: {}",
-        PtargNucCoM.getTimeLikeComponent() / 1_GeV,
-        PtargNucCoM.getSpaceLikeComponents().getComponents() / 1_GeV);
-
-    // sample target nucleon number
-    //
-    // proton stand-in for nucleon
-    const auto beamId = Code::Proton;
-    auto const* const currentNode = projectile.getNode();
-    const auto& mediumComposition =
-        currentNode->getModelProperties().getNuclearComposition();
-    CORSIKA_LOG_DEBUG("get nucleon-nucleus cross sections for target materials..");
-    // get cross sections for target materials
-    // using nucleon-target-nucleus cross section!!!
-    /*
-      Here we read the cross section from the interaction model again,
-      should be passed from getInteractionLength if possible
-    */
-    auto const& compVec = mediumComposition.getComponents();
-    std::vector<CrossSectionType> cross_section_of_components(compVec.size());
-
-    for (size_t i = 0; i < compVec.size(); ++i) {
-      auto const targetId = compVec[i];
-      if (targetId == Code::Argon) continue; // NEED TO IGNORE Argon ....
-      CORSIKA_LOG_DEBUG("target component: {}", get_name(targetId));
-      CORSIKA_LOG_DEBUG("beam id: {}", get_name(beamId));
-      const auto [sigProd, sigEla] =
-          hadronicInteraction_.getCrossSection(beamId, targetId, EcmNN);
-      cross_section_of_components[i] = sigProd;
-      [[maybe_unused]] auto sigElaCopy = sigEla; // ONLY TO AVOID COMPILER WARNINGS
-    }
-
-    const auto targetCode =
-        mediumComposition.sampleTarget(cross_section_of_components, RNG_);
-    CORSIKA_LOG_DEBUG("Interaction: target selected: {}", get_name(targetCode));
-    /*
-      FOR NOW: allow nuclei with A<18 or protons only.
-      when medium composition becomes more complex, approximations will have to be
-      allowed air in atmosphere also contains some Argon.
-    */
-    int kATarget = -1;
-    if (is_nucleus(targetCode))
-      kATarget = get_nucleus_A(targetCode);
-    else if (targetCode == Code::Proton)
-      kATarget = 1;
-    CORSIKA_LOG_DEBUG("NuclearInteraction: nuclib target code: " +
-                      std::to_string(kATarget));
-    if (!hadronicInteraction_.isValidTarget(targetCode))
-      throw std::runtime_error("target outside range. ");
-    // end of target sampling
-
-    // superposition
-    CORSIKA_LOG_DEBUG(
-        "NuclearInteraction: sampling nuc. multiple interaction structure.. ");
-    // get nucleon-nucleon cross section
-    // (needed to determine number of nucleon-nucleon scatterings)
-    const auto protonId = Code::Proton;
-    const auto [prodCrossSection, elaCrossSection] =
-        hadronicInteraction_.getCrossSection(protonId, protonId, EcmNN);
-    const double sigProd = prodCrossSection / 1_mb;
-    const double sigEla = elaCrossSection / 1_mb;
-    // sample number of interactions (only input variables, output in common cnucms)
-    // nuclear multiple scattering according to glauber (r.i.p.)
-    int_nuc_(kATarget, kAProj, sigProd, sigEla);
-
-    CORSIKA_LOG_DEBUG(
-        "number of nucleons in target           : {}\n"
-        "number of wounded nucleons in target   : {}\n"
-        "number of nucleons in projectile       : {}\n"
-        "number of wounded nucleons in project. : {}\n"
-        "number of inel. nuc.-nuc. interactions : {}\n"
-        "number of elastic nucleons in target   : {}\n"
-        "number of elastic nucleons in project. : {}\n"
-        "impact parameter: {}",
-        kATarget, cnucms_.na, kAProj, cnucms_.nb, cnucms_.ni, cnucms_.nael, cnucms_.nbel,
-        cnucms_.b);
-
-    // calculate fragmentation
-    CORSIKA_LOG_DEBUG("calculating nuclear fragments..");
-    // number of interactions
-    // include elastic
-    const int nElasticNucleons = cnucms_.nbel;
-    const int nInelNucleons = cnucms_.nb;
-    const int nIntProj = nInelNucleons + nElasticNucleons;
-    const double impactPar = cnucms_.b; // only needed to avoid passing common var.
-    int nFragments = 0;
-    // number of fragments is limited to 60
-    int AFragments[60];
-    // call fragmentation routine
-    // input: target A, projectile A, number of int. nucleons in projectile, impact
-    // parameter (fm) output: nFragments, AFragments in addition the momenta ar stored
-    // in pf in common fragments, neglected
-    fragm_(kATarget, kAProj, nIntProj, impactPar, nFragments, AFragments);
-
-    // this should not occur but well :)  (LCOV_EXCL_START)
-    if (nFragments > (int)getMaxNFragments())
-      throw std::runtime_error("Number of nuclear fragments in NUCLIB exceeded!");
-    // (LCOV_EXCL_STOP)
-
-    CORSIKA_LOG_DEBUG("number of fragments: " + std::to_string(nFragments));
-    CORSIKA_LOG_DEBUG("adding nuclear fragments to particle stack..");
-    // put nuclear fragments on corsika stack
-    for (int j = 0; j < nFragments; ++j) {
-      CORSIKA_LOG_DEBUG("fragment {}: A={} px={} py={} pz={}", j, AFragments[j],
-                        fragments_.ppp[j][0], fragments_.ppp[j][1], fragments_.ppp[j][2]);
-      const auto nuclA = AFragments[j];
-      // get Z from stability line
-      const auto nuclZ = int(nuclA / 2.15 + 0.7);
-
-      // TODO: do we need to catch single nucleons??
-      Code specCode = Code::Neutron; //  sample neutron or proton ?
-      if (nuclA > 1) specCode = get_nucleus_code(nuclA, nuclZ);
-      HEPMassType const mass = get_mass(specCode);
-
-      CORSIKA_LOG_DEBUG("NuclearInteraction: adding fragment: {}", get_name(specCode));
-      CORSIKA_LOG_DEBUG("NuclearInteraction: A,Z: {}, {}", nuclA, nuclZ);
-      CORSIKA_LOG_DEBUG("NuclearInteraction: mass: {} GeV", std::to_string(mass / 1_GeV));
-
-      // CORSIKA 7 way
-      // spectators inherit momentum from original projectile
-      const double mass_ratio = mass / ProjMass;
-
-      CORSIKA_LOG_DEBUG("NuclearInteraction: mass ratio " + std::to_string(mass_ratio));
-
-      auto const Plab = PprojLab * mass_ratio;
-
-      CORSIKA_LOG_DEBUG("NuclearInteraction: fragment momentum: {}",
-                        Plab.getSpaceLikeComponents().getComponents() / 1_GeV);
-
-      projectile.addSecondary(
-          std::make_tuple(specCode, Plab.getSpaceLikeComponents(), pOrig, tOrig));
-    }
-
-    // add elastic nucleons to corsika stack
-    // TODO: the elastic interaction could be external like the inelastic interaction,
-    // e.g. use existing ElasticModel
-    CORSIKA_LOG_DEBUG("adding elastically scattered nucleons to particle stack..");
-    for (int j = 0; j < nElasticNucleons; ++j) {
-      // TODO: sample proton or neutron
-      auto const elaNucCode = Code::Proton;
-
-      // CORSIKA 7 way
-      // elastic nucleons inherit momentum from original projectile
-      // neglecting momentum transfer in interaction
-      const double mass_ratio = get_mass(elaNucCode) / ProjMass;
-      auto const Plab = PprojLab * mass_ratio;
-
-      projectile.addSecondary(
-          std::make_tuple(elaNucCode, Plab.getSpaceLikeComponents(), pOrig, tOrig));
-    }
-
-    // add inelastic interactions
-    CORSIKA_LOG_DEBUG("calculate inelastic nucleon-nucleon interactions..");
-    for (int j = 0; j < nInelNucleons; ++j) {
-      // TODO: sample neutron or proton
-      auto pCode = Code::Proton;
-      // temporarily add to stack, will be removed after interaction in DoInteraction
-      CORSIKA_LOG_DEBUG("inelastic interaction no. {}", j);
-      typename TSecondaryView::inner_stack_value_type nucleonStack;
-      auto inelasticNucleon = nucleonStack.addParticle(
-          std::make_tuple(pCode, PprojNucLab.getSpaceLikeComponents(), pOrig, tOrig));
-      inelasticNucleon.setNode(projectile.getNode());
-      // create inelastic interaction for each nucleon
-      CORSIKA_LOG_TRACE("calling HadronicInteraction...");
-      // create new StackView for each of the nucleons
-      TSecondaryView nucleon_secondaries(inelasticNucleon);
-      // all inner hadronic event generator
-      hadronicInteraction_.doInteraction(nucleon_secondaries);
-      for (const auto& pSec : nucleon_secondaries) {
-        projectile.addSecondary(std::make_tuple(pSec.getPID(), pSec.getMomentum(),
-                                                pSec.getPosition(), pSec.getTime()));
-      }
-    }
-
-    CORSIKA_LOG_DEBUG("NuclearInteraction: DoInteraction: done");
-  }
-
-} // namespace corsika::sibyll
diff --git a/corsika/detail/modules/sibyll/NuclearInteractionModel.inl b/corsika/detail/modules/sibyll/NuclearInteractionModel.inl
new file mode 100644
index 0000000000000000000000000000000000000000..4a564c7e96bcc64e059dfec58b43eb3f5b8ed7ac
--- /dev/null
+++ b/corsika/detail/modules/sibyll/NuclearInteractionModel.inl
@@ -0,0 +1,370 @@
+/*
+ * (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <corsika/media/Environment.hpp>
+#include <corsika/media/NuclearComposition.hpp>
+
+#include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/framework/utility/COMBoost.hpp>
+#include <corsika/framework/core/Logging.hpp>
+
+#include <nuclib.hpp>
+
+namespace corsika::sibyll {
+
+  template <typename TEnvironment, typename TNucleonModel>
+  inline NuclearInteractionModel<TEnvironment, TNucleonModel>::NuclearInteractionModel(
+      TNucleonModel& hadint, TEnvironment const& env)
+      : environment_(env)
+      , hadronicInteraction_(hadint) {
+
+    // initialize nuclib
+    // TODO: make sure this does not overlap with sibyll
+    nuc_nuc_ini_();
+
+    // initialize cross sections
+    initializeNuclearCrossSections();
+  }
+
+  template <typename TEnvironment, typename TNucleonModel>
+  inline NuclearInteractionModel<TEnvironment,
+                                 TNucleonModel>::~NuclearInteractionModel() {
+    CORSIKA_LOG_DEBUG("Nuclib::NuclearInteractionModel n={} Nnuc={}", count_, nucCount_);
+  }
+
+  template <typename TEnvironment, typename TNucleonModel>
+  inline bool constexpr NuclearInteractionModel<TEnvironment, TNucleonModel>::isValid(
+      Code const projectileId, Code const targetId, HEPEnergyType const sqrtSnn) const {
+
+    // also depends on underlying model, for Proton/Neutron projectile
+    if (!hadronicInteraction_.isValid(Code::Proton, targetId, sqrtSnn)) { return false; }
+
+    // projectile limits:
+    if (!is_nucleus(projectileId)) { return false; }
+    unsigned int projectileA = get_nucleus_A(projectileId);
+    if (projectileA > getMaxNucleusAProjectile() || projectileA < 2) { return false; }
+    return true;
+  } // namespace corsika::sibyll
+
+  template <typename TEnvironment, typename TNucleonModel>
+  inline void
+  NuclearInteractionModel<TEnvironment, TNucleonModel>::printCrossSectionTable(
+      Code const pCode) const {
+    if (!hadronicInteraction_.isValid(Code::Proton, pCode, 100_GeV)) { // LCOV_EXCL_START
+      CORSIKA_LOG_ERROR("Invalid target type {} for hadron interaction model.", pCode);
+      return;
+    } // LCOV_EXCL_STOP
+
+    int const k = targetComponentsIndex_.at(pCode);
+    Code const pNuclei[] = {Code::Helium, Code::Lithium7, Code::Oxygen,
+                            Code::Neon,   Code::Argon,    Code::Iron};
+
+    std::ostringstream table;
+    table << "Nuclear CrossSectionTable pCode=" << pCode << " :\n en/A ";
+    for (auto& j : pNuclei) table << std::setw(9) << j;
+    table << "\n";
+
+    // loop over energy bins
+    for (unsigned int i = 0; i < getNEnergyBins(); ++i) {
+      table << " " << i << "  ";
+
+      for (auto& n : pNuclei) {
+        auto const j = get_nucleus_A(n);
+        table << " " << std::setprecision(5) << std::setw(8)
+              << cnucsignuc_.sigma[j - 1][k][i];
+      }
+      table << "\n";
+    }
+    CORSIKA_LOG_DEBUG(table.str());
+  }
+
+  template <typename TEnvironment, typename TNucleonModel>
+  inline void
+  NuclearInteractionModel<TEnvironment, TNucleonModel>::initializeNuclearCrossSections() {
+
+    auto& universe = *(environment_.getUniverse());
+    // generate complete list of all nuclei types in universe
+
+    auto const allElementsInUniverse = std::invoke([&]() {
+      std::set<Code> allElementsInUniverse;
+      auto collectElements = [&](auto& vtn) {
+        if (vtn.hasModelProperties()) {
+          auto const& comp =
+              vtn.getModelProperties().getNuclearComposition().getComponents();
+          for (auto const c : comp) allElementsInUniverse.insert(c);
+        }
+      };
+      universe.walk(collectElements);
+      return allElementsInUniverse;
+    });
+
+    CORSIKA_LOG_DEBUG("initializing nuclear cross sections...");
+
+    // loop over target components, at most 4!!
+    int k = -1;
+    for (Code const ptarg : allElementsInUniverse) {
+      ++k;
+      CORSIKA_LOG_DEBUG("init target component: {} A={}", ptarg, get_nucleus_A(ptarg));
+      int const ib = get_nucleus_A(ptarg);
+      if (!hadronicInteraction_.isValid(Code::Proton, ptarg, 100_GeV)) {
+        CORSIKA_LOG_ERROR("Invalid target type {} for hadron interaction model.", ptarg);
+        continue;
+      }
+      targetComponentsIndex_.insert(std::pair<Code, int>(ptarg, k));
+      // loop over energies, fNEnBins log. energy bins
+      for (size_t i = 0; i < getNEnergyBins(); ++i) {
+        // hard coded energy grid, has to be aligned to definition in signuc2!!, no
+        // comment..
+        HEPEnergyType const Ecm = pow(10., 1. + 1. * i) * 1_GeV;
+        // head-on pp collision:
+        HEPEnergyType const EcmHalve = Ecm / 2;
+        HEPMomentumType const pcm =
+            sqrt(EcmHalve * EcmHalve - Proton::mass * Proton::mass);
+        CoordinateSystemPtr cs = get_root_CoordinateSystem();
+        FourMomentum projectileP4(EcmHalve, {cs, pcm, 0_eV, 0_eV});
+        FourMomentum targetP4(EcmHalve, {cs, -pcm, 0_eV, 0_eV});
+        // get p-p cross sections
+        if (!hadronicInteraction_.isValid(Code::Proton, Code::Proton, Ecm)) {
+          throw std::runtime_error("invalid projectile,target,ecm combination");
+        }
+        auto const [siginel, sigela] = hadronicInteraction_.getCrossSectionInelEla(
+            Code::Proton, Code::Proton, projectileP4, targetP4);
+        double const dsig = siginel / 1_mb;
+        double const dsigela = sigela / 1_mb;
+        // loop over projectiles, mass numbers from 2 to fMaxNucleusAProjectile
+        CORSIKA_LOG_TRACE("Ecm={} siginel={} sigela={}", Ecm / 1_GeV, dsig, dsigela);
+        for (size_t j = 1; j < gMaxNucleusAProjectile_; ++j) {
+          const int jj = j + 1;
+          double sig_out, dsig_out, sigqe_out, dsigqe_out;
+          sigma_mc_(jj, ib, dsig, dsigela, gNSample_, sig_out, dsig_out, sigqe_out,
+                    dsigqe_out);
+          // write to table
+          cnucsignuc_.sigma[j][k][i] = sig_out;
+          cnucsignuc_.sigqe[j][k][i] = sigqe_out;
+          CORSIKA_LOG_TRACE("nuc A={} sig={} qe={}", j, sig_out, sigqe_out);
+        }
+      }
+    }
+    CORSIKA_LOG_DEBUG("cross sections for {} components initialized!",
+                      targetComponentsIndex_.size());
+    for (auto& ptarg : allElementsInUniverse) { printCrossSectionTable(ptarg); }
+  }
+
+  template <typename TEnvironment, typename TNucleonModel>
+  inline CrossSectionType
+  NuclearInteractionModel<TEnvironment, TNucleonModel>::readCrossSectionTable(
+      int const ia, Code const pTarget, HEPEnergyType const elabnuc) const {
+
+    int const ib = targetComponentsIndex_.at(pTarget) + 1; // table index in fortran
+    auto const ECoMNuc = sqrt(2. * constants::nucleonMass * elabnuc);
+    if (ECoMNuc < getMinEnergyPerNucleonCoM() || ECoMNuc > getMaxEnergyPerNucleonCoM()) {
+      throw std::runtime_error("energy outside tabulated range!");
+    }
+    double const e0 = elabnuc / 1_GeV;
+    double sig;
+    CORSIKA_LOG_DEBUG("ReadCrossSectionTable: {} {} {}", ia, ib, e0);
+    signuc2_(ia, ib, e0, sig);
+    CORSIKA_LOG_DEBUG("ReadCrossSectionTable: sig={}", sig);
+    return sig * 1_mb;
+  }
+
+  template <typename TEnvironment, typename TNucleonModel>
+  CrossSectionType inline NuclearInteractionModel<
+      TEnvironment, TNucleonModel>::getCrossSection(Code const projectileId,
+                                                    Code const targetId,
+                                                    FourMomentum const& projectileP4,
+                                                    FourMomentum const& targetP4) const {
+
+    HEPEnergyType const sqrtSnn = (projectileP4 + targetP4).getNorm();
+    if (!isValid(projectileId, targetId, sqrtSnn)) { return CrossSectionType::zero(); }
+    HEPEnergyType const LabEnergyPerNuc =
+        static_pow<2>(sqrtSnn) / (2 * constants::nucleonMass);
+    auto const sigProd =
+        readCrossSectionTable(get_nucleus_A(projectileId), targetId, LabEnergyPerNuc);
+    CORSIKA_LOG_DEBUG("cross section (mb): {}", sigProd / 1_mb);
+    return sigProd;
+  }
+
+  template <typename TEnvironment, typename TNucleonModel>
+  template <typename TSecondaryView>
+  inline void NuclearInteractionModel<TEnvironment, TNucleonModel>::doInteraction(
+      TSecondaryView& view, Code const projectileId, Code const targetId,
+      FourMomentum const& projectileP4, FourMomentum const& targetP4) {
+
+    // model is only designed for projectile nuclei. Collisions are broken down into
+    // "nucleon-target" collisions.
+    if (!is_nucleus(projectileId)) {
+      throw std::runtime_error("Can only handle nuclear projectiles.");
+    }
+    size_t const projectileA = get_nucleus_A(projectileId);
+
+    // this is center-of-mass for projectile_nucleon - target
+    FourMomentum const nucleonP4 = projectileP4 / projectileA;
+    HEPEnergyType const sqrtSnucleon = (nucleonP4 + targetP4).getNorm();
+    if (!isValid(projectileId, targetId, sqrtSnucleon)) {
+      throw std::runtime_error("Invalid projectile/target/energy combination.");
+    }
+    // projectile is always nucleus!
+    // Elab corresponding to sqrtSnucleon -> fixed target projectile
+    COMBoost const boost(nucleonP4, targetP4);
+
+    CORSIKA_LOG_DEBUG("pId={} tId={} sqrtSnucleon={}GeV Aproj={}", projectileId, targetId,
+                      sqrtSnucleon / 1_GeV, projectileA);
+    count_++;
+
+    // lab. momentum per projectile nucleon
+    HEPMomentumType const pNucleonLab = nucleonP4.getSpaceLikeComponents().getNorm();
+    // nucleon momentum in direction of CM motion (lab system)
+    MomentumVector const p3NucleonLab(boost.getRotatedCS(), {0_GeV, 0_GeV, pNucleonLab});
+
+    /*
+      FOR NOW: allow nuclei with A<18 or protons/nucleon only.
+      when medium composition becomes more complex, approximations will have to be
+      allowed air in atmosphere also contains some Argon.
+    */
+    int kATarget = -1;
+    size_t targetA = 1;
+    if (is_nucleus(targetId)) {
+      kATarget = get_nucleus_A(targetId);
+      targetA = kATarget;
+    } else if (targetId == Code::Proton || targetId == Code::Neutron ||
+               targetId == Code::Hydrogen) {
+      kATarget = 1;
+    }
+    CORSIKA_LOG_DEBUG("nuclib target code: {}", kATarget);
+
+    // end of target sampling
+
+    // superposition
+    CORSIKA_LOG_DEBUG("sampling nuc. multiple interaction structure.. ");
+    // get nucleon-nucleon cross section
+    // (needed to determine number of nucleon-nucleon scatterings)
+    auto const protonId = Code::Proton;
+    auto const [prodCrossSection, elaCrossSection] =
+        hadronicInteraction_.getCrossSectionInelEla(
+            protonId, protonId, nucleonP4,
+            targetP4 / targetA); // todo check, wrong RU
+    double const sigProd = prodCrossSection / 1_mb;
+    double const sigEla = elaCrossSection / 1_mb;
+    // sample number of interactions (only input variables, output in common cnucms)
+    // nuclear multiple scattering according to glauber (r.i.p.)
+    int_nuc_(kATarget, projectileA, sigProd, sigEla);
+
+    CORSIKA_LOG_DEBUG(
+        "number of nucleons in target           : {}\n"
+        "number of wounded nucleons in target   : {}\n"
+        "number of nucleons in projectile       : {}\n"
+        "number of wounded nucleons in project. : {}\n"
+        "number of inel. nuc.-nuc. interactions : {}\n"
+        "number of elastic nucleons in target   : {}\n"
+        "number of elastic nucleons in project. : {}\n"
+        "impact parameter: {}",
+        kATarget, cnucms_.na, projectileA, cnucms_.nb, cnucms_.ni, cnucms_.nael,
+        cnucms_.nbel, cnucms_.b);
+
+    // calculate fragmentation
+    CORSIKA_LOG_DEBUG("calculating nuclear fragments..");
+    // number of interactions
+    // include elastic
+    int const nElasticNucleons = cnucms_.nbel;
+    int const nInelNucleons = cnucms_.nb;
+    int const nIntProj = nInelNucleons + nElasticNucleons;
+    double const impactPar = cnucms_.b; // only needed to avoid passing common var.
+    int nFragments = 0;
+    // number of fragments is limited to 60
+    int AFragments[60];
+    // call fragmentation routine
+    // input: target A, projectile A, number of int. nucleons in projectile, impact
+    // parameter (fm) output: nFragments, AFragments in addition the momenta ar stored
+    // in pf in common fragments, neglected
+    fragm_(kATarget, projectileA, nIntProj, impactPar, nFragments, AFragments);
+
+    // this should not occur but well :)  (LCOV_EXCL_START)
+    if (nFragments > (int)getMaxNFragments()) {
+      throw std::runtime_error("Number of nuclear fragments in NUCLIB exceeded!");
+    }
+    // (LCOV_EXCL_STOP)
+
+    // position and time of interaction, not used in NUCLIB
+    auto const& projectile = view.parent();
+    // position and time of interaction, not used in NUCLI
+    Point const& pOrig = projectile.getPosition();
+    TimeType const delay = projectile.getTime();
+
+    CORSIKA_LOG_DEBUG("Interaction: position of interaction: {}, {} ns",
+                      pOrig.getCoordinates(), delay / 1_ns);
+    CORSIKA_LOG_DEBUG("number of fragments: {}", nFragments);
+    CORSIKA_LOG_DEBUG("adding nuclear fragments to particle stack..");
+    // put nuclear fragments on corsika stack
+    for (int j = 0; j < nFragments; ++j) {
+      CORSIKA_LOG_DEBUG("fragment {}: A={} px={} py={} pz={}", j, AFragments[j],
+                        fragments_.ppp[j][0], fragments_.ppp[j][1], fragments_.ppp[j][2]);
+      auto const nuclA = AFragments[j];
+      // get Z from stability line
+      auto const nuclZ = int(nuclA / 2.15 + 0.7);
+
+      // TODO: do we need to catch single nucleons??
+      Code const specCode = (nuclA == 1 ?
+                                        // TODO: sample neutron or proton
+                                 Code::Proton
+                                        : get_nucleus_code(nuclA, nuclZ));
+      HEPMassType const mass = get_mass(specCode);
+
+      CORSIKA_LOG_DEBUG("adding fragment: {}", get_name(specCode));
+      CORSIKA_LOG_DEBUG("A,Z: {}, {}", nuclA, nuclZ);
+      CORSIKA_LOG_DEBUG("mass: {} GeV", mass / 1_GeV);
+
+      // CORSIKA 7 way
+      // spectators inherit momentum from original projectile
+      auto const p3lab = p3NucleonLab * nuclA;
+      CORSIKA_LOG_DEBUG("fragment momentum {}", p3lab.getComponents() / 1_GeV);
+      view.addSecondary(std::make_tuple(specCode, p3lab, pOrig, delay));
+    }
+
+    // add elastic nucleons to corsika stack
+    // TODO: the elastic interaction could be external like the inelastic interaction,
+    // e.g. use existing ElasticModel
+    CORSIKA_LOG_DEBUG("adding elastically scattered nucleons to particle stack..");
+    for (int j = 0; j < nElasticNucleons; ++j) {
+      // TODO: sample proton or neutron
+      Code const elaNucCode = Code::Proton;
+
+      // CORSIKA 7 way
+      // elastic nucleons inherit momentum from original projectile
+      // neglecting momentum transfer in interaction
+      auto const p3lab = p3NucleonLab;
+      view.addSecondary(std::make_tuple(elaNucCode, p3lab, pOrig, delay));
+    }
+
+    // add inelastic interactions
+    CORSIKA_LOG_DEBUG("calculate inelastic nucleon-nucleon interactions..");
+    for (int j = 0; j < nInelNucleons; ++j) {
+      // TODO: sample neutron or proton
+      auto const pCode = Code::Proton;
+      // temporarily add to stack, will be removed after interaction in DoInteraction
+      CORSIKA_LOG_DEBUG("inelastic interaction no. {}", j);
+      typename TSecondaryView::inner_stack_value_type nucleonStack;
+      auto inelasticNucleon =
+          nucleonStack.addParticle(std::make_tuple(pCode, p3NucleonLab, pOrig, delay));
+      inelasticNucleon.setNode(view.getProjectile().getNode());
+      // create inelastic interaction for each nucleon
+      CORSIKA_LOG_TRACE("calling HadronicInteraction...");
+      // create new StackView for each of the nucleons
+      TSecondaryView nucleon_secondaries(inelasticNucleon);
+      // all inner hadronic event generator
+      hadronicInteraction_.doInteraction(nucleon_secondaries, pCode, targetId, nucleonP4,
+                                         targetP4);
+      for (const auto& pSec : nucleon_secondaries) {
+        view.addSecondary(std::make_tuple(pSec.getPID(), pSec.getMomentum(),
+                                          pSec.getPosition(), pSec.getTime()));
+      }
+    }
+  }
+} // namespace corsika::sibyll
diff --git a/corsika/detail/modules/urqmd/ParticleConversion.inl b/corsika/detail/modules/urqmd/ParticleConversion.inl
new file mode 100644
index 0000000000000000000000000000000000000000..de533d0488df20a2f376c6ad876109d57270c102
--- /dev/null
+++ b/corsika/detail/modules/urqmd/ParticleConversion.inl
@@ -0,0 +1,104 @@
+/*
+ * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <corsika/modules/urqmd/ParticleConversion.hpp>
+
+#include <corsika/framework/core/ParticleProperties.hpp>
+#include <corsika/framework/core/PhysicalUnits.hpp>
+
+#include <urqmd.hpp>
+
+namespace corsika::urqmd {
+
+  inline bool canInteract(Code const vCode) {
+    // According to the manual, UrQMD can use all mesons, baryons and nucleons
+    // which are modeled also as input particles. I think it is safer to accept
+    // only the usual long-lived species as input.
+    // TODO: Charmed mesons should be added to the list, too
+
+    static std::array constexpr validProjectileCodes{
+        Code::Proton,  Code::AntiProton, Code::Neutron, Code::AntiNeutron, Code::PiPlus,
+        Code::PiMinus, Code::KPlus,      Code::KMinus,  Code::K0Short,     Code::K0Long};
+
+    return std::find(std::cbegin(validProjectileCodes), std::cend(validProjectileCodes),
+                     vCode) != std::cend(validProjectileCodes);
+  }
+
+  inline std::pair<int, int> convertToUrQMD(Code const code) {
+    static const std::map<int, std::pair<int, int>> mapPDGToUrQMD{
+        // data mostly from github.com/afedynitch/ParticleDataTool
+        {22, {100, 0}},      // photon
+        {111, {101, 0}},     // pi0
+        {211, {101, 2}},     // pi+
+        {-211, {101, -2}},   // pi-
+        {321, {106, 1}},     // K+
+        {-321, {-106, -1}},  // K-
+        {311, {106, -1}},    // K0
+        {-311, {-106, 1}},   // K0bar
+        {2212, {1, 1}},      // p
+        {2112, {1, -1}},     // n
+        {-2212, {-1, -1}},   // pbar
+        {-2112, {-1, 1}},    // nbar
+        {221, {102, 0}},     // eta
+        {213, {104, 2}},     // rho+
+        {-213, {104, -2}},   // rho-
+        {113, {104, 0}},     // rho0
+        {323, {108, 2}},     // K*+
+        {-323, {108, -2}},   // K*-
+        {313, {108, 0}},     // K*0
+        {-313, {-108, 0}},   // K*0-bar
+        {223, {103, 0}},     // omega
+        {333, {109, 0}},     // phi
+        {3222, {40, 2}},     // Sigma+
+        {3212, {40, 0}},     // Sigma0
+        {3112, {40, -2}},    // Sigma-
+        {3322, {49, 0}},     // Xi0
+        {3312, {49, -1}},    // Xi-
+        {3122, {27, 0}},     // Lambda0
+        {2224, {17, 4}},     // Delta++
+        {2214, {17, 2}},     // Delta+
+        {2114, {17, 0}},     // Delta0
+        {1114, {17, -2}},    // Delta-
+        {3224, {41, 2}},     // Sigma*+
+        {3214, {41, 0}},     // Sigma*0
+        {3114, {41, -2}},    // Sigma*-
+        {3324, {50, 0}},     // Xi*0
+        {3314, {50, -1}},    // Xi*-
+        {3334, {55, 0}},     // Omega-
+        {411, {133, 2}},     // D+
+        {-411, {133, -2}},   // D-
+        {421, {133, 0}},     // D0
+        {-421, {-133, 0}},   // D0-bar
+        {441, {107, 0}},     // etaC
+        {431, {138, 1}},     // Ds+
+        {-431, {138, -1}},   // Ds-
+        {433, {139, 1}},     // Ds*+
+        {-433, {139, -1}},   // Ds*-
+        {413, {134, 1}},     // D*+
+        {-413, {134, -1}},   // D*-
+        {10421, {134, 0}},   // D*0
+        {-10421, {-134, 0}}, // D*0-bar
+        {443, {135, 0}},     // jpsi
+    };
+
+    return mapPDGToUrQMD.at(static_cast<int>(get_PDG(code)));
+  }
+
+  inline Code convertFromUrQMD(int vItyp, int vIso3) {
+    int const pdgInt =
+        ::urqmd::pdgid_(vItyp, vIso3); // use the conversion function provided by UrQMD
+    if (pdgInt == 0) {                 // ::urqmd::pdgid_ returns 0 on error
+      throw std::runtime_error("UrQMD pdgid() returned 0");
+    }
+    auto const pdg = static_cast<PDGCode>(pdgInt);
+    return convert_from_PDG(pdg);
+  }
+
+} // namespace corsika::urqmd
diff --git a/corsika/detail/modules/urqmd/UrQMD.inl b/corsika/detail/modules/urqmd/UrQMD.inl
index 80da719edae6312513999344fae7b819999980b9..1bf0f0d59a42428df231717f2df869f72c30bd45 100644
--- a/corsika/detail/modules/urqmd/UrQMD.inl
+++ b/corsika/detail/modules/urqmd/UrQMD.inl
@@ -9,11 +9,13 @@
 #pragma once
 
 #include <corsika/modules/urqmd/UrQMD.hpp>
+#include <corsika/modules/urqmd/ParticleConversion.hpp>
 
 #include <corsika/framework/core/ParticleProperties.hpp>
 #include <corsika/framework/core/PhysicalUnits.hpp>
 #include <corsika/framework/geometry/QuantityVector.hpp>
 #include <corsika/framework/geometry/Vector.hpp>
+#include <corsika/framework/utility/COMBoost.hpp>
 
 #include <boost/filesystem.hpp>
 #include <boost/multi_array.hpp>
@@ -35,12 +37,21 @@ namespace corsika::urqmd {
     ::urqmd::iniurqmdc8_();
   }
 
-  inline CrossSectionType UrQMD::getTabulatedCrossSection(Code projectileCode,
-                                                          Code targetCode,
-                                                          HEPEnergyType labEnergy) const {
+  inline bool UrQMD::isValid(Code const projectileId, Code const targetId) const {
+
+    if (!is_hadron(projectileId) || !corsika::urqmd::canInteract(projectileId)) {
+      return false;
+    }
+    if (!is_nucleus(targetId)) { return false; }
+    return true;
+  }
+
+  inline CrossSectionType UrQMD::getTabulatedCrossSection(
+      Code const projectileId, Code const targetId, HEPEnergyType const labEnergy) const {
+
     // translated to C++ from CORSIKA 7 subroutine cxtot_u
 
-    auto const kinEnergy = labEnergy - get_mass(projectileCode);
+    auto const kinEnergy = labEnergy - get_mass(projectileId);
 
     assert(kinEnergy >= HEPEnergyType::zero());
 
@@ -54,7 +65,7 @@ namespace corsika::urqmd {
     w[2 - 1] = w[2 - 1] - 2 * w[3 - 1];
 
     int projectileIndex;
-    switch (projectileCode) {
+    switch (projectileId) {
       case Code::Proton:
         projectileIndex = 0;
         break;
@@ -88,14 +99,14 @@ namespace corsika::urqmd {
         projectileIndex = 8;
         break;
       default: { // LCOV_EXCL_START since this can never happen due to canInteract
-        CORSIKA_LOG_WARN("UrQMD cross-section not tabulated for {}", projectileCode);
+        CORSIKA_LOG_WARN("UrQMD cross-section not tabulated for {}", projectileId);
         return CrossSectionType::zero();
         // LCOV_EXCL_STOP
       }
     }
 
     int targetIndex;
-    switch (targetCode) {
+    switch (targetId) {
       case Code::Nitrogen:
         targetIndex = 0;
         break;
@@ -107,7 +118,7 @@ namespace corsika::urqmd {
         break;
       default:
         std::stringstream ss;
-        ss << "UrQMD cross-section not tabluated for target " << targetCode;
+        ss << "UrQMD cross-section not tabluated for target " << targetId;
         throw std::runtime_error(ss.str().data());
     }
 
@@ -119,53 +130,17 @@ namespace corsika::urqmd {
 
     CORSIKA_LOG_TRACE(
         "UrQMD::GetTabulatedCrossSection proj={}, targ={}, E={} GeV, sigma={}",
-        get_name(projectileCode), get_name(targetCode), labEnergy / 1_GeV, result);
+        get_name(projectileId), get_name(targetId), labEnergy / 1_GeV, result);
 
     return result;
   }
 
-  inline CrossSectionType UrQMD::getCrossSection(Code vProjectileCode, Code vTargetCode,
-                                                 HEPEnergyType vLabEnergy,
-                                                 int vAProjectile = 1) {
-
-    // the following is a translation of ptsigtot() into C++
-    if (!is_nucleus(vProjectileCode) &&
-        !is_nucleus(vTargetCode)) { // both particles are "special"
-      auto const mProj = get_mass(vProjectileCode);
-      auto const mTar = get_mass(vTargetCode);
-      double sqrtS =
-          sqrt(static_pow<2>(mProj) + static_pow<2>(mTar) + 2 * vLabEnergy * mTar) *
-          (1 / 1_GeV);
-
-      // we must set some UrQMD globals first...
-      auto const [ityp, iso3] = convertToUrQMD(vProjectileCode);
-      ::urqmd::inputs_.spityp[0] = ityp;
-      ::urqmd::inputs_.spiso3[0] = iso3;
-
-      auto const [itypTar, iso3Tar] = convertToUrQMD(vTargetCode);
-      ::urqmd::inputs_.spityp[1] = itypTar;
-      ::urqmd::inputs_.spiso3[1] = iso3Tar;
-
-      int one = 1;
-      int two = 2;
-      return ::urqmd::sigtot_(one, two, sqrtS) * 1_mb;
-    } else {
-      int const Ap = vAProjectile;
-      int const At = is_nucleus(vTargetCode) ? get_nucleus_A(vTargetCode) : 1;
-
-      double const maxImpact = ::urqmd::nucrad_(Ap) + ::urqmd::nucrad_(At) +
-                               2 * ::urqmd::options_.CTParam[30 - 1];
-      return 10_mb * M_PI * static_pow<2>(maxImpact);
-      // is a constant cross-section really reasonable?
-    }
-  }
-
-  template <typename TParticle>
-  inline CrossSectionType UrQMD::getCrossSection(TParticle const& projectile,
-                                                 Code targetCode) const {
-    auto const projectileCode = projectile.getPID();
+  inline CrossSectionType UrQMD::getCrossSection(Code const projectileId,
+                                                 Code const targetId,
+                                                 FourMomentum const& projectileP4,
+                                                 FourMomentum const& targetP4) const {
 
-    if (is_nucleus(projectileCode)) {
+    if (!isValid(projectileId, targetId)) {
       /*
        * unfortunately unavoidable at the moment until we have tools to get the actual
        * inealstic cross-section from UrQMD
@@ -173,72 +148,61 @@ namespace corsika::urqmd {
       return CrossSectionType::zero();
     }
 
-    return getTabulatedCrossSection(projectileCode, targetCode, projectile.getEnergy());
-  }
-
-  inline bool UrQMD::canInteract(Code vCode) const {
-    // According to the manual, UrQMD can use all mesons, baryons and nucleons
-    // which are modeled also as input particles. I think it is safer to accept
-    // only the usual long-lived species as input.
-    // TODO: Charmed mesons should be added to the list, too
+    // define projectile, in lab frame
+    auto const sqrtS2 = (projectileP4 + targetP4).getNormSqr();
+    HEPEnergyType const Elab = (sqrtS2 - static_pow<2>(get_mass(projectileId)) -
+                                static_pow<2>(get_mass(targetId))) /
+                               (2 * get_mass(targetId));
 
-    static std::array constexpr validProjectileCodes{
-        Code::Proton,  Code::AntiProton, Code::Neutron, Code::AntiNeutron, Code::PiPlus,
-        Code::PiMinus, Code::KPlus,      Code::KMinus,  Code::K0Short,     Code::K0Long};
+    bool const tabulated = true;
+    if (tabulated) { return getTabulatedCrossSection(projectileId, targetId, Elab); }
 
-    return std::find(std::cbegin(validProjectileCodes), std::cend(validProjectileCodes),
-                     vCode) != std::cend(validProjectileCodes);
-  }
+    // the following is a translation of ptsigtot() into C++
+    if (!is_nucleus(projectileId) &&
+        !is_nucleus(targetId)) { // both particles are "special"
 
-  template <typename TParticle>
-  inline GrammageType UrQMD::getInteractionLength(TParticle const& vParticle) const {
+      double sqrtS = sqrt(sqrtS2) / 1_GeV;
 
-    if (!canInteract(vParticle.getPID())) {
-      // we could do the canInteract check in getCrossSection, too but if
-      // we do it here we have the advantage of avoiding the loop
-      return std::numeric_limits<double>::infinity() * 1_g / (1_cm * 1_cm);
-    }
+      // we must set some UrQMD globals first...
+      auto const [ityp, iso3] = corsika::urqmd::convertToUrQMD(projectileId);
+      ::urqmd::inputs_.spityp[0] = ityp;
+      ::urqmd::inputs_.spiso3[0] = iso3;
 
-    auto const& mediumComposition =
-        vParticle.getNode()->getModelProperties().getNuclearComposition();
-    using namespace std::placeholders;
+      auto const [itypTar, iso3Tar] = corsika::urqmd::convertToUrQMD(targetId);
+      ::urqmd::inputs_.spityp[1] = itypTar;
+      ::urqmd::inputs_.spiso3[1] = iso3Tar;
 
-    CrossSectionType const weightedProdCrossSection = mediumComposition.getWeightedSum(
-        std::bind(&UrQMD::getCrossSection<decltype(vParticle)>, this, vParticle, _1));
+      int one = 1;
+      int two = 2;
+      return ::urqmd::sigtot_(one, two, sqrtS) * 1_mb;
+    }
 
-    return mediumComposition.getAverageMassNumber() * constants::u /
-           weightedProdCrossSection;
+    // at least one of them is a nucleus
+    int const Ap = is_nucleus(projectileId) ? get_nucleus_A(projectileId) : 1;
+    int const At = is_nucleus(targetId) ? get_nucleus_A(targetId) : 1;
+    double const maxImpact = ::urqmd::nucrad_(Ap) + ::urqmd::nucrad_(At) +
+                             2 * ::urqmd::options_.CTParam[30 - 1];
+    return 10_mb * M_PI * static_pow<2>(maxImpact);
+    // is a constant cross-section really reasonable?
   }
 
   template <typename TView>
-  inline void UrQMD::doInteraction(TView& view) {
-
-    auto projectile = view.getProjectile();
-
-    Code projectileCode = projectile.getPID();
-    auto const projectileEnergyLab = projectile.getEnergy();
-    auto const& projectileMomentumLab = projectile.getMomentum();
-    auto const& projectilePosition = projectile.getPosition();
-    auto const projectileTime = projectile.getTime();
-
-    // sample target particle
-    auto const& mediumComposition =
-        projectile.getNode()->getModelProperties().getNuclearComposition();
-    auto const componentCrossSections = std::invoke([&]() {
-      auto const& components = mediumComposition.getComponents();
-      std::vector<CrossSectionType> crossSections;
-      crossSections.reserve(components.size());
-
-      for (auto const c : components) {
-        crossSections.push_back(getCrossSection(projectile, c));
-      }
-
-      return crossSections;
-    });
+  inline void UrQMD::doInteraction(TView& view, Code const projectileId,
+                                   Code const targetId, FourMomentum const& projectileP4,
+                                   FourMomentum const& targetP4) {
+
+    // define projectile, in lab frame
+    auto const sqrtS2 = (projectileP4 + targetP4).getNormSqr();
+    HEPEnergyType const Elab = (sqrtS2 - static_pow<2>(get_mass(projectileId)) -
+                                static_pow<2>(get_mass(targetId))) /
+                               (2 * get_mass(targetId));
+
+    if (!isValid(projectileId, targetId)) {
+      throw std::runtime_error("invalid target,projectile,energy combination");
+    }
 
-    auto const targetCode = mediumComposition.sampleTarget(componentCrossSections, RNG_);
-    auto const targetA = get_nucleus_A(targetCode);
-    auto const targetZ = get_nucleus_Z(targetCode);
+    size_t const targetA = get_nucleus_A(targetId);
+    size_t const targetZ = get_nucleus_Z(targetId);
 
     ::urqmd::inputs_.nevents = 1;
     ::urqmd::sys_.eos = 0; // could be configurable in principle
@@ -246,14 +210,13 @@ namespace corsika::urqmd {
     ::urqmd::sys_.nsteps = 1;
 
     // initialization regarding projectile
-    if (is_nucleus(projectileCode)) {
+    if (is_nucleus(projectileId)) {
       // is this everything?
       ::urqmd::inputs_.prspflg = 0;
 
-      ::urqmd::sys_.Ap = get_nucleus_A(projectileCode);
-      ::urqmd::sys_.Zp = get_nucleus_Z(projectileCode);
-      ::urqmd::rsys_.ebeam =
-          (projectileEnergyLab - projectile.getMass()) * (1 / 1_GeV) / ::urqmd::sys_.Ap;
+      ::urqmd::sys_.Ap = get_nucleus_A(projectileId);
+      ::urqmd::sys_.Zp = get_nucleus_Z(projectileId);
+      ::urqmd::rsys_.ebeam = (Elab - get_mass(projectileId)) / 1_GeV / ::urqmd::sys_.Ap;
 
       ::urqmd::rsys_.bdist = ::urqmd::nucrad_(targetA) +
                              ::urqmd::nucrad_(::urqmd::sys_.Ap) +
@@ -267,20 +230,19 @@ namespace corsika::urqmd {
           1; // even for non-baryons this has to be set, see vanilla UrQMD.f
       ::urqmd::rsys_.bdist = ::urqmd::nucrad_(targetA) + ::urqmd::nucrad_(1) +
                              2 * ::urqmd::options_.CTParam[30 - 1];
-      ::urqmd::rsys_.ebeam = (projectileEnergyLab - projectile.getMass()) * (1 / 1_GeV);
+      ::urqmd::rsys_.ebeam = (Elab - get_mass(projectileId)) / 1_GeV;
 
-      if (projectileCode == Code::K0Long || projectileCode == Code::K0Short) {
-        projectileCode = booleanDist_(RNG_) ? Code::K0 : Code::K0Bar;
-      }
-
-      auto const [ityp, iso3] = convertToUrQMD(projectileCode);
+      auto const [ityp, iso3] = corsika::urqmd::convertToUrQMD(
+          (projectileId == Code::K0Long || projectileId == Code::K0Short)
+              ? (booleanDist_(RNG_) ? Code::K0 : Code::K0Bar)
+              : projectileId);
       // todo: conversion of K_long/short into strong eigenstates;
       ::urqmd::inputs_.spityp[0] = ityp;
       ::urqmd::inputs_.spiso3[0] = iso3;
     }
 
-    // initilazation regarding target
-    if (is_nucleus(targetCode)) {
+    // initialization regarding target
+    if (is_nucleus(targetId)) {
       ::urqmd::sys_.Zt = targetZ;
       ::urqmd::sys_.At = targetA;
       ::urqmd::inputs_.trspflg = 0; // nucleus as target
@@ -288,7 +250,7 @@ namespace corsika::urqmd {
       ::urqmd::cascinit_(::urqmd::sys_.Zt, ::urqmd::sys_.At, id);
     } else {
       ::urqmd::inputs_.trspflg = 1; // special particle as target
-      auto const [ityp, iso3] = convertToUrQMD(targetCode);
+      auto const [ityp, iso3] = corsika::urqmd::convertToUrQMD(targetId);
       ::urqmd::inputs_.spityp[1] = ityp;
       ::urqmd::inputs_.spiso3[1] = iso3;
     }
@@ -297,103 +259,36 @@ namespace corsika::urqmd {
         iflb_; // flag for retrying interaction in case of empty event, 0 means retry
     ::urqmd::urqmd_(iflb);
 
+    auto projectile = view.getProjectile();
+    auto const& projectilePosition = projectile.getPosition();
+    auto const projectileTime = projectile.getTime();
+
     // now retrieve secondaries from UrQMD
-    auto const& originalCS = projectileMomentumLab.getCoordinateSystem();
-    CoordinateSystemPtr const& zAxisFrame =
-        make_rotationToZ(originalCS, projectileMomentumLab);
+    COMBoost const boost(projectileP4, targetP4);
+    auto const& originalCS = boost.getOriginalCS();
+    auto const& csPrime = boost.getRotatedCS();
 
     for (int i = 0; i < ::urqmd::sys_.npart; ++i) {
-      auto code = convertFromUrQMD(::urqmd::isys_.ityp[i], ::urqmd::isys_.iso3[i]);
+      auto code = corsika::urqmd::convertFromUrQMD(::urqmd::isys_.ityp[i],
+                                                   ::urqmd::isys_.iso3[i]);
       if (code == Code::K0 || code == Code::K0Bar) {
         code = booleanDist_(RNG_) ? Code::K0Short : Code::K0Long;
       }
 
       // "coor_.p0[i] * 1_GeV" is likely off-shell as UrQMD doesn't preserve masses well
-      auto momentum =
-          Vector(zAxisFrame,
-                 QuantityVector<dimensionless_d>{
-                     ::urqmd::coor_.px[i], ::urqmd::coor_.py[i], ::urqmd::coor_.pz[i]} *
-                     1_GeV);
+      MomentumVector momentum{csPrime,
+                              {::urqmd::coor_.px[i] * 1_GeV, ::urqmd::coor_.py[i] * 1_GeV,
+                               ::urqmd::coor_.pz[i] * 1_GeV}};
 
       momentum.rebase(originalCS); // transform back into standard lab frame
       CORSIKA_LOG_DEBUG(" {} {} {} ", i, code, momentum.getComponents());
 
-      projectile.addSecondary(
+      view.addSecondary(
           std::make_tuple(code, momentum, projectilePosition, projectileTime));
     }
     CORSIKA_LOG_DEBUG("UrQMD generated {} secondaries!", ::urqmd::sys_.npart);
   }
 
-  inline Code convertFromUrQMD(int vItyp, int vIso3) {
-    int const pdgInt =
-        ::urqmd::pdgid_(vItyp, vIso3); // use the conversion function provided by UrQMD
-    if (pdgInt == 0) {                 // ::urqmd::pdgid_ returns 0 on error
-      throw std::runtime_error("UrQMD pdgid() returned 0");
-    }
-    auto const pdg = static_cast<PDGCode>(pdgInt);
-    return convert_from_PDG(pdg);
-  }
-
-  inline std::pair<int, int> convertToUrQMD(Code code) {
-    static const std::map<int, std::pair<int, int>> mapPDGToUrQMD{
-        // data mostly from github.com/afedynitch/ParticleDataTool
-        {22, {100, 0}},      // photon
-        {111, {101, 0}},     // pi0
-        {211, {101, 2}},     // pi+
-        {-211, {101, -2}},   // pi-
-        {321, {106, 1}},     // K+
-        {-321, {-106, -1}},  // K-
-        {311, {106, -1}},    // K0
-        {-311, {-106, 1}},   // K0bar
-        {2212, {1, 1}},      // p
-        {2112, {1, -1}},     // n
-        {-2212, {-1, -1}},   // pbar
-        {-2112, {-1, 1}},    // nbar
-        {221, {102, 0}},     // eta
-        {213, {104, 2}},     // rho+
-        {-213, {104, -2}},   // rho-
-        {113, {104, 0}},     // rho0
-        {323, {108, 2}},     // K*+
-        {-323, {108, -2}},   // K*-
-        {313, {108, 0}},     // K*0
-        {-313, {-108, 0}},   // K*0-bar
-        {223, {103, 0}},     // omega
-        {333, {109, 0}},     // phi
-        {3222, {40, 2}},     // Sigma+
-        {3212, {40, 0}},     // Sigma0
-        {3112, {40, -2}},    // Sigma-
-        {3322, {49, 0}},     // Xi0
-        {3312, {49, -1}},    // Xi-
-        {3122, {27, 0}},     // Lambda0
-        {2224, {17, 4}},     // Delta++
-        {2214, {17, 2}},     // Delta+
-        {2114, {17, 0}},     // Delta0
-        {1114, {17, -2}},    // Delta-
-        {3224, {41, 2}},     // Sigma*+
-        {3214, {41, 0}},     // Sigma*0
-        {3114, {41, -2}},    // Sigma*-
-        {3324, {50, 0}},     // Xi*0
-        {3314, {50, -1}},    // Xi*-
-        {3334, {55, 0}},     // Omega-
-        {411, {133, 2}},     // D+
-        {-411, {133, -2}},   // D-
-        {421, {133, 0}},     // D0
-        {-421, {-133, 0}},   // D0-bar
-        {441, {107, 0}},     // etaC
-        {431, {138, 1}},     // Ds+
-        {-431, {138, -1}},   // Ds-
-        {433, {139, 1}},     // Ds*+
-        {-433, {139, -1}},   // Ds*-
-        {413, {134, 1}},     // D*+
-        {-413, {134, -1}},   // D*-
-        {10421, {134, 0}},   // D*0
-        {-10421, {-134, 0}}, // D*0-bar
-        {443, {135, 0}},     // jpsi
-    };
-
-    return mapPDGToUrQMD.at(static_cast<int>(get_PDG(code)));
-  }
-
   inline void UrQMD::readXSFile(boost::filesystem::path const filename) {
     boost::filesystem::ifstream file(filename, std::ios::in);
 
diff --git a/corsika/detail/stack/VectorStack.inl b/corsika/detail/stack/VectorStack.inl
index 9d2a9434c464df9ff1d7c8b1e1b0cdfc4c5143ff..57bc094a438dc6f7383baa39215434099777690b 100644
--- a/corsika/detail/stack/VectorStack.inl
+++ b/corsika/detail/stack/VectorStack.inl
@@ -41,7 +41,7 @@ namespace corsika {
 
   template <typename StackIteratorInterface>
   inline void ParticleInterface<StackIteratorInterface>::setParticleData(
-      ParticleInterface<StackIteratorInterface> const&,
+      ParticleInterface<StackIteratorInterface> const& parent,
       particle_data_momentum_type const& v) {
     this->setPID(std::get<0>(v));
     MomentumVector const p = std::get<1>(v);
@@ -54,7 +54,7 @@ namespace corsika {
       this->setDirection(p / sqrt(P2));
     }
     this->setPosition(std::get<2>(v));
-    this->setTime(std::get<3>(v));
+    this->setTime(std::get<3>(v) + parent.getTime()); // parent time is added
   }
 
   template <typename StackIteratorInterface>
@@ -69,12 +69,13 @@ namespace corsika {
 
   template <typename StackIteratorInterface>
   inline void ParticleInterface<StackIteratorInterface>::setParticleData(
-      ParticleInterface<StackIteratorInterface> const&, particle_data_type const& v) {
+      ParticleInterface<StackIteratorInterface> const& parent,
+      particle_data_type const& v) {
     this->setPID(std::get<0>(v));
     this->setKineticEnergy(std::get<1>(v));
     this->setDirection(std::get<2>(v));
     this->setPosition(std::get<3>(v));
-    this->setTime(std::get<4>(v));
+    this->setTime(std::get<4>(v) + parent.getTime()); // parent time is added
   }
 
   template <typename StackIteratorInterface>
diff --git a/corsika/framework/core/Cascade.hpp b/corsika/framework/core/Cascade.hpp
index 1163950f3806050b9fdd04c93aa6905a9c8cc6f0..ac7a899eb52538c432995375ea3657df89add166 100644
--- a/corsika/framework/core/Cascade.hpp
+++ b/corsika/framework/core/Cascade.hpp
@@ -12,11 +12,12 @@
 
 #include <corsika/framework/process/ProcessReturn.hpp>
 #include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/framework/core/Logging.hpp>
 #include <corsika/framework/random/ExponentialDistribution.hpp>
 #include <corsika/framework/random/RNGManager.hpp>
 #include <corsika/framework/random/UniformRealDistribution.hpp>
 #include <corsika/framework/stack/SecondaryView.hpp>
-#include <corsika/framework/core/Logging.hpp>
+#include <corsika/framework/geometry/FourVector.hpp>
 
 #include <corsika/media/Environment.hpp>
 
@@ -96,7 +97,7 @@ namespace corsika {
 
     /**
      * set the nodes for all particles on the stack according to their numerical
-     * position
+     * position.
      */
     void setNodes();
 
@@ -127,8 +128,9 @@ namespace corsika {
     void step(particle_type& vParticle);
 
     ProcessReturn decay(stack_view_type& view, InverseTimeType initial_inv_decay_time);
-    ProcessReturn interaction(stack_view_type& view,
-                              InverseGrammageType initial_inv_int_length);
+    ProcessReturn interaction(stack_view_type& view, FourMomentum const& projectileP4,
+                              NuclearComposition const& composition,
+                              CrossSectionType const initial_cross_section);
     void setEventType(stack_view_type& view, history::EventType);
 
     // data members
diff --git a/corsika/framework/core/ParticleProperties.hpp b/corsika/framework/core/ParticleProperties.hpp
index 8b9af126837c5d9c54942cb34b00c898283acd88..b26fdb26d4b9993f345cb807e7a92a8868aff11c 100644
--- a/corsika/framework/core/ParticleProperties.hpp
+++ b/corsika/framework/core/ParticleProperties.hpp
@@ -138,6 +138,13 @@ namespace corsika {
    */
   HEPMassType constexpr get_nucleus_mass(Code const code);
 
+  /**
+   * @brief Calculates the mass of nucleus.
+   *
+   * @return HEPMassType the mass of (A,Z) nucleus, disregarding binding energy.
+   */
+  HEPMassType constexpr get_nucleus_mass(unsigned int const A, unsigned int const Z);
+
   /**
    * @brief Get the nucleus name.
    *
diff --git a/corsika/framework/geometry/FourVector.hpp b/corsika/framework/geometry/FourVector.hpp
index 0b87accec038a6d5d796ea91318d86fbbe004317..0a23f4e94f2d34ff892937040aea7a51a5eaccf5 100644
--- a/corsika/framework/geometry/FourVector.hpp
+++ b/corsika/framework/geometry/FourVector.hpp
@@ -9,36 +9,42 @@
 #pragma once
 
 #include <corsika/framework/core/PhysicalUnits.hpp>
-#include <corsika/framework/geometry/Vector.hpp>
+#include <corsika/framework/core/PhysicalGeometry.hpp>
 #include <type_traits>
 
+/**
+ * @file FourVector.hpp
+ * @author Ralf Ulrich
+ * @brief General FourVector object.
+ * @date 2021-10-16
+ */
+
 namespace corsika {
 
   /**
-     Description of physical four-vectors
-
-     FourVector fully supports units, e.g. E in [GeV/c] and p in [GeV],
-     or also t in [s] and r in [m], etc.
-
-     However, for HEP applications it is also possible to use E and p
-     both in [GeV].
-
-     Thus, the input units of time-like and space-like coordinates
-     must either be idential (e.g. GeV) or scaled by "c" as in
-     [E/c]=[p].
-
-
-     The FourVector can return its squared-norm \ref getNormSqr and its
-     norm \ref getNorm, whereas norm is sqrt(abs(norm-squared)). The
-     physical units are always calculated and returned properly.
-
-     FourVector can also return if it is TimeLike, SpaceLike or PhotonLike.
-
-     When a FourVector is initialized with a lvalue references,
-     e.g. as `FourVector<TimeType&, Vector<length_d>&>`, references
-     are also used as internal data types, which should lead to
-     complete disappearance of the FourVector class during
-     optimization.
+   * Description of physical four-vectors
+   *
+   * FourVector fully supports units, e.g. E in [GeV/c] and p in [GeV],
+   * or also t in [s] and r in [m], etc.
+   *
+   * However, for HEP applications it is also possible to use E and p
+   * both in [GeV].
+   *
+   * Thus, the input units of time-like and space-like coordinates
+   * must either be idential (e.g. GeV) or scaled by "c" as in
+   * [E/c]=[p].
+   *
+   * The FourVector can return its squared-norm \ref getNormSqr and its
+   * norm \ref getNorm, whereas norm is sqrt(abs(norm-squared)). The
+   * physical units are always calculated and returned properly.
+   *
+   * FourVector can also return if it is TimeLike, SpaceLike or PhotonLike.
+   *
+   * When a FourVector is initialized with a lvalue references,
+   * e.g. as `FourVector<TimeType&, Vector<length_d>&>`, references
+   * are also used as internal data types, which should lead to
+   * complete disappearance of the FourVector class during
+   * optimization.
    */
 
   template <typename TTimeType, typename TSpaceVecType>
@@ -73,13 +79,11 @@ namespace corsika {
         , spaceLike_(eS) {}
 
     /**
-     *
      * @return timeLike_
      */
     TTimeType getTimeLikeComponent() const;
 
     /**
-     *
      * @return spaceLike_
      */
     TSpaceVecType& getSpaceLikeComponents();
@@ -124,12 +128,12 @@ namespace corsika {
     FourVector& operator/(double const);
 
     /**
-       Scalar product of two FourVectors
-
-       Note that the product between two 4-vectors assumes that you use
-       the same "c" convention for both. Only the LHS vector is checked
-       for this. You cannot mix different conventions due to
-       unit-checking.
+     * Scalar product of two FourVectors.
+     *
+     *  Note that the product between two 4-vectors assumes that you use
+     *  the same "c" convention for both. Only the LHS vector is checked
+     *  for this. You cannot mix different conventions due to
+     *  unit-checking.
      */
     norm_type operator*(FourVector const& b);
 
@@ -152,7 +156,7 @@ namespace corsika {
      *  value-copies.
      * @{
      *
-     **/
+     */
     friend FourVector<time_type, space_vec_type> operator+(FourVector const& a,
                                                            FourVector const& b) {
       return FourVector<time_type, space_vec_type>(a.timeLike_ + b.timeLike_,
@@ -179,20 +183,25 @@ namespace corsika {
 
   private:
     /**
-       This function is there to automatically remove the eventual
-       extra factor of "c" for the time-like quantity.
+     * This function is there to automatically remove the eventual
+     * extra factor of "c" for the time-like quantity.
      */
     norm_square_type getTimeSquared() const;
   };
 
   /**
    * streaming operator
-   **/
+   */
 
   template <typename TTimeType, typename TSpaceVecType>
   std::ostream& operator<<(std::ostream& os,
                            corsika::FourVector<TTimeType, TSpaceVecType> const& qv);
 
+  /**
+   * @typedef FourMomentum A FourVector with HEPEnergyType and MomentumVector.
+   */
+  typedef FourVector<HEPEnergyType, MomentumVector> FourMomentum;
+
 } // namespace corsika
 
 #include <corsika/detail/framework/geometry/FourVector.inl>
diff --git a/corsika/framework/process/BaseProcess.hpp b/corsika/framework/process/BaseProcess.hpp
index 23e8667b20c9b92c9200135fcfe1b107c6e6c386..a871a065e4e2fe8d810bbed469ca3a50a7cf4c21 100644
--- a/corsika/framework/process/BaseProcess.hpp
+++ b/corsika/framework/process/BaseProcess.hpp
@@ -41,8 +41,8 @@ namespace corsika {
     /** @name getRef Return reference to underlying type
         @{
      */
-    TDerived& ref() { return static_cast<TDerived&>(*this); }
-    const TDerived& ref() const { return static_cast<const TDerived&>(*this); }
+    TDerived& getRef() { return static_cast<TDerived&>(*this); }
+    const TDerived& getRef() const { return static_cast<const TDerived&>(*this); }
     //! @}
 
   public:
diff --git a/corsika/framework/process/DecayProcess.hpp b/corsika/framework/process/DecayProcess.hpp
index 6fa5a754cdf08059062fb6348f4a20ccc2e57d34..a104343922949c58e6efd5f3c703c0ad23227fee 100644
--- a/corsika/framework/process/DecayProcess.hpp
+++ b/corsika/framework/process/DecayProcess.hpp
@@ -50,7 +50,7 @@ namespace corsika {
   template <typename TDerived>
   struct DecayProcess : BaseProcess<TDerived> {
   public:
-    using BaseProcess<TDerived>::ref;
+    using BaseProcess<TDerived>::getRef;
 
     template <typename TParticle>
     InverseTimeType getInverseLifetime(TParticle const& particle) {
@@ -61,7 +61,7 @@ namespace corsika {
                     "getInteractionLength(TParticle const&)\" required for "
                     "InteractionProcess<TDerived>. ");
 
-      return 1. / ref().getLifetime(particle);
+      return 1. / getRef().getLifetime(particle);
     }
   };
 
diff --git a/corsika/framework/process/InteractionCounter.hpp b/corsika/framework/process/InteractionCounter.hpp
index e38722049eaa0b00c62a4c231000821a4960120f..3b2bb182706871023119770b05fbd7b79a6d2fd2 100644
--- a/corsika/framework/process/InteractionCounter.hpp
+++ b/corsika/framework/process/InteractionCounter.hpp
@@ -10,24 +10,25 @@
 
 #include <corsika/framework/process/InteractionHistogram.hpp>
 #include <corsika/framework/process/InteractionProcess.hpp>
+#include <corsika/framework/geometry/FourVector.hpp>
 
 namespace corsika {
 
-  /*!
-    @ingroup Processes
-    @{
-
+  /**
+   * @ingroup Processes
+   * @{
+   *
    * 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
+   * lab and center-of-mass frame) and species.
    *
-   * Use by wrapping a normal InteractionProcess
+   * Use by wrapping a normal InteractionProcess:
    * @code{.cpp}
    * InteractionProcess collision1;
    * InteractionClounter<collision1> counted_collision1;
    * @endcode
-   *
    */
+
   template <class TCountedProcess>
   class InteractionCounter
       : public InteractionProcess<InteractionCounter<TCountedProcess>> {
@@ -35,17 +36,24 @@ namespace corsika {
   public:
     InteractionCounter(TCountedProcess& process);
 
-    //! wrapper around internall process doInteraction
+    /**
+     * Wrapper around internal process doInteraction.
+     */
     template <typename TSecondaryView>
-    void doInteraction(TSecondaryView& view);
+    void doInteraction(TSecondaryView& view, Code const, Code const, FourMomentum const&,
+                       FourMomentum const&);
 
-    ///! returns internal process getInteractionLength
-    template <typename TParticle>
-    GrammageType getInteractionLength(TParticle const& particle) const;
+    /**
+     * Wrapper around internal process getCrossSection.
+     */
+    CrossSectionType getCrossSection(Code const, Code const, FourMomentum const&,
+                                     FourMomentum const&) const;
 
-    /** returns the filles histograms
-        @return InteractionHistogram, which contains the histogram data
-    */
+    /**
+     * returns the filles histograms.
+     *
+     * @return InteractionHistogram, which contains the histogram data
+     */
     InteractionHistogram const& getHistogram() const;
 
   private:
diff --git a/corsika/framework/process/InteractionProcess.hpp b/corsika/framework/process/InteractionProcess.hpp
index c446e6f90ba8c8f17b54932e3c3226f8caa8b236..1736038281869041d62242937f0051c5bdceb990 100644
--- a/corsika/framework/process/InteractionProcess.hpp
+++ b/corsika/framework/process/InteractionProcess.hpp
@@ -10,67 +10,54 @@
 
 #include <corsika/framework/process/BaseProcess.hpp>
 #include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/media/NuclearComposition.hpp>
 
 #include <corsika/detail/framework/process/InteractionProcess.hpp> // for extra traits, method/interface checking
 
 namespace corsika {
 
   /**
-     @ingroup Processes
-     @{
-
-     Process describing the interaction of particles
-
-     Create a new InteractionProcess, e.g. for XYModel, via
-     @code
-     class XYModel : public InteractionProcess<XYModel> {};
-     @endcode
-
-     and provide the two necessary interface methods
-     @code
-     template <typename TSecondaryView>
-     void XYModel::doInteraction(TSecondaryView&);
-
-     template <typename TParticle>
-     GrammageType XYModel::getInteractionLength(TParticle const&)
-     @endcode
-
-     Where, of course, SecondaryView and Particle are the valid
-     classes to access particles on the Stack. In user code, those two methods do
-     not need to be templated, they could use the types
-     e.g. corsika::setup::Stack::particle_type -- but by the cost of
-     loosing all flexibility otherwise provided.
-
-     SecondaryView allows to retrieve the properties of the projectile
-     particles, AND to store new particles (secondaries) which then
-     subsequently can be processes by SecondariesProcess. This is how
-     the output of interactions can be studied right away.
-
+   * @ingroup Processes
+   * @{
+   *
+   * Process describing the interaction of particles.
+   *
+   * Create a new InteractionProcess, e.g. for XYModel, via:
+   * @code
+   * class XYModel : public InteractionProcess<XYModel> {};
+   * @endcode
+   *
+   * and provide the two necessary interface methods:
+   * @code
+   * template <typename TSecondaryView>
+   * void XYModel::doInteraction(TSecondaryView&);
+   *
+   * template <typename TParticle>
+   * GrammageType XYModel::getInteractionLength(TParticle const&)
+   * @endcode
+   *
+   * Where, of course, SecondaryView and Particle are the valid
+   * classes to access particles on the Stack. In user code, those two methods do
+   * not need to be templated, they could use the types
+   * e.g. corsika::setup::Stack::particle_type -- but by the cost of
+   * loosing all flexibility otherwise provided.
+   *
+   * SecondaryView allows to retrieve the properties of the projectile
+   * particles, AND to store new particles (secondaries) which then
+   * subsequently can be processes by SecondariesProcess. This is how
+   * the output of interactions can be studied right away.
    */
 
-  template <typename TDerived>
-  class InteractionProcess : public BaseProcess<TDerived> {
+  template <typename TModel>
+  class InteractionProcess : public BaseProcess<TModel> {
 
   public:
-    using BaseProcess<TDerived>::ref;
-
-    template <typename TParticle>
-    InverseGrammageType getInverseInteractionLength(TParticle const& particle) {
-
-      // interface checking on TProcess1
-      static_assert(
-          has_method_getInteractionLength_v<TDerived, GrammageType, TParticle const&>,
-          "TDerived has no method with correct signature \"GrammageType "
-          "getInteractionLength(TParticle const&)\" required for "
-          "InteractionProcess<TDerived>. ");
-
-      return 1. / ref().getInteractionLength(particle);
-    }
+    using BaseProcess<TModel>::getRef;
   };
 
   /**
-   * ProcessTraits specialization to flag InteractionProcess objects
-   **/
+   * ProcessTraits specialization to flag InteractionProcess objects.
+   */
   template <typename TProcess>
   struct is_interaction_process<
       TProcess, std::enable_if_t<
diff --git a/corsika/framework/process/ProcessSequence.hpp b/corsika/framework/process/ProcessSequence.hpp
index 407f403495c7c4d281880a897366f3b76b22a123..e6d3600b8cff72e305a1b57f64a526a39772341f 100644
--- a/corsika/framework/process/ProcessSequence.hpp
+++ b/corsika/framework/process/ProcessSequence.hpp
@@ -25,14 +25,21 @@
 #include <corsika/framework/process/SecondariesProcess.hpp>
 #include <corsika/framework/process/StackProcess.hpp>
 #include <corsika/framework/process/NullModel.hpp>
+
 #include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/framework/core/ParticleProperties.hpp>
+
+#include <corsika/framework/geometry/FourVector.hpp>
 
 namespace corsika {
 
+  class COMBoost;           // fwd-decl
+  class NuclearComposition; // fwd-decl
+
   /**
-     count_processes traits specialization to increase process count by
-     getNumberOfProcesses(). This is used to statically count processes in the sequence
-  **/
+   * count_processes traits specialization to increase process count by
+   * getNumberOfProcesses(). This is used to statically count processes in the sequence.
+   */
   template <typename TProcess, int N>
   struct count_processes<
       TProcess, N,
@@ -43,110 +50,109 @@ namespace corsika {
   };
 
   /**
-     @defgroup Processes Physics Processes and Modules
-
-     Physics processes in CORSIKA 8 are clustered in ProcessSequence and
-     SwitchProcessSequence containers. The former is a mere (ordered) collection, while
-     the latter has the option to switch between two alternative ProcessSequences.
-
-     Depending on the type of data to act on and on the allowed actions of processes there
-     are several interface options:
-     - InteractionProcess
-     - DecayProcess
-     - ContinuousProcess
-     - StackProcess
-     - SecondariesProcess
-     - BoundaryCrossingProcess
-
-     And all processes (including ProcessSequence and SwitchProcessSequence) are derived
-     from BaseProcess.
-
-     Processes of any type (e.g. p1, p2, p3,...) can be assembled into a ProcessSequence
-     using the `make_sequence` factory function.
-
-     @code{.cpp}
-       auto sequence1 = make_sequence(p1, p2, p3);
-       auto sequence2 = make_sequence(p4, p5, p6, p7);
-       auto sequence3 = make_sequence(sequence1, sequemce2, p8, p9);
-     @endcode
-
-     Note, if the order of processes
-     matters, the order of occurence
-     in the ProcessSequence determines
-     the executiion order.
-
-     SecondariesProcess alyways act on
-     new secondaries produced (i.e. in
-     InteractionProcess and
-     DecayProcess) in the scope of
-     their ProcessSequence. For
-     example if i1 and i2 are
-     InteractionProcesses and s1 is a
-     SecondariesProcess, then
-
-     @code{.cpp}
-       auto sequence = make_sequence(i1, make_sequence(i2, s1))
-     @endcode
-
-     will result in s1 acting only on
-     the particles produced by i2 and
-     not by i1. This can be very
-     useful, e.g. to fine tune thinning.
-
-     A special type of ProcessSequence
-     is SwitchProcessSequence, which
-     has two branches and a functor
-     that can select between these two
-     branches.
-
-     @code{.cpp}
-       auto sequence = make_switch(sequence1, sequence2, selector);
-     @endcode
-
-     where the only requirement to
-     `selector` is that it
-     provides a `SwitchResult operator()(Particle const& particle) const` method. Thus,
-     based on the dynamic properties
-     of `particle` the functor
-     can make its decision. This is
-     clearly important for switching
-     between low-energy and
-     high-energy models, but not
-     limited to this. The selection
-     can even be done with a lambda
-     function.
-
-
-     @class ProcessSequence
-     @ingroup Processes
-
-       Definition of a static process list/sequence
-
-       A compile time static list of processes. The compiler will
-       generate a new type based on template logic containing all the
-       elements provided by the user.
-
-       TProcess1 and TProcess2 must both be derived from BaseProcess,
-       and are both references if possible (lvalue), otherwise (rvalue)
-       they are just classes. This allows us to handle both, rvalue as
-       well as lvalue Processes in the ProcessSequence.
-
-       (For your potential interest,
-       the static version of the
-       ProcessSequence and all Process
-       types are based on the CRTP C++
-       design pattern)
-
-      Template parameters:
-        @tparam TProcess1 is of type BaseProcess, either a dedicatd process, or a
-     ProcessSequence
-        @tparam TProcess2 is of type BaseProcess, either a dedicatd process, or a
-     ProcessSequence
-      @tparam IndexFirstProcess to count and index each Process in the entire
-  process-chain. The offset is the starting value for this ProcessSequence
-      @tparam IndexOfProcess1 index of TProcess1 (counting of Process)
-      @tparam IndexOfProcess2 index of TProcess2 (counting of Process)
-  **/
+   * @defgroup Processes Physics Processes and Modules
+   *
+   * Physics processes in CORSIKA 8 are clustered in ProcessSequence and
+   * SwitchProcessSequence containers. The former is a mere (ordered) collection, while
+   * the latter has the option to switch between two alternative ProcessSequences.
+   *
+   * Depending on the type of data to act on and on the allowed actions of
+   * processes there are several interface options:
+   * - InteractionProcess
+   * - DecayProcess
+   * - ContinuousProcess
+   * - StackProcess
+   * - SecondariesProcess
+   * - BoundaryCrossingProcess
+   *
+   * And all processes (including ProcessSequence and SwitchProcessSequence) are derived
+   * from BaseProcess.
+   *
+   * Processes of any type (e.g. p1, p2, p3,...) can be assembled into a ProcessSequence
+   * using the `make_sequence` factory function.
+   *
+   * @code{.cpp}
+   *   auto sequence1 = make_sequence(p1, p2, p3);
+   *   auto sequence2 = make_sequence(p4, p5, p6, p7);
+   *   auto sequence3 = make_sequence(sequence1, sequemce2, p8, p9);
+   * @endcode
+   *
+   * Note, if the order of processes
+   * matters, the order of occurence
+   * in the ProcessSequence determines
+   * the executiion order.
+   *
+   * SecondariesProcess alyways act on
+   * new secondaries produced (i.e. in
+   * InteractionProcess and
+   * DecayProcess) in the scope of
+   * their ProcessSequence. For
+   * example if i1 and i2 are
+   * InteractionProcesses and s1 is a
+   * SecondariesProcess, then:
+   *
+   * @code{.cpp}
+   *   auto sequence = make_sequence(i1, make_sequence(i2, s1))
+   * @endcode
+   *
+   * will result in s1 acting only on
+   * the particles produced by i2 and
+   * not by i1. This can be very
+   * useful, e.g. to fine tune thinning.
+   *
+   * A special type of ProcessSequence
+   * is SwitchProcessSequence, which
+   * has two branches and a functor
+   * that can select between these two
+   * branches.
+   *
+   * @code{.cpp}
+   *   auto sequence = make_switch(sequence1, sequence2, selector);
+   * @endcode
+   *
+   * where the only requirement to
+   * `selector` is that it
+   * provides a `SwitchResult operator()(Particle const& particle) const` method. Thus,
+   * based on the dynamic properties
+   * of `particle` the functor
+   * can make its decision. This is
+   * clearly important for switching
+   * between low-energy and
+   * high-energy models, but not
+   * limited to this. The selection
+   * can even be done with a lambda
+   * function.
+   *
+   * @class ProcessSequence
+   * @ingroup Processes
+   *
+   *   Definition of a static process list/sequence.
+   *
+   *  A compile time static list of processes. The compiler will
+   *  generate a new type based on template logic containing all the
+   *  elements provided by the user.
+   *
+   *  TProcess1 and TProcess2 must both be derived from BaseProcess,
+   *  and are both references if possible (lvalue), otherwise (rvalue)
+   *  they are just classes. This allows us to handle both, rvalue as
+   *  well as lvalue Processes in the ProcessSequence.
+   *
+   *  (For your potential interest,
+   *  the static version of the
+   *  ProcessSequence and all Process
+   *  types are based on the CRTP C++
+   *  design pattern).
+   *
+   * Template parameters:
+   *   @tparam TProcess1 is of type BaseProcess, either a dedicatd process, or a
+   *           ProcessSequence.
+   *   @tparam TProcess2 is of type BaseProcess, either a dedicatd process, or a
+   *           ProcessSequence.
+   * @tparam IndexFirstProcess to count and index each Process in the entire
+   *         process-chain. The offset is the starting value for this ProcessSequence.
+   *  @tparam IndexOfProcess1 index of TProcess1 (counting of Process).
+   *  @tparam IndexOfProcess2 index of TProcess2 (counting of Process).
+   */
 
   template <typename TProcess1, typename TProcess2 = NullModel,
             int ProcessIndexOffset = 0,
@@ -171,17 +177,26 @@ namespace corsika {
     static bool const is_process_sequence = true;
 
     /**
-      Only valid user constructor will create fully initialized object
-
-      ProcessSequence supports and encourages move semantics. You can
-      use object, l-value references or r-value references to
-      construct sequences.
-
-      @param in_A BaseProcess or switch/process list
-      @param in_B BaseProcess or switch/process list
-     **/
+     * Only valid user constructor will create fully initialized object.
+     *
+     * ProcessSequence supports and encourages move semantics. You can
+     * use object, l-value references or r-value references to
+     * construct sequences.
+     *
+     * @param in_A BaseProcess or switch/process list.
+     * @param in_B BaseProcess or switch/process list.
+     */
     ProcessSequence(TProcess1 in_A, TProcess2 in_B);
 
+    /**
+     * List of all BoundaryProcess.
+     *
+     * @tparam TParticle
+     * @param particle The particle.
+     * @param from Volume the particle is exiting.
+     * @param to Volume the particle is entering.
+     * @return ProcessReturn
+     */
     template <typename TParticle>
     ProcessReturn doBoundaryCrossing(TParticle& particle,
                                      typename TParticle::node_type const& from,
@@ -191,21 +206,30 @@ namespace corsika {
     ProcessReturn doContinuous(TParticle& particle, TTrack& vT,
                                ContinuousProcessIndex const limitID);
 
+    /**
+     * Process all secondaries in TSecondaries.
+     *
+     * The seondaries produced by other processes and accessible via TSecondaries
+     * are processed by all SecondariesProcesse via a call here.
+     *
+     * @tparam TSecondaries
+     * @param vS
+     */
     template <typename TSecondaries>
     void doSecondaries(TSecondaries& vS);
 
     /**
-       The processes of type StackProcess do have an internal counter,
-       so they can be exectuted only each N steps. Often these are
-       "maintenacne processes" that do not need to run after each
-       single step of the simulations. In the CheckStep function it is
-       tested if either A_ or B_ are StackProcess and if they are due
-       for execution.
+     * The processes of type StackProcess do have an internal counter,
+     * so they can be exectuted only each N steps. Often these are
+     * "maintenacne processes" that do not need to run after each
+     * single step of the simulations. In the CheckStep function it is
+     * tested if either A_ or B_ are StackProcess and if they are due
+     * for execution.
      */
     bool checkStep();
 
     /**
-       Execute the StackProcess-es in the ProcessSequence
+     * Execute the StackProcess-es in the ProcessSequence.
      */
     template <typename TStack>
     void doStack(TStack& stack);
@@ -223,49 +247,80 @@ namespace corsika {
 
     /**
      * Calculate the maximum allowed length of the next tracking step, based on all
-     * ContinuousProcess-es
+     * ContinuousProcess-es.
      *
      * The maximum allowed step length is the minimum of the allowed track lenght over all
      * ContinuousProcess-es in the ProcessSequence.
      *
      * @return ContinuousProcessStepLength which contains the step length itself in
      *          LengthType, and a unique identifier of the related ContinuousProcess.
-     **/
+     */
 
     template <typename TParticle, typename TTrack>
-    ContinuousProcessStepLength getMaxStepLength(TParticle& particle, TTrack& vTrack);
-
-    template <typename TParticle>
-    GrammageType getInteractionLength(TParticle&& particle) {
-      return 1. / getInverseInteractionLength(particle);
-    }
+    ContinuousProcessStepLength getMaxStepLength(TParticle&& particle, TTrack&& vTrack);
 
+    /**
+     * @brief Calculates the cross section of a projectile with a target.
+     *
+     * @tparam TParticle
+     * @param projectile
+     * @param targetId
+     * @param targetP4
+     * @return CrossSectionType
+     */
     template <typename TParticle>
-    InverseGrammageType getInverseInteractionLength(TParticle&& particle);
-
-    template <typename TSecondaryView>
-    ProcessReturn selectInteraction(
-        TSecondaryView& view, [[maybe_unused]] InverseGrammageType lambda_inv_select,
-        [[maybe_unused]] InverseGrammageType lambda_inv_sum =
-            InverseGrammageType::zero());
+    CrossSectionType getCrossSection(TParticle const& projectile, Code const targetId,
+                                     FourMomentum const& targetP4) const;
 
     template <typename TParticle>
     TimeType getLifetime(TParticle& particle) {
       return 1. / getInverseLifetime(particle);
     }
 
+    /**
+     * Selects one concrete InteractionProcess and samples a target nucleus from
+     * the material.
+     *
+     * The selectInteraction method statically loops over all active InteractionProcess
+     * and calculates the material-weighted cross section for all of them. In an iterative
+     * way those cross sections are summed up. The random number cx_select, uniformely
+     * drawn from the cross section before energy losses, is used to discriminate the
+     * selected sub-process here. If the cross section after the step smaller than it was
+     * before, there is a non-zero probability that the particle survives and no
+     * interaction takes place. This method becomes imprecise when cross section rise with
+     * falling energies.
+     *
+     * If a sub-process was selected, the target nucleus is selected from the material
+     * (weighted with cross section). The interaction is then executed.
+     *
+     * @tparam TSecondaryView Object type as storage for new secondary particles.
+     * @tparam TRNG Object type to produce random numbers.
+     * @param view Object to store new secondary particles.
+     * @param projectileP4 The four momentum of the projectile.
+     * @param composition The environment/material composition.
+     * @param rng Random number object.
+     * @param cx_select Drawn random numer, uniform between [0, cx_initial]
+     * @param cx_sum For interal use, to sum up cross section contributions.
+     * @return ProcessReturn
+     */
+    template <typename TSecondaryView, typename TRNG>
+    inline ProcessReturn selectInteraction(
+        TSecondaryView&& view, FourMomentum const& projectileP4,
+        NuclearComposition const& composition, TRNG&& rng,
+        CrossSectionType const cx_select,
+        CrossSectionType cx_sum = CrossSectionType::zero());
+
     template <typename TParticle>
     InverseTimeType getInverseLifetime(TParticle&& particle);
 
     // select decay process
     template <typename TSecondaryView>
-    ProcessReturn selectDecay(
-        TSecondaryView& view, [[maybe_unused]] InverseTimeType decay_inv_select,
-        [[maybe_unused]] InverseTimeType decay_inv_sum = InverseTimeType::zero());
+    ProcessReturn selectDecay(TSecondaryView&& view, InverseTimeType decay_inv_select,
+                              InverseTimeType decay_inv_sum = InverseTimeType::zero());
 
     /**
      * static counter to uniquely index (count) all ContinuousProcess in switch sequence.
-     **/
+     */
     static unsigned int constexpr getNumberOfProcesses() { return numberOfProcesses_; }
 
 #ifdef CORSIKA_UNIT_TESTING
@@ -274,40 +329,40 @@ namespace corsika {
 #endif
 
   private:
-    TProcess1 A_; /// process/list A, this is a reference, if possible
-    TProcess2 B_; /// process/list B, this is a reference, if possible
+    TProcess1 A_; //! process/list A, this is a reference, if possible
+    TProcess2 B_; //! process/list B, this is a reference, if possible
 
     static unsigned int constexpr numberOfProcesses_ = IndexOfProcess1; // static counter
   };
 
   /**
-    @fn make_sequence
-    @ingroup Processes
-
-    Factory function to create a ProcessSequence
-
-    to construct ProcessSequences in a flexible and dynamic way the
-    `sequence` factory functions are provided
-
-    Any objects of type
-     - BaseProcess
-     - ContinuousProcess and
-     - InteractionProcess/DecayProcess
-     - StackProcess
-     - SecondariesProcess
-     - BoundaryCrossingProcess
-
-    can be assembled into a ProcessSequence, all
-    combinatorics are allowed.
-
-    The sequence function checks that all its arguments are all of
-    types derived from BaseProcess. Also the ProcessSequence itself
-    is derived from type BaseProcess
-
-    @tparam TProcesses parameter pack with objects of type BaseProcess
-    @tparam TProcess1 another BaseProcess
-    @param vA needs to derive from BaseProcess
-    @param vB paramter-pack, needs to derive BaseProcess
+   * @fn make_sequence
+   * @ingroup Processes
+   *
+   * Factory function to create a ProcessSequence.
+   *
+   * to construct ProcessSequences in a flexible and dynamic way the
+   * `sequence` factory functions are provided.
+   *
+   * Any objects of type
+   *  - BaseProcess
+   *  - ContinuousProcess and
+   *  - InteractionProcess/DecayProcess
+   *  - StackProcess
+   *  - SecondariesProcess
+   *  - BoundaryCrossingProcess
+   *
+   * can be assembled into a ProcessSequence, all
+   * combinatorics are allowed.
+   *
+   * The sequence function checks that all its arguments are all of
+   * types derived from BaseProcess. Also the ProcessSequence itself
+   * is derived from type BaseProcess.
+   *
+   * @tparam TProcesses parameter pack with objects of type BaseProcess.
+   * @tparam TProcess1 another BaseProcess.
+   * @param vA needs to derive from BaseProcess.
+   * @param vB paramter-pack, needs to derive BaseProcess.
    */
   template <typename... TProcesses, typename TProcess1>
   ProcessSequence<TProcess1, decltype(make_sequence(std::declval<TProcesses>()...))>
@@ -318,34 +373,34 @@ namespace corsika {
   }
 
   /**
-    @fn make_sequence
-    @ingroup Processes
-
-    Factory function to create ProcessSequence
-
-    specialization for two input objects (no paramter pack in vB).
-
-    @tparam TProcess1 another BaseProcess
-    @tparam TProcess2 another BaseProcess
-    @param vA needs to derive from BaseProcess
-    @param vB needs to derive BaseProcess
-  */
+   * @fn make_sequence
+   * @ingroup Processes
+   *
+   * Factory function to create ProcessSequence.
+   *
+   * specialization for two input objects (no paramter pack in vB).
+   *
+   * @tparam TProcess1 another BaseProcess
+   * @tparam TProcess2 another BaseProcess
+   * @param vA needs to derive from BaseProcess
+   * @param vB needs to derive BaseProcess
+   */
   template <typename TProcess1, typename TProcess2>
   ProcessSequence<TProcess1, TProcess2> make_sequence(TProcess1&& vA, TProcess2&& vB) {
     return ProcessSequence<TProcess1, TProcess2>(vA, vB);
   }
 
   /**
-    @fn make_sequence
-    @ingroup Processes
-
-    Factory function to create ProcessSequence from a single BaseProcess
-
-    also allow a single Process in ProcessSequence, accompany by
-    `NullModel`
-
-    @tparam TProcess1 another BaseProcess
-    @param vA needs to derive from BaseProcess
+   * @fn make_sequence
+   * @ingroup Processes
+   *
+   * Factory function to create ProcessSequence from a single BaseProcess.
+   *
+   * also allow a single Process in ProcessSequence, accompany by
+   * `NullModel`.
+   *
+   * @tparam TProcess1 another BaseProcess
+   * @param vA needs to derive from BaseProcess
    */
   template <typename TProcess>
   ProcessSequence<TProcess, NullModel> make_sequence(TProcess&& vA) {
diff --git a/corsika/framework/process/SwitchProcessSequence.hpp b/corsika/framework/process/SwitchProcessSequence.hpp
index 09edeea43e551f7f37b51e8b105c78cc92ba6f6c..2b619036994b57ba62586980589cbe1888f5d8cc 100644
--- a/corsika/framework/process/SwitchProcessSequence.hpp
+++ b/corsika/framework/process/SwitchProcessSequence.hpp
@@ -146,18 +146,15 @@ namespace corsika {
     ContinuousProcessStepLength getMaxStepLength(TParticle& particle, TTrack& vTrack);
 
     template <typename TParticle>
-    GrammageType getInteractionLength(TParticle&& particle) {
-      return 1. / getInverseInteractionLength(particle);
-    }
-
-    template <typename TParticle>
-    InverseGrammageType getInverseInteractionLength(TParticle&& particle);
-
-    template <typename TSecondaryView>
-    ProcessReturn selectInteraction(
-        TSecondaryView& view, [[maybe_unused]] InverseGrammageType lambda_inv_select,
-        [[maybe_unused]] InverseGrammageType lambda_inv_sum =
-            InverseGrammageType::zero());
+    CrossSectionType getCrossSection(TParticle const& projectile, Code const targetId,
+                                     FourMomentum const& targetP4) const;
+
+    template <typename TSecondaryView, typename TRNG>
+    ProcessReturn selectInteraction(TSecondaryView& view,
+                                    FourMomentum const& projectileP4,
+                                    NuclearComposition const& composition, TRNG& rng,
+                                    CrossSectionType const cx_select,
+                                    CrossSectionType cx_sum = CrossSectionType::zero());
 
     template <typename TParticle>
     TimeType getLifetime(TParticle&& particle) {
diff --git a/corsika/framework/utility/COMBoost.hpp b/corsika/framework/utility/COMBoost.hpp
index eac8dde53f3eadc461e26c559e8b8ed36463b647..98be9fc070a90c2918975de8d0debbe15505ed3c 100644
--- a/corsika/framework/utility/COMBoost.hpp
+++ b/corsika/framework/utility/COMBoost.hpp
@@ -19,56 +19,71 @@
 namespace corsika {
 
   /**
-     @defgroup Utilities
-
-     Collection of classes and methods to perform recurring tasks.
-   **/
+   * @defgroup Utilities
+   *
+   * Collection of classes and methods to perform recurring tasks.
+   */
 
   /**
-     @class COMBoost
-     @ingroup Utilities
-
-     This utility class handles Lorentz boost between different
-     referenence frames, using FourVector.
-
-     The class is initialized with projectile and optionally target
-     energy/momentum data. During initialization, a rotation matrix is
-     calculated to represent the projectile movement (and thus the
-     boost) along the z-axis. Also the inverse of this rotation is
-     calculated. The Lorentz boost matrix and its inverse are
-     determined as 2x2 matrices considering the energy and
-     pz-momentum.
-
-     Different constructors are offered with different specialization
-     for the cases of collisions (projectile-target) or just decays
-     (projectile only).
+   * @class COMBoost
+   * @ingroup Utilities
+   *
+   * This utility class handles Lorentz boost (in one spatial direction)
+   * between different referenence frames, using FourVector.
+   *
+   * The class is initialized with projectile and optionally target
+   * energy/momentum data. During initialization, a rotation matrix is
+   * calculated to represent the projectile movement (and thus the
+   * boost) along the z-axis. Also the inverse of this rotation is
+   * calculated. The Lorentz boost matrix and its inverse are
+   * determined as 2x2 matrices considering the energy and
+   * pz-momentum.
+   *
+   * Different constructors are offered with different specialization
+   * for the cases of collisions (projectile-target) or just decays
+   * (projectile only).
    */
 
   class COMBoost {
 
   public:
-    //! construct a COMBoost given four-vector of projectile and mass of target (target at
-    //! rest)
-    COMBoost(FourVector<HEPEnergyType, MomentumVector> const& Pprojectile,
-             HEPEnergyType const massTarget);
-
-    //! construct a COMBoost to boost into the rest frame given a 3-momentum and mass
-    COMBoost(MomentumVector const& momentum, HEPEnergyType mass);
+    /**
+     * Construct a COMBoost given four-vector of projectile and mass of target (target at
+     * rest).
+     *
+     * The FourMomentum and mass define the lab system.
+     */
+    COMBoost(FourMomentum const& P4projectile, HEPEnergyType const massTarget);
+
+    /**
+     * Construct a COMBoost to boost into the rest frame given a 3-momentum and mass.
+     */
+    COMBoost(MomentumVector const& momentum, HEPEnergyType const mass);
+
+    /**
+     * Construct a COMBoost given two four-vectors of projectile target.
+     *
+     * The tow FourMomentum can define an arbitrary system.
+     */
+    COMBoost(FourMomentum const& P4projectile, FourMomentum const& P4target);
 
     //! transforms a 4-momentum from lab frame to the center-of-mass frame
     template <typename FourVector>
-    FourVector toCoM(FourVector const& p) const;
+    FourVector toCoM(FourVector const& p4) const;
 
     //! transforms a 4-momentum from the center-of-mass frame back to lab frame
     template <typename FourVector>
-    FourVector fromCoM(FourVector const& p) const;
+    FourVector fromCoM(FourVector const& p4) const;
 
-    //! returns the rotated coordinate system
+    //! returns the rotated coordinate system: +z is projectile direction
     CoordinateSystemPtr getRotatedCS() const;
 
+    //! returns the original coordinate system of the projectile (lab)
+    CoordinateSystemPtr getOriginalCS() const;
+
   protected:
     //! internal method
-    void setBoost(double coshEta, double sinhEta);
+    void setBoost(double const coshEta, double const sinhEta);
 
   private:
     Eigen::Matrix2d boost_;
diff --git a/corsika/media/IMediumPropertyModel.hpp b/corsika/media/IMediumPropertyModel.hpp
index 860fa22da0e5d8d09ea4b9dbb9aeb8219cfc0492..94e80fb51a4f24b0dc7deb8f770d1a457f4c9914 100644
--- a/corsika/media/IMediumPropertyModel.hpp
+++ b/corsika/media/IMediumPropertyModel.hpp
@@ -16,10 +16,9 @@
 namespace corsika {
 
   /**
-   * An interface for type of media, needed e.g. to determine energy losses
+   * An interface for type of media, needed e.g. to determine energy losses.
    *
    * This is the base interface for media types.
-   *
    */
   template <typename TModel>
   class IMediumPropertyModel : public TModel {
diff --git a/corsika/media/MediumPropertyModel.hpp b/corsika/media/MediumPropertyModel.hpp
index ab89a8a543a0d25da648d06e45d34adfb5cecec1..97bce6ff8fd15e60d8a81e3c33694087cb77512d 100644
--- a/corsika/media/MediumPropertyModel.hpp
+++ b/corsika/media/MediumPropertyModel.hpp
@@ -14,7 +14,6 @@ namespace corsika {
 
   /**
    * A model for the energy loss property of a medium.
-   *
    */
   template <typename T>
   class MediumPropertyModel : public T {
diff --git a/corsika/media/NuclearComposition.hpp b/corsika/media/NuclearComposition.hpp
index e4ad9513a24684eed2ca1c3b9862441003f2c0fa..184853ef2cdc620c821b516c4760144d503ffc63 100644
--- a/corsika/media/NuclearComposition.hpp
+++ b/corsika/media/NuclearComposition.hpp
@@ -20,48 +20,69 @@
 
 namespace corsika {
 
-  /** Describes the composition of matter
-   *  Allowes and handles the creation of custom matter compositions
-   **/
+  /**
+   * Describes the composition of matter
+   * Allowes and handles the creation of custom matter compositions.
+   */
+
   class NuclearComposition {
   public:
-    /** Constructor
+    /**
+     * Constructor
      *  The constructore takes a list of elements and a list which describe the relative
      *  amount. Booth lists need to have the same length and the sum all of fractions
-     *  should be 1. Otherwise an exception is thrown
-     *  @param pComponents List of particle types
+     *  should be 1. Otherwise an exception is thrown.
+     *
+     *  @param pComponents List of particle types.
      *  @param pFractions List of fractions how much each particle contributes. The sum
-     *         needs to add up to 1
-     **/
+     *         needs to add up to 1.
+     */
     NuclearComposition(std::vector<Code> const& pComponents,
-                       std::vector<float> const& pFractions);
+                       std::vector<double> const& pFractions);
+
+    /**
+     * Returns a vector of the same length as elements in the material with the weighted
+     * return of "func". The typical default application is for cross section weighted
+     * with fraction in the material.
+     *
+     *  @tparam TFunction Type of functions for the weights. The type should be
+     *          Code -> CrossSectionType.
+     *  @param func Functions for reweighting specific elements.
+     *  @retval returns the vector with weighted return types of func.
+     */
+    template <typename TFunction>
+    auto getWeighted(TFunction const& func) const;
 
-    /** Sum all all relative composition weighted by func(element)
+    /**
+     * Sum all all relative composition weighted by func(element)
      *  This function sums all relative compositions given during this classes
-     *construction. Each entry is weighted by the user defined function func given to this
-     *function.
+     * construction. Each entry is weighted by the user defined function func given to
+     * this function.
+     *
      *  @tparam TFunction Type of functions for the weights. The type should be
-     *          Code -> float
-     *  @param func Functions for reweighting specific elements
-     *  @retval returns the weighted sum with the type defined by the return type of func
-     **/
+     *          Code -> double.
+     *  @param func Functions for reweighting specific elements.
+     *  @retval returns the weighted sum with the type defined by the return type of func.
+     */
     template <typename TFunction>
-    auto getWeightedSum(TFunction const& func) const;
+    auto getWeightedSum(TFunction const& func) const
+        -> decltype(func(std::declval<Code>()));
 
-    /** Number of elements in the composition array
-     *  @retval returns the number of elements in the composition array
-     **/
+    /**
+     * Number of elements in the composition array
+     *  @retval returns the number of elements in the composition array.
+     */
     size_t getSize() const;
 
     //! Returns a const reference to the fraction
-    std::vector<float> const& getFractions() const;
+    std::vector<double> const& getFractions() const;
     //! Returns a const reference to the fraction
     std::vector<Code> const& getComponents() const;
     double const getAverageMassNumber() const;
 
     template <class TRNG>
     Code sampleTarget(std::vector<CrossSectionType> const& sigma,
-                      TRNG& randomStream) const;
+                      TRNG&& randomStream) const;
 
     // Note: when this class ever modifies its internal data, the hash
     // must be updated, too!
@@ -73,8 +94,8 @@ namespace corsika {
   private:
     void updateHash();
 
-    std::vector<float> const numberFractions_; //!< relative fractions of number density
-    std::vector<Code> const components_;       //!< particle codes of consitutents
+    std::vector<double> const numberFractions_; //!< relative fractions of number density
+    std::vector<Code> const components_;        //!< particle codes of consitutents
 
     double const avgMassNumber_;
 
diff --git a/corsika/modules/Epos.hpp b/corsika/modules/Epos.hpp
index c25be9b14b615e797bb821d06d2bc27b7b23353a..b4aa9086cf563e35b0ffb23d4aa9c85f6375cf60 100644
--- a/corsika/modules/Epos.hpp
+++ b/corsika/modules/Epos.hpp
@@ -9,4 +9,22 @@
 #pragma once
 
 #include <corsika/modules/epos/ParticleConversion.hpp>
-#include <corsika/modules/epos/Interaction.hpp>
+#include <corsika/modules/epos/InteractionModel.hpp>
+#include <corsika/framework/process/InteractionProcess.hpp>
+
+/**
+ * @file Sibyll.hpp
+ *
+ * Includes all the parts of the EPOS model. Defines the InteractionProcess<TModel>
+ * classes needed for the ProcessSequence.
+ */
+
+namespace corsika::epos {
+  /**
+   * epos::Interaction is the process for ProcessSequence.
+   *
+   * The epos::InteractionModel is wrapped as an InteractionProcess here in order
+   * to provide all the functions for ProcessSequence.
+   */
+  class Interaction : public InteractionModel, public InteractionProcess<Interaction> {};
+} // namespace corsika::epos
\ No newline at end of file
diff --git a/corsika/modules/LongitudinalProfile.hpp b/corsika/modules/LongitudinalProfile.hpp
index 1d84462b0573f5407efb9a531ce5299df2b9cd58..b5d199fb87949e5456e47b097eea65248d8d5be2 100644
--- a/corsika/modules/LongitudinalProfile.hpp
+++ b/corsika/modules/LongitudinalProfile.hpp
@@ -31,8 +31,7 @@ namespace corsika {
    * simulation into a projected grammage range and counts for
    * different particle species when they cross dX (default: 10g/cm2)
    * boundaries.
-   *
-   **/
+   */
 
   class LongitudinalProfile : public ContinuousProcess<LongitudinalProfile> {
 
diff --git a/corsika/modules/QGSJetII.hpp b/corsika/modules/QGSJetII.hpp
index 9540c4ad116471d54a5ef178124bd97b979d9758..dd7422442e06c8a5a36f94b65cf9dd0fe659ea4c 100644
--- a/corsika/modules/QGSJetII.hpp
+++ b/corsika/modules/QGSJetII.hpp
@@ -8,4 +8,23 @@
 
 #pragma once
 
-#include <corsika/modules/qgsjetII/Interaction.hpp>
+#include <corsika/modules/qgsjetII/InteractionModel.hpp>
+
+#include <corsika/framework/process/InteractionProcess.hpp>
+
+/**
+ * @file QGSJetII.hpp
+ *
+ * Includes all the parts of the QGSJetII model. Defines the InteractionProcess<TModel>
+ * classes needed for the ProcessSequence.
+ */
+
+namespace corsika::qgsjetII {
+  /**
+   * @brief qgsjetII::Interaction is the process for ProcessSequence.
+   *
+   * The qgsjetII::InteractionModel is wrapped as an InteractionProcess here in order
+   * to provide all the functions for ProcessSequence.
+   */
+  class Interaction : public InteractionModel, public InteractionProcess<Interaction> {};
+} // namespace corsika::qgsjetII
diff --git a/corsika/modules/Sibyll.hpp b/corsika/modules/Sibyll.hpp
index 44bebf0ff174d4dab8234e2d17d0d67b1ff58416..8d781def6b124e179ec0ad0eb2abe9955de242dd 100644
--- a/corsika/modules/Sibyll.hpp
+++ b/corsika/modules/Sibyll.hpp
@@ -9,6 +9,41 @@
 #pragma once
 
 #include <corsika/modules/sibyll/ParticleConversion.hpp>
-#include <corsika/modules/sibyll/Interaction.hpp>
+#include <corsika/modules/sibyll/InteractionModel.hpp>
 #include <corsika/modules/sibyll/Decay.hpp>
-#include <corsika/modules/sibyll/NuclearInteraction.hpp>
+#include <corsika/modules/sibyll/NuclearInteractionModel.hpp>
+
+#include <corsika/framework/process/InteractionProcess.hpp>
+
+/**
+ * @file Sibyll.hpp
+ *
+ * Includes all the parts of the Sibyll model. Defines the InteractionProcess<TModel>
+ * classes needed for the ProcessSequence.
+ */
+
+namespace corsika::sibyll {
+  /**
+   * @brief sibyll::Interaction is the process for ProcessSequence.
+   *
+   * The sibyll::InteractionModel is wrapped as an InteractionProcess here in order
+   * to provide all the functions for ProcessSequence.
+   */
+  class Interaction : public InteractionModel, public InteractionProcess<Interaction> {};
+
+  /**
+   * @brief sibyll::NuclearInteraction is the process for ProcessSequence.
+   *
+   * The sibyll::NuclearInteractionModel is wrapped as an InteractionProcess here in order
+   * to provide all the functions for ProcessSequence.
+   */
+  template <class TEnvironment, class TNucleonModel>
+  class NuclearInteraction
+      : public NuclearInteractionModel<TEnvironment, TNucleonModel>,
+        public InteractionProcess<NuclearInteraction<TEnvironment, TNucleonModel>> {
+  public:
+    NuclearInteraction(TNucleonModel& model, TEnvironment const& env)
+        : NuclearInteractionModel<TEnvironment, TNucleonModel>(model, env) {}
+  };
+
+} // namespace corsika::sibyll
diff --git a/corsika/modules/epos/Interaction.hpp b/corsika/modules/epos/InteractionModel.hpp
similarity index 64%
rename from corsika/modules/epos/Interaction.hpp
rename to corsika/modules/epos/InteractionModel.hpp
index 56900ca01cdf99f41f6dc38b7cc9578bc6e7afad..0f08a919b7cc640d20351235dfc2164b155dbe50 100644
--- a/corsika/modules/epos/Interaction.hpp
+++ b/corsika/modules/epos/InteractionModel.hpp
@@ -10,20 +10,18 @@
 
 #include <corsika/framework/core/ParticleProperties.hpp>
 #include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/framework/geometry/FourVector.hpp>
 #include <corsika/framework/random/RNGManager.hpp>
-#include <corsika/framework/process/InteractionProcess.hpp>
 #include <tuple>
 
 namespace corsika::epos {
 
-  class Interaction : public InteractionProcess<Interaction> {
-    std::string data_path_;
-    unsigned int count_ = 0;
-    bool epos_listing_;
+  class InteractionModel {
 
   public:
-    Interaction(std::string const& dataPath = "", bool const epos_printout_on = false);
-    ~Interaction();
+    InteractionModel(std::string const& dataPath = "",
+                     bool const epos_printout_on = false);
+    ~InteractionModel();
 
     //! returns production and elastic cross section for hadrons in epos. Inputs are:
     //! CorsikaId of beam particle, CorsikaId of target particle, center-of-mass energy.
@@ -41,27 +39,39 @@ namespace corsika::epos {
     //! returns production and elastic cross section. Allowed configurations are
     //! hadron-nucleon, hadron-nucleus and nucleus-nucleus. Inputs are particle id's mass
     //! and charge numbers and total energy in the lab.
-    std::tuple<CrossSectionType, CrossSectionType> getCrossSectionLab(
-        Code const, int const, int const, Code const, int const, int const,
-        HEPEnergyType const) const;
-
-    template <typename TParticle>
-    GrammageType getInteractionLength(TParticle const&) const;
+    std::tuple<CrossSectionType, CrossSectionType> getCrossSectionInelEla(
+        Code const projectileId, Code const targetId, FourMomentum const& projectileP4,
+        FourMomentum const& targetP4) const;
 
     /**
-       In this function EPOSLHC is called to produce one event. The
-       event is copied into the shower lab frame.
+     * Checks validity of projectile, target and energy combination.
      */
-    template <typename TSecondaries>
-    void doInteraction(TSecondaries&);
+    bool isValid(Code const projectileId, Code const targetId,
+                 HEPEnergyType const sqrtS) const;
 
-    bool isValidCoMEnergy(HEPEnergyType const ecm) const {
-      return (minEnergyCoM_ <= ecm) && (ecm <= maxEnergyCoM_);
+    /**
+     * Get the inelatic/production cross section.
+     *
+     * @param projectileId
+     * @param targetId
+     * @param projectileP4
+     * @param targetP4
+     * @return CrossSectionType
+     */
+    CrossSectionType getCrossSection(Code const projectileId, Code const targetId,
+                                     FourMomentum const& projectileP4,
+                                     FourMomentum const& targetP4) const {
+      return std::get<0>(
+          getCrossSectionInelEla(projectileId, targetId, projectileP4, targetP4));
     }
 
-    //! eposlhc only accepts nuclei with X<=A<=Y as targets, or protons aka Hydrogen or
-    //! neutrons (p,n == nucleon)
-    bool isValidTarget(Code const) const;
+    /**
+     * In this function EPOSLHC is called to produce one event. The
+     * event is copied into the shower lab frame.
+     */
+    template <typename TSecondaries>
+    void doInteraction(TSecondaries&, Code const projectileId, Code const targetId,
+                       FourMomentum const& projectileP4, FourMomentum const& targetP4);
 
     void initialize() const;
     void initializeEventCoM(Code const, int const, int const, Code const, int const,
@@ -73,6 +83,12 @@ namespace corsika::epos {
     void setParticlesStable() const;
 
   private:
+    inline static bool isInitialized_ = false;
+
+    std::string data_path_;
+    unsigned int count_ = 0;
+    bool epos_listing_;
+
     default_prng_type& RNG_ = RNGManager<>::getInstance().getRandomStream("epos");
     std::shared_ptr<spdlog::logger> logger_ = get_logger("corsika_epos_Interaction");
     HEPEnergyType const minEnergyCoM_ = 6 * 1e9 * electronvolt;
@@ -83,4 +99,4 @@ namespace corsika::epos {
 
 } // namespace corsika::epos
 
-#include <corsika/detail/modules/epos/Interaction.inl>
+#include <corsika/detail/modules/epos/InteractionModel.inl>
diff --git a/corsika/modules/proposal/Interaction.hpp b/corsika/modules/proposal/Interaction.hpp
index b6f2fabbab60f772f69eb83bd9f78697a08fb8f8..3b769bc34281487885aab4f3e022e21a54fe564b 100644
--- a/corsika/modules/proposal/Interaction.hpp
+++ b/corsika/modules/proposal/Interaction.hpp
@@ -13,6 +13,7 @@
 #include <corsika/framework/core/ParticleProperties.hpp>
 #include <corsika/framework/process/InteractionProcess.hpp>
 #include <corsika/framework/process/ProcessReturn.hpp>
+#include <corsika/framework/geometry/FourVector.hpp>
 #include <corsika/framework/random/RNGManager.hpp>
 #include <corsika/framework/random/UniformRealDistribution.hpp>
 
@@ -32,7 +33,7 @@ namespace corsika::proposal {
                                     std::unique_ptr<PROPOSAL::Interaction>>;
 
     std::unordered_map<calc_key_t, calculator_t, hash>
-        calc; //!< Stores the secondaries and interaction calculators.
+        calc_; //!< Stores the secondaries and interaction calculators.
 
     //!
     //! Build the secondaries and interaction calculators and add it to calc.
@@ -53,13 +54,15 @@ namespace corsika::proposal {
     //! produce the corresponding secondaries and store them on the particle stack.
     //!
     template <typename TSecondaryView>
-    ProcessReturn doInteraction(TSecondaryView&);
+    ProcessReturn doInteraction(TSecondaryView&, Code const projectileId,
+                                FourMomentum const& projectileP4);
 
     //!
-    //! Calculates the  mean free path length
+    //! Calculates and returns the cross section.
     //!
     template <typename TParticle>
-    GrammageType getInteractionLength(TParticle const& p);
+    CrossSectionType getCrossSection(TParticle const& p, Code const projectileId,
+                                     FourMomentum const& projectileP4);
   };
 } // namespace corsika::proposal
 
diff --git a/corsika/modules/pythia8/Interaction.hpp b/corsika/modules/pythia8/Interaction.hpp
index c099997a4019de54deb00980ef7349669b744e08..25ebf826b3f886065d5759fffa4eaa94f3e659ac 100644
--- a/corsika/modules/pythia8/Interaction.hpp
+++ b/corsika/modules/pythia8/Interaction.hpp
@@ -21,35 +21,76 @@ namespace corsika::pythia8 {
   class Interaction : public InteractionProcess<Interaction>, public Pythia8::Pythia {
 
   public:
-    Interaction(const bool print_listing = false);
+    Interaction(bool const print_listing = false);
     ~Interaction();
 
     void setStable(std::vector<Code> const&);
-    void setUnstable(const Code);
-    void setStable(const Code);
+    void setUnstable(Code const);
+    void setStable(Code const);
 
-    bool isValidCoMEnergy(HEPEnergyType ecm) { return (10_GeV < ecm) && (ecm < 1_PeV); }
+    bool isValidCoMEnergy(HEPEnergyType const ecm) const {
+      return (10_GeV < ecm) && (ecm < 1_PeV);
+    }
 
-    bool canInteract(const Code);
-    void configureLabFrameCollision(const Code, const Code, const HEPEnergyType);
+    bool canInteract(Code const) const;
+    void configureLabFrameCollision(Code const, Code const, HEPEnergyType const);
 
-    std::tuple<CrossSectionType, CrossSectionType> getCrossSection(
-        const Code BeamId, const Code TargetId, const HEPEnergyType CoMenergy);
+    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;
 
-    template <typename TParticle>
-    GrammageType getInteractionLength(TParticle 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 {
+      return std::get<0>(
+          getCrossSectionInelEla(projectile, target, projectileP4, targetP4));
+    }
 
     /**
-       In this function PYTHIA is called to produce one event. The
-       event is copied (and boosted) into the shower lab frame.
+     * 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&);
+    void doInteraction(TView& output, Code const projectileId, Code const targetId,
+                       FourMomentum const& projectileP4, FourMomentum const& targetP4);
 
   private:
     default_prng_type& RNG_ = RNGManager<>::getInstance().getRandomStream("pythia");
     Pythia8::SigmaTotal sigma_;
-    const bool internalDecays_ = true;
+    bool const internalDecays_ = true;
     int count_ = 0;
     bool print_listing_ = false;
   };
diff --git a/corsika/modules/qgsjetII/Interaction.hpp b/corsika/modules/qgsjetII/Interaction.hpp
deleted file mode 100644
index 0030de2fccc3438377c11a801e0206e62d55a0c0..0000000000000000000000000000000000000000
--- a/corsika/modules/qgsjetII/Interaction.hpp
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
- *
- * 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.
- */
-
-#pragma once
-
-#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/qgsjetII/ParticleConversion.hpp>
-#include <corsika/framework/utility/CorsikaData.hpp>
-
-#include <boost/filesystem/path.hpp>
-
-#include <qgsjet-II-04.hpp>
-#include <string>
-
-namespace corsika::qgsjetII {
-
-  class Interaction : public corsika::InteractionProcess<Interaction> {
-
-  public:
-    Interaction(boost::filesystem::path dataPath = corsika_data("QGSJetII"));
-    ~Interaction();
-
-    bool wasInitialized() { return initialized_; }
-    unsigned int getMaxTargetMassNumber() const { return maxMassNumber_; }
-    bool isValidTarget(corsika::Code TargetId) const {
-      return is_nucleus(TargetId) && (get_nucleus_A(TargetId) < maxMassNumber_);
-    }
-
-    CrossSectionType getCrossSection(const Code, const Code, const HEPEnergyType,
-                                     const unsigned int Abeam = 0,
-                                     const unsigned int Atarget = 0) const;
-
-    template <typename TParticle>
-    GrammageType getInteractionLength(TParticle const&) const;
-
-    /**
-       In this function QGSJETII is called to produce one event. The
-       event is copied (and boosted) into the shower lab frame.
-     */
-
-    template <typename TSecondaryView>
-    void doInteraction(TSecondaryView&);
-
-  private:
-    int count_ = 0;
-    bool initialized_ = false;
-    QgsjetIIHadronType alternate_ =
-        QgsjetIIHadronType::PiPlusType; // for pi0, rho0 projectiles
-
-    corsika::default_prng_type& rng_ =
-        corsika::RNGManager<>::getInstance().getRandomStream("qgsjet");
-    static unsigned int constexpr maxMassNumber_ = 208;
-  };
-
-} // namespace corsika::qgsjetII
-
-#include <corsika/detail/modules/qgsjetII/Interaction.inl>
diff --git a/corsika/modules/qgsjetII/InteractionModel.hpp b/corsika/modules/qgsjetII/InteractionModel.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..0e3896df5f594874f15c3d4c132d8d41cb8e3192
--- /dev/null
+++ b/corsika/modules/qgsjetII/InteractionModel.hpp
@@ -0,0 +1,78 @@
+/*
+ * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * 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 ofp
+ * the license.
+ */
+
+#pragma once
+
+#include <corsika/modules/qgsjetII/ParticleConversion.hpp>
+#include <qgsjet-II-04.hpp>
+
+#include <corsika/framework/core/ParticleProperties.hpp>
+#include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/framework/random/RNGManager.hpp>
+#include <corsika/framework/utility/COMBoost.hpp>
+#include <corsika/framework/utility/CorsikaData.hpp>
+
+#include <boost/filesystem/path.hpp>
+
+namespace corsika::qgsjetII {
+
+  class InteractionModel {
+
+  public:
+    InteractionModel(boost::filesystem::path dataPath = corsika_data("QGSJetII"));
+    ~InteractionModel();
+
+    /**
+     * Throws exception if invalid system is passed.
+     *
+     * @param beamId
+     * @param targetId
+     */
+    bool isValid(Code const beamId, Code const targetId, HEPEnergyType const sqrtS) const;
+
+    /**
+     * Return the QGSJETII 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 Aprojectile 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.
+     */
+    CrossSectionType getCrossSection(Code const projectile, Code const target,
+                                     FourMomentum const& projectileP4,
+                                     FourMomentum const& targetP4) const;
+
+    /**
+     * In this function QGSJETII is called to produce one event.
+     *
+     * The event is copied (and boosted) into the shower lab frame.
+     */
+    template <typename TSecondaries>
+    void doInteraction(TSecondaries&, Code const projectile, Code const target,
+                       FourMomentum const& projectileP4, FourMomentum const& targetP4);
+
+  private:
+    int count_ = 0;
+    QgsjetIIHadronType alternate_ =
+        QgsjetIIHadronType::PiPlusType; // for pi0, rho0 projectiles
+
+    corsika::default_prng_type& rng_ =
+        corsika::RNGManager<>::getInstance().getRandomStream("qgsjet");
+    static size_t constexpr maxMassNumber_ = 208;
+    static HEPEnergyType constexpr sqrtSmin_ = 10_GeV;
+  };
+
+} // namespace corsika::qgsjetII
+
+#include <corsika/detail/modules/qgsjetII/InteractionModel.inl>
diff --git a/corsika/modules/qgsjetII/ParticleConversion.hpp b/corsika/modules/qgsjetII/ParticleConversion.hpp
index 3b4f689b8b7b1af73367f5f6a9355c273206d657..0a3ff8c08c74d2e3571bf49c562a1aef45bbc01e 100644
--- a/corsika/modules/qgsjetII/ParticleConversion.hpp
+++ b/corsika/modules/qgsjetII/ParticleConversion.hpp
@@ -15,13 +15,13 @@
 namespace corsika::qgsjetII {
 
   /**
-     These are the possible secondaries produced by QGSJetII
+   * These are the possible secondaries produced by QGSJetII.
    */
   enum class QgsjetIICode : int8_t;
   using QgsjetIICodeIntType = std::underlying_type<QgsjetIICode>::type;
 
   /**
-     These are the possible projectile for which QGSJetII knwos cross section
+   * These are the possible projectile for which QGSJetII knwos cross section.
    */
   enum class QgsjetIIXSClass : int8_t {
     CannotInteract = 0,
@@ -32,7 +32,7 @@ namespace corsika::qgsjetII {
   using QgsjetIIXSClassIntType = std::underlying_type<QgsjetIIXSClass>::type;
 
   /**
-     These are the only possible projectile types in QGSJetII
+   *  These are the only possible projectile types in QGSJetII.
    */
   enum class QgsjetIIHadronType : int8_t {
     UndefinedType = 0,
@@ -58,7 +58,7 @@ namespace corsika::qgsjetII {
 
 namespace corsika::qgsjetII {
 
-  QgsjetIICode constexpr convertToQgsjetII(Code pCode) {
+  QgsjetIICode constexpr convertToQgsjetII(Code const pCode) {
     return corsika2qgsjetII[static_cast<CodeIntType>(pCode)];
   }
 
diff --git a/corsika/modules/sibyll/Interaction.hpp b/corsika/modules/sibyll/Interaction.hpp
deleted file mode 100644
index b53fb63080b4a0a44b751a9abf9e73f60b940401..0000000000000000000000000000000000000000
--- a/corsika/modules/sibyll/Interaction.hpp
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu
- *
- * 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.
- */
-
-#pragma once
-
-#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 <tuple>
-
-namespace corsika::sibyll {
-
-  class Interaction : public InteractionProcess<Interaction> {
-
-  public:
-    Interaction(bool const sibyll_printout_on = false);
-    ~Interaction();
-
-    bool isValidCoMEnergy(HEPEnergyType const ecm) const {
-      return (minEnergyCoM_ <= ecm) && (ecm <= maxEnergyCoM_);
-    }
-    //! sibyll only accepts nuclei with 4<=A<=18 as targets, or protons aka Hydrogen or
-    //! neutrons (p,n == nucleon)
-    bool isValidTarget(Code const TargetId) const {
-      return (is_nucleus(TargetId) && (get_nucleus_A(TargetId) >= minNuclearTargetA_) &&
-              (get_nucleus_A(TargetId) < maxTargetMassNumber_)) ||
-             (TargetId == Code::Proton || TargetId == Code::Hydrogen ||
-              TargetId == Code::Neutron);
-    }
-
-    //! returns production and elastic cross section for hadrons in sibyll. Inputs are:
-    //! CorsikaId of beam particle, CorsikaId of target particle and center-of-mass
-    //! energy. Allowed targets are: nuclei or single nucleons (p,n,hydrogen).
-    std::tuple<CrossSectionType, CrossSectionType> getCrossSection(
-        Code const, Code const, HEPEnergyType const) const;
-
-    template <typename TParticle>
-    GrammageType getInteractionLength(TParticle const&) const;
-
-    /**
-       In this function SIBYLL is called to produce one event. The
-       event is copied (and boosted) into the shower lab frame.
-     */
-
-    template <typename TSecondaries>
-    void doInteraction(TSecondaries&);
-
-  private:
-    unsigned int constexpr getMaxTargetMassNumber() const { return maxTargetMassNumber_; }
-    HEPEnergyType getMinEnergyCoM() const { return minEnergyCoM_; }
-    HEPEnergyType getMaxEnergyCoM() const { return maxEnergyCoM_; }
-
-    default_prng_type& RNG_ = RNGManager<>::getInstance().getRandomStream("sibyll");
-    const HEPEnergyType minEnergyCoM_ = 10. * 1e9 * electronvolt;
-    const HEPEnergyType maxEnergyCoM_ = 1.e6 * 1e9 * electronvolt;
-    static unsigned int constexpr maxTargetMassNumber_ = 18;
-    static unsigned int constexpr minNuclearTargetA_ = 4;
-
-    // data members
-    int count_ = 0;
-    int nucCount_ = 0;
-    bool sibyll_listing_;
-  };
-
-} // namespace corsika::sibyll
-
-#include <corsika/detail/modules/sibyll/Interaction.inl>
diff --git a/corsika/modules/sibyll/InteractionModel.hpp b/corsika/modules/sibyll/InteractionModel.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..177e77857b305d385da162a3c0b705a8c5c276f9
--- /dev/null
+++ b/corsika/modules/sibyll/InteractionModel.hpp
@@ -0,0 +1,122 @@
+/*
+ * (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <corsika/framework/core/ParticleProperties.hpp>
+#include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/framework/random/RNGManager.hpp>
+#include <corsika/framework/geometry/FourVector.hpp>
+
+#include <tuple>
+
+namespace corsika::sibyll {
+
+  /**
+   * @brief sibyll::InteractionModel provides the SIBYLL proton-nucleus interaction model.
+   *
+   * This is a TModel argument for InteractionProcess<TModel>.
+   */
+
+  class InteractionModel {
+
+  public:
+    InteractionModel();
+    ~InteractionModel();
+
+    /**
+     * @brief Set the Verbose flag.
+     *
+     * If flag is true, SIBYLL will printout additional secondary particle information
+     * lists, etc.
+     *
+     * @param flag to switch.
+     */
+    void setVerbose(bool const flag);
+
+    /**
+     * @brief evaluated validity of collision system.
+     *
+     * sibyll only accepts nuclei with 4<=A<=18 as targets, or protons aka Hydrogen or
+     * neutrons (p,n == nucleon).
+     */
+    bool constexpr isValid(Code const projectileId, Code const targetId,
+                           HEPEnergyType const sqrtSnn) 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 {
+      return std::get<0>(
+          getCrossSectionInelEla(projectile, target, projectileP4, targetP4));
+    }
+
+    /**
+     * In this function SIBYLL is called to produce one event. The
+     * event is copied (and boosted) into the shower lab frame.
+     */
+
+    template <typename TSecondaries>
+    void doInteraction(TSecondaries& view, Code const projectile, Code const target,
+                       FourMomentum const& projectileP4, FourMomentum const& targetP4);
+
+  private:
+    HEPEnergyType constexpr getMinEnergyCoM() const { return minEnergyCoM_; }
+    HEPEnergyType constexpr getMaxEnergyCoM() const { return maxEnergyCoM_; }
+
+    // hard model limits
+    static HEPEnergyType constexpr minEnergyCoM_ = 10. * 1e9 * electronvolt;
+    static HEPEnergyType constexpr maxEnergyCoM_ = 1.e6 * 1e9 * electronvolt;
+    static unsigned int constexpr maxTargetMassNumber_ = 18;
+    static unsigned int constexpr minNuclearTargetA_ = 4;
+
+    default_prng_type& RNG_ = RNGManager<>::getInstance().getRandomStream("sibyll");
+
+    // data members
+    int count_ = 0;
+    int nucCount_ = 0;
+    bool sibyll_listing_;
+  };
+
+} // namespace corsika::sibyll
+
+#include <corsika/detail/modules/sibyll/InteractionModel.inl>
diff --git a/corsika/modules/sibyll/NuclearInteraction.hpp b/corsika/modules/sibyll/NuclearInteraction.hpp
deleted file mode 100644
index 7196363131bd7bca26961bbe427d995927e134d0..0000000000000000000000000000000000000000
--- a/corsika/modules/sibyll/NuclearInteraction.hpp
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu
- *
- * 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.
- */
-
-#pragma once
-
-#include <corsika/framework/core/ParticleProperties.hpp>
-#include <corsika/framework/random/RNGManager.hpp>
-#include <corsika/framework/process/InteractionProcess.hpp>
-
-namespace corsika::sibyll {
-
-  class Interaction; // fwd-decl
-
-  /**
-   *
-   *
-   **/
-  template <class TEnvironment>
-  class NuclearInteraction : public InteractionProcess<NuclearInteraction<TEnvironment>> {
-
-  public:
-    NuclearInteraction(sibyll::Interaction&, TEnvironment const&);
-    ~NuclearInteraction();
-
-    void initializeNuclearCrossSections();
-    void printCrossSectionTable(Code);
-    CrossSectionType readCrossSectionTable(int const, Code const, HEPEnergyType const);
-    HEPEnergyType getMinEnergyPerNucleonCoM() { return gMinEnergyPerNucleonCoM_; }
-    HEPEnergyType getMaxEnergyPerNucleonCoM() { return gMaxEnergyPerNucleonCoM_; }
-    unsigned int constexpr getMaxNucleusAProjectile() { return gMaxNucleusAProjectile_; }
-    unsigned int constexpr getMaxNFragments() { return gMaxNFragments_; }
-    unsigned int constexpr getNEnergyBins() { return gNEnBins_; }
-
-    template <typename Particle>
-    std::tuple<CrossSectionType, CrossSectionType> getCrossSection(Particle const& p,
-                                                                   const Code TargetId);
-
-    template <typename Particle>
-    GrammageType getInteractionLength(Particle const&);
-
-    template <typename TSecondaryView>
-    void doInteraction(TSecondaryView&);
-
-  private:
-    int count_ = 0;
-    int nucCount_ = 0;
-
-    TEnvironment const& environment_;
-    sibyll::Interaction& hadronicInteraction_;
-    std::map<Code, int> targetComponentsIndex_;
-    default_prng_type& RNG_ = RNGManager<>::getInstance().getRandomStream("sibyll");
-    static unsigned int constexpr gNSample_ =
-        500; // number of samples in MC estimation of cross section
-    static unsigned int constexpr gMaxNucleusAProjectile_ = 56;
-    static unsigned int constexpr gNEnBins_ = 6;
-    static unsigned int constexpr gMaxNFragments_ = 60;
-    // energy limits defined by table used for cross section in signuc.f
-    // 10**1 GeV to 10**6 GeV
-    static HEPEnergyType constexpr gMinEnergyPerNucleonCoM_ = 10. * 1e9 * electronvolt;
-    static HEPEnergyType constexpr gMaxEnergyPerNucleonCoM_ = 1.e6 * 1e9 * electronvolt;
-  };
-
-} // namespace corsika::sibyll
-
-#include <corsika/detail/modules/sibyll/NuclearInteraction.inl>
diff --git a/corsika/modules/sibyll/NuclearInteractionModel.hpp b/corsika/modules/sibyll/NuclearInteractionModel.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2d7d81c2ce0717e33567715b554b93fb93ce15c9
--- /dev/null
+++ b/corsika/modules/sibyll/NuclearInteractionModel.hpp
@@ -0,0 +1,76 @@
+/*
+ * (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <corsika/framework/core/ParticleProperties.hpp>
+#include <corsika/framework/random/RNGManager.hpp>
+#include <corsika/framework/geometry/FourVector.hpp>
+
+namespace corsika::sibyll {
+
+  /**
+   * The sibyll::NuclearInteractionModel provides the SIBYLL semi superposition model.
+   *
+   * can transform a proton-nucleus interaction model into a nucleus-nucleus interaction
+   * model.
+   *
+   * @tparam TNucleonModel
+   */
+  template <class TEnvironment, class TNucleonModel>
+  class NuclearInteractionModel {
+
+  public:
+    NuclearInteractionModel(TNucleonModel&, TEnvironment const&);
+    ~NuclearInteractionModel();
+
+    bool constexpr isValid(Code const projectileId, Code const targetId,
+                           HEPEnergyType const sqrtSnn) const;
+
+    void initializeNuclearCrossSections();
+    void printCrossSectionTable(Code) const;
+    CrossSectionType readCrossSectionTable(int const, Code const,
+                                           HEPEnergyType const) const;
+    HEPEnergyType getMinEnergyPerNucleonCoM() const { return gMinEnergyPerNucleonCoM_; }
+    HEPEnergyType getMaxEnergyPerNucleonCoM() const { return gMaxEnergyPerNucleonCoM_; }
+    unsigned int constexpr getMaxNucleusAProjectile() const {
+      return gMaxNucleusAProjectile_;
+    }
+    unsigned int constexpr getMaxNFragments() const { return gMaxNFragments_; }
+    unsigned int constexpr getNEnergyBins() const { return gNEnBins_; }
+
+    CrossSectionType getCrossSection(Code const, Code const,
+                                     FourMomentum const& projectileP4,
+                                     FourMomentum const& targetP4) const;
+
+    template <typename TSecondaryView>
+    void doInteraction(TSecondaryView&, Code const, Code const,
+                       FourMomentum const& projectileP4, FourMomentum const& targetP4);
+
+  private:
+    int count_ = 0;
+    int nucCount_ = 0;
+
+    TEnvironment const& environment_;
+    TNucleonModel& hadronicInteraction_;
+    std::map<Code, int> targetComponentsIndex_;
+    default_prng_type& RNG_ = RNGManager<>::getInstance().getRandomStream("sibyll");
+    static unsigned int constexpr gNSample_ =
+        500; // number of samples in MC estimation of cross section
+    static unsigned int constexpr gMaxNucleusAProjectile_ = 56;
+    static unsigned int constexpr gNEnBins_ = 6;
+    static unsigned int constexpr gMaxNFragments_ = 60;
+    // energy limits defined by table used for cross section in signuc.f
+    // 10**1 GeV to 10**6 GeV
+    static HEPEnergyType constexpr gMinEnergyPerNucleonCoM_ = 10. * 1e9 * electronvolt;
+    static HEPEnergyType constexpr gMaxEnergyPerNucleonCoM_ = 1.e6 * 1e9 * electronvolt;
+  };
+
+} // namespace corsika::sibyll
+
+#include <corsika/detail/modules/sibyll/NuclearInteractionModel.inl>
diff --git a/corsika/modules/sibyll/ParticleConversion.hpp b/corsika/modules/sibyll/ParticleConversion.hpp
index 653b83fc3c05304c509ee394f592b6783f0b058e..86b0a83d5eba4fabe6fe18d1d1c720abde49eb05 100644
--- a/corsika/modules/sibyll/ParticleConversion.hpp
+++ b/corsika/modules/sibyll/ParticleConversion.hpp
@@ -33,14 +33,14 @@ namespace corsika::sibyll {
 
 #include <corsika/modules/sibyll/Generated.inc>
 
-  SibyllCode constexpr convertToSibyll(corsika::Code pCode) {
-    return corsika2sibyll[static_cast<corsika::CodeIntType>(pCode)];
+  SibyllCode constexpr convertToSibyll(Code const pCode) {
+    return corsika2sibyll[static_cast<CodeIntType>(pCode)];
   }
 
-  corsika::Code constexpr convertFromSibyll(SibyllCode pCode) {
+  Code constexpr convertFromSibyll(SibyllCode const pCode) {
     auto const s = static_cast<SibyllCodeIntType>(pCode);
     auto const corsikaCode = sibyll2corsika[s - minSibyll];
-    if (corsikaCode == corsika::Code::Unknown) {
+    if (corsikaCode == Code::Unknown) {
       throw std::runtime_error(std::string("SIBYLL/CORSIKA conversion of ")
                                    .append(std::to_string(s))
                                    .append(" impossible"));
@@ -53,13 +53,15 @@ namespace corsika::sibyll {
   }
 
   int constexpr getSibyllXSCode(Code const code) {
+    if (is_nucleus(code))
+      return static_cast<SibyllXSClassIntType>(SibyllXSClass::CannotInteract);
     return static_cast<SibyllXSClassIntType>(
-        corsika2sibyllXStype[static_cast<corsika::CodeIntType>(code)]);
+        corsika2sibyllXStype[static_cast<CodeIntType>(code)]);
   }
 
-  bool constexpr canInteract(corsika::Code pCode) { return getSibyllXSCode(pCode) > 0; }
+  bool constexpr canInteract(Code const pCode) { return getSibyllXSCode(pCode) > 0; }
 
-  HEPMassType getSibyllMass(corsika::Code const);
+  HEPMassType getSibyllMass(Code const);
 
 } // namespace corsika::sibyll
 
diff --git a/corsika/modules/urqmd/ParticleConversion.hpp b/corsika/modules/urqmd/ParticleConversion.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..85ae95d0d47bb85cd49ce369983ee42156fda815
--- /dev/null
+++ b/corsika/modules/urqmd/ParticleConversion.hpp
@@ -0,0 +1,38 @@
+/*
+ * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <corsika/framework/core/ParticleProperties.hpp>
+
+#include <array>
+#include <utility>
+#include <string>
+
+namespace corsika::urqmd {
+
+  /**
+   * Checks if particle with Code can interaction in UrQMD.
+   */
+  bool canInteract(Code const);
+
+  /**
+   * convert CORSIKA code to UrQMD code tuple.
+   *
+   * In the current implementation a detour via the PDG code is made.
+   */
+  std::pair<int, int> convertToUrQMD(Code const);
+
+  /**
+   * convert UrQMD code to CORSIKA code.
+   */
+  Code convertFromUrQMD(int vItyp, int vIso3);
+
+} // namespace corsika::urqmd
+
+#include <corsika/detail/modules/urqmd/ParticleConversion.inl>
diff --git a/corsika/modules/urqmd/UrQMD.hpp b/corsika/modules/urqmd/UrQMD.hpp
index 2f3c278043fa83728d36fdc52b8ca181053031ad..1ae0ff80af53f99ce31bbb383a459510e362a3e1 100644
--- a/corsika/modules/urqmd/UrQMD.hpp
+++ b/corsika/modules/urqmd/UrQMD.hpp
@@ -10,9 +10,9 @@
 
 #include <corsika/framework/core/ParticleProperties.hpp>
 #include <corsika/framework/core/PhysicalUnits.hpp>
-#include <corsika/framework/process/InteractionProcess.hpp>
 #include <corsika/framework/random/RNGManager.hpp>
 #include <corsika/framework/utility/CorsikaData.hpp>
+#include <corsika/framework/geometry/FourVector.hpp>
 
 #include <boost/filesystem/path.hpp>
 #include <boost/multi_array.hpp>
@@ -23,32 +23,30 @@
 
 namespace corsika::urqmd {
 
-  class UrQMD : public InteractionProcess<UrQMD> {
+  class UrQMD {
   public:
     /**
-     * @param path Location of UrQMD XS data file
+     * The UrQMD interaction model.
+     *
+     * @param path Location of UrQMD XS data file.
      * @param retryFlag Internal UrQMD flag for retrying interaction in case of empty
-     * event, 0 means retry
+     * event, 0 means retry.
      */
     UrQMD(boost::filesystem::path const path = corsika_data("UrQMD/UrQMD-1.3.1-xs.dat"),
           int const retryFlag = 0);
 
-    template <typename TParticle>
-    GrammageType getInteractionLength(TParticle const&) const;
+    bool isValid(Code const projectileId, Code const targetId) const;
 
-    CrossSectionType getTabulatedCrossSection(Code, Code, HEPEnergyType) const;
+    CrossSectionType getTabulatedCrossSection(Code const, Code const,
+                                              HEPEnergyType const) const;
 
-    template <typename TParticle>
-    CrossSectionType getCrossSection(TParticle const&, Code) const;
+    CrossSectionType getCrossSection(Code const projectileId, Code const targetId,
+                                     FourMomentum const& projP4,
+                                     FourMomentum const& targP4) const;
 
     template <typename TView>
-    void doInteraction(TView&);
-
-    bool canInteract(Code) const;
-
-    void blob(int) {}
-
-    static CrossSectionType getCrossSection(Code, Code, HEPEnergyType, int);
+    void doInteraction(TView&, Code const projectile, Code const targetId,
+                       FourMomentum const& projP4, FourMomentum const& targP4);
 
   private:
     void readXSFile(boost::filesystem::path);
@@ -60,14 +58,6 @@ namespace corsika::urqmd {
     boost::multi_array<CrossSectionType, 3> xs_interp_support_table_;
   };
 
-  /**
-   * convert CORSIKA code to UrQMD code tuple
-   *
-   * In the current implementation a detour via the PDG code is made.
-   */
-  std::pair<int, int> convertToUrQMD(Code);
-  Code convertFromUrQMD(int vItyp, int vIso3);
-
 } // namespace corsika::urqmd
 
 #include <corsika/detail/modules/urqmd/UrQMD.inl>
diff --git a/corsika/stack/VectorStack.hpp b/corsika/stack/VectorStack.hpp
index 2a96ef3df912765c7fc81f67aceceb24cb7c6f07..22873790c22c1e478a0c6883008ed95e54a0562b 100644
--- a/corsika/stack/VectorStack.hpp
+++ b/corsika/stack/VectorStack.hpp
@@ -53,31 +53,29 @@ namespace corsika {
     /**
      * Set data of new particle.
      *
-     * @param p parent particle
+     * @param parent parent particle
      * @param v tuple containing: PID, Momentum Vector, Position, Time
      *
      *  MomentumVector is only used to determine the DirectionVector, the normalization
      * is lost.
      */
-    void setParticleData(ParticleInterface<TStackIterator> const& p,
+    void setParticleData(ParticleInterface<TStackIterator> const& parent,
                          particle_data_type const& v);
 
     /**
      * Set data of new particle.
      *
      * @param v tuple containing: PID, kinetic Energy, Direction Vector, Position, Time
-     *
      */
     void setParticleData(particle_data_momentum_type const& v);
 
     /**
      * Set data of new particle.
      *
-     * @param p parent particle
+     * @param parent parent particle
      * @param v tuple containing: PID, kinetic Energy, Direction Vector, Position, Time
-     *
      */
-    void setParticleData(ParticleInterface<TStackIterator> const& p,
+    void setParticleData(ParticleInterface<TStackIterator> const& parent,
                          particle_data_momentum_type const& v);
 
     ///! Set particle corsika::Code
@@ -97,9 +95,9 @@ namespace corsika {
     }
 
     /**
-       The MomentumVector v is used to determine the DirectionVector, and to update the
-       particle energy.
-    */
+     * The MomentumVector v is used to determine the DirectionVector, and to update the
+     * particle energy.
+     */
     void setMomentum(MomentumVector const& v) {
       HEPMomentumType const P = v.getNorm();
       if (P == 0_eV) {
diff --git a/examples/boundary_example.cpp b/examples/boundary_example.cpp
index 6e369e8a2085c66a9e9f3e31da4db3d8dc3eb9ed..8b28769038504483db2cc3b516d62f162d2dbd10 100644
--- a/examples/boundary_example.cpp
+++ b/examples/boundary_example.cpp
@@ -103,7 +103,7 @@ int main() {
 
   auto const props = world->setModelProperties<MyHomogeneousModel>(
       Medium::AirDry1Atm, Vector(rootCS, 0_T, 0_T, 0_T), 1_kg / (1_m * 1_m * 1_m),
-      NuclearComposition(std::vector<Code>{Code::Proton}, std::vector<float>{1.f}));
+      NuclearComposition({Code::Proton}, {1.}));
 
   // add a "target" sphere with 5km readius at 0,0,0
   auto target = EnvType::createNode<Sphere>(Point{rootCS, 0_m, 0_m, 0_m}, 5_km);
diff --git a/examples/cascade_example.cpp b/examples/cascade_example.cpp
index dde77011d5888a9266b5d9a981f44a1d5d8cfd91..cf01b33ca499cf8d1df3d61bcc142f0aee921cdf 100644
--- a/examples/cascade_example.cpp
+++ b/examples/cascade_example.cpp
@@ -78,12 +78,11 @@ int main() {
       UniformMagneticField<HomogeneousMedium<setup::EnvironmentInterface>>>;
 
   // fraction of oxygen
-  float const fox = 0.20946;
+  double const fox = 0.20946;
   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(std::vector<Code>{Code::Nitrogen, Code::Oxygen},
-                         std::vector<float>{1.f - fox, fox}));
+      NuclearComposition({Code::Nitrogen, Code::Oxygen}, {1. - fox, fox}));
 
   auto innerMedium =
       setup::Environment::createNode<Sphere>(Point{rootCS, 0_m, 0_m, 0_m}, 5000_m);
@@ -147,11 +146,8 @@ int main() {
   BetheBlochPDG eLoss{showerAxis};
 
   // assemble all processes into an ordered process list
-  auto sequence = make_sequence(
-      stackInspect,
-      make_select([](auto const& particle) { return is_nucleus(particle.getPID()); },
-                  sibyllNuc, sibyll),
-      decay, eLoss, cut, trackWriter);
+  auto sequence = make_sequence(stackInspect, make_sequence(sibyllNuc, sibyll), decay,
+                                eLoss, cut, trackWriter);
 
   // define air shower object, run simulation
   Cascade EAS(env, tracking, sequence, output, stack);
diff --git a/examples/cascade_proton_example.cpp b/examples/cascade_proton_example.cpp
index 053a0c13e2529bd1ae1883fa0b52b661413b2c3c..fda0a3af6062b3011ee073c30e7fd2438349ae91 100644
--- a/examples/cascade_proton_example.cpp
+++ b/examples/cascade_proton_example.cpp
@@ -80,9 +80,7 @@ int main() {
 
   world->setModelProperties<MyHomogeneousModel>(
       Medium::AirDry1Atm, MagneticFieldVector(rootCS, 0_T, 0_T, 1_mT),
-      1_kg / (1_m * 1_m * 1_m),
-      NuclearComposition(std::vector<Code>{Code::Hydrogen},
-                         std::vector<float>{(float)1.}));
+      1_kg / (1_m * 1_m * 1_m), NuclearComposition({Code::Hydrogen}, {1.}));
 
   universe.addChild(std::move(world));
 
diff --git a/examples/corsika.cpp b/examples/corsika.cpp
index a3db1ccfa0c4d35899c25d9603e9655c69a7443d..7df6dcbde54c0d40190da9a377c166d9472c588b 100644
--- a/examples/corsika.cpp
+++ b/examples/corsika.cpp
@@ -240,11 +240,11 @@ int main(int argc, char** argv) {
   HEPEnergyType mass = get_mass(beamCode);
 
   // particle energy
-  HEPEnergyType const E0 = 1_GeV * app["--energy"]->as<float>();
+  HEPEnergyType const E0 = 1_GeV * app["--energy"]->as<double>();
 
   // direction of the shower in (theta, phi) space
-  auto const thetaRad = app["--zenith"]->as<float>() / 180. * M_PI;
-  auto const phiRad = app["--azimuth"]->as<float>() / 180. * M_PI;
+  auto const thetaRad = app["--zenith"]->as<double>() / 180. * M_PI;
+  auto const phiRad = app["--azimuth"]->as<double>() / 180. * M_PI;
 
   // convert Elab to Plab
   HEPMomentumType P0 = sqrt((E0 - mass) * (E0 + mass));
@@ -286,8 +286,7 @@ int main(int argc, char** argv) {
   InteractionCounter sibyllCounted(sibyll);
   corsika::sibyll::NuclearInteraction sibyllNuc(sibyll, env);
   InteractionCounter sibyllNucCounted(sibyllNuc);
-  auto heModelCounted = make_select([](auto const& p) { return is_nucleus(p.getPID()); },
-                                    sibyllNucCounted, sibyllCounted);
+  auto heModelCounted = make_sequence(sibyllNucCounted, sibyllCounted);
 
   corsika::pythia8::Decay decayPythia;
 
@@ -317,8 +316,10 @@ int main(int argc, char** argv) {
   HEPEnergyType const emcut = 1_GeV;
   HEPEnergyType const hadcut = 1_GeV;
   ParticleCut cut(emcut, emcut, hadcut, hadcut, true);
+
   corsika::proposal::Interaction emCascade(env);
-  InteractionCounter emCascadeCounted(emCascade);
+  // NOT available for PROPOSAL due to interface trouble:
+  //  InteractionCounter emCascadeCounted(emCascade);
   // corsika::proposal::ContinuousProcess emContinuous(env);
   BetheBlochPDG emContinuous(showerAxis);
 
@@ -335,7 +336,7 @@ int main(int argc, char** argv) {
     HEPEnergyType cutE_;
     EnergySwitch(HEPEnergyType cutE)
         : cutE_(cutE) {}
-    bool operator()(const Particle& p) { return (p.getKineticEnergy() < cutE_); }
+    bool operator()(const Particle& p) const { return (p.getKineticEnergy() < cutE_); }
   };
   auto hadronSequence = make_select(EnergySwitch(63.1_GeV), urqmdCounted, heModelCounted);
   auto decaySequence = make_sequence(decayPythia, decaySibyll);
@@ -352,8 +353,8 @@ int main(int argc, char** argv) {
   output.add("particles", observationLevel);
 
   // assemble the final process sequence
-  auto sequence = make_sequence(stackInspect, hadronSequence, decaySequence,
-                                emCascadeCounted, cut, emContinuous, // trackWriter,
+  auto sequence = make_sequence(stackInspect, hadronSequence, decaySequence, cut,
+                                emCascade, emContinuous, // trackWriter,
                                 observationLevel, longprof);
   /* === END: SETUP PROCESS LIST === */
 
diff --git a/examples/em_shower.cpp b/examples/em_shower.cpp
index 62aa91e564010d12a0fbcb3ab48062917ca42da4..b01d7b608b85d661d7fdce6004a1ef9efcd3cd4b 100644
--- a/examples/em_shower.cpp
+++ b/examples/em_shower.cpp
@@ -144,7 +144,9 @@ int main(int argc, char** argv) {
   ParticleCut cut(60_GeV, 60_GeV, 100_PeV, 100_PeV, true);
   corsika::proposal::Interaction emCascade(env);
   corsika::proposal::ContinuousProcess emContinuous(env);
-  InteractionCounter emCascadeCounted(emCascade);
+
+  //  NOT possible right now, due to interface differenc in PROPOSAL
+  //  InteractionCounter emCascadeCounted(emCascade);
 
   TrackWriter trackWriter;
   output.add("tracks", trackWriter); // register TrackWriter
@@ -157,8 +159,8 @@ int main(int argc, char** argv) {
       obsPlane, DirectionVector(rootCS, {1., 0., 0.}), "particles.dat");
   output.add("obsplane", observationLevel);
 
-  auto sequence = make_sequence(emCascadeCounted, emContinuous, longprof, cut,
-                                observationLevel, trackWriter);
+  auto sequence = make_sequence(emCascade, emContinuous, longprof, cut, observationLevel,
+                                trackWriter);
   // define air shower object, run simulation
   setup::Tracking tracking;
   Cascade EAS(env, tracking, sequence, output, stack);
@@ -183,9 +185,6 @@ int main(int argc, char** argv) {
   cut.reset();
   emContinuous.reset();
 
-  auto const hists = emCascadeCounted.getHistogram();
-  save_hist(hists.labHist(), "inthist_lab_emShower.npz", true);
-  save_hist(hists.CMSHist(), "inthist_cms_emShower.npz", true);
   longprof.save("longprof_emShower.txt");
 
   output.endOfLibrary();
diff --git a/examples/hybrid_MC.cpp b/examples/hybrid_MC.cpp
index 4a85351b0e9e5ebdb4c24910a9fa77655c14215a..0ff10af164ff51fc3db6cde5bbb7324dea410d24 100644
--- a/examples/hybrid_MC.cpp
+++ b/examples/hybrid_MC.cpp
@@ -248,7 +248,7 @@ int main(int argc, char** argv) {
     HEPEnergyType cutE_;
     EnergySwitch(HEPEnergyType cutE)
         : cutE_(cutE) {}
-    bool operator()(const setup::Stack::particle_type& p) {
+    bool operator()(const setup::Stack::particle_type& p) const {
       return (p.getEnergy() < cutE_);
     }
   };
diff --git a/examples/mars.cpp b/examples/mars.cpp
index 14eab4096b2c07aa34d65ffa7fafdc3e12d3f1e7..afa8c71b70b8e2526785312acc38e1c7b684484c 100644
--- a/examples/mars.cpp
+++ b/examples/mars.cpp
@@ -271,11 +271,11 @@ int main(int argc, char** argv) {
   HEPEnergyType const mass = get_mass(beamCode);
 
   // particle energy
-  HEPEnergyType const E0 = 1_GeV * app["--energy"]->as<float>();
+  HEPEnergyType const E0 = 1_GeV * app["--energy"]->as<double>();
 
   // direction of the shower in (theta, phi) space
-  auto const thetaRad = app["--zenith"]->as<float>() / 180. * M_PI;
-  auto const phiRad = app["--azimuth"]->as<float>() / 180. * M_PI;
+  auto const thetaRad = app["--zenith"]->as<double>() / 180. * M_PI;
+  auto const phiRad = app["--azimuth"]->as<double>() / 180. * M_PI;
 
   // convert Elab to Plab
   HEPMomentumType P0 = sqrt((E0 - mass) * (E0 + mass));
@@ -313,8 +313,7 @@ int main(int argc, char** argv) {
   InteractionCounter sibyllCounted(sibyll);
   corsika::sibyll::NuclearInteraction sibyllNuc(sibyll, env);
   InteractionCounter sibyllNucCounted(sibyllNuc);
-  auto heModelCounted = make_select([](auto const& p) { return is_nucleus(p.getPID()); },
-                                    sibyllNucCounted, sibyllCounted);
+  auto heModelCounted = make_sequence(sibyllNucCounted, sibyllCounted);
 
   corsika::pythia8::Decay decayPythia;
 
@@ -345,7 +344,8 @@ int main(int argc, char** argv) {
   HEPEnergyType const hadcut = 1_GeV;
   ParticleCut cut(emcut, emcut, hadcut, hadcut, true);
   corsika::proposal::Interaction emCascade(env);
-  InteractionCounter emCascadeCounted(emCascade);
+  // NOT possible right now, due to interface difference for PROPOSAL:
+  //  InteractionCounter emCascadeCounted(emCascade);
   // corsika::proposal::ContinuousProcess emContinuous(env);
   BetheBlochPDG emContinuous(showerAxis);
 
@@ -360,7 +360,7 @@ int main(int argc, char** argv) {
     HEPEnergyType cutE_;
     EnergySwitch(HEPEnergyType cutE)
         : cutE_(cutE) {}
-    bool operator()(const Particle& p) { return (p.getKineticEnergy() < cutE_); }
+    bool operator()(Particle const& p) const { return (p.getKineticEnergy() < cutE_); }
   };
   auto hadronSequence = make_select(EnergySwitch(63.1_GeV), urqmdCounted, heModelCounted);
   auto decaySequence = make_sequence(decayPythia, decaySibyll);
@@ -378,8 +378,8 @@ int main(int argc, char** argv) {
 
   // assemble the final process sequence
   auto sequence =
-      make_sequence(stackInspect, hadronSequence, decaySequence, emCascadeCounted,
-                    emContinuous, cut, trackWriter, observationLevel, longprof);
+      make_sequence(stackInspect, hadronSequence, decaySequence, emCascade, emContinuous,
+                    cut, trackWriter, observationLevel, longprof);
   /* === END: SETUP PROCESS LIST === */
 
   // create the cascade object using the default stack and tracking implementation
diff --git a/examples/staticsequence_example.cpp b/examples/staticsequence_example.cpp
index 3f6c5aff75c72ec9058773fabbb6347e0db15274..8f95e66c486eaed05a913176d7cac77e152388c3 100644
--- a/examples/staticsequence_example.cpp
+++ b/examples/staticsequence_example.cpp
@@ -105,7 +105,6 @@ void modular() {
 int main() {
 
   logging::set_level(logging::level::info);
-  corsika_logger->set_pattern("[%n:%^%-8l%$] custom pattern: %v");
 
   std::cout << "staticsequence_example" << std::endl;
 
diff --git a/examples/vertical_EAS.cpp b/examples/vertical_EAS.cpp
index 911f45c528f6f440c45b7a94048c2e3478b5a8a3..6bb97291aa2b2d7d3106fcf68a6b887aaf99c7d9 100644
--- a/examples/vertical_EAS.cpp
+++ b/examples/vertical_EAS.cpp
@@ -236,12 +236,10 @@ int main(int argc, char** argv) {
     HEPEnergyType cutE_;
     EnergySwitch(HEPEnergyType cutE)
         : cutE_(cutE) {}
-    bool operator()(const Particle& p) { return (p.getEnergy() < cutE_); }
+    bool operator()(const Particle& p) const { return (p.getEnergy() < cutE_); }
   };
-  auto hadronSequence =
-      make_select(EnergySwitch(55_GeV), urqmdCounted,
-                  make_select([](auto const& p) { return is_nucleus(p.getPID()); },
-                              sibyllNucCounted, sibyllCounted));
+  auto hadronSequence = make_select(EnergySwitch(55_GeV), urqmdCounted,
+                                    make_sequence(sibyllNucCounted, sibyllCounted));
   auto decaySequence = make_sequence(decayPythia, decaySibyll);
 
   // directory for outputs
diff --git a/modules/sibyll/CMakeLists.txt b/modules/sibyll/CMakeLists.txt
index 7f03881a4462e66016290e218fe52d46021a95e3..21e45060dbf71e4cb71a9a6c4fbdac21d1d34098 100644
--- a/modules/sibyll/CMakeLists.txt
+++ b/modules/sibyll/CMakeLists.txt
@@ -15,6 +15,7 @@ set (
 
 enable_language (Fortran)
 add_library (Sibyll_static STATIC ${MODEL_SOURCES})
+add_library (Sibyll SHARED ${MODEL_SOURCES})
 
 set_target_properties (
   Sibyll_static
@@ -28,13 +29,24 @@ target_include_directories (
   $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
   $<INSTALL_INTERFACE:include/corsika_modules/sibyll>
   )
+  target_include_directories (
+    Sibyll
+    PUBLIC
+    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+    $<INSTALL_INTERFACE:include/corsika_modules/sibyll>
+    )
 
 target_link_libraries (
   Sibyll_static
   PUBLIC
   gfortran
   )
-
+target_link_libraries (
+  Sibyll
+  PUBLIC
+  gfortran
+  )
+  
 install (
   FILES
   ${MODEL_HEADERS}
@@ -42,7 +54,7 @@ install (
   )
 
 install (
-  TARGETS Sibyll_static
+  TARGETS Sibyll_static Sibyll
   EXPORT CORSIKA8PublicTargets
   ARCHIVE DESTINATION lib/corsika
   )
diff --git a/tests/common/SetupTestEnvironment.hpp b/tests/common/SetupTestEnvironment.hpp
index fa48ec023fc37358198582f7f15139604d4cfa36..358ed6df265851c96198e8f013b03e71a3cae963 100644
--- a/tests/common/SetupTestEnvironment.hpp
+++ b/tests/common/SetupTestEnvironment.hpp
@@ -49,7 +49,7 @@ namespace corsika::setup::testing {
 
     world->setModelProperties<MyHomogeneousModel>(
         Medium::AirDry1Atm, Vector(cs, 0_T, 0_T, BfieldZ), 1_kg / (1_m * 1_m * 1_m),
-        NuclearComposition(std::vector<Code>{vTargetCode}, std::vector<float>{1.}));
+        NuclearComposition(std::vector<Code>{vTargetCode}, std::vector<double>{1.}));
 
     setup::Environment::BaseNodeType* nodePtr = world.get();
     universe.addChild(std::move(world));
diff --git a/tests/common/SetupTestStack.hpp b/tests/common/SetupTestStack.hpp
index a5bfb3d23622a3da7f0a1ea4102f514735005906..a6141e582262b77e4062eb020bfdc7e3a19adece 100644
--- a/tests/common/SetupTestStack.hpp
+++ b/tests/common/SetupTestStack.hpp
@@ -18,7 +18,7 @@
  * \file SetupTestStack
  *
  * standard stack setup for unit tests.
- **/
+ */
 
 namespace corsika::setup::testing {
 
@@ -32,10 +32,10 @@ namespace corsika::setup::testing {
    *
    * \return a tuple with element 0 being a Stack object filled with
    * one particle, and element 1 the StackView on it.
-   **/
+   */
 
   inline std::tuple<std::unique_ptr<setup::Stack>, std::unique_ptr<setup::StackView>>
-  setup_stack(Code vProjectileType, HEPEnergyType vMomentum,
+  setup_stack(Code const vProjectileType, HEPEnergyType const vMomentum,
               setup::Environment::BaseNodeType* const vNodePtr,
               CoordinateSystemPtr const& cs) {
 
diff --git a/tests/framework/testCOMBoost.cpp b/tests/framework/testCOMBoost.cpp
index d092a4b61ea46dfbcb734a4c8f31013550451204..f9542e3942c61d8430f567048e25d3f8bfd6b54a 100644
--- a/tests/framework/testCOMBoost.cpp
+++ b/tests/framework/testCOMBoost.cpp
@@ -23,17 +23,19 @@ CoordinateSystemPtr rootCS = get_root_CoordinateSystem();
 
 /**
  * \todo such helper functions should be moved to the FourVector class:
- **/
+ */
 // helper function for energy-momentum
 // relativistic energy
-auto const energy = [](HEPMassType m, MomentumVector const& p) {
+auto const energy = [](HEPMassType const m, MomentumVector const& p) {
   return sqrt(m * m + p.getSquaredNorm());
 };
 
-auto const momentum = [](HEPEnergyType E, HEPMassType m) { return sqrt(E * E - m * m); };
+auto const momentum = [](HEPEnergyType const E, HEPMassType const m) {
+  return sqrt(E * E - m * m);
+};
 
 // helper function for mandelstam-s
-auto const s = [](HEPEnergyType E, QuantityVector<hepmomentum_d> const& p) {
+auto const s = [](HEPEnergyType const E, QuantityVector<hepmomentum_d> const& p) {
   return E * E - p.getSquaredNorm();
 };
 
@@ -170,7 +172,7 @@ TEST_CASE("rotation") {
 
 TEST_CASE("boosts") {
 
-  logging::set_level(logging::level::info);
+  logging::set_level(logging::level::trace);
 
   // define target kinematics in lab frame
   HEPMassType const targetMass = 1_GeV;
@@ -322,6 +324,49 @@ TEST_CASE("boosts") {
         PprojCoM.getSpaceLikeComponents() + PtargCoM.getSpaceLikeComponents();
     CHECK(sumPCoM.getNorm() / P0 == Approx(0).margin(absMargin)); // MAKE RELATIVE CHECK
   }
+
+  SECTION("CoM system") {
+
+    MomentumVector pCM{rootCS, 0_GeV, 0_GeV, 5_GeV};
+
+    COMBoost boostCMS({energy(1_GeV, pCM), pCM}, {energy(1_GeV, pCM), -pCM});
+
+    auto test1 = boostCMS.fromCoM(FourMomentum{
+        0_GeV, MomentumVector(boostCMS.getOriginalCS(), {0_GeV, 0_GeV, 0_GeV})});
+    CHECK(test1.getNorm() == 0_GeV);
+    auto test2 = boostCMS.fromCoM(FourMomentum{
+        0_GeV, MomentumVector(boostCMS.getRotatedCS(), {0_GeV, 0_GeV, 0_GeV})});
+    CHECK(test2.getNorm() == 0_GeV);
+
+    auto test3 = boostCMS.toCoM(FourMomentum{
+        0_GeV, MomentumVector(boostCMS.getOriginalCS(), {0_GeV, 0_GeV, 0_GeV})});
+    CHECK(test3.getNorm() == 0_GeV);
+    auto test4 = boostCMS.toCoM(FourMomentum{
+        0_GeV, MomentumVector(boostCMS.getRotatedCS(), {0_GeV, 0_GeV, 0_GeV})});
+    CHECK(test4.getNorm() == 0_GeV);
+
+    HEPEnergyType const sqrtS =
+        (FourMomentum{energy(1_GeV, pCM), pCM} + FourMomentum{energy(1_GeV, pCM), -pCM})
+            .getNorm();
+    HEPEnergyType const eLab =
+        (static_pow<2>(sqrtS) - 2 * static_pow<2>(1_GeV)) / (2 * 1_GeV);
+    COMBoost boostLab({eLab, MomentumVector{rootCS, momentum(eLab, 1_GeV), 0_eV, 0_eV}},
+                      {1_GeV, MomentumVector{rootCS, 0_eV, 0_eV, 0_eV}});
+
+    FourMomentum p4lab_trans(
+        10_GeV,
+        MomentumVector(boostLab.getOriginalCS(), {0_eV, momentum(10_GeV, 1_GeV), 0_eV}));
+    FourMomentum p4lab_long(
+        10_GeV,
+        MomentumVector(boostLab.getOriginalCS(), {momentum(10_GeV, 1_GeV), 0_GeV, 0_eV}));
+    // boost of transverse momentum
+    CHECK(boostLab.toCoM(p4lab_trans).getNorm() / 1_GeV == Approx(1));
+    CHECK(boostLab.toCoM(p4lab_trans).getTimeLikeComponent() / 1_GeV == Approx(50.99));
+    // boost of longitudinal momentum
+    CHECK(boostLab.toCoM(p4lab_long).getNorm() / 1_GeV == Approx(1));
+    CHECK(boostLab.toCoM(p4lab_long).getTimeLikeComponent() / 1_GeV ==
+          Approx(1.24).margin(0.1));
+  }
 }
 
 TEST_CASE("rest frame") {
diff --git a/tests/framework/testCascade.cpp b/tests/framework/testCascade.cpp
index 9c9918729adf17fc8d7f261c81132bab5ae869c7..18732cb1f5faaa646fd9333a89faa1e9e2861973 100644
--- a/tests/framework/testCascade.cpp
+++ b/tests/framework/testCascade.cpp
@@ -18,8 +18,9 @@
 #include <corsika/framework/core/Logging.hpp>
 
 #include <corsika/framework/geometry/Point.hpp>
-#include <corsika/framework/geometry/RootCoordinateSystem.hpp>
 #include <corsika/framework/geometry/Vector.hpp>
+#include <corsika/framework/geometry/FourVector.hpp>
+#include <corsika/framework/geometry/RootCoordinateSystem.hpp>
 
 #include <corsika/media/HomogeneousMedium.hpp>
 #include <corsika/media/NuclearComposition.hpp>
@@ -55,8 +56,8 @@ auto make_dummy_env() {
       Point{env.getCoordinateSystem(), 0_m, 0_m, 0_m},
       1_km * std::numeric_limits<double>::infinity());
 
-  using MyEmptyModel = Empty<IEmpty>;
-  world->setModelProperties<MyEmptyModel>();
+  NuclearComposition const composition({Code::Proton}, {1.});
+  world->setModelProperties<TestEnvironmentInterface>(19.2_g / cube(1_cm), composition);
 
   universe.addChild(std::move(world));
   return env;
@@ -86,16 +87,14 @@ public:
 
 class ProcessSplit : public InteractionProcess<ProcessSplit> {
 
-  int calls_ = 0;
-
 public:
-  template <typename Particle>
-  GrammageType getInteractionLength(Particle const&) const {
-    return 0_g / square(1_cm);
+  CrossSectionType getCrossSection(Code const, Code const, FourMomentum const&,
+                                   FourMomentum const&) const {
+    return 1_mb;
   }
 
   template <typename TView>
-  void doInteraction(TView& view) {
+  void doInteraction(TView& view, Code, Code, FourMomentum const&, FourMomentum const&) {
     ++calls_;
     auto vP = view.getProjectile();
     const HEPEnergyType Ekin = vP.getKineticEnergy();
@@ -106,17 +105,16 @@ public:
   }
 
   int getCalls() const { return calls_; }
+
+private:
+  int calls_ = 0;
 };
 
 class ProcessCut : public SecondariesProcess<ProcessCut> {
 
-  int count_ = 0;
-  int calls_ = 0;
-  HEPEnergyType fEcrit;
-
 public:
-  ProcessCut(HEPEnergyType e)
-      : fEcrit(e) {}
+  ProcessCut(HEPEnergyType const e)
+      : Ecrit_(e) {}
 
   template <typename TStack>
   void doSecondaries(TStack& vS) {
@@ -124,7 +122,7 @@ public:
     auto p = vS.begin();
     while (p != vS.end()) {
       HEPEnergyType E = p.getEnergy();
-      if (E < fEcrit) {
+      if (E < Ecrit_) {
         p.erase();
         count_++;
       }
@@ -136,6 +134,11 @@ public:
 
   int getCount() const { return count_; }
   int getCalls() const { return calls_; }
+
+private:
+  int count_ = 0;
+  int calls_ = 0;
+  HEPEnergyType Ecrit_;
 };
 
 TEST_CASE("Cascade", "[Cascade]") {
@@ -153,7 +156,7 @@ TEST_CASE("Cascade", "[Cascade]") {
   StackInspector<TestCascadeStack> stackInspect(100, true, E0);
   NullModel nullModel;
 
-  const HEPEnergyType Ecrit = 85_MeV;
+  HEPEnergyType const Ecrit = 85_MeV;
   ProcessSplit split;
   ProcessCut cut(Ecrit);
   auto sequence = make_sequence(nullModel, stackInspect, split, cut);
@@ -172,7 +175,6 @@ TEST_CASE("Cascade", "[Cascade]") {
 
   SECTION("full cascade") {
     EAS.run();
-
     CHECK(cut.getCount() == 2048);
     CHECK(cut.getCalls() == 2047); // final particle is still on stack and not yet deleted
     CHECK(split.getCalls() == 2047);
diff --git a/tests/framework/testCascade.hpp b/tests/framework/testCascade.hpp
index cb6e0b5a0c8e06ab7a51fbf6c5a2565760171e97..8c37a22d28e6f1dc380415a20bd2d5bf1dd84288 100644
--- a/tests/framework/testCascade.hpp
+++ b/tests/framework/testCascade.hpp
@@ -9,14 +9,15 @@
 #pragma once
 
 #include <corsika/media/Environment.hpp>
-#include <corsika/media/IEmpty.hpp>
+#include <corsika/media/IMediumModel.hpp>
+#include <corsika/media/HomogeneousMedium.hpp>
 
 #include <corsika/framework/stack/CombinedStack.hpp>
 #include <corsika/framework/stack/SecondaryView.hpp>
 #include <corsika/stack/GeometryNodeStackExtension.hpp>
 #include <corsika/stack/VectorStack.hpp>
 
-using TestEnvironmentInterface = corsika::IEmpty;
+using TestEnvironmentInterface = corsika::HomogeneousMedium<corsika::IMediumModel>;
 using TestEnvironmentType = corsika::Environment<TestEnvironmentInterface>;
 
 template <typename T>
diff --git a/tests/framework/testInteractionCounter.cpp b/tests/framework/testInteractionCounter.cpp
index 4ea5d49b04f462e43517ed96b3bf6f565a29fba3..50c2397c9e2dd831401f60be0ad99bc6a89b7712 100644
--- a/tests/framework/testInteractionCounter.cpp
+++ b/tests/framework/testInteractionCounter.cpp
@@ -7,17 +7,11 @@
  */
 
 #include <corsika/framework/process/InteractionCounter.hpp>
-#include <corsika/media/Environment.hpp>
-#include <corsika/media/HomogeneousMedium.hpp>
-#include <corsika/media/NuclearComposition.hpp>
 #include <corsika/framework/geometry/Point.hpp>
 #include <corsika/framework/geometry/RootCoordinateSystem.hpp>
 #include <corsika/framework/geometry/Vector.hpp>
 #include <corsika/framework/core/PhysicalUnits.hpp>
 
-#include <SetupTestStack.hpp>
-#include <SetupTestEnvironment.hpp>
-
 #include <catch2/catch.hpp>
 
 #include <numeric>
@@ -32,37 +26,46 @@ using namespace corsika;
 const std::string refDataDir = std::string(REFDATADIR); // from cmake
 
 struct DummyProcess {
-  template <typename TParticle>
-  GrammageType getInteractionLength(TParticle const&) {
-    return 100_g / 1_cm / 1_cm;
+
+  CrossSectionType getCrossSection(Code const, Code const, FourMomentum const&,
+                                   FourMomentum const&) {
+    return 100_mb;
   }
+
   template <typename TParticle>
-  void doInteraction(TParticle&) {}
+  void doInteraction(TParticle&, Code const, Code const, FourMomentum const&,
+                     FourMomentum const&) {}
 };
 
-TEST_CASE("InteractionCounter", "[process]") {
+struct DummyOutput {
+  /* can do nothing */
+};
+
+TEST_CASE("InteractionCounter", "process") {
 
   logging::set_level(logging::level::info);
 
   DummyProcess d;
   InteractionCounter countedProcess(d);
 
-  SECTION("getInteractionLength") {
-    CHECK(countedProcess.getInteractionLength(nullptr) == 100_g / 1_cm / 1_cm);
-  }
+  auto const rootCS = get_root_CoordinateSystem();
+  DummyOutput output;
 
-  auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Oxygen);
-  [[maybe_unused]] auto& env_dummy = env;
+  SECTION("cross section pass-through") {
+    CHECK(countedProcess.getCrossSection(
+              Code::Oxygen, Code::Proton, {10_GeV, {rootCS, {0_eV, 0_eV, 0_eV}}},
+              {10_GeV, {rootCS, {0_eV, 0_eV, 0_eV}}}) == 100_mb);
+  }
 
-  SECTION("DoInteraction nucleus") {
+  SECTION("doInteraction nucleus") {
     unsigned short constexpr A = 14, Z = 7;
-    auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
-        get_nucleus_code(A, Z), 105_TeV, (setup::Environment::BaseNodeType* const)nodePtr,
-        *csPtr);
-    CHECK(stackPtr->getEntries() == 1);
-    CHECK(secViewPtr->getEntries() == 0);
+    Code const pid = get_nucleus_code(A, Z);
 
-    countedProcess.doInteraction(*secViewPtr);
+    countedProcess.doInteraction(
+        output, pid, Code::Oxygen,
+        {sqrt(static_pow<2>(105_TeV) + static_pow<2>(get_mass(pid))),
+         {rootCS, {105_TeV, 0_GeV, 0_GeV}}},
+        {Oxygen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}});
 
     auto const& h = countedProcess.getHistogram().labHist();
     CHECK(h.at(h.axis(0).index(1'000'070'140), h.axis(1).index(1.05e14)) == 1);
@@ -99,14 +102,14 @@ TEST_CASE("InteractionCounter", "[process]") {
     }
   }
 
-  SECTION("DoInteraction Lambda") {
-    auto constexpr code = Code::Lambda0;
-    auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
-        code, 105_TeV, (setup::Environment::BaseNodeType* const)nodePtr, *csPtr);
-    CHECK(stackPtr->getEntries() == 1);
-    CHECK(secViewPtr->getEntries() == 0);
+  SECTION("doInteraction Lambda") {
+    auto constexpr pid = Code::Lambda0;
 
-    countedProcess.doInteraction(*secViewPtr);
+    countedProcess.doInteraction(
+        output, pid, Code::Oxygen,
+        {sqrt(static_pow<2>(105_TeV) + static_pow<2>(get_mass(pid))),
+         {rootCS, {105_TeV, 0_GeV, 0_GeV}}},
+        {Oxygen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}});
 
     auto const& h = countedProcess.getHistogram().labHist();
     CHECK(h.at(h.axis(0).index(3122), h.axis(1).index(1.05e14)) == 1);
diff --git a/tests/framework/testProcessSequence.cpp b/tests/framework/testProcessSequence.cpp
index cf9dc523f4a3fe571e3d3323b957534ac790b285..9b6dce5056a78abe10529b96f75a32e521b31055 100644
--- a/tests/framework/testProcessSequence.cpp
+++ b/tests/framework/testProcessSequence.cpp
@@ -9,10 +9,15 @@
 
 #include <corsika/framework/process/ProcessSequence.hpp>
 #include <corsika/framework/process/SwitchProcessSequence.hpp>
-#include <corsika/framework/core/PhysicalUnits.hpp>
 #include <corsika/framework/process/ProcessTraits.hpp>
 #include <corsika/framework/process/ContinuousProcessStepLength.hpp>
 
+#include <corsika/framework/core/PhysicalUnits.hpp>
+
+#include <corsika/framework/utility/COMBoost.hpp>
+
+#include <corsika/media/NuclearComposition.hpp>
+
 #include <catch2/catch.hpp>
 
 #include <array>
@@ -30,6 +35,12 @@
 using namespace corsika;
 using namespace std;
 
+struct DummyRNG {
+  int max() const { return 10; }
+  int min() const { return 0; }
+  double operator()() const { return 0.5; }
+};
+
 static int const nData = 10;
 
 // DummyNode is only needed for BoundaryCrossingProcess
@@ -39,15 +50,21 @@ struct DummyNode {
   int data_ = 0;
 };
 
-// The stack is non-existent for this example
-struct DummyStack {};
-
 // our data object (particle) is a simple arrary of doubles
 struct DummyData {
   double data_[nData] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
   typedef DummyNode node_type; // for BoundaryCrossingProcess
+  Code getPID() const { return Code::Proton; }
+  MomentumVector getMomentum() const {
+    // only need the coordinate system
+    return MomentumVector{get_root_CoordinateSystem(), 0_eV, 0_eV, 0_eV};
+  }
+  HEPEnergyType getEnergy() const { return 10_GeV; }
 };
 
+// The stack is non-existent for this example
+struct DummyStack {};
+
 // there is no real trajectory/track
 struct DummyTrajectory {};
 
@@ -58,6 +75,7 @@ struct DummyView {
       : p_(p) {}
   DummyData& p_;
   DummyData& parent() { return p_; }
+  // this is only needed because of PROPOSAL interface right now:
 };
 
 int globalCount = 0; // simple counter
@@ -193,14 +211,15 @@ public:
   }
 
   template <typename TView>
-  void doInteraction(TView& v) const {
+  void doInteraction(TView& v, Code const, Code const, FourMomentum const&,
+                     FourMomentum const&) const {
     checkInteract |= 1;
     for (int i = 0; i < nData; ++i) v.parent().data_[i] += 1 + i;
   }
 
-  template <typename TParticle>
-  GrammageType getInteractionLength(TParticle&) const {
-    return 10_g / square(1_cm);
+  CrossSectionType getCrossSection(Code const, Code const, FourMomentum const&,
+                                   FourMomentum const&) const {
+    return 10_mb;
   }
 
 private:
@@ -219,15 +238,17 @@ public:
   }
 
   template <typename TView>
-  void doInteraction(TView& v) const {
+  void doInteraction(TView& v, Code const, Code const, FourMomentum const&,
+                     FourMomentum const&) const {
     checkInteract |= 2;
     for (int i = 0; i < nData; ++i) v.parent().data_[i] /= 1.1;
     CORSIKA_LOG_DEBUG("Process2::doInteraction");
   }
-  template <typename Particle>
-  GrammageType getInteractionLength(Particle&) const {
-    CORSIKA_LOG_DEBUG("Process2::GetInteractionLength");
-    return 20_g / (1_cm * 1_cm);
+
+  CrossSectionType getCrossSection(Code const, Code const, FourMomentum const&,
+                                   FourMomentum const&) const {
+    CORSIKA_LOG_DEBUG("Process2::getCrossSection");
+    return 20_mb;
   }
 
 private:
@@ -246,15 +267,17 @@ public:
   }
 
   template <typename TView>
-  void doInteraction(TView& v) const {
+  void doInteraction(TView& v, Code const, Code const, FourMomentum const&,
+                     FourMomentum const&) const {
     checkInteract |= 4;
     for (int i = 0; i < nData; ++i) v.parent().data_[i] *= 1.01;
     CORSIKA_LOG_DEBUG("Process3::doInteraction");
   }
-  template <typename Particle>
-  GrammageType getInteractionLength(Particle&) const {
-    CORSIKA_LOG_DEBUG("Process3::GetInteractionLength");
-    return 30_g / (1_cm * 1_cm);
+
+  CrossSectionType getCrossSection(Code const, Code const, FourMomentum const&,
+                                   FourMomentum const&) const {
+    CORSIKA_LOG_DEBUG("Process3::getCrossSection");
+    return 30_mb;
   }
 
 private:
@@ -280,7 +303,8 @@ public:
     return ProcessReturn::Ok;
   }
   template <typename TView>
-  void doInteraction(TView&) const {
+  void doInteraction(TView&, Code const, Code const, FourMomentum const&,
+                     FourMomentum const&) const {
     checkInteract |= 8;
   }
 
@@ -430,27 +454,6 @@ TEST_CASE("ProcessSequence General", "ProcessSequence") {
               sequence2_rv.getProcess2().getProcess2())>); // Process3
   }
 
-  SECTION("interaction length") {
-    globalCount = 0;
-    ContinuousProcess1 cp1(0, 1_m);
-    Process2 m2(1);
-    Process3 m3(2);
-
-    DummyData particle;
-
-    auto sequence2 = make_sequence(cp1, m2, m3);
-    GrammageType const tot = sequence2.getInteractionLength(particle);
-    InverseGrammageType const tot_inv = sequence2.getInverseInteractionLength(particle);
-    CORSIKA_LOG_DEBUG(
-        "lambda_tot={}"
-        "; lambda_tot_inv={}",
-        tot, tot_inv);
-
-    CHECK(tot / 1_g * square(1_cm) == 12);
-    CHECK(tot_inv * 1_g / square(1_cm) == 1. / 12);
-    globalCount = 0;
-  }
-
   SECTION("lifetime") {
     globalCount = 0;
     ContinuousProcess1 cp1(0, 1_m);
@@ -597,6 +600,8 @@ TEST_CASE("SwitchProcessSequence", "ProcessSequence") {
 
   logging::set_level(logging::level::info);
 
+  CoordinateSystemPtr rootCS = get_root_CoordinateSystem();
+
   /**
    * In this example switching is done only by "data_[0]>0", where
    * data in an arrray of doubles, DummyData.
@@ -614,12 +619,20 @@ TEST_CASE("SwitchProcessSequence", "ProcessSequence") {
   auto sec1 = Secondaries1();
   auto sec2 = Secondaries2();
 
-  auto sequence1 = make_sequence(Process1(0), cp2, Decay1(0), sec1, Boundary1(1.0));
-  auto sequence2 = make_sequence(cp3, Process2(0), Boundary1(-1.0), Decay2(0), sec2);
+  auto sequence1 =
+      make_sequence(Process1(0), cp2, Decay1(0), sec1, Boundary1(1.0)); // 10 mb
+  auto sequence2 =
+      make_sequence(cp3, Process2(0), Boundary1(-1.0), Decay2(0), sec2); // 20 mb
 
-  auto sequence3 = make_sequence(cp1, Process3(0),
+  auto sequence3 = make_sequence(cp1, Process3(0), // 30 mb
                                  SwitchProcessSequence(select1, sequence1, sequence2));
 
+  // it is even more typical to have just one sub-process inside the branches of
+  // SwitchProcessSequence
+  auto sequence3_short =
+      make_sequence(cp1, Process3(0), // 30 mb
+                    SwitchProcessSequence(select1, Process1(0), Process2(0)));
+
   auto sequence4 =
       make_sequence(cp1, Boundary1(2.0), Process3(0),
                     SwitchProcessSequence(select1, sequence1, Boundary1(-1.0)));
@@ -686,24 +699,28 @@ TEST_CASE("SwitchProcessSequence", "ProcessSequence") {
     CHECK(checkCont == 0b101);
     CHECK(checkSec == 0);
 
-    // 1/(30g/cm2) is Process3
-    InverseGrammageType lambda_select = .9 / 30. * square(1_cm) / 1_g;
-    InverseTimeType time_select = 0.1 / second;
+    // 30_mb is Process3
+    CrossSectionType cx_select = .9 * 30_mb;
+    InverseTimeType time_select = 0.1 / second; // for decay
 
     checkDecay = 0;
     checkInteract = 0;
     checkSec = 0;
     checkCont = 0;
     particle.data_[0] = 100; // data positive   --> sequence1
-    sequence3.selectInteraction(view, lambda_select);
+
+    DummyRNG rng;
+    FourMomentum const projectileP4{10_GeV, {rootCS, {0_eV, 0_eV, 0_eV}}};
+    NuclearComposition const noComposition({Code::Nitrogen}, {1});
+    sequence3.selectInteraction(view, projectileP4, noComposition, rng, cx_select);
     sequence3.selectDecay(view, time_select);
     CHECK(checkInteract == 0b100); // this is Process3
     CHECK(checkDecay == 0b001);    // this is Decay1
     CHECK(checkCont == 0);
     CHECK(checkSec == 0);
-    lambda_select = 1.01 / 30. * square(1_cm) / 1_g;
+    cx_select = 1.01 * 30_mb;
     checkInteract = 0;
-    sequence3.selectInteraction(view, lambda_select);
+    sequence3.selectInteraction(view, projectileP4, noComposition, rng, cx_select);
     CHECK(checkInteract == 0b001); // this is Process1
 
     checkDecay = 0;
@@ -711,7 +728,7 @@ TEST_CASE("SwitchProcessSequence", "ProcessSequence") {
     checkSec = 0;
     checkCont = 0;
     particle.data_[0] = -100; // data negative   --> sequence2
-    sequence3.selectInteraction(view, lambda_select);
+    sequence3.selectInteraction(view, projectileP4, noComposition, rng, cx_select);
     sequence3.selectDecay(view, time_select);
     CHECK(checkInteract == 0b010); // this is Process2
     CHECK(checkDecay == 0b010);    // this is Decay2
@@ -739,7 +756,7 @@ TEST_CASE("SwitchProcessSequence", "ProcessSequence") {
     checkSec = 0;
     checkCont = 0;
     particle.data_[0] = -100; // data negative --> sequence1
-    sequence4.selectInteraction(view, lambda_select);
+    sequence4.selectInteraction(view, projectileP4, noComposition, rng, cx_select);
     sequence4.doSecondaries(view);
     sequence4.selectDecay(view, time_select);
     sequence4.doSecondaries(view);
@@ -748,15 +765,96 @@ TEST_CASE("SwitchProcessSequence", "ProcessSequence") {
     CHECK(checkCont == 0);
     CHECK(checkSec == 0);
 
-    // check that large "select" value will correctly ignore the call
-    lambda_select = 1e5 * square(1_cm) / 1_g;
-    time_select = 1e5 / second;
-    checkDecay = 0;
-    checkInteract = 0;
-    sequence3.selectInteraction(view, lambda_select);
-    sequence3.selectDecay(view, time_select);
-    CHECK(checkInteract == 0);
-    CHECK(checkDecay == 0);
+    // now check sequence3, which contains a SwitchProcessSequence that contains two
+    // longer sequences in each branch.
+    {
+      // check that large "select" value will correctly ignore the call
+      cx_select = 1e5_mb;
+      time_select = 1e5 / second;
+      checkDecay = 0;
+      checkInteract = 0;
+      sequence3.selectInteraction(view, projectileP4, noComposition, rng, cx_select);
+      sequence3.selectDecay(view, time_select);
+      CHECK(checkInteract == 0);
+      CHECK(checkDecay == 0);
+
+      // for a small cx_select selection must be sucessful
+      cx_select = 28_mb; // -> Process3
+      checkInteract = 0;
+      particle.data_[0] = -100; // data negative --> sequence2
+      CHECK(sequence3.getCrossSection(particle, Code::Oxygen,
+                                      {Oxygen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}) /
+                1_mb ==
+            Approx(50.));
+      sequence3.selectInteraction(view, projectileP4, noComposition, rng, cx_select);
+      CHECK(checkInteract == 4); // 2^3
+
+      particle.data_[0] = 100; // data positive --> sequence1
+      checkInteract = 0;
+      CHECK(sequence3.getCrossSection(particle, Code::Oxygen,
+                                      {Oxygen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}) /
+                1_mb ==
+            Approx(40.));
+      sequence3.selectInteraction(view, projectileP4, noComposition, rng, cx_select);
+      CHECK(checkInteract == 4); // 2^3
+
+      cx_select = 32_mb; // -> Process2 or Process1
+      checkInteract = 0;
+      particle.data_[0] = -100; // data negative --> Process2
+      sequence3.selectInteraction(view, projectileP4, noComposition, rng, cx_select);
+      CHECK(checkInteract == 2); // 2^2
+
+      particle.data_[0] = 100; // data positive --> Process1
+      checkInteract = 0;
+      sequence3.selectInteraction(view, projectileP4, noComposition, rng, cx_select);
+      CHECK(checkInteract == 1); // 2^1
+    }
+
+    // now check sequence3, which contains a SwitchProcessSequence that contains just two
+    // bare InteractionProcess-es in each branch.
+    {
+      // check that large "select" value will correctly ignore the call
+      cx_select = 1e5_mb;
+      checkInteract = 0;
+      sequence3_short.selectInteraction(view, projectileP4, noComposition, rng,
+                                        cx_select);
+      CHECK(checkInteract == 0);
+
+      // for a small cx_select selection must be sucessful
+      cx_select = 28_mb; // -> Process3
+      checkInteract = 0;
+      particle.data_[0] = -100; // data negative --> sequence2
+      CHECK(sequence3_short.getCrossSection(
+                particle, Code::Oxygen, {Oxygen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}) /
+                1_mb ==
+            Approx(50.));
+      sequence3_short.selectInteraction(view, projectileP4, noComposition, rng,
+                                        cx_select);
+      CHECK(checkInteract == 4); // 2^3
+
+      particle.data_[0] = 100; // data positive --> sequence1
+      checkInteract = 0;
+      CHECK(sequence3_short.getCrossSection(
+                particle, Code::Oxygen, {Oxygen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}) /
+                1_mb ==
+            Approx(40.));
+      sequence3_short.selectInteraction(view, projectileP4, noComposition, rng,
+                                        cx_select);
+      CHECK(checkInteract == 4); // 2^3
+
+      cx_select = 32_mb; // -> Process2 or Process1
+      checkInteract = 0;
+      particle.data_[0] = -100; // data negative --> Process2
+      sequence3_short.selectInteraction(view, projectileP4, noComposition, rng,
+                                        cx_select);
+      CHECK(checkInteract == 2); // 2^2
+
+      particle.data_[0] = 100; // data positive --> Process1
+      checkInteract = 0;
+      sequence3_short.selectInteraction(view, projectileP4, noComposition, rng,
+                                        cx_select);
+      CHECK(checkInteract == 1); // 2^1
+    }
   }
 
   SECTION("Check SecondariesProcesses in SwitchProcessSequence") {
diff --git a/tests/media/CMakeLists.txt b/tests/media/CMakeLists.txt
index ffcbe11d185219c6c35e34d91e4a4dcf48b75794..c1265193c49d0f6f1e1990718f7d2615672f8bed 100644
--- a/tests/media/CMakeLists.txt
+++ b/tests/media/CMakeLists.txt
@@ -1,5 +1,6 @@
 set (test_media_sources
   TestMain.cpp
+  testNuclearComposition.cpp
   testEnvironment.cpp
   testShowerAxis.cpp
   testMedium.cpp
diff --git a/tests/media/testEnvironment.cpp b/tests/media/testEnvironment.cpp
index 43476644e38aa44b987a0386c2d123a7e630cf67..81c1ac75701baaf8c670ddb7525d3d41dfc2d1f5 100644
--- a/tests/media/testEnvironment.cpp
+++ b/tests/media/testEnvironment.cpp
@@ -82,9 +82,6 @@ TEST_CASE("HomogeneousMedium") {
   NuclearComposition const protonComposition(std::vector<Code>{Code::Proton}, {1.});
   HomogeneousMedium<IMediumModel> const medium(19.2_g / cube(1_cm), protonComposition);
 
-  CHECK(protonComposition.getFractions() == std::vector<float>{1.});
-  CHECK(protonComposition.getComponents() == std::vector<Code>{Code::Proton});
-
   CHECK_THROWS(NuclearComposition({Code::Proton}, {1.1}));
   CHECK_THROWS(NuclearComposition({Code::Proton}, {0.99}));
 }
@@ -104,7 +101,7 @@ TEST_CASE("FlatExponential") {
   LengthType const length = 2_m;
   TimeType const tEnd = length / speed;
 
-  CHECK(medium.getNuclearComposition().getFractions() == std::vector<float>{1.});
+  CHECK(medium.getNuclearComposition().getFractions() == std::vector<double>{1.});
   CHECK(medium.getNuclearComposition().getComponents() ==
         std::vector<Code>{Code::Proton});
 
@@ -514,7 +511,7 @@ TEST_CASE("InhomogeneousMedium") {
 
   LengthType const length = tEnd * speed;
 
-  NuclearComposition const composition{{Code::Proton}, {1.f}};
+  NuclearComposition const composition{{Code::Proton}, {1.}};
   InhomogeneousMedium<IMediumModel, decltype(rho)> const inhMedium(composition, rho);
 
   CORSIKA_LOG_INFO("test={} l={} {} {}", rho.getIntegrateGrammage(trajectory), length,
diff --git a/tests/media/testMagneticField.cpp b/tests/media/testMagneticField.cpp
index 6e9f347b8c7447cf227afdd191acc5362c7e1283..083ccf56881426aad23f7201cdb9a520aecb796a 100644
--- a/tests/media/testMagneticField.cpp
+++ b/tests/media/testMagneticField.cpp
@@ -34,8 +34,7 @@ TEST_CASE("UniformMagneticField w/ Homogeneous Medium") {
   using AtmModel = UniformMagneticField<HomogeneousMedium<IModelInterface>>;
 
   // the composition we use for the homogenous medium
-  NuclearComposition const protonComposition(std::vector<Code>{Code::Proton},
-                                             std::vector<float>{1.f});
+  NuclearComposition const protonComposition({Code::Proton}, {1.});
 
   // create a magnetic field vector
   Vector B0(gCS, 0_T, 0_T, 0_T);
diff --git a/tests/media/testMedium.cpp b/tests/media/testMedium.cpp
index fb55aaa59aa28d1aef411a4f31b143cdd50dd21a..2cbf6a7bb4ab55962ff8970cace93aacde7ac007 100644
--- a/tests/media/testMedium.cpp
+++ b/tests/media/testMedium.cpp
@@ -58,8 +58,7 @@ TEST_CASE("MediumPropertyModel w/ Homogeneous") {
   const auto density{19.2_g / cube(1_cm)};
 
   // the composition we use for the homogenous medium
-  NuclearComposition const protonComposition(std::vector<Code>{Code::Proton},
-                                             std::vector<float>{1.f});
+  NuclearComposition const protonComposition({Code::Proton}, {1.});
 
   // the refrative index that we use
   const Medium type = corsika::Medium::AirDry1Atm;
diff --git a/tests/media/testNuclearComposition.cpp b/tests/media/testNuclearComposition.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..79b019dc94f435c461eb051627c6665296717796
--- /dev/null
+++ b/tests/media/testNuclearComposition.cpp
@@ -0,0 +1,69 @@
+/*
+ * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * 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/framework/core/ParticleProperties.hpp>
+#include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/framework/core/Logging.hpp>
+#include <corsika/media/NuclearComposition.hpp>
+
+#include <catch2/catch.hpp>
+
+using namespace corsika;
+
+struct DummyRNG {
+  double v_;
+  DummyRNG(double v)
+      : v_(v) {}
+  int max() const { return 10; }
+  int min() const { return 0; }
+  double operator()() const { return v_; }
+};
+
+TEST_CASE("NuclearComposition") {
+
+  logging::set_level(logging::level::info);
+
+  // incompatible input: wrong vectors
+  CHECK_THROWS(
+      NuclearComposition({Code::Oxygen, Code::Carbon}, {0.20, 0.05, 1 - 0.20 - 0.05}));
+  // incompatible input: wrong fractions
+  CHECK_THROWS(
+      NuclearComposition({Code::Oxygen, Code::Carbon}, {0.21, 0.05, 1 - 0.20 - 0.05}));
+  // incompatible input: wrong fractions
+  CHECK_THROWS(
+      NuclearComposition({Code::Oxygen, Code::Carbon}, {0.19, 0.05, 1 - 0.20 - 0.05}));
+
+  NuclearComposition const testComposition({Code::Oxygen, Code::Carbon, Code::Nitrogen},
+                                           {0.20, 0.05, 1 - 0.20 - 0.05});
+
+  CHECK(testComposition.getSize() == 3);
+  CHECK(testComposition.getFractions() == std::vector<double>{0.2, 0.05, 1 - 0.2 - 0.05});
+  CHECK(testComposition.getComponents() ==
+        std::vector<Code>{Code::Oxygen, Code::Carbon, Code::Nitrogen});
+
+  CHECK(testComposition.getHash() ==
+        18183071370474897160U); // we need a stable hasing algorithm
+  CHECK(testComposition.getAverageMassNumber() == 14.3);
+
+  CHECK(testComposition.getWeighted([](Code) -> double { return 1; }) ==
+        std::vector<double>{0.2, 0.05, 1 - 0.2 - 0.05});
+
+  std::vector<CrossSectionType> const testCX =
+      testComposition.getWeighted([](Code) -> CrossSectionType { return 1_mb; });
+  std::vector<CrossSectionType> const checkCX{0.2_mb, 0.05_mb, 1_mb - 0.2_mb - 0.05_mb};
+  for (auto i1 = testCX.begin(), i2 = checkCX.begin(); i1 != testCX.end(); ++i1, ++i2) {
+    CHECK(*i1 / 1_mb == Approx(*i2 / 1_mb));
+  }
+
+  CHECK(testComposition.getWeightedSum([](Code) -> double { return 1; }) == 1);
+
+  CHECK(testComposition.getWeightedSum([](Code) -> CrossSectionType { return 1_mb; }) ==
+        1_mb);
+
+  CHECK(testComposition.sampleTarget(testCX, DummyRNG(0.1)) == Code::Oxygen);
+}
diff --git a/tests/media/testRefractiveIndex.cpp b/tests/media/testRefractiveIndex.cpp
index 62422f7a8c8d76da32f25a0972b7391ee7fb3ccf..53c9a694c7c6c3f4f2a05974597856f47bb36140 100644
--- a/tests/media/testRefractiveIndex.cpp
+++ b/tests/media/testRefractiveIndex.cpp
@@ -42,8 +42,7 @@ TEST_CASE("UniformRefractiveIndex w/ Homogeneous") {
   const auto density{19.2_g / cube(1_cm)};
 
   // the composition we use for the homogenous medium
-  NuclearComposition const protonComposition(std::vector<Code>{Code::Proton},
-                                             std::vector<float>{1.f});
+  NuclearComposition const protonComposition({Code::Proton}, {1.});
 
   // the refrative index that we use
   const double n{1.000327};
@@ -113,8 +112,7 @@ TEST_CASE("ExponentialRefractiveIndex w/ Homogeneous medium") {
   const auto density{19.2_g / cube(1_cm)};
 
   // the composition we use for the homogenous medium
-  NuclearComposition const protonComposition(std::vector<Code>{Code::Proton},
-                                             std::vector<float>{1.f});
+  NuclearComposition const protonComposition({Code::Proton}, {1.});
 
   // a new refractive index
   const double n0{2};
diff --git a/tests/media/testShowerAxis.cpp b/tests/media/testShowerAxis.cpp
index 9378d9b41fcab19f9bf3a1b1547c3f9cdb59e6b4..d8285e322749d9269e0f8ff7b37bd9c4a733bce0 100644
--- a/tests/media/testShowerAxis.cpp
+++ b/tests/media/testShowerAxis.cpp
@@ -36,8 +36,7 @@ auto setupEnvironment(Code vTargetCode) {
 
   using MyHomogeneousModel = HomogeneousMedium<IMediumModel>;
   theMedium->setModelProperties<MyHomogeneousModel>(
-      density,
-      NuclearComposition(std::vector<Code>{vTargetCode}, std::vector<float>{1.}));
+      density, NuclearComposition({vTargetCode}, {1.}));
 
   auto const* nodePtr = theMedium.get();
   universe.addChild(std::move(theMedium));
diff --git a/tests/modules/CMakeLists.txt b/tests/modules/CMakeLists.txt
index 121eb11f05324c229696fde27aef25aa064b39ac..a4e1c1b63c1b86df66e347d3ab28ca294985e97e 100644
--- a/tests/modules/CMakeLists.txt
+++ b/tests/modules/CMakeLists.txt
@@ -2,13 +2,13 @@ set (test_modules_sources
   TestMain.cpp
   testStackInspector.cpp
   testTracking.cpp
-  # testExecTime.cpp --> needs to be fixed, see #326
+  ## testExecTime.cpp --> needs to be fixed, see #326
   testObservationPlane.cpp
   testQGSJetII.cpp
   testPythia8.cpp
   testUrQMD.cpp
   testCONEX.cpp
-  # testOnShellCheck.cpp
+  ## testOnShellCheck.cpp
   testParticleCut.cpp
   testSibyll.cpp
   testEpos.cpp
diff --git a/tests/modules/testCONEX.cpp b/tests/modules/testCONEX.cpp
index 16539ba56ee241c6afed50bd3748a7ab941314c8..d04f3ac808c051defdd4de6c4ca6bafa9d59adad 100644
--- a/tests/modules/testCONEX.cpp
+++ b/tests/modules/testCONEX.cpp
@@ -100,7 +100,7 @@ TEST_CASE("CONEXSourceCut") {
 
   // need to initialize Sibyll, done in constructor:
   corsika::sibyll::Interaction sibyll;
-  [[maybe_unused]] corsika::sibyll::NuclearInteraction sibyllNuc(sibyll, env);
+  [[maybe_unused]] corsika::sibyll::NuclearInteractionModel sibyllNuc(sibyll, env);
 
   CONEXhybrid conex(center, showerAxis, t, injectionHeight, E0, get_PDG(Code::Proton));
   conex.initCascadeEquations();
diff --git a/tests/modules/testEpos.cpp b/tests/modules/testEpos.cpp
index 3eab3eaafb7ddf1cfc169798dddd9cc7120d80ec..d0f13d94fa941ae6931b9874b43d208240d9b5ad 100644
--- a/tests/modules/testEpos.cpp
+++ b/tests/modules/testEpos.cpp
@@ -29,7 +29,7 @@
 using namespace corsika;
 using namespace corsika::epos;
 
-TEST_CASE("epos", "module,process") {
+TEST_CASE("EposBasics", "module,process") {
 
   logging::set_level(logging::level::trace);
 
@@ -71,6 +71,7 @@ TEST_CASE("epos", "module,process") {
 
   SECTION("epos mass") {
     CHECK_FALSE(corsika::epos::getEposMass(Code::Electron) / 1_GeV == Approx(0));
+    CHECK_THROWS(corsika::epos::getEposMass(Code::Unknown));
   }
 
   /*
@@ -88,6 +89,7 @@ TEST_CASE("epos", "module,process") {
           CHECK(p == convert_from_PDG(getEposPDGId(p)));
       }
     }
+    CHECK_THROWS(getEposPDGId(Code::Oxygen));
   }
 }
 
@@ -118,138 +120,179 @@ auto sqs2elab(HEPEnergyType const sqs, HEPEnergyType const ma, HEPEnergyType con
   return (sqs * sqs - ma * ma - mb * mb) / 2. / mb;
 }
 
-TEST_CASE("EposInterface", "modules") {
+TEST_CASE("Epos", "modules") {
 
   logging::set_level(logging::level::trace);
 
+  RNGManager<>::getInstance().registerRandomStream("epos");
+  InteractionModel model;
+
   auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Oxygen);
   auto const& cs = *csPtr;
   [[maybe_unused]] auto const& env_dummy = env;
 
-  RNGManager<>::getInstance().registerRandomStream("epos");
-
   SECTION("InteractionInterface - random number") {
     auto const rndm = ::epos::rangen_();
     CHECK(rndm > 0);
     CHECK(rndm < 1);
   }
 
-  SECTION("InteractionInterface - valid targets") {
+  SECTION("InteractionInterface - isValid") {
 
-    Interaction model;
-    CHECK_FALSE(model.isValidTarget(Code::Electron));
-    CHECK(model.isValidTarget(Code::Hydrogen));
-    CHECK(model.isValidTarget(Code::Helium));
-    CHECK_FALSE(model.isValidTarget(Code::Iron));
-    CHECK(model.isValidTarget(Code::Oxygen));
+    CHECK_FALSE(model.isValid(Code::Proton, Code::Electron, 100_GeV));
+    CHECK(model.isValid(Code::Proton, Code::Hydrogen, 100_GeV));
+    CHECK(model.isValid(Code::Proton, Code::Helium, 100_GeV));
+    CHECK_FALSE(model.isValid(Code::Proton, Code::Iron, 100_GeV));
+    CHECK(model.isValid(Code::Proton, Code::Oxygen, 100_GeV));
+  }
+
+  SECTION("InteractionInterface - getCrossSectionInelEla") {
 
     // hydrogen target == proton target == neutron target
-    auto const [xs_prod_pp, xs_ela_pp] =
-        model.getCrossSectionLab(Code::Proton, 1, 1, Code::Proton, 1, 1, 100_GeV);
-    auto const [xs_prod_pn, xs_ela_pn] =
-        model.getCrossSectionLab(Code::Proton, 1, 1, Code::Neutron, 1, 0, 100_GeV);
-    auto const [xs_prod_pHydrogen, xs_ela_pHydrogen] =
-        model.getCrossSectionLab(Code::Proton, 1, 1, Code::Hydrogen, 1, 1, 100_GeV);
+    auto const [xs_prod_pp, xs_ela_pp] = model.getCrossSectionInelEla(
+        Code::Proton, Code::Proton,
+        {sqrt(static_pow<2>(100_GeV) + static_pow<2>(Proton::mass)),
+         {cs, 100_GeV, 0_GeV, 0_GeV}},
+        {Proton::mass, {cs, 0_GeV, 0_GeV, 0_GeV}});
+
+    auto const [xs_prod_pn, xs_ela_pn] = model.getCrossSectionInelEla(
+        Code::Proton, Code::Neutron,
+        {sqrt(static_pow<2>(100_GeV) + static_pow<2>(Proton::mass)),
+         {cs, 100_GeV, 0_GeV, 0_GeV}},
+        {Neutron::mass, {cs, 0_GeV, 0_GeV, 0_GeV}});
+
+    auto const [xs_prod_pHydrogen, xs_ela_pHydrogen] = model.getCrossSectionInelEla(
+        Code::Proton, Code::Hydrogen,
+        {sqrt(static_pow<2>(100_GeV) + static_pow<2>(Proton::mass)),
+         {cs, 100_GeV, 0_GeV, 0_GeV}},
+        {Hydrogen::mass, {cs, 0_GeV, 0_GeV, 0_GeV}});
+
     CHECK(xs_prod_pp == xs_prod_pHydrogen);
     CHECK(xs_prod_pp == xs_prod_pn);
     CHECK(xs_ela_pp == xs_ela_pHydrogen);
     CHECK(xs_ela_pn == xs_ela_pHydrogen);
-  }
 
-  SECTION("InteractionInterface - hadron cross sections") {
+    // invalid system
+    auto const [xs_prod_0, xs_ela_0] = model.getCrossSectionInelEla(
+        Code::Electron, Code::Electron,
+        {sqrt(static_pow<2>(100_GeV) + static_pow<2>(Electron::mass)),
+         {cs, 100_GeV, 0_GeV, 0_GeV}},
+        {Electron::mass, {cs, 0_GeV, 0_GeV, 0_GeV}});
+    CHECK(xs_prod_0 / 1_mb == Approx(0));
+    CHECK(xs_ela_0 / 1_mb == Approx(0));
+  }
 
-    Interaction model;
+  SECTION("InteractionModelInterface - hadron cross sections") {
 
     // p-p at 7TeV around 70mb according to LHC
-    auto const [xs_prod, xs_ela] =
-        model.getCrossSectionLab(Code::Proton, 1, 1, Code::Proton, 1, 1,
-                                 sqs2elab(7_TeV, Proton::mass, Proton::mass));
+    auto const xs_prod = model.getCrossSection(
+        Code::Proton, Code::Proton,
+        {3.5_TeV,
+         {cs, sqrt(static_pow<2>(3.5_TeV) - static_pow<2>(Proton::mass)), 0_GeV, 0_GeV}},
+        {3.5_TeV,
+         {cs, -sqrt(static_pow<2>(3.5_TeV) - static_pow<2>(Proton::mass)), 0_GeV,
+          0_GeV}});
     CHECK(xs_prod / 1_mb == Approx(70.7).margin(2.1));
-    { [[maybe_unused]] auto const& dum_xs = xs_ela; }
 
     // pi-n at 7TeV
-    auto const [xs_prod1, xs_ela1] =
-        model.getCrossSectionLab(Code::PiPlus, 0, 0, Code::Neutron, 1, 0,
-                                 sqs2elab(7_TeV, PiPlus::mass, Neutron::mass));
+    auto const xs_prod1 = model.getCrossSection(
+        Code::PiPlus, Code::Neutron,
+        {3.5_TeV,
+         {cs, sqrt(static_pow<2>(3.5_TeV) - static_pow<2>(PiPlus::mass)), 0_GeV, 0_GeV}},
+        {3.5_TeV,
+         {cs, -sqrt(static_pow<2>(3.5_TeV) - static_pow<2>(Neutron::mass)), 0_GeV,
+          0_GeV}});
     CHECK(xs_prod1 / 1_mb == Approx(52.7).margin(2.1));
-    { [[maybe_unused]] auto const& dum_xs = xs_ela1; }
 
     // k-p at 7TeV
-    auto const [xs_prod2, xs_ela2] =
-        model.getCrossSectionLab(Code::KPlus, 0, 0, Code::Proton, 1, 1,
-                                 sqs2elab(7_TeV, KPlus::mass, Proton::mass));
+    auto const xs_prod2 = model.getCrossSection(
+        Code::KPlus, Code::Proton,
+        {3.5_TeV,
+         {cs, sqrt(static_pow<2>(3.5_TeV) - static_pow<2>(KPlus::mass)), 0_GeV, 0_GeV}},
+        {3.5_TeV,
+         {cs, -sqrt(static_pow<2>(3.5_TeV) - static_pow<2>(Proton::mass)), 0_GeV,
+          0_GeV}});
     CHECK(xs_prod2 / 1_mb == Approx(45.7).margin(2.1));
-    { [[maybe_unused]] auto const& dum_xs = xs_ela2; }
   }
 
   SECTION("InteractionInterface - nuclear cross sections") {
 
-    Interaction model;
-
-    auto const [xs_prod, xs_ela] = model.getCrossSectionLab(
-        Code::Proton, 1, 1, Code::Oxygen, Oxygen::nucleus_A, Oxygen::nucleus_Z, 100_GeV);
+    auto const xs_prod = model.getCrossSection(
+        Code::Proton, Code::Oxygen,
+        {100_GeV,
+         {cs, sqrt(static_pow<2>(100_GeV) - static_pow<2>(Proton::mass)), 0_GeV, 0_GeV}},
+        {Oxygen::mass, {cs, 0_GeV, 0_GeV, 0_GeV}});
     CHECK(xs_prod / 1_mb == Approx(287.0).margin(5.1));
-    { [[maybe_unused]] auto const& dum_xs = xs_ela; }
 
-    auto const [xs_prod2, xs_ela2] = model.getCrossSectionLab(
-        Code::Nitrogen, Nitrogen::nucleus_A, Nitrogen::nucleus_Z, Code::Oxygen,
-        Oxygen::nucleus_A, Oxygen::nucleus_Z, 400_GeV);
+    auto const xs_prod2 = model.getCrossSection(
+        Code::Nitrogen, Code::Oxygen,
+        {400_GeV,
+         {cs, sqrt(static_pow<2>(400_GeV) - static_pow<2>(Nitrogen::mass)), 0_GeV,
+          0_GeV}},
+        {Oxygen::mass, {cs, 0_GeV, 0_GeV, 0_GeV}});
     CHECK(xs_prod2 / 1_mb == Approx(1076.7).margin(3.1));
-    { [[maybe_unused]] auto const& dum_xs = xs_ela2; }
-
-    // nuclear stack extension, particle "Nucleus"
-    auto const [xs_prod3, xs_ela3] = model.getCrossSectionLab(
-        Code::Nucleus, Nitrogen::nucleus_A, Nitrogen::nucleus_Z, Code::Oxygen,
-        Oxygen::nucleus_A, Oxygen::nucleus_Z, 400_GeV);
-    CHECK(xs_prod2 / xs_prod3 == 1);
-    { [[maybe_unused]] auto const& dum_xs = xs_ela3; }
-  }
-
-  SECTION("InteractionInterface - low energy") {
-
-    const HEPEnergyType P0 = 60_GeV;
-    auto [stack, viewPtr] = setup::testing::setup_stack(
-        Code::Proton, P0, (setup::Environment::BaseNodeType* const)nodePtr, cs);
-    MomentumVector plab =
-        MomentumVector(cs, {P0, 0_eV, 0_eV}); // this is secret knowledge about setupStack
-    setup::StackView& view = *viewPtr;
-
-    auto particle = stack->first();
-
-    Interaction model;
-    model.doInteraction(view);
-
-    auto const pSum = sumMomentum(view, cs);
-
-    // this is not physics validation
-    CHECK(pSum.getComponents(cs).getX() / P0 == Approx(1).margin(0.05));
-    CHECK(pSum.getComponents(cs).getY() / 1_GeV == Approx(0).margin(.5));
-    CHECK(pSum.getComponents(cs).getZ() / 1_GeV == Approx(0).margin(.5));
-
-    CHECK((pSum - plab).getNorm() / 1_GeV ==
-          Approx(0).margin(plab.getNorm() * 0.05 / 1_GeV));
-    CHECK(pSum.getNorm() / P0 == Approx(1).margin(0.05));
-
-    [[maybe_unused]] const GrammageType length = model.getInteractionLength(particle);
-    CHECK(length / 1_g * 1_cm * 1_cm == Approx(93.3).margin(0.1));
   }
 
-  SECTION("InteractionInterface - nuclear projectile") {
-
-    const HEPEnergyType P0 = 10_TeV;
+  /*
+    SECTION("InteractionInterface - invalid") {
+      Code const pid = Code::Electron;
+      HEPEnergyType const P0 = 10_TeV;
+      auto [stack, viewPtr] = setup::testing::setup_stack(
+          pid, P0, (setup::Environment::BaseNodeType* const)nodePtr, cs);
+      setup::StackView& view = *viewPtr;
+      CHECK_THROWS(model.doInteraction(
+          view, pid, Code::Oxygen,
+          {sqrt(static_pow<2>(P0) + static_pow<2>(get_mass(pid))), {cs, P0, 0_GeV,
+    0_GeV}}, {Oxygen::mass, {cs, 0_GeV, 0_GeV, 0_GeV}}));
+    }
+  */
+  /*
+    SECTION("InteractionInterface - nuclear projectile") {
+
+      HEPEnergyType const P0 = 10_TeV;
+      Code const pid = get_nucleus_code(40, 20);
+      auto [stack, viewPtr] = setup::testing::setup_stack(
+          pid, P0, (setup::Environment::BaseNodeType* const)nodePtr, cs);
+      MomentumVector plab =
+          MomentumVector(cs, {P0, 0_eV, 0_eV}); // this is secret knowledge about
+    setupStack setup::StackView& view = *viewPtr;
+
+      // @todo This is very obscure since it fails for -O2, but for both clang and gcc ???
+      model.doInteraction(view, pid, Code::Oxygen,
+                          {sqrt(static_pow<2>(P0) + static_pow<2>(get_mass(pid))), plab},
+                          {Oxygen::mass, {cs, 0_GeV, 0_GeV, 0_GeV}});
+
+      auto const pSum = sumMomentum(view, cs);
+
+      CHECK(pSum.getComponents(cs).getX() / P0 == Approx(1).margin(0.05));
+      CHECK(pSum.getComponents(cs).getY() / 1_GeV ==
+            Approx(0).margin(0.5)); // this is not physics validation
+      CHECK(pSum.getComponents(cs).getZ() / 1_GeV ==
+            Approx(0).margin(0.5)); // this is not physics validation
+
+      CHECK((pSum - plab).getNorm() / 1_GeV ==
+            Approx(0).margin(plab.getNorm() * 0.05 / 1_GeV));
+      CHECK(pSum.getNorm() / P0 == Approx(1).margin(0.05));
+      //    [[maybe_unused]] const GrammageType length =
+      //    model.getInteractionLength(particle);
+      //  CHECK(length / 1_g * 1_cm * 1_cm ==
+      //      Approx(30).margin(20)); // this is no physics validation
+    }*/
+
+  // SECTION("InteractionInterface")
+  {
+    HEPEnergyType const P0 = 10_TeV;
+    Code const pid = Code::Proton;
     auto [stack, viewPtr] = setup::testing::setup_stack(
-        get_nucleus_code(8, 4), P0, (setup::Environment::BaseNodeType* const)nodePtr, cs);
+        pid, P0, (setup::Environment::BaseNodeType* const)nodePtr, cs);
     MomentumVector plab =
-        MomentumVector(cs, {P0, 0_eV, 0_eV}); // this is secret knowledge about setupStack
+        MomentumVector(cs, {P0, 0_eV, 0_eV}); // this is secret knowledge about
     setup::StackView& view = *viewPtr;
 
-    auto particle = stack->first();
-
-    Interaction model;
-
-#ifndef __clang__
-    // This is very obscure since it fails for -O2, but for both clang and gcc ???
-    model.doInteraction(view);
+    // @todo This is very obscure since it fails for -O2, but for both clang and gcc ???
+    model.doInteraction(view, pid, Code::Oxygen,
+                        {sqrt(static_pow<2>(P0) + static_pow<2>(get_mass(pid))), plab},
+                        {Oxygen::mass, {cs, 0_GeV, 0_GeV, 0_GeV}});
 
     auto const pSum = sumMomentum(view, cs);
 
@@ -262,9 +305,9 @@ TEST_CASE("EposInterface", "modules") {
     CHECK((pSum - plab).getNorm() / 1_GeV ==
           Approx(0).margin(plab.getNorm() * 0.05 / 1_GeV));
     CHECK(pSum.getNorm() / P0 == Approx(1).margin(0.05));
-#endif
-    [[maybe_unused]] const GrammageType length = model.getInteractionLength(particle);
-    CHECK(length / 1_g * 1_cm * 1_cm ==
-          Approx(30).margin(20)); // this is no physics validation
+    //    [[maybe_unused]] const GrammageType length =
+    //    model.getInteractionLength(particle);
+    //  CHECK(length / 1_g * 1_cm * 1_cm ==
+    //      Approx(30).margin(20)); // this is no physics validation
   }
-}
+}
\ No newline at end of file
diff --git a/tests/modules/testPythia8.cpp b/tests/modules/testPythia8.cpp
index 0e449f16b6a38eaeac30b95a3475c7b302532f76..d54324be5dc00409defa1fc183c2f545f5263350 100644
--- a/tests/modules/testPythia8.cpp
+++ b/tests/modules/testPythia8.cpp
@@ -94,6 +94,8 @@ auto sumMomentum(TStackView const& view, CoordinateSystemPtr const& vCS) {
 TEST_CASE("Pythia8Interface", "modules") {
 
   logging::set_level(logging::level::info);
+
+  auto const rootCS = get_root_CoordinateSystem();
   auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Proton);
   auto const& cs = *csPtr;
   {
@@ -166,7 +168,6 @@ TEST_CASE("Pythia8Interface", "modules") {
     auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
         Code::Proton, 7_TeV, (setup::Environment::BaseNodeType* const)nodePtr, *csPtr);
     auto& view = *secViewPtr;
-    auto const particle = stackPtr->getNextParticle();
 
     corsika::pythia8::Interaction collision;
 
@@ -179,34 +180,20 @@ TEST_CASE("Pythia8Interface", "modules") {
     CHECK_FALSE(collision.canInteract(Code::Electron));
 
     // nuclei not supported
-    CHECK_THROWS(collision.getCrossSection(Code::Proton, Code::Helium, 1_TeV));
     std::tuple<CrossSectionType, CrossSectionType> xs_test =
-        collision.getCrossSection(Code::Iron, Code::Hydrogen, 1_GeV);
-    CHECK(std::get<0>(xs_test) == std::numeric_limits<double>::infinity() * 1_mb);
-    CHECK(std::get<1>(xs_test) == std::numeric_limits<double>::infinity() * 1_mb);
-
-    collision.getInteractionLength(particle);
-
-    collision.doInteraction(view);
-    [[maybe_unused]] const GrammageType length = collision.getInteractionLength(particle);
-    CHECK(length / 1_kg * square(1_m) == Approx(43.04).margin(5e-1));
-    CHECK(view.getSize() == 38);
-  }
-
-  SECTION("pythia nucleus projectile") {
-
-    // this is a projectile nucleus with very little energy
-    auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
-        Code::Oxygen, 17_GeV, (setup::Environment::BaseNodeType* const)nodePtr, *csPtr);
-    auto& view = *secViewPtr;
-    auto particle = stackPtr->first();
-
-    corsika::pythia8::Interaction collision;
-
-    GrammageType lambda_test = collision.getInteractionLength(particle);
-    CHECK(lambda_test == std::numeric_limits<double>::infinity() * 1_g / (1_cm * 1_cm));
-
-    CHECK_THROWS(collision.doInteraction(view));
+        collision.getCrossSectionInelEla(
+            Code::Proton, Code::Hydrogen,
+            {sqrt(static_pow<2>(Proton::mass) + static_pow<2>(100_GeV)),
+             {rootCS, {0_eV, 0_eV, 100_GeV}}},
+            {Hydrogen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}});
+    CHECK(std::get<0>(xs_test) / 1_mb == Approx(314).margin(2));
+    CHECK(std::get<1>(xs_test) / 1_mb == Approx(69).margin(2));
+
+    collision.doInteraction(view, Code::Proton, Code::Hydrogen,
+                            {sqrt(static_pow<2>(Proton::mass) + static_pow<2>(100_GeV)),
+                             {rootCS, {0_eV, 0_eV, 100_GeV}}},
+                            {Hydrogen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}});
+    CHECK(view.getSize() == 12);
   }
 
   SECTION("pythia too low energy") {
@@ -215,14 +202,14 @@ TEST_CASE("Pythia8Interface", "modules") {
     auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
         Code::Neutron, 1_GeV, (setup::Environment::BaseNodeType* const)nodePtr, *csPtr);
     auto& view = *secViewPtr;
-    auto particle = stackPtr->first();
 
     corsika::pythia8::Interaction collision;
 
-    GrammageType lambda_test = collision.getInteractionLength(particle);
-    CHECK(lambda_test == std::numeric_limits<double>::infinity() * 1_g / (1_cm * 1_cm));
-
-    CHECK_THROWS(collision.doInteraction(view));
+    CHECK_THROWS(collision.doInteraction(
+        view, Code::Neutron, Code::Hydrogen,
+        {sqrt(static_pow<2>(Neutron::mass) + static_pow<2>(1_GeV)),
+         {rootCS, {0_eV, 0_eV, 1_GeV}}},
+        {Hydrogen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}));
   }
 
   SECTION("pythia wrong target") {
@@ -244,6 +231,34 @@ TEST_CASE("Pythia8Interface", "modules") {
 
     corsika::pythia8::Interaction collision;
 
-    CHECK_THROWS(collision.doInteraction(view));
+    CHECK(collision.getCrossSection(
+              Code::Proton, Code::Iron,
+              {sqrt(static_pow<2>(Proton::mass) + static_pow<2>(100_GeV)),
+               {rootCS, {0_eV, 0_eV, 100_GeV}}},
+              {Iron::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}) /
+              1_mb ==
+          Approx(0));
+
+    CHECK_THROWS(collision.doInteraction(
+        view, Code::Proton, Code::Iron,
+        {sqrt(static_pow<2>(Proton::mass) + static_pow<2>(100_GeV)),
+         {rootCS, {0_eV, 0_eV, 100_GeV}}},
+        {Iron::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}));
+  }
+
+  SECTION("pythia wrong projectile") {
+
+    // resonable projectile, but tool low energy
+    auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
+        Code::Iron, 1_GeV, (setup::Environment::BaseNodeType* const)nodePtr, *csPtr);
+    { [[maybe_unused]] auto const& dummy_StackPtr = stackPtr; }
+
+    corsika::pythia8::Interaction collision;
+
+    CHECK_THROWS(
+        collision.doInteraction(*secViewPtr, Code::Iron, Code::Hydrogen,
+                                {sqrt(static_pow<2>(Iron::mass) + static_pow<2>(100_GeV)),
+                                 {rootCS, {0_eV, 0_eV, 100_GeV}}},
+                                {Hydrogen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}}));
   }
 }
diff --git a/tests/modules/testQGSJetII.cpp b/tests/modules/testQGSJetII.cpp
index 84f1b4ebe8fc7f6edcd9c2aac0c2ec1b0336f8da..966928f9a06d8dfade6ddebca63c9639a7d2a90c 100644
--- a/tests/modules/testQGSJetII.cpp
+++ b/tests/modules/testQGSJetII.cpp
@@ -6,8 +6,7 @@
  * the license.
  */
 
-#include <corsika/modules/qgsjetII/Interaction.hpp>
-#include <corsika/modules/qgsjetII/ParticleConversion.hpp>
+#include <corsika/modules/QGSJetII.hpp>
 
 #include <corsika/framework/core/ParticleProperties.hpp>
 #include <corsika/framework/core/PhysicalUnits.hpp>
@@ -49,6 +48,7 @@ auto sumMomentum(TStackView const& view, CoordinateSystemPtr const& vCS) {
 TEST_CASE("QgsjetII", "[processes]") {
 
   logging::set_level(logging::level::info);
+  RNGManager<>::getInstance().registerRandomStream("qgsjet");
 
   SECTION("Corsika -> QgsjetII") {
     CHECK(corsika::qgsjetII::convertToQgsjetII(PiMinus::code) ==
@@ -91,6 +91,15 @@ TEST_CASE("QgsjetII", "[processes]") {
     CHECK(corsika::qgsjetII::getQgsjetIIXSCode(Code::PiMinus) ==
           corsika::qgsjetII::QgsjetIIXSClass::LightMesons);
   }
+
+  SECTION("valid") {
+
+    corsika::qgsjetII::InteractionModel model;
+
+    CHECK_FALSE(model.isValid(Code::Electron, Code::Proton, 1_TeV));
+    CHECK_FALSE(model.isValid(Code::Proton, Code::Electron, 1_TeV));
+    CHECK_FALSE(model.isValid(Code::Proton, Code::Proton, 1_GeV));
+  }
 }
 
 #include <corsika/framework/geometry/Point.hpp>
@@ -115,25 +124,23 @@ TEST_CASE("QgsjetIIInterface", "interaction,processes") {
   logging::set_level(logging::level::info);
 
   auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Oxygen);
+  auto const& cs = *csPtr;
   [[maybe_unused]] auto const& env_dummy = env;
   [[maybe_unused]] auto const& node_dummy = nodePtr;
 
-  RNGManager<>::getInstance().registerRandomStream("qgsjet");
-
   SECTION("InteractionInterface") {
 
     auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
         Code::Proton, 110_GeV, (setup::Environment::BaseNodeType* const)nodePtr, *csPtr);
     setup::StackView& view = *(secViewPtr.get());
-    auto particle = stackPtr->first();
     auto projectile = secViewPtr->getProjectile();
     auto const projectileMomentum = projectile.getMomentum();
 
-    corsika::qgsjetII::Interaction model;
-    model.doInteraction(view);
-    [[maybe_unused]] const GrammageType length = model.getInteractionLength(particle);
-
-    CHECK(length / (1_g / square(1_cm)) == Approx(93.04).margin(0.1));
+    corsika::qgsjetII::InteractionModel model;
+    model.doInteraction(view, Code::Proton, Code::Oxygen,
+                        {sqrt(static_pow<2>(110_GeV) + static_pow<2>(Proton::mass)),
+                         MomentumVector{cs, 110_GeV, 0_GeV, 0_GeV}},
+                        {Oxygen::mass, MomentumVector{cs, {0_eV, 0_eV, 0_eV}}});
 
     /* **********************************
      As it turned out already two times (#291 and #307) that the detailed output of
@@ -152,24 +159,25 @@ TEST_CASE("QgsjetIIInterface", "interaction,processes") {
 
   SECTION("InteractionInterface Nuclei") {
 
+    HEPEnergyType const P0 = 20100_GeV;
+    MomentumVector const plab = MomentumVector(cs, {P0, 0_eV, 0_eV});
+    Code const pid = get_nucleus_code(60, 30);
     auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
-        get_nucleus_code(60, 30), 20100_GeV,
-        (setup::Environment::BaseNodeType* const)nodePtr, *csPtr);
+        pid, P0, (setup::Environment::BaseNodeType* const)nodePtr, *csPtr);
     setup::StackView& view = *(secViewPtr.get());
-    auto particle = stackPtr->first();
-    auto projectile = secViewPtr->getProjectile();
-    auto const projectileMomentum = projectile.getMomentum();
 
-    corsika::qgsjetII::Interaction model;
-    model.doInteraction(view); // this also should produce some fragments
+    HEPEnergyType const Elab = sqrt(static_pow<2>(P0) + static_pow<2>(get_mass(pid)));
+    FourMomentum const projectileP4(Elab, plab);
+    FourMomentum const targetP4(Oxygen::mass, MomentumVector(cs, {0_eV, 0_eV, 0_eV}));
+    view.clear();
+
+    corsika::qgsjetII::InteractionModel model;
+    model.doInteraction(view, pid, Code::Oxygen, projectileP4,
+                        targetP4); // this also should produce some fragments
     CHECK(view.getSize() == Approx(300).margin(150)); // this is not physics validation
     int countFragments = 0;
     for (auto const& sec : view) { countFragments += (is_nucleus(sec.getPID())); }
     CHECK(countFragments == Approx(4).margin(2)); // this is not physics validation
-    [[maybe_unused]] const GrammageType length = model.getInteractionLength(particle);
-
-    CHECK(length / (1_g / square(1_cm)) ==
-          Approx(12).margin(2)); // this is not physics validation
   }
 
   SECTION("Heavy nuclei") {
@@ -178,38 +186,36 @@ TEST_CASE("QgsjetIIInterface", "interaction,processes") {
         get_nucleus_code(1000, 1000), 1100_GeV,
         (setup::Environment::BaseNodeType* const)nodePtr, *csPtr);
     setup::StackView& view = *(secViewPtr.get());
-    auto particle = stackPtr->first();
     auto projectile = secViewPtr->getProjectile();
     auto const projectileMomentum = projectile.getMomentum();
 
-    corsika::qgsjetII::Interaction model;
+    corsika::qgsjetII::InteractionModel model;
 
+    FourMomentum const aP4(100_GeV, {cs, 99_GeV, 0_GeV, 0_GeV});
+    FourMomentum const bP4(1_TeV, {cs, 0.9_TeV, 0_GeV, 0_GeV});
+
+    CHECK(model.getCrossSection(get_nucleus_code(10, 5), get_nucleus_code(1000, 500), aP4,
+                                bP4) /
+              1_mb ==
+          Approx(0));
+    CHECK(model.getCrossSection(Code::Nucleus, Code::Nucleus, aP4, bP4) / 1_mb ==
+          Approx(0));
     CHECK_THROWS(
-        model.getCrossSection(Code::Nucleus, Code::Nucleus, 100_GeV, 10., 1000.));
-    CHECK_THROWS(
-        model.getCrossSection(Code::Nucleus, Code::Nucleus, 100_GeV, 1000., 10.));
-    CHECK_THROWS(model.doInteraction(view));
-    CHECK_THROWS(model.getInteractionLength(particle));
+        model.doInteraction(view, get_nucleus_code(1000, 500), Code::Oxygen, aP4, bP4));
   }
 
   SECTION("Allowed Particles") {
-    { // electron
-      auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
-          Code::Electron, 100_GeV, (setup::Environment::BaseNodeType* const)nodePtr,
-          *csPtr);
-      [[maybe_unused]] setup::StackView& view = *(secViewPtr.get());
-      auto particle = stackPtr->first();
-      corsika::qgsjetII::Interaction model;
-      GrammageType const length = model.getInteractionLength(particle);
-      CHECK(length / (1_g / square(1_cm)) == std::numeric_limits<double>::infinity());
-    }
+
     { // pi0 is internally converted into pi+/pi-
       auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
           Code::Pi0, 1000_GeV, (setup::Environment::BaseNodeType* const)nodePtr, *csPtr);
       [[maybe_unused]] setup::StackView& view = *(secViewPtr.get());
       [[maybe_unused]] auto particle = stackPtr->first();
-      corsika::qgsjetII::Interaction model;
-      model.doInteraction(view);
+      corsika::qgsjetII::InteractionModel model;
+      model.doInteraction(view, Code::Pi0, Code::Oxygen,
+                          {sqrt(static_pow<2>(1_TeV) + static_pow<2>(Pi0::mass)),
+                           MomentumVector{cs, 1_TeV, 0_GeV, 0_GeV}},
+                          {Oxygen::mass, MomentumVector{cs, 0_eV, 0_eV, 0_eV}});
       CHECK(view.getSize() == Approx(10).margin(8)); // this is not physics validation
     }
     { // rho0 is internally converted into pi-/pi+
@@ -217,8 +223,11 @@ TEST_CASE("QgsjetIIInterface", "interaction,processes") {
           Code::Rho0, 1000_GeV, (setup::Environment::BaseNodeType* const)nodePtr, *csPtr);
       [[maybe_unused]] setup::StackView& view = *(secViewPtr.get());
       [[maybe_unused]] auto particle = stackPtr->first();
-      corsika::qgsjetII::Interaction model;
-      model.doInteraction(view);
+      corsika::qgsjetII::InteractionModel model;
+      model.doInteraction(view, Code::Rho0, Code::Oxygen,
+                          {sqrt(static_pow<2>(1_TeV) + static_pow<2>(Rho0::mass)),
+                           MomentumVector{cs, 1_TeV, 0_GeV, 0_GeV}},
+                          {Oxygen::mass, MomentumVector{cs, 0_eV, 0_eV, 0_eV}});
       CHECK(view.getSize() == Approx(25).margin(20)); // this is not physics validation
     }
     { // Lambda is internally converted into neutron
@@ -227,8 +236,11 @@ TEST_CASE("QgsjetIIInterface", "interaction,processes") {
           *csPtr);
       [[maybe_unused]] setup::StackView& view = *(secViewPtr.get());
       [[maybe_unused]] auto particle = stackPtr->first();
-      corsika::qgsjetII::Interaction model;
-      model.doInteraction(view);
+      corsika::qgsjetII::InteractionModel model;
+      model.doInteraction(view, Code::Lambda0, Code::Oxygen,
+                          {sqrt(static_pow<2>(100_GeV) + static_pow<2>(Lambda0::mass)),
+                           MomentumVector{cs, 100_GeV, 0_GeV, 0_GeV}},
+                          {Oxygen::mass, MomentumVector{cs, 0_eV, 0_eV, 0_eV}});
       CHECK(view.getSize() == Approx(25).margin(20)); // this is not physics validation
     }
     { // AntiLambda is internally converted into anti neutron
@@ -237,8 +249,11 @@ TEST_CASE("QgsjetIIInterface", "interaction,processes") {
           *csPtr);
       [[maybe_unused]] setup::StackView& view = *(secViewPtr.get());
       [[maybe_unused]] auto particle = stackPtr->first();
-      corsika::qgsjetII::Interaction model;
-      model.doInteraction(view);
+      corsika::qgsjetII::InteractionModel model;
+      model.doInteraction(view, Code::Lambda0Bar, Code::Oxygen,
+                          {sqrt(static_pow<2>(1_TeV) + static_pow<2>(Lambda0Bar::mass)),
+                           MomentumVector{cs, 1_TeV, 0_GeV, 0_GeV}},
+                          {Oxygen::mass, MomentumVector{cs, 0_eV, 0_eV, 0_eV}});
       CHECK(view.getSize() == Approx(70).margin(67)); // this is not physics validation
     }
   }
diff --git a/tests/modules/testSibyll.cpp b/tests/modules/testSibyll.cpp
index 14051538285f203739b84ae4a3b7eaebad29ada1..042dc72a873d657b3e7da682fdead5d15df67e86 100644
--- a/tests/modules/testSibyll.cpp
+++ b/tests/modules/testSibyll.cpp
@@ -13,6 +13,7 @@
 #include <corsika/framework/core/PhysicalUnits.hpp>
 #include <corsika/framework/geometry/Point.hpp>
 #include <corsika/framework/random/RNGManager.hpp>
+#include <corsika/framework/utility/COMBoost.hpp>
 
 #include <catch2/catch.hpp>
 #include <tuple>
@@ -55,23 +56,23 @@ TEST_CASE("Sibyll", "modules") {
     CHECK_FALSE(corsika::sibyll::canInteract(Code::Electron));
     CHECK_FALSE(corsika::sibyll::canInteract(Code::SigmaC0));
 
-    CHECK_FALSE(corsika::sibyll::canInteract(Code::Nucleus));
+    CHECK_FALSE(corsika::sibyll::canInteract(Code::Iron));
     CHECK_FALSE(corsika::sibyll::canInteract(Code::Helium));
   }
 
   SECTION("cross-section type") {
-    CHECK(corsika::sibyll::getSibyllXSCode(Code::Helium) == 0);
     CHECK(corsika::sibyll::getSibyllXSCode(Code::Proton) == 1);
     CHECK(corsika::sibyll::getSibyllXSCode(Code::Electron) == 0);
     CHECK(corsika::sibyll::getSibyllXSCode(Code::K0Long) == 3);
     CHECK(corsika::sibyll::getSibyllXSCode(Code::SigmaPlus) == 1);
     CHECK(corsika::sibyll::getSibyllXSCode(Code::PiMinus) == 2);
+    CHECK(corsika::sibyll::getSibyllXSCode(Code::Helium) == 0);
   }
 
   SECTION("sibyll mass") {
     CHECK_FALSE(corsika::sibyll::getSibyllMass(Code::Electron) == 0_GeV);
     // Nucleus not a particle
-    CHECK_THROWS(corsika::sibyll::getSibyllMass(Code::Nucleus));
+    CHECK_THROWS(corsika::sibyll::getSibyllMass(Code::Iron));
     // Higgs not a particle in Sibyll
     CHECK_THROWS(corsika::sibyll::getSibyllMass(Code::H0));
   }
@@ -82,7 +83,6 @@ TEST_CASE("Sibyll", "modules") {
 #include <corsika/framework/geometry/Vector.hpp>
 
 #include <corsika/framework/core/PhysicalUnits.hpp>
-
 #include <corsika/framework/core/ParticleProperties.hpp>
 
 #include <SetupTestEnvironment.hpp>
@@ -100,68 +100,76 @@ auto sumMomentum(TStackView const& view, CoordinateSystemPtr const& vCS) {
   return sum;
 }
 
-TEST_CASE("SibyllInteractionInterface", "modules") {
+TEST_CASE("SibyllInterface", "modules") {
 
-  logging::set_level(logging::level::info);
+  logging::set_level(logging::level::trace);
 
+  // the environment and stack should eventually disappear from here
   auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Oxygen);
   auto const& cs = *csPtr;
   { [[maybe_unused]] auto const& env_dummy = env; }
 
+  auto [stack, viewPtr] = setup::testing::setup_stack(
+      Code::Proton, 10_GeV, (setup::Environment::BaseNodeType* const)nodePtr, cs);
+  setup::StackView& view = *viewPtr;
+
   RNGManager<>::getInstance().registerRandomStream("sibyll");
 
   SECTION("InteractionInterface - valid targets") {
 
-    Interaction model;
+    corsika::sibyll::InteractionModel model;
     // sibyll only accepts protons or nuclei with 4<=A<=18 as targets
-    CHECK_FALSE(model.isValidTarget(Code::Electron));
-    CHECK(model.isValidTarget(Code::Hydrogen));
-    CHECK_FALSE(model.isValidTarget(Code::Deuterium));
-    CHECK(model.isValidTarget(Code::Helium));
-    CHECK_FALSE(model.isValidTarget(Code::Helium3));
-    CHECK_FALSE(model.isValidTarget(Code::Iron));
-    CHECK(model.isValidTarget(Code::Oxygen));
+    CHECK_FALSE(model.isValid(Code::Proton, Code::Electron, 100_GeV));
+    CHECK(model.isValid(Code::Proton, Code::Hydrogen, 100_GeV));
+    CHECK_FALSE(model.isValid(Code::Proton, Code::Deuterium, 100_GeV));
+    CHECK(model.isValid(Code::Proton, Code::Helium, 100_GeV));
+    CHECK_FALSE(model.isValid(Code::Proton, Code::Helium3, 100_GeV));
+    CHECK_FALSE(model.isValid(Code::Proton, Code::Iron, 100_GeV));
+    CHECK(model.isValid(Code::Proton, Code::Oxygen, 100_GeV));
+    // beam particles
+    CHECK_FALSE(model.isValid(Code::Electron, Code::Oxygen, 100_GeV));
+    CHECK_FALSE(model.isValid(Code::Iron, Code::Oxygen, 100_GeV));
+    // energy too low
+    CHECK_FALSE(model.isValid(Code::Proton, Code::Proton, 9_GeV));
+    CHECK(model.isValid(Code::Proton, Code::Proton, 11_GeV));
+    // energy too high
+    CHECK_FALSE(model.isValid(Code::Proton, Code::Proton, 1000001_GeV));
+    CHECK(model.isValid(Code::Proton, Code::Proton, 999999_GeV));
 
     //  hydrogen target == proton target == neutron target
+    FourMomentum const aP4(100_GeV, {cs, 99_GeV, 0_GeV, 0_GeV});
+    FourMomentum const bP4(1_GeV, {cs, 0_GeV, 0_GeV, 0_GeV});
     auto const [xs_prod_pp, xs_ela_pp] =
-        model.getCrossSection(Code::Proton, Code::Proton, 100_GeV);
+        model.getCrossSectionInelEla(Code::Proton, Code::Proton, aP4, bP4);
     auto const [xs_prod_pn, xs_ela_pn] =
-        model.getCrossSection(Code::Proton, Code::Neutron, 100_GeV);
+        model.getCrossSectionInelEla(Code::Proton, Code::Neutron, aP4, bP4);
     auto const [xs_prod_pHydrogen, xs_ela_pHydrogen] =
-        model.getCrossSection(Code::Proton, Code::Hydrogen, 100_GeV);
+        model.getCrossSectionInelEla(Code::Proton, Code::Hydrogen, aP4, bP4);
     CHECK(xs_prod_pp == xs_prod_pHydrogen);
     CHECK(xs_prod_pp == xs_prod_pn);
     CHECK(xs_ela_pp == xs_ela_pHydrogen);
     CHECK(xs_ela_pn == xs_ela_pHydrogen);
 
-    CHECK_THROWS(convertFromSibyll(corsika::sibyll::SibyllCode::Unknown));
+    // invalids
+    auto const xs_prod_0 = model.getCrossSection(Code::Electron, Code::Proton, aP4, bP4);
+    CHECK(xs_prod_0 / 1_mb == Approx(0));
+    CHECK_THROWS(model.doInteraction(view, Code::Electron, Code::Proton, aP4, bP4));
 
-    // out of range
-    // beam particle
-    CHECK_THROWS(
-        std::get<0>(model.getCrossSection(Code::Electron, Code::Hydrogen, 100_GeV)));
-    // target particle
-    CHECK(std::get<0>(model.getCrossSection(Code::Proton, Code::Electron, 100_GeV)) ==
-          std::numeric_limits<double>::infinity() * 1_mb);
-    // energy out of range
-    CHECK_THROWS(std::get<0>(model.getCrossSection(Code::Proton, Code::Hydrogen, 5_GeV)));
+    CHECK_THROWS(convertFromSibyll(corsika::sibyll::SibyllCode::Unknown));
   }
 
   SECTION("InteractionInterface - low energy") {
 
     const HEPEnergyType P0 = 60_GeV;
-    auto [stack, viewPtr] = setup::testing::setup_stack(
-        Code::Proton, P0, (setup::Environment::BaseNodeType* const)nodePtr, cs);
-    MomentumVector plab =
-        MomentumVector(cs, {P0, 0_eV, 0_eV}); // this is secret knowledge about setupStack
-    setup::StackView& view = *viewPtr;
-
-    auto particle = stack->first();
-
+    MomentumVector const plab = MomentumVector(cs, {P0, 0_eV, 0_eV});
     // also print particles after sibyll was called
-    Interaction model(true);
-
-    model.doInteraction(view);
+    corsika::sibyll::InteractionModel model;
+    model.setVerbose(true);
+    HEPEnergyType const Elab = sqrt(static_pow<2>(P0) + static_pow<2>(Proton::mass));
+    FourMomentum const projectileP4(Elab, plab);
+    FourMomentum const nucleusP4(Oxygen::mass, MomentumVector(cs, {0_eV, 0_eV, 0_eV}));
+    view.clear();
+    model.doInteraction(view, Code::Proton, Code::Oxygen, projectileP4, nucleusP4);
     auto const pSum = sumMomentum(view, cs);
 
     /*
@@ -227,82 +235,44 @@ TEST_CASE("SibyllInteractionInterface", "modules") {
     CHECK((pSum - plab).getNorm() / 1_GeV ==
           Approx(0).margin(plab.getNorm() * 0.05 / 1_GeV));
     CHECK(pSum.getNorm() / P0 == Approx(1).margin(0.05));
-    [[maybe_unused]] GrammageType const length = model.getInteractionLength(particle);
-    CHECK(length / 1_g * 1_cm * 1_cm == Approx(88.7).margin(0.1));
+    [[maybe_unused]] CrossSectionType const cx =
+        model.getCrossSection(Code::Proton, Code::Oxygen, projectileP4, nucleusP4);
+    CHECK(cx / 1_mb == Approx(300).margin(1));
     // CHECK(view.getEntries() == 9); //! \todo: this was 20 before refactory-2020: check
     //                                           "also sibyll not stable wrt. to compiler
     //                                           changes"
   }
 
-  SECTION("InteractionInterface - energy too low") {
-
-    const HEPEnergyType P0 = 5_GeV;
-    auto [stack, viewPtr] = setup::testing::setup_stack(
-        Code::Proton, P0, (setup::Environment::BaseNodeType* const)nodePtr, cs);
-    MomentumVector plab =
-        MomentumVector(cs, {P0, 0_eV, 0_eV}); // this is secret knowledge about setupStack
-    setup::StackView& view = *viewPtr;
-
-    auto particle = stack->first();
-
-    Interaction model;
-    CHECK_THROWS(model.doInteraction(view));
-
-    [[maybe_unused]] GrammageType const length = model.getInteractionLength(particle);
-    CHECK(model.getInteractionLength(particle) / 1_g * 1_cm * 1_cm ==
-          std::numeric_limits<double>::infinity());
-  }
-
-  SECTION("InteractionInterface - energy too high") {
-
-    const HEPEnergyType P0 = 1000_EeV;
-    auto [stack, viewPtr] = setup::testing::setup_stack(
-        Code::Proton, P0, (setup::Environment::BaseNodeType* const)nodePtr, cs);
-    { [[maybe_unused]] auto const& dummy1 = stack; }
-    MomentumVector plab =
-        MomentumVector(cs, {P0, 0_eV, 0_eV}); // this is secret knowledge about setupStack
-    setup::StackView& view = *viewPtr;
-
-    Interaction model;
-    CHECK_THROWS(model.doInteraction(view));
-  }
-
-  SECTION("InteractionInterface - target nucleus out of range") {
-    auto [env1, csPtr1, nodePtr1] = setup::testing::setup_environment(Code::Argon);
-    { [[maybe_unused]] auto const& dummy1 = env1; }
-    auto const& cs1 = *csPtr1;
-    const HEPEnergyType P0 = 150_GeV;
-    auto [stack, viewPtr] = setup::testing::setup_stack(
-        Code::Electron, P0, (setup::Environment::BaseNodeType* const)nodePtr1, cs1);
-    { [[maybe_unused]] auto const& dummy1 = stack; }
-    MomentumVector plab = MomentumVector(
-        cs1, {P0, 0_eV, 0_eV}); // this is secret knowledge about setupStack
-    setup::StackView& view = *viewPtr;
-
-    Interaction model;
-    CHECK_THROWS(model.doInteraction(view));
-  }
-
   SECTION("NuclearInteractionInterface") {
 
-    auto [stack, viewPtr] =
-        setup::testing::setup_stack(get_nucleus_code(8, 4), 900_GeV,
-                                    (setup::Environment::BaseNodeType* const)nodePtr, cs);
-    setup::StackView& view = *viewPtr;
-    auto particle = stack->first();
-
-    Interaction hmodel;
-    NuclearInteraction model(hmodel, *env);
-
-    model.doInteraction(view);
-    [[maybe_unused]] const GrammageType length = model.getInteractionLength(particle);
-    // Felix, are those changes OK? Below are the checks before refactory-2020
-    // CHECK(length / 1_g * 1_cm * 1_cm == Approx(44.2).margin(.1));
-    // CHECK(view.getSize() == 11);
-    CHECK(length / 1_g * 1_cm * 1_cm ==
-          Approx(31).margin(5)); // this is not physics validation
-    // CHECK(view.getSize() == 20); // also sibyll not stable wrt. to compiler changes
-    CHECK(view.getSize() == Approx(100).margin(90)); // this is not physics validation
+    HEPMomentumType const P0 = 50_TeV;
+    MomentumVector const plab = MomentumVector(cs, {P0, 0_eV, 0_eV});
+    corsika::sibyll::InteractionModel hmodel;
+    NuclearInteractionModel model(hmodel, *env);
+
+    CHECK(model.isValid(Code::Helium, Code::Oxygen, 100_GeV));
+    CHECK_FALSE(model.isValid(Code::PiPlus, Code::Oxygen, 100_GeV));
+    CHECK_FALSE(model.isValid(Code::Electron, Code::Oxygen, 100_GeV));
+
+    Code const pid = Code::Oxygen;
+    HEPEnergyType const Elab = sqrt(static_pow<2>(P0) + static_pow<2>(get_mass(pid)));
+    FourMomentum const P4(Elab, plab);
+    FourMomentum const targetP4(get_mass(Code::Oxygen),
+                                MomentumVector(cs, {0_eV, 0_eV, 0_eV}));
+    model.doInteraction(view, pid, Code::Oxygen, P4, targetP4);
+    CrossSectionType const cx = model.getCrossSection(pid, Code::Oxygen, P4, targetP4);
+    CHECK(cx / 1_mb == Approx(1250).margin(100));     // this is not physics validation
+    CHECK(view.getSize() == Approx(150).margin(140)); // this is not physics validation
+
+    // invalid to underlying model
+    FourMomentum P4mu(
+        100_GeV,
+        {cs, {sqrt(static_pow<2>(100_GeV) - static_pow<2>(MuPlus::mass)), 0_eV, 0_eV}});
+    CrossSectionType const cx0 =
+        model.getCrossSection(Code::MuPlus, Code::Oxygen, P4mu, targetP4);
+    CHECK(cx0 / 1_mb == Approx(0));
+
+    CHECK_THROWS(model.doInteraction(view, Code::MuPlus, Code::Oxygen, P4mu, targetP4));
   }
 }
 
@@ -341,7 +311,7 @@ TEST_CASE("SibyllDecayInterface", "modules") {
 
     Decay model;
     model.printDecayConfig();
-    [[maybe_unused]] const TimeType time = model.getLifetime(particle);
+    [[maybe_unused]] TimeType const time = model.getLifetime(particle);
     auto const gamma = particle.getEnergy() / particle.getMass();
     CHECK(time == get_lifetime(Code::Lambda0) * gamma);
     model.doDecay(view);
@@ -372,7 +342,7 @@ TEST_CASE("SibyllDecayInterface", "modules") {
     CHECK(model.isDecayHandled(Code::PiMinus));
     CHECK_FALSE(model.isDecayHandled(Code::KPlus));
 
-    const std::vector<Code> particleTestList = {Code::PiPlus, Code::PiMinus, Code::KPlus,
+    std::vector<Code> const particleTestList = {Code::PiPlus, Code::PiMinus, Code::KPlus,
                                                 Code::Lambda0Bar, Code::D0Bar};
 
     // setup decays
diff --git a/tests/modules/testTracking.cpp b/tests/modules/testTracking.cpp
index d1d967d6f38e58a9b5cff769589939bf0042ed48..1710844b19c6423842bc7b2a9a95bed9b084ea6b 100644
--- a/tests/modules/testTracking.cpp
+++ b/tests/modules/testTracking.cpp
@@ -75,7 +75,7 @@ TEMPLATE_TEST_CASE("Tracking", "tracking", tracking_leapfrog_curved::Tracking,
   // for algorithms that know magnetic deflections choose: +-50uT, 0uT
   // otherwise just 0uT
   auto Bfield = GENERATE_COPY(filter(
-      [isParallel]([[maybe_unused]] MagneticFluxType v) {
+      []([[maybe_unused]] MagneticFluxType v) {
         if constexpr (std::is_same_v<TestType, tracking_line::Tracking>)
           return v == 0_uT;
         else
@@ -141,19 +141,19 @@ TEMPLATE_TEST_CASE("Tracking", "tracking", tracking_leapfrog_curved::Tracking,
     MagneticFieldVector magneticfield(cs, 0_T, 0_T, Bfield);
     target->setModelProperties<MyHomogeneousModel>(
         Medium::AirDry1Atm, magneticfield, 1_g / (1_m * 1_m * 1_m),
-        NuclearComposition(std::vector<Code>{Code::Oxygen}, std::vector<float>{1.}));
+        NuclearComposition(std::vector<Code>{Code::Oxygen}, std::vector<double>{1.}));
     target_neutral->setModelProperties<MyHomogeneousModel>(
         Medium::AirDry1Atm, magneticfield, 1_g / (1_m * 1_m * 1_m),
-        NuclearComposition(std::vector<Code>{Code::Oxygen}, std::vector<float>{1.}));
+        NuclearComposition(std::vector<Code>{Code::Oxygen}, std::vector<double>{1.}));
     target_2->setModelProperties<MyHomogeneousModel>(
         Medium::AirDry1Atm, magneticfield, 1_g / (1_m * 1_m * 1_m),
-        NuclearComposition(std::vector<Code>{Code::Oxygen}, std::vector<float>{1.}));
+        NuclearComposition(std::vector<Code>{Code::Oxygen}, std::vector<double>{1.}));
     target_2_behind->setModelProperties<MyHomogeneousModel>(
         Medium::AirDry1Atm, magneticfield, 1_g / (1_m * 1_m * 1_m),
-        NuclearComposition(std::vector<Code>{Code::Oxygen}, std::vector<float>{1.}));
+        NuclearComposition(std::vector<Code>{Code::Oxygen}, std::vector<double>{1.}));
     target_2_partly_behind->setModelProperties<MyHomogeneousModel>(
         Medium::AirDry1Atm, magneticfield, 1_g / (1_m * 1_m * 1_m),
-        NuclearComposition(std::vector<Code>{Code::Oxygen}, std::vector<float>{1.}));
+        NuclearComposition(std::vector<Code>{Code::Oxygen}, std::vector<double>{1.}));
     auto* targetPtr = target.get();
     auto* targetPtr_2 = target_2.get();
     auto* targetPtr_neutral = target_neutral.get();
@@ -287,7 +287,7 @@ TEST_CASE("TrackingLeapFrogCurved") {
     MagneticFieldVector magneticfield(cs, 100_T, 0_T, 0_uT);
     target->setModelProperties<MyHomogeneousModel>(
         Medium::AirDry1Atm, magneticfield, 1_g / (1_m * 1_m * 1_m),
-        NuclearComposition(std::vector<Code>{Code::Oxygen}, std::vector<float>{1.}));
+        NuclearComposition(std::vector<Code>{Code::Oxygen}, std::vector<double>{1.}));
     auto* targetPtr = target.get();
     worldPtr->addChild(std::move(target));
 
diff --git a/tests/modules/testUrQMD.cpp b/tests/modules/testUrQMD.cpp
index 89bde00d2e1772b943f933311ce3f258e438502b..f75d59b75b44a41d25376d013c9d9e0d813efa32 100644
--- a/tests/modules/testUrQMD.cpp
+++ b/tests/modules/testUrQMD.cpp
@@ -11,8 +11,8 @@
 #include <corsika/framework/core/ParticleProperties.hpp>
 #include <corsika/framework/core/PhysicalConstants.hpp>
 #include <corsika/framework/core/PhysicalUnits.hpp>
-#include <corsika/framework/geometry/Point.hpp>
 #include <corsika/framework/geometry/RootCoordinateSystem.hpp>
+#include <corsika/framework/geometry/Point.hpp>
 #include <corsika/framework/geometry/Vector.hpp>
 #include <corsika/framework/random/RNGManager.hpp>
 #include <corsika/framework/utility/CorsikaFenv.hpp>
@@ -71,73 +71,50 @@ TEST_CASE("UrQMD") {
   RNGManager<>::getInstance().registerRandomStream("urqmd");
   UrQMD urqmd;
 
-  SECTION("interaction length") {
-    auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Nitrogen);
-    auto const& cs = *csPtr;
-    { [[maybe_unused]] auto const& env_dummy = env; }
-
-    Code validProjectileCodes[] = {Code::PiPlus,     Code::PiMinus,     Code::Proton,
-                                   Code::AntiProton, Code::AntiNeutron, Code::Neutron,
-                                   Code::KPlus,      Code::KMinus,      Code::K0,
-                                   Code::K0Bar,      Code::K0Long};
-
-    for (auto code : validProjectileCodes) {
-      auto [stack, view] = setup::testing::setup_stack(code, 100_GeV, nodePtr, cs);
-      CHECK(stack->getEntries() == 1);
-      CHECK(view->getEntries() == 0);
-
-      // simple check whether the cross-section is non-vanishing
-      // only nuclei with available tabluated data so far
-      CHECK(urqmd.getInteractionLength(stack->getNextParticle()) > 1_g / square(1_cm));
-    }
+  auto const rootCS = get_root_CoordinateSystem();
+
+  SECTION("valid") {
+    // this is how it is currently done
+    CHECK_FALSE(urqmd.isValid(Code::K0, Code::Proton));
+    CHECK_FALSE(urqmd.isValid(Code::DPlus, Code::Proton));
+    CHECK_FALSE(urqmd.isValid(Code::Electron, Code::Proton));
+    CHECK_FALSE(urqmd.isValid(Code::Proton, Code::Electron));
+    CHECK_FALSE(urqmd.isValid(Code::Oxygen, Code::Oxygen));
+    CHECK_FALSE(urqmd.isValid(Code::PiPlus, Code::Omega));
+    CHECK_FALSE(
+        urqmd.isValid(Code::PiPlus, Code::Proton)); // Proton is not a valid target....
+
+    CHECK_NOTHROW(urqmd.isValid(Code::Proton, Code::Oxygen));
+    CHECK_NOTHROW(urqmd.isValid(Code::PiPlus, Code::Argon));
   }
 
-  SECTION("targets options") {
-    auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Argon);
-    auto const& cs = *csPtr;
-    { [[maybe_unused]] auto const& env_dummy = env; }
-    auto [stack, view] = setup::testing::setup_stack(Code::Proton, 100_GeV, nodePtr, cs);
-    [[maybe_unused]] setup::StackView& viewRef = *(view.get());
-    CHECK(urqmd.getInteractionLength(stack->getNextParticle()) / 1_g * square(1_cm) ==
-          Approx(105).margin(5));
-  }
+  SECTION("cross sections") {
 
-  SECTION("invalid targets options") {
-    auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Omega);
-    auto const& cs = *csPtr;
-    { [[maybe_unused]] auto const& env_dummy = env; }
-    auto [stack, view] = setup::testing::setup_stack(Code::Neutron, 100_GeV, nodePtr, cs);
-    [[maybe_unused]] setup::StackView& viewRef = *(view.get());
-    CHECK_THROWS(urqmd.getInteractionLength(stack->getNextParticle()));
-  }
+    FourMomentum const targetP4{Nitrogen::mass, {rootCS, {0_eV, 0_eV, 0_eV}}};
 
-  SECTION("nucleus projectile") {
-    auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Oxygen);
-    [[maybe_unused]] auto const& env_dummy = env;      // against warnings
-    [[maybe_unused]] auto const& node_dummy = nodePtr; // against warnings
+    HEPMomentumType const P0 = 100_GeV;
+    Code const validProjectileCodes[] = {
+        Code::PiPlus,  Code::PiMinus, Code::Proton, Code::AntiProton, Code::AntiNeutron,
+        Code::Neutron, Code::KPlus,   Code::KMinus, Code::K0Long};
+    // Code::K0, Code::K0Bar  are not valid projectiles (no mass eigenstates)
+    CrossSectionType const checkCX[] = {219_mb, 222_mb, 303_mb, 324_mb, 324_mb,
+                                        303_mb, 189_mb, 198_mb, 172_mb};
 
-    unsigned short constexpr A = 14, Z = 7;
-    auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
-        get_nucleus_code(A, Z), 40_GeV, (setup::Environment::BaseNodeType* const)nodePtr,
-        *csPtr);
-    [[maybe_unused]] setup::StackView& viewRef = *(secViewPtr.get());
-    CHECK(stackPtr->getEntries() == 1);
-    CHECK(secViewPtr->getEntries() == 0);
-
-    // must be assigned to variable, cannot be used as rvalue?!
-    auto projectile = secViewPtr->getProjectile();
-    auto const projectileMomentum = projectile.getMomentum();
-    urqmd.doInteraction(*secViewPtr);
-
-    CHECK(sumCharge(*secViewPtr) == Z + get_charge_number(Code::Oxygen));
+    int i = 0;
+    for (auto code : validProjectileCodes) {
+      FourMomentum const projectileP4{
+          sqrt(static_pow<2>(get_mass(code)) + static_pow<2>(P0)),
+          {rootCS, {0_GeV, 0_GeV, P0}}};
+      auto const cx = urqmd.getCrossSection(code, Code::Nitrogen, projectileP4, targetP4);
+      CORSIKA_LOG_INFO("UrQMD cross seciton for {} is {} mb", code, cx / 1_mb);
+      CHECK(cx / 1_mb == Approx(checkCX[i++] / 1_mb).margin(1));
+    }
 
-    auto const secMomSum =
-        sumMomentum(*secViewPtr, projectileMomentum.getCoordinateSystem());
-    CHECK((secMomSum - projectileMomentum).getNorm() / projectileMomentum.getNorm() ==
-          Approx(0).margin(1e-2));
+    // invalid
+    CHECK_THROWS(urqmd.getTabulatedCrossSection(Code::Proton, Code::Proton, 100_GeV));
   }
 
-  SECTION("\"special\" projectile") {
+  SECTION("pion+ projectile") {
     auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Oxygen);
     [[maybe_unused]] auto const& env_dummy = env;      // against warnings
     [[maybe_unused]] auto const& node_dummy = nodePtr; // against warnings
@@ -151,7 +128,11 @@ TEST_CASE("UrQMD") {
     auto projectile = secViewPtr->getProjectile();
     auto const projectileMomentum = projectile.getMomentum();
 
-    urqmd.doInteraction(*secViewPtr);
+    FourMomentum const projectileP4{
+        sqrt(static_pow<2>(PiPlus::mass) + static_pow<2>(40_GeV)),
+        {rootCS, {40_GeV, 0_GeV, 0_GeV}}};
+    FourMomentum const targetP4{Oxygen::mass, {rootCS, {0_GeV, 0_GeV, 0_GeV}}};
+    urqmd.doInteraction(*secViewPtr, Code::PiPlus, Code::Oxygen, projectileP4, targetP4);
 
     CHECK(sumCharge(*secViewPtr) ==
           get_charge_number(Code::PiPlus) + get_charge_number(Code::Oxygen));
@@ -162,44 +143,6 @@ TEST_CASE("UrQMD") {
           Approx(0).margin(1e-2));
   }
 
-  SECTION("\"special\" projectile and target") {
-    {
-      auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Proton);
-      [[maybe_unused]] auto const& env_dummy = env;      // against warnings
-      [[maybe_unused]] auto const& node_dummy = nodePtr; // against warnings
-
-      auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
-          Code::PiPlus, 40_GeV, (setup::Environment::BaseNodeType* const)nodePtr, *csPtr);
-      [[maybe_unused]] auto particle = stackPtr->first();
-      CHECK_THROWS(urqmd.doInteraction(*secViewPtr)); // Code::Proton not a valid target
-    }
-
-    {
-      auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Oxygen);
-      [[maybe_unused]] auto const& env_dummy = env;      // against warnings
-      [[maybe_unused]] auto const& node_dummy = nodePtr; // against warnings
-
-      auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
-          Code::PiPlus, 40_GeV, (setup::Environment::BaseNodeType* const)nodePtr, *csPtr);
-      CHECK(stackPtr->getEntries() == 1);
-      CHECK(secViewPtr->getEntries() == 0);
-
-      // must be assigned to variable, cannot be used as rvalue?!
-      auto projectile = secViewPtr->getProjectile();
-      auto const projectileMomentum = projectile.getMomentum();
-
-      urqmd.doInteraction(*secViewPtr);
-
-      CHECK(sumCharge(*secViewPtr) ==
-            get_charge_number(Code::PiPlus) + get_charge_number(Code::Oxygen));
-
-      auto const secMomSum =
-          sumMomentum(*secViewPtr, projectileMomentum.getCoordinateSystem());
-      CHECK((secMomSum - projectileMomentum).getNorm() / projectileMomentum.getNorm() ==
-            Approx(0).margin(1e-2));
-    }
-  }
-
   SECTION("K0Long projectile") {
     auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Oxygen);
     [[maybe_unused]] auto const& env_dummy = env;      // against warnings
@@ -214,7 +157,11 @@ TEST_CASE("UrQMD") {
     auto projectile = secViewPtr->getProjectile();
     auto const projectileMomentum = projectile.getMomentum();
 
-    urqmd.doInteraction(*secViewPtr);
+    FourMomentum const projectileP4{
+        sqrt(static_pow<2>(K0Long::mass) + static_pow<2>(40_GeV)),
+        {rootCS, {40_GeV, 0_GeV, 0_GeV}}};
+    FourMomentum const targetP4{Oxygen::mass, {rootCS, {0_GeV, 0_GeV, 0_GeV}}};
+    urqmd.doInteraction(*secViewPtr, Code::K0Long, Code::Oxygen, projectileP4, targetP4);
 
     CHECK(sumCharge(*secViewPtr) ==
           get_charge_number(Code::K0Long) + get_charge_number(Code::Oxygen));