diff --git a/CMakeLists.txt b/CMakeLists.txt
index 344257e5b308456242d15444b4853dc97b3fc96e..5432ca03c7c6ee14036ac64ffc3d09d57cad51a8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -258,6 +258,7 @@ add_subdirectory (modules/qgsjetII)
 add_subdirectory (modules/urqmd)
 add_subdirectory (modules/conex)
 add_subdirectory (modules/epos)
+add_subdirectory (modules/fluka)
 
 #+++++++++++++++++++++++++++++++
 # unit testing
diff --git a/README.md b/README.md
index 13e840d6a54083e55d3268612164d24b84aa427f..a41b18bf83481f705c1b863723e96f4dc683a05a 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ You will also need:
 - cmake 
 - git
 - g++, gfortran, binutils, make
+- optional: FLUKA (see below)
 
 On a bare Ubuntu 20.04, just add:
 ``` shell
@@ -98,6 +99,17 @@ make -j8
 make install
 ```
 
+### FLUKA support
+For legal reasons we do not distribute/bundle FLUKA together with CORSIKA 8.
+If you want to use FLUKA as low-energy hadronic interaction model, you have to download
+it separately from (http://www.fluka.org/), which requires registering there as FLUKA user.
+You need to download binaries suitable for your system (check compiler and glibc versions)
+and unpack the binaries into a directory of your choice. You also need the data archive
+(fluka20xy.z-data.tar.gz) unpacked in the same directory. To compile CORSIKA 8 with FLUKA,
+you need to specify the path of libflukahp.a when invoking cmake with the option
+`-DC8_FLUKALIB=<path>`. CMake will print a status message indicating whether FLUKA support
+is enabled or disabled when the library is (not) found.
+
 
 ## Installation (using docker containers)
 
diff --git a/corsika/detail/framework/core/ParticleProperties.inl b/corsika/detail/framework/core/ParticleProperties.inl
index 06bca17cf981b5f8b96300660c3aad713c7b0dc1..b5b4410aa60c1a0c5e8aed7ad4b4b0a8f159ef11 100644
--- a/corsika/detail/framework/core/ParticleProperties.inl
+++ b/corsika/detail/framework/core/ParticleProperties.inl
@@ -132,6 +132,11 @@ namespace corsika {
     if (std::abs(k) <= maxPDG) {
       return particle::detail::conversionArray[k + maxPDG];
     } else {
+      if (1000000000 <= k && k <= 1009999990) { // nucleus (no L or I)
+        int const Z = (k - 1000000000) / 10000;
+        int const A = (k - 1000000000 - 10000 * Z) / 10;
+        return get_nucleus_code(A, Z);
+      }
       return particle::detail::conversionMap.at(p);
     }
   }
diff --git a/corsika/detail/framework/geometry/CoordinateSystem.inl b/corsika/detail/framework/geometry/CoordinateSystem.inl
index 12e914ba9d4131209d4be8fa7eec066e9961a97a..f8297585599d2b448a2daa2e00fa3e3f4132409b 100644
--- a/corsika/detail/framework/geometry/CoordinateSystem.inl
+++ b/corsika/detail/framework/geometry/CoordinateSystem.inl
@@ -81,7 +81,13 @@ namespace corsika {
   template <typename TDim>
   inline CoordinateSystemPtr make_rotationToZ(CoordinateSystemPtr const& cs,
                                               Vector<TDim> const& vVec) {
-    auto const a = vVec.normalized().getComponents(cs).getEigenVector();
+    auto const vVecComp = vVec.getComponents(cs);
+    if (vVecComp.getX().magnitude() == 0 && vVecComp.getY().magnitude() == 0 &&
+        vVecComp.getZ().magnitude() == 0) {
+      return cs;
+    }
+
+    auto const a = vVecComp.normalized().getEigenVector();
     auto const a1 = a(0), a2 = a(1), a3 = a(2);
 
     Eigen::Matrix3d A, B;
diff --git a/corsika/detail/modules/fluka/InteractionModel.inl b/corsika/detail/modules/fluka/InteractionModel.inl
new file mode 100644
index 0000000000000000000000000000000000000000..eb3f2473bac1f244953312629fa7bcd0bb0726a7
--- /dev/null
+++ b/corsika/detail/modules/fluka/InteractionModel.inl
@@ -0,0 +1,238 @@
+/*
+ * (c) Copyright 2022 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 <algorithm>
+#include <cstdlib>
+#include <vector>
+#include <iterator>
+#include <set>
+#include <utility>
+
+#include <boost/iterator/zip_iterator.hpp>
+#include <Eigen/Dense>
+
+#include <corsika/media/Environment.hpp>
+#include <corsika/media/NuclearComposition.hpp>
+#include <corsika/framework/geometry/FourVector.hpp>
+#include <corsika/framework/core/ParticleProperties.hpp>
+#include <corsika/framework/core/EnergyMomentumOperations.hpp>
+
+#include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/modules/fluka/ParticleConversion.hpp>
+
+#include <FLUKA.hpp>
+
+namespace corsika::fluka {
+  template <typename TEnvironment>
+  inline InteractionModel::InteractionModel(TEnvironment const& env)
+      : materials_{genFlukaMaterials(env)}
+      , cumsgx_{std::make_unique<double[]>(materials_.size() * 3)} {
+    for (auto const& [code, matno] : materials_) {
+      CORSIKA_LOGGER_DEBUG(logger_, "FLUKA material initialization: {} -> {}",
+                           get_name(code, full_name{}), matno);
+    }
+
+    if (int const ndmhep = ::fluka::ndmhep_(); ::fluka::nmxhep != ndmhep) {
+      CORSIKA_LOGGER_CRITICAL(logger_, "HEPEVT dimension mismatch. FLUKA reports %d",
+                              ndmhep);
+      throw std::runtime_error{"FLUKA HEPEVT dimension mismatch"};
+    }
+  }
+
+  inline bool InteractionModel::isValid(Code projectileID, int material,
+                                        HEPEnergyType /*sqrtS*/) const {
+    if (!fluka::canInteract(projectileID)) {
+      // invalid projectile
+      return false;
+    }
+
+    if (material < 0) {
+      // invalid/uninitialized target
+      return false;
+    }
+
+    // TODO: check validity range of sqrtS
+    return true;
+  }
+
+  inline bool InteractionModel::isValid(Code projectileID, Code targetID,
+                                        HEPEnergyType sqrtS) const {
+    return isValid(projectileID, getMaterialIndex(targetID), sqrtS);
+  }
+
+  inline int InteractionModel::getMaterialIndex(Code targetID) const {
+    auto compare = [=](std::pair<Code, int> const& p) { return p.first == targetID; };
+    if (auto it = std::find_if(materials_.cbegin(), materials_.cend(), compare);
+        it == materials_.cend()) {
+      return -1;
+    } else {
+      return it->second;
+    }
+  }
+
+  inline CrossSectionType InteractionModel::getCrossSection(
+      Code const projectileId, Code const targetId, FourMomentum const& projectileP4,
+      FourMomentum const& targetP4) const {
+    auto const flukaMaterial = getMaterialIndex(targetId);
+
+    HEPEnergyType const sqrtS = (projectileP4 + targetP4).getNorm();
+    if (!isValid(projectileId, flukaMaterial, sqrtS)) { return CrossSectionType::zero(); }
+
+    COMBoost const targetRestBoost{targetP4.getSpaceLikeComponents(), get_mass(targetId)};
+    FourMomentum const projectileLab4mom = targetRestBoost.toCoM(projectileP4);
+    HEPEnergyType const Elab = projectileLab4mom.getTimeLikeComponent();
+    auto constexpr invGeV = 1 / 1_GeV;
+    double const labMomentum =
+        projectileLab4mom.getSpaceLikeComponents().getNorm() * invGeV;
+
+    auto const plab = projectileLab4mom.getSpaceLikeComponents();
+
+    CORSIKA_LOGGER_DEBUG(logger_, fmt::format("Elab = {} GeV", Elab * invGeV));
+    auto const flukaCodeProj =
+        static_cast<FLUKACodeIntType>(convertToFluka(projectileId));
+
+    double const dummyEkin = 0;
+    CrossSectionType const xs = ::fluka::sgmxyz_(&flukaCodeProj, &flukaMaterial,
+                                                 &dummyEkin, &labMomentum, &iflxyz_) *
+                                1_mb;
+    return xs;
+  }
+
+  template <typename TSecondaryView>
+  inline void InteractionModel::doInteraction(TSecondaryView& view,
+                                              Code const projectileId,
+                                              Code const targetId,
+                                              FourMomentum const& projectileP4,
+                                              FourMomentum const& targetP4) {
+
+    auto const flukaCodeProj =
+        static_cast<FLUKACodeIntType>(convertToFluka(projectileId));
+    auto const flukaMaterial = getMaterialIndex(targetId);
+
+    HEPEnergyType const sqrtS = (projectileP4 + targetP4).getNorm();
+    if (!isValid(projectileId, flukaMaterial, sqrtS)) {
+      std::string const errmsg = fmt::format(
+          "Event generation with invalid configuration requested: proj: {}, target: {}",
+          get_name(projectileId, full_name{}), get_name(targetId, full_name{}));
+      CORSIKA_LOGGER_CRITICAL(logger_, errmsg);
+      throw std::runtime_error{errmsg.c_str()};
+    }
+
+    COMBoost const targetRestBoost{targetP4.getSpaceLikeComponents(), get_mass(targetId)};
+    FourMomentum const projectileLab4mom = targetRestBoost.toCoM(projectileP4);
+    auto constexpr invGeV = 1 / 1_GeV;
+
+    auto const plab = projectileLab4mom.getSpaceLikeComponents();
+    auto const& cs = plab.getCoordinateSystem();
+    auto const labMomentum = plab.getNorm();
+    double const labMomentumGeV = labMomentum * invGeV;
+
+    auto const direction = (plab / labMomentum).getComponents().getEigenVector();
+
+    double const dummyEkin = 0;
+    ::fluka::evtxyz_(&flukaCodeProj, &flukaMaterial, &dummyEkin, &labMomentumGeV,
+                     &direction[0], &direction[1], &direction[2], &iflxyz_, cumsgx_.get(),
+                     cumsgx_.get() + materials_.size(),
+                     cumsgx_.get() + materials_.size() * 2);
+
+    for (int i = 0; i < ::fluka::hepevt_.nhep; ++i) {
+      int const status = ::fluka::hepevt_.isthep[i];
+      if (status != 1) // skip non-final-state particles
+        continue;
+
+      auto const pdg = static_cast<corsika::PDGCode>(::fluka::hepevt_.idhep[i]);
+      auto const c8code = corsika::convert_from_PDG(pdg);
+      auto const mom = QuantityVector<hepenergy_d>{
+          Eigen::Map<Eigen::Vector3d>(&::fluka::hepevt_.phep[i][0]) *
+          (1_GeV).magnitude()};
+      auto const pPrime = mom.getNorm();
+      auto const c8mass = corsika::get_mass(c8code);
+
+      auto const fourMomCollisionFrame =
+          FourVector{calculate_total_energy(pPrime, c8mass), MomentumVector{cs, mom}};
+      auto const fourMomOrigFrame = targetRestBoost.fromCoM(fourMomCollisionFrame);
+      auto const momOrigFrame = fourMomOrigFrame.getSpaceLikeComponents();
+      auto const p = momOrigFrame.getNorm();
+
+      view.addSecondary(std::tuple{c8code, corsika::calculate_kinetic_energy(p, c8mass),
+                                   momOrigFrame / p});
+    }
+  }
+
+  template <typename TEnvironment>
+  inline std::vector<std::pair<Code, int>> InteractionModel::genFlukaMaterials(
+      TEnvironment const& env) {
+    auto const& universe = *(env.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;
+    });
+
+    /*
+     * We define one material per element/isotope we have in C8. Cross-section averaging
+     * and target isotope selection happen in C8.
+     */
+
+    int const nElements = allElementsInUniverse.size();
+    auto nelmfl = std::make_unique<int[]>(nElements);
+    std::vector<int> izelfl;
+    izelfl.reserve(nElements);
+    auto wfelml = std::make_unique<double[]>(nElements);
+    auto const mxelfl = nElements;
+    double const pptmax = 1e11; // GeV
+    double const ef2dp3 = 0;    // GeV, 0 means default is used
+    double const df2dp3 = -1;   // default
+    bool const lprint = true;
+    auto mtflka = std::make_unique<int[]>(mxelfl);
+    // magic number that FLUKA uses to see if it's the right version
+    char const crvrck[] = "76466879";
+    int const size = 8;
+
+    std::fill(&nelmfl[0], &nelmfl[nElements], 1);
+    std::fill(&wfelml[0], &wfelml[nElements], 1.);
+    std::transform(allElementsInUniverse.cbegin(), allElementsInUniverse.cend(),
+                   std::back_inserter(izelfl), get_nucleus_Z);
+
+    // check if FLUPRO is set
+    if (std::getenv("FLUPRO") == nullptr) {
+      throw std::runtime_error{"FLUPRO not set; required to initialize FLUKA"};
+    }
+
+    // call FLUKA
+    ::fluka::stpxyz_(&nElements, nelmfl.get(), izelfl.data(), wfelml.get(), &mxelfl,
+                     &pptmax, &ef2dp3, &df2dp3, &iflxyz_, &lprint, mtflka.get(), crvrck,
+                     &size);
+
+    // now create & fill vector of (C8 Code, FLUKA mat. no.) pairs
+    std::vector<std::pair<Code, int>> mapping;
+    mapping.reserve(nElements);
+
+    auto it = boost::make_zip_iterator(
+        boost::make_tuple(allElementsInUniverse.begin(), &mtflka[0]));
+    auto const end = boost::make_zip_iterator(
+        boost::make_tuple(allElementsInUniverse.end(), &mtflka[nElements]));
+    for (; it != end; ++it) {
+      boost::tuple<Code const&, int&> tup = *it;
+      mapping.emplace_back(tup.get<0>(), tup.get<1>());
+    }
+
+    return mapping;
+  }
+} // namespace corsika::fluka
diff --git a/corsika/framework/utility/COMBoost.hpp b/corsika/framework/utility/COMBoost.hpp
index 76cbe20d7dbb1799c6c39c0d7265ffda2c34a881..f797debd2f4d909951c0684f5028face4d1f847c 100644
--- a/corsika/framework/utility/COMBoost.hpp
+++ b/corsika/framework/utility/COMBoost.hpp
@@ -86,7 +86,7 @@ namespace corsika {
     //! internal method
     void setBoost(double const coshEta, double const sinhEta);
 
-  private:
+  public:
     Eigen::Matrix2d boost_;
     Eigen::Matrix2d inverseBoost_;
     CoordinateSystemPtr const originalCS_;
diff --git a/corsika/modules/Epos.hpp b/corsika/modules/Epos.hpp
index b4aa9086cf563e35b0ffb23d4aa9c85f6375cf60..0fb3db62407f93b713048182faa6a82e870dab72 100644
--- a/corsika/modules/Epos.hpp
+++ b/corsika/modules/Epos.hpp
@@ -13,7 +13,7 @@
 #include <corsika/framework/process/InteractionProcess.hpp>
 
 /**
- * @file Sibyll.hpp
+ * @file Epos.hpp
  *
  * Includes all the parts of the EPOS model. Defines the InteractionProcess<TModel>
  * classes needed for the ProcessSequence.
@@ -27,4 +27,4 @@ namespace corsika::epos {
    * to provide all the functions for ProcessSequence.
    */
   class Interaction : public InteractionModel, public InteractionProcess<Interaction> {};
-} // namespace corsika::epos
\ No newline at end of file
+} // namespace corsika::epos
diff --git a/corsika/modules/FLUKA.hpp b/corsika/modules/FLUKA.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..43f4e32e30ebf2d05d7f2edaca9b9c6b41bdf700
--- /dev/null
+++ b/corsika/modules/FLUKA.hpp
@@ -0,0 +1,35 @@
+/*
+ * (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/fluka/InteractionModel.hpp>
+#include <corsika/framework/process/InteractionProcess.hpp>
+
+/**
+ * @file FLUKA.hpp
+ *
+ * Includes all the parts of the FLUKA model. Defines the InteractionProcess<TModel>
+ * classes needed for the ProcessSequence.
+ */
+
+namespace corsika::fluka {
+  /**
+   * fluka::Interaction is the process for ProcessSequence.
+   *
+   * The fluka::InteractionModel is wrapped as an InteractionProcess here in order
+   * to provide all the functions for ProcessSequence.
+   */
+  class Interaction : public fluka::InteractionModel,
+                      public corsika::InteractionProcess<Interaction> {
+  public:
+    template <typename TEnvironment>
+    Interaction(TEnvironment const& env)
+        : fluka::InteractionModel{env} {}
+  };
+} // namespace corsika::fluka
diff --git a/corsika/modules/Random.hpp b/corsika/modules/Random.hpp
index a81bc1ad2d262f0cc049067d58aaa13df372b6b9..60d8b9a9a03a8d35a6655dc24e7ad2894b4e34b5 100644
--- a/corsika/modules/Random.hpp
+++ b/corsika/modules/Random.hpp
@@ -22,3 +22,4 @@
 #include <corsika/modules/urqmd/Random.hpp>
 #include <corsika/modules/qgsjetII/Random.hpp>
 #include <corsika/modules/conex/Random.hpp>
+#include <corsika/modules/fluka/Random.hpp>
diff --git a/corsika/modules/epos/ParticleConversion.hpp b/corsika/modules/epos/ParticleConversion.hpp
index 8e501d67f544c3ac6fc0bd23c247d21e9f5e1680..7faf53da628a4a72c0abfd42432cf23f3e16f5fb 100644
--- a/corsika/modules/epos/ParticleConversion.hpp
+++ b/corsika/modules/epos/ParticleConversion.hpp
@@ -8,6 +8,10 @@
 
 #pragma once
 
+#include <array>
+#include <cstdint>
+#include <type_traits>
+
 #include <corsika/framework/core/ParticleProperties.hpp>
 #include <corsika/framework/core/PhysicalUnits.hpp>
 
diff --git a/corsika/modules/fluka/InteractionModel.hpp b/corsika/modules/fluka/InteractionModel.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8796e48b8e8c4fbb018d6002ce944dd4ea970a2b
--- /dev/null
+++ b/corsika/modules/fluka/InteractionModel.hpp
@@ -0,0 +1,73 @@
+/*
+ * (c) Copyright 2022 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 <vector>
+#include <utility>
+#include <memory>
+
+#include <corsika/media/Environment.hpp>
+#include <corsika/media/NuclearComposition.hpp>
+#include <corsika/framework/geometry/FourVector.hpp>
+#include <corsika/framework/utility/COMBoost.hpp>
+#include <corsika/framework/core/Logging.hpp>
+#include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/framework/random/RNGManager.hpp>
+
+namespace corsika::fluka {
+  /**
+   * This class exposes the (hadronic) interactions of FLUKA. FLUKA needs to be
+   * initialized with a predefined set of target materials and a flag describing the type
+   * of interactions (elastic, inelastic, electromagnetic dissociation). Currently, only
+   * inelastic events are supported.
+   */
+  class InteractionModel {
+  public:
+    /**
+     * Create a new InteractionModel. The FLUKA materials are collected from the elements
+     * present in the environment. Each element is its own FLUKA material, no FLUKA
+     * compounds are used.
+     */
+    template <typename TEnvironment>
+    InteractionModel(TEnvironment const&);
+
+    //! Return the cross-section of a given combination of projectile/target.
+    CrossSectionType getCrossSection(Code projectileId, Code targetId,
+                                     FourMomentum const& projectileP4,
+                                     FourMomentum const& targetP4) const;
+
+    bool isValid(Code projectileID, Code targetID, HEPEnergyType sqrtS) const;
+    bool isValid(Code projectileID, int material, HEPEnergyType sqrtS) const;
+
+    //! convert target Code to FLUKA material number
+    int getMaterialIndex(Code targetID) const;
+
+    /**
+     * Perform an interaction. Since FLUKA expects a fixed-target configuration, we
+     * perform a Lorentz transform into the rest frame of the target.
+     */
+    template <typename TSecondaryView>
+    void doInteraction(TSecondaryView& view, Code const projectileId, Code const targetId,
+                       FourMomentum const& projectileP4, FourMomentum const& targetP4);
+
+  private:
+    default_prng_type& RNG_ = RNGManager<>::getInstance().getRandomStream("fluka");
+    std::vector<std::pair<Code, int>> const
+        materials_; //!< map target Code to FLUKA material no.
+    std::shared_ptr<spdlog::logger> logger_ = get_logger("corsika_FLUKA_Interaction");
+    std::unique_ptr<double[]> cumsgx_; //!< dump for evtxyz cumsg*, never read again
+
+    template <typename TEnvironment>
+    static std::vector<std::pair<Code, int>> genFlukaMaterials(TEnvironment const&);
+  };
+
+  inline static int const iflxyz_ = 1;
+} // namespace corsika::fluka
+
+#include <corsika/detail/modules/fluka/InteractionModel.inl>
diff --git a/corsika/modules/fluka/ParticleConversion.hpp b/corsika/modules/fluka/ParticleConversion.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..228d568ac9a889e08e2f9aa5137b3571ffc8de99
--- /dev/null
+++ b/corsika/modules/fluka/ParticleConversion.hpp
@@ -0,0 +1,50 @@
+/*
+ * (c) Copyright 2023 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 <array>
+#include <cstdint>
+#include <type_traits>
+#include <vector>
+
+#include <corsika/framework/core/ParticleProperties.hpp>
+#include <fmt/core.h>
+
+#include <string>
+
+namespace corsika::fluka {
+
+  enum class FLUKACode : int;
+  using FLUKACodeIntType = std::underlying_type<FLUKACode>::type;
+
+#include <corsika/modules/fluka/Generated.inc>
+
+  FLUKACode constexpr convertToFluka(Code const c8id) {
+    if (is_nucleus(c8id)) {
+      throw std::runtime_error{"nucleus conversion to FLUKA not implemented"};
+    }
+
+    FLUKACode const flukaID = corsika2fluka[static_cast<corsika::CodeIntType>(c8id)];
+    if (flukaID == FLUKACode::Unknown) {
+      throw std::runtime_error{
+          fmt::format("no correspondig FLUKA id for {}", get_name(c8id)).c_str()};
+    }
+
+    return flukaID;
+  }
+
+  int constexpr convertToFlukaRaw(Code const code) {
+    return static_cast<FLUKACodeIntType>(convertToFluka(code));
+  }
+
+  bool const canInteract(Code const code) {
+    if (is_nucleus(code)) return false; // nuclei support not yet implemented
+    return flukaCanInteract[static_cast<CodeIntType>(code)];
+  }
+} // namespace corsika::fluka
diff --git a/corsika/modules/fluka/Random.hpp b/corsika/modules/fluka/Random.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..73cf5377a37d0453248b7fdfff0c04f9607ab981
--- /dev/null
+++ b/corsika/modules/fluka/Random.hpp
@@ -0,0 +1,26 @@
+/*
+ * (c) Copyright 2023 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 <random>
+
+#include <corsika/framework/random/RNGManager.hpp>
+
+namespace fluka {
+  /**
+   * the random number generator function for FLUKA
+   */
+  double rndm_interface() {
+    static corsika::default_prng_type& rng =
+        corsika::RNGManager<>::getInstance().getRandomStream("fluka");
+    static std::uniform_real_distribution<double> dist;
+    return dist(rng);
+  }
+
+} // namespace fluka
diff --git a/modules/fluka/CMakeLists.txt b/modules/fluka/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b9cad711bba440c7f9a7d5c6dea34711872768d7
--- /dev/null
+++ b/modules/fluka/CMakeLists.txt
@@ -0,0 +1,55 @@
+set(C8_FLUKALIB CACHE STRING "path to libflukahp.a")
+
+if (EXISTS "${C8_FLUKALIB}")
+    message("libflukahp.a fount at ${C8_FLUKALIB}; FLUKA support is enabled.")
+    set (input_dir ${PROJECT_SOURCE_DIR}/src/modules/fluka)
+    
+    # we remove flrndm_.o from the original libflukahp.a and save the result as libflukahp-norndm.a
+    add_custom_command(
+        OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/libflukahp-norndm.a
+        COMMAND ${input_dir}/strip_flukahp.sh ${C8_FLUKALIB} ${CMAKE_CURRENT_BINARY_DIR}
+    )
+    add_custom_target(generate_libfluka-norndm DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/libflukahp-norndm.a)
+
+    add_library(flukahp-norndm STATIC IMPORTED)
+    add_dependencies(flukahp-norndm generate_libfluka-norndm)
+    set_property(TARGET flukahp-norndm PROPERTY IMPORTED_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/libflukahp-norndm.a)
+
+    find_library(MATH_LIBRARY m)
+    target_link_libraries(flukahp-norndm INTERFACE gfortran ${MATH_LIBRARY})
+    target_include_directories (flukahp-norndm INTERFACE
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+        $<INSTALL_INTERFACE:include/corsika_modules/fluka>
+    )
+    
+    # build shared library containing FLUKA + our own implementation of flrndm_()
+    # Note: This apparently works only when built as shared lib, not possible so easily as static lib
+    # Note also that this lib is not self-contained as it doesn't contain fluka::rndm_interface(), so
+    # you can't load it at runtime
+    add_library(fluka SHARED fluka.cpp)
+    target_link_libraries(fluka PRIVATE flukahp-norndm)
+
+    target_include_directories (fluka PUBLIC
+      $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+      $<INSTALL_INTERFACE:include/corsika_modules/fluka>
+      )
+    
+    install (
+      FILES
+      fluka.hpp
+      DESTINATION include/corsika_modules/fluka
+      )
+    
+    install (
+      TARGETS fluka
+      EXPORT CORSIKA8PublicTargets
+      ARCHIVE DESTINATION lib/corsika
+      INCLUDES DESTINATION include/corsika_modules/fluka
+      )
+    
+    # add fluka to corsika8 build
+    add_dependencies (CORSIKA8 fluka)
+    target_link_libraries (CORSIKA8 INTERFACE fluka)
+else()
+    message("C8_FLUKALIB not set/invalid, building without FLUKA support")
+endif()
diff --git a/modules/fluka/FLUKA.hpp b/modules/fluka/FLUKA.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..12b8a17562cee175314c8ba6c6ff0a5154eb683b
--- /dev/null
+++ b/modules/fluka/FLUKA.hpp
@@ -0,0 +1,229 @@
+/*
+ * (c) Copyright 2022 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 <cstddef>
+#include <array>
+
+namespace fluka {
+  /**
+   * \fluka fluka::rndm_interface
+   *
+   * this is the random number hook to external packages.
+   *
+   * CORSIKA8, for example, has to provide an implementation of this.
+   **/
+  extern double rndm_interface();
+
+  size_t constexpr nmxhep = 10000;
+  template <typename T>
+  using hepmc_array = std::array<T, nmxhep>;
+
+  extern "C" {
+  /**
+   *---------------------------------------------------------------------------
+   *                   hep standard event commonblock.
+   *---------------------------------------------------------------------------
+   *---------------------------------------------------------------------------
+   *
+   *         nevhep      -   event number
+   *         nhep        -   number of entries in the event record
+   *
+   *         isthep(i)   -   status code
+   *         idhep(i)    -   particle id (particle data group standard)
+   *
+   *         jmohep(1,i) -   position of mother particle in list
+   *         jmohep(2,i) -   position of second mother particle in list
+   *         jdahep(1,i) -   position of first daughter in list
+   *         jdahep(2,i) -   position of first daughter in list
+   *
+   *         phep(1,i)   -   p_x momentum in gev/c
+   *         phep(2,i)   -   p_y momentum in gev/c
+   *         phep(3,i)   -   p_z momentum in gev/c
+   *         phep(4,i)   -   energy in gev
+   *         phep(5,i)   -   mass in gev/c**2
+   *
+   *         vhep(1,i)   -   x position of production vertex in mm
+   *         vhep(2,i)   -   y position of production vertex in mm
+   *         vhep(3,i)   -   z position of production vertex in mm
+   *         vhep(4,i)   -   time of production  in mm/c
+   */
+
+  extern struct {
+    int nevhep;                  // event number
+    int nhep;                    // number of entries
+    hepmc_array<int> isthep;     // status code
+    hepmc_array<int> idhep;      // PDG particle id
+    hepmc_array<int[2]> jmohep;  // position of first, second mother
+    hepmc_array<int[2]> jdahep;  // position of first, last daughter
+    hepmc_array<double[5]> phep; // 4-momemtum, mass (GeV)
+    hepmc_array<double[4]> vhep; // vertex, production time in mm
+  } hepevt_;
+
+  /*  Iflxyz must be consistent in all calls, to Stpxyz, Sgmxyz, Evtxyz
+   *    Iflxyz =  1 -> only inelastic
+   *    Iflxyz = 10 -> only elastic
+   *    Iflxyz = 11 -> inelastic + elastic
+   *    Iflxyz =100 -> only emd
+   *    Iflxyz =101 -> inelastic + emd
+   *    Iflxyz =110 -> elastic + emd
+   *    Iflxyz =111 -> inelastic + elastic + emd
+   */
+
+  /**---------------------------------------------------------------------*
+   *                                                                      *
+   *     Copyright (C) 2022-2022      by    Alfredo Ferrari & Paola Sala  *
+   *     All Rights Reserved.                                             *
+   *                                                                      *
+   *                                                                      *
+   *     SeTuP for X/Y/Z interaction types:                               *
+   *                                                                      *
+   *     Authors:                           Alfredo Ferrari & Paola Sala  *
+   *                                                                      *
+   *                                                                      *
+   *     Created on  24 October 2022  by    Alfredo Ferrari & Paola Sala  *
+   *                                            Private        Private    *
+   *                                                                      *
+   *     Last change on  01-Nov-22    by             Alfredo Ferrari      *
+   *                                                     Private          *
+   *                                                                      *
+   *     Input variables:                                                 *
+   *                                                                      *
+   *           Nmatfl    = Number of requested Fluka materials            *
+   *           Nelmfl(m) = Number of elements in the m_th requested Fluka *
+   *                       material, Nelmfl(m)=1 means simple element, no *
+   *                       compound                                       *
+   *           Izelfl(n) = Cumulative array of the Z of the elements con- *
+   *                       stituting the requested Fluka materials        *
+   *                      (the Z for the elements of the m_th materials   *
+   *                       start at n = Sum_i=1^m-1 [Nelmfl(i)] + 1)      *
+   *           Wfelml(n) = Cumulative array of the weight fractions of the*
+   *                       elements constituting the requested Fluka      *
+   *                       materials (the weight fractions for the elem-  *
+   *                       ents of the m_th materials start at            *
+   *                        n = Sum_i=1^m-1 [Nelmfl(i)] + 1)              *
+   *           Mxelfl    = Dimension of the Iz/Wfelml arrays, it must be  *
+   *                       Mxelfl >= Sum_i=1^nmatfl [Nelmfl(i)]           *
+   *           Pptmax    = Maximum momentum (GeV/c) to be used in initia- *
+   *                       lization (optional)                            *
+   *           Ef2dp3    = Transition energy (GeV) from Fluka (Peanut) to *
+   *                       Dpmjet3 for hA interactions (optional)         *
+   *           Df2dp3    = Smearing (+/-Df2dp3) (GeV) of the transition   *
+   *                       energy from Fluka (Peanut) to Dpmjet3 for hA   *
+   *                       interactions (optional)                        *
+   *           Lprint    = Material printout flag                         *
+   *                                                                      *
+   *     Output variables:                                                *
+   *                                                                      *
+   *           Mtflka(m) = Fluka material number corresponding to the m_th*
+   *                       requested material                             *
+   *                                                                      *
+   *----------------------------------------------------------------------*/
+  void stpxyz_(int const* NMATFL, int const NELMFL[], int const IZELFL[],
+               double const WFELFL[], int const* MXELFL, double const* PPTMAX,
+               double const* EF2DP3, double const* DF2DP3, int const* IFLXYZ,
+               bool const* LPRINT, int* MTFLKA, char const* CRVRCK, int const*);
+
+  /**---------------------------------------------------------------------*
+   *                                                                      *
+   *     Copyright (C) 2022-2022      by    Alfredo Ferrari & Paola Sala  *
+   *     All Rights Reserved.                                             *
+   *                                                                      *
+   *                                                                      *
+   *     EVenT for X/Y/Z interaction types:                               *
+   *                                                                      *
+   *     Authors:                           Alfredo Ferrari & Paola Sala  *
+   *                                                                      *
+   *                                                                      *
+   *     Created on  24 October 2022  by    Alfredo Ferrari & Paola Sala  *
+   *                                            Private        Private    *
+   *                                                                      *
+   *     Last change on  28-Oct-22    by             Alfredo Ferrari      *
+   *                                                     Private          *
+   *                                                                      *
+   *     Output variables:                                                *
+   *                                                                      *
+   *           Cumsgi(i) = cumulative macroscopic inelastic cross section *
+   *                       (cm^2/g x rho) for the i_th element of the mmat*
+   *                       material                                       *
+   *           Cumsge(i) = cumulative macroscopic elastic   cross section *
+   *                       (cm^2/g x rho) for the i_th element of the mmat*
+   *                       material                                       *
+   *           Cumsgm(i) = cumulative macroscopic emd       cross section *
+   *                       (cm^2/g x rho) for the i_th element of the mmat*
+   *                       material                                       *
+   *                                                                      *
+   *                                                                      *
+   *----------------------------------------------------------------------*/
+  void evtxyz_(int const* KPROJ, int const* MMAT, double const* EKIN, double const* PPROJ,
+               double const* TXX, double const* TYY, double const* TZZ, int const* IFLXYZ,
+               double CUMSGI[], double CUMSGE[], double CUMSGM[]);
+
+  /**---------------------------------------------------------------------*
+   *                                                                      *
+   *     Copyright (C) 2022-2022      by    Alfredo Ferrari & Paola Sala  *
+   *     All Rights Reserved.                                             *
+   *                                                                      *
+   *     SiGMa (mb) for X/Y/Z interaction types:                          *
+   *                                                                      *
+   *     Authors:                           Alfredo Ferrari & Paola Sala  *
+   *                                                                      *
+   *                                                                      *
+   *     Created on  24 October 2022  by    Alfredo Ferrari & Paola Sala  *
+   *                                            Private        Private    *
+   *                                                                      *
+   *     Last change on  27-Oct-22    by             Alfredo Ferrari      *
+   *                                                     Private          *
+   *                                                                      *
+   *----------------------------------------------------------------------*/
+  double sgmxyz_(int const* KPROJ, int const* MMAT, double const* EKIN,
+                 double const* PPROJ, int const* IFLXYZ);
+
+  /**---------------------------------------------------------------------*
+   *                                                                      *
+   *     Copyright (C) 2023-2023      by    Alfredo Ferrari & Paola Sala  *
+   *     All Rights Reserved.                                             *
+   *                                                                      *
+   *     FiLL HEP common:                                                 *
+   *                                                                      *
+   *     Authors:                           Alfredo Ferrari & Paola Sala  *
+   *                                                                      *
+   *                                                                      *
+   *     Created on 06 February 2023  by    Alfredo Ferrari & Paola Sala  *
+   *                                            Private        Private    *
+   *                                                                      *
+   *     Last change on  07-Feb-23    by             Alfredo Ferrari      *
+   *                                                     Private          *
+   *                                                                      *
+   *----------------------------------------------------------------------*/
+  void fllhep_();
+
+  /**----------------------------------------------------------------------*
+   *                                                                      *
+   *     Copyright (C) 2023-2023      by    Alfredo Ferrari & Paola Sala  *
+   *     All Rights Reserved.                                             *
+   *                                                                      *
+   *     N DiMension of the HEP common:                                   *
+   *                                                                      *
+   *     Authors:                           Alfredo Ferrari & Paola Sala  *
+   *                                                                      *
+   *                                                                      *
+   *     Created on   09 March 2023   by    Alfredo Ferrari & Paola Sala  *
+   *                                            Private        Private    *
+   *                                                                      *
+   *     Last change on  09-Mar-23    by             Alfredo Ferrari      *
+   *                                                     Private          *
+   *                                                                      *
+   *----------------------------------------------------------------------*/
+  int ndmhep_();
+
+  //! random-number generator called from within FLUKA
+  double flrndm_();
+  }
+} // namespace fluka
diff --git a/modules/fluka/fluka.cpp b/modules/fluka/fluka.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..985bde38bd414cae76dd2113c4df266820578f16
--- /dev/null
+++ b/modules/fluka/fluka.cpp
@@ -0,0 +1,32 @@
+/*
+ * (c) Copyright 2023 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 <FLUKA.hpp>
+
+namespace fluka {
+  double (*rndmPtr)() = &rndm_interface;
+
+  extern "C" {
+  double flrndm_() { return ::fluka::rndmPtr(); }
+
+  //! overwrite function pointer to be used as FLUKA RNG (flrndm_())
+  void setFlukaRNG(double (*func)()) { ::fluka::rndmPtr = func; }
+
+  /**
+   * The following (function) pointers make sure the corresponding objects in libflukahp.a
+   * won't get dropped from the final file during linking.
+   */
+
+  [[maybe_unused]] extern auto* const hepevt_ptr = &hepevt_;
+  [[maybe_unused]] extern auto* const stpxyc_ptr = &stpxyz_;
+  [[maybe_unused]] extern auto* const evtxyz_ptr = &evtxyz_;
+  [[maybe_unused]] extern auto* const sgmxyz_ptr = &sgmxyz_;
+  [[maybe_unused]] extern auto* const ndmhep_ptr = &ndmhep_;
+  }
+
+} // namespace fluka
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index f8d5df55a95bd8e4547d58322fecb6cc23e5f262..34fe8bdd3040ce796ec5a6c435c4fa5680f2c0e0 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,6 +1,7 @@
-add_subdirectory (framework/core) 
+add_subdirectory (framework/core)
 add_subdirectory (media)
-add_subdirectory (modules/sibyll) 
-add_subdirectory (modules/sophia) 
-add_subdirectory (modules/qgsjetII) 
-add_subdirectory (modules/epos) 
+add_subdirectory (modules/sibyll)
+add_subdirectory (modules/sophia)
+add_subdirectory (modules/qgsjetII)
+add_subdirectory (modules/epos)
+add_subdirectory (modules/fluka)
diff --git a/src/modules/fluka/CMakeLists.txt b/src/modules/fluka/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2d2d26b447c277b20da5760a7236cab69c334f5e
--- /dev/null
+++ b/src/modules/fluka/CMakeLists.txt
@@ -0,0 +1,26 @@
+set (input_dir ${PROJECT_SOURCE_DIR}/src/modules/fluka)
+set (output_dir ${PROJECT_BINARY_DIR}/corsika/modules/fluka)
+
+file (MAKE_DIRECTORY ${output_dir})
+
+add_custom_command (
+  OUTPUT  ${output_dir}/Generated.inc
+  COMMAND ${input_dir}/code_generator.py 
+          ${PROJECT_BINARY_DIR}/corsika/framework/core/particle_db.pkl
+          ${input_dir}/fluka_codes.dat
+  DEPENDS ${input_dir}/code_generator.py
+          ${input_dir}/fluka_codes.dat
+          GenParticlesHeaders # for particle_db.pkl
+  WORKING_DIRECTORY
+          ${output_dir}/
+  COMMENT "Generate conversion tables for particle codes FLUKA <-> CORSIKA"
+  VERBATIM
+  )
+
+add_custom_target (SourceDirLinkFLUKA DEPENDS ${output_dir}/Generated.inc)
+add_dependencies (CORSIKA8 SourceDirLinkFLUKA)
+
+install (
+  FILES ${output_dir}/Generated.inc
+  DESTINATION include/corsika/modules/fluka
+)
diff --git a/src/modules/fluka/code_generator.py b/src/modules/fluka/code_generator.py
new file mode 100755
index 0000000000000000000000000000000000000000..3574fa25756193f42f21166088e70506a457ede2
--- /dev/null
+++ b/src/modules/fluka/code_generator.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+
+# (c) Copyright 2023 CORSIKA Project, corsika-project@lists.kit.edu
+#
+# See file AUTHORS for a list of contributors.
+#
+# This software is distributed under the terms of the GNU General Public
+# Licence version 3 (GPL Version 3). See file LICENSE for a full version of
+# the license.
+
+
+import pickle, sys, itertools
+
+
+def load_particledb(filename):
+    '''
+    loads the pickled particle_db (which is an OrderedDict)
+    '''
+    with open(filename, "rb") as f:
+        particle_db = pickle.load(f)
+    return particle_db
+
+
+def read_fluka_codes(filename, particle_db):
+    '''
+    reads to FLUKA codes data file
+
+    For particles that can interact in FLUKA, add can-interact flag
+    '''
+    with open(filename) as f:
+        for line in f:
+            line = line.strip()
+            if len(line) == 0 or line[0].startswith('#'):
+                continue
+            line = line.split()
+            if len(line) == 2:
+                canInteractFlag = "no"
+                identifier, fluka_code = line
+            else:
+                identifier, fluka_code, canInteractFlag = line
+
+            try:
+                particle_db[identifier]["fluka_code"] = int(fluka_code)
+                particle_db[identifier]["fluka_canInteract"] = (canInteractFlag == "yes")
+            except KeyError as e:
+                raise Exception("Identifier '{:s}' not found in CORSIKA8 particle_db".format(identifier))
+
+
+def generate_fluka_enum(particle_db):
+    '''
+     generates the enum to access epos particles by readable names
+    '''
+    output = ("enum class FLUKACode : int {\n"
+              "  Unknown = 0,\n")
+    for identifier, pData in particle_db.items():
+        if 'fluka_code' in pData:
+            output += "  {:s} = {:d},\n".format(identifier, pData['fluka_code'])
+    output += "};\n"
+    return output
+
+
+def generate_corsika2fluka(particle_db):    
+    '''
+    generates the look-up table to convert corsika codes to epos codes
+    '''
+    string = "static inline std::array<FLUKACode, {:d}> constexpr corsika2fluka = {{\n".format(len(particle_db))
+    for identifier, pData in particle_db.items():
+        if pData['isNucleus']: continue
+        if 'fluka_code' in pData:
+            string += "  FLUKACode::{:s}, \n".format(identifier)
+        else:
+            string += "  FLUKACode::Unknown, // {:s}\n".format(identifier + ' not implemented in FLUKA')
+    string += "};\n"
+    return string
+
+
+def generate_fluka_canInteract(particle_db):    
+    '''
+    generates the look-up table to convert corsika codes to epos codes
+    '''
+    string = "static inline std::vector<bool> const flukaCanInteract = {\n"
+    for identifier, pData in particle_db.items():
+        canInteract = pData.get("fluka_canInteract", False)
+        string += "  {:s}, // {:s}\n".format(str(canInteract).lower(), identifier)
+    string += "};\n"
+    return string
+
+
+if __name__ == "__main__":
+    if len(sys.argv) != 3:
+        print("usage: {:s} <particle_db.pkl> <fluka_codes.dat>".format(sys.argv[0]), file=sys.stderr)
+        sys.exit(1)
+        
+    print("code_generator.py for FLUKA")
+    
+    particle_db = load_particledb(sys.argv[1])
+    read_fluka_codes(sys.argv[2], particle_db)
+    
+    with open("Generated.inc", "w") as f:
+        print("// this file is automatically generated\n// edit at your own risk!\n", file=f)
+        print(generate_fluka_enum(particle_db), file=f)
+        print(generate_corsika2fluka(particle_db), file=f)
+        print(generate_fluka_canInteract(particle_db), file=f)
diff --git a/src/modules/fluka/fluka_codes.dat b/src/modules/fluka/fluka_codes.dat
new file mode 100644
index 0000000000000000000000000000000000000000..b7f42677d1b5658672e308c01ceb1afb8346179f
--- /dev/null
+++ b/src/modules/fluka/fluka_codes.dat
@@ -0,0 +1,60 @@
+# FLUKA particle code conversion table
+# C8 identifier, FLUKA code, has cross-section/can interact
+Proton	1	yes
+AntiProton	2	yes
+Electron	3
+Positron	4
+NuE	5
+NuEBar	6
+Photon	7
+Neutron	8	yes
+AntiNeutron	9	yes
+MuPlus	10
+MuMinus	11
+K0Long	12	yes
+PiPlus	13	yes
+PiMinus	14	yes
+KPlus	15	yes
+KMinus	16	yes
+Lambda0	17	yes
+Lambda0Bar	18	yes
+K0Short	19	yes
+SigmaMinus	20 yes
+SigmaPlus	21  yes
+Sigma0	22  yes
+Pi0	23  yes
+K0 24
+K0Bar 25
+NuMu	27
+NuMuBar	28
+SigmaMinusBar	31  yes
+Sigma0Bar	32  yes
+SigmaPlusBar	33  yes
+Xi0	34  yes
+Xi0Bar	35  yes
+XiMinus	36  yes
+XiPlusBar	37  yes
+OmegaMinus	38  yes
+OmegaPlusBar	39  yes
+TauPlus	41
+TauMinus	42
+NuTau	43
+NuTauBar	44
+DPlus	45
+DMinus	46
+D0	47
+D0Bar	48
+DsPlus	49
+DsMinus	50
+LambdaCPlus	51
+XiCPlus	52
+XiC0	53
+XiPrimeCPlus	54
+XiPrimeC0	55
+OmegaC0	56
+LambdaCMinusBar	57
+XiCMinusBar	58
+XiC0Bar	59
+XiPrimeCMinusBar	60
+XiPrimeC0Bar	61
+OmegaC0Bar	62
diff --git a/src/modules/fluka/strip_flukahp.sh b/src/modules/fluka/strip_flukahp.sh
new file mode 100755
index 0000000000000000000000000000000000000000..0ee9693bfcb16c61d90d48929fa1b431031e4127
--- /dev/null
+++ b/src/modules/fluka/strip_flukahp.sh
@@ -0,0 +1,36 @@
+#!/bin/sh
+
+# (c) Copyright 2023 CORSIKA Project, corsika-project@lists.kit.edu
+#
+# See file AUTHORS for a list of contributors.
+#
+# This software is distributed under the terms of the GNU General Public
+# Licence version 3 (GPL Version 3). See file LICENSE for a full version of
+# the license.
+
+# This script strips off flrndm() from the libflukahp.a so that we can provide our own
+# implementation.
+
+flukalibOrig=`realpath $1`
+target="$2"
+
+if [ ! -r "$flukalibOrig" ]; then
+    echo "\"$flukalibOrig\" not readable" 1>&2
+    exit 1
+fi
+
+tmpdir=`mktemp -d fluka_objectsXXXXXX`
+workdir=`pwd`
+
+echo "extracting objects from $flukalibOrig into `realpath $tmpdir`..."
+cd "$tmpdir"
+ar x "$flukalibOrig"
+cd "$workdir"
+rm "$tmpdir/flrndm.o"
+
+[ -f "libflukahp-norndm.a" ] && rm "libflukahp-norndm.a"
+
+echo "creating libflukahp-norndm.a..."
+ar -rcs "$target/libflukahp-norndm.a" "$tmpdir"/*.o
+
+rm -r "$tmpdir"
diff --git a/tests/common/SetupStack.hpp b/tests/common/SetupStack.hpp
index 4a7ceff3ff4590f1bd20a8736aec0ddc4581d704..a598aa30af96517181f75b0afb3e72d6089ac1c6 100644
--- a/tests/common/SetupStack.hpp
+++ b/tests/common/SetupStack.hpp
@@ -24,7 +24,7 @@ namespace corsika::test {
   /*
    * the version with history
    */
-  using Stack = detaill::StackWithHistory;
+  using Stack = detail::StackWithHistory;
 
 #else // WITH_HISTORY
 
diff --git a/tests/modules/CMakeLists.txt b/tests/modules/CMakeLists.txt
index 015093588f7ec42d0496ba5bb0d323badc425f4d..16a0e66f0d34824f0ed173a5a66a759999dab17f 100644
--- a/tests/modules/CMakeLists.txt
+++ b/tests/modules/CMakeLists.txt
@@ -15,7 +15,11 @@ set (test_modules_sources
   testRadio.cpp
   testEMThinning.cpp
   testSophia.cpp
-  )
+)
+  
+if (EXISTS "${C8_FLUKALIB}")
+    list(APPEND test_modules_sources "testFluka.cpp")
+endif()
 
 CORSIKA_ADD_TEST (testModules SOURCES ${test_modules_sources})
 
diff --git a/tests/modules/testFluka.cpp b/tests/modules/testFluka.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1978c3f0478c365f682b75b7d0f035dc6745f23b
--- /dev/null
+++ b/tests/modules/testFluka.cpp
@@ -0,0 +1,191 @@
+/*
+ * (c) Copyright 2023 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/FLUKA.hpp>
+#include <corsika/modules/fluka/Random.hpp>
+
+#include <corsika/framework/core/EnergyMomentumOperations.hpp>
+
+#include <corsika/framework/geometry/Point.hpp>
+#include <corsika/framework/geometry/FourVector.hpp>
+#include <corsika/framework/geometry/CoordinateSystem.hpp>
+
+#include <corsika/media/Environment.hpp>
+#include <corsika/media/IMediumModel.hpp>
+#include <corsika/media/HomogeneousMedium.hpp>
+
+#include <catch2/catch.hpp>
+
+#include <fstream>
+#include <iomanip>
+
+#include <SetupTestStack.hpp>
+#include <SetupTestEnvironment.hpp>
+
+using namespace corsika;
+
+template <typename TStackView>
+auto sumMomentum(TStackView const& view, CoordinateSystemPtr const& vCS) {
+  MomentumVector sum{vCS, 0_eV, 0_eV, 0_eV};
+  for (auto const& p : view) { sum += p.getMomentum(); }
+  return sum;
+}
+
+TEST_CASE("FLUKACodeConversion") {
+  REQUIRE(corsika::fluka::convertToFluka(Code::PiPlus) ==
+          corsika::fluka::FLUKACode::PiPlus);
+  REQUIRE(corsika::fluka::convertToFlukaRaw(Code::PiPlus) == 13);
+  REQUIRE(corsika::fluka::convertToFlukaRaw(Code::Proton) == 1);
+  REQUIRE(corsika::fluka::convertToFlukaRaw(Code::Lambda0) == 17);
+
+  REQUIRE_THROWS(corsika::fluka::convertToFluka(Code::WPlus));
+}
+
+auto setupEnvironment() {
+  using DummyEnvironmentInterface =
+      IMediumPropertyModel<IMagneticFieldModel<IMediumModel>>;
+  using DummyEnvironment = Environment<DummyEnvironmentInterface>;
+  using MyHomogeneousModel = MediumPropertyModel<
+      UniformMagneticField<HomogeneousMedium<DummyEnvironmentInterface>>>;
+  RNGManager<>::getInstance().registerRandomStream("fluka");
+  DummyEnvironment env;
+  auto& universe = *env.getUniverse();
+  CoordinateSystemPtr const& cs = env.getCoordinateSystem();
+  universe.setModelProperties<MyHomogeneousModel>(
+      Medium::AirDry1Atm, Vector(cs, 0_T, 0_T, 0_T), 1_kg / (1_m * 1_m * 1_m),
+      NuclearComposition{
+          std::vector<Code>{Code::Hydrogen, Code::Oxygen, Code::Nitrogen, Code::Argon},
+          std::vector<double>{.25, .25, .25, .25}});
+
+  return env;
+}
+
+static auto const env = setupEnvironment();
+static corsika::fluka::InteractionModel flukaModel{env};
+static auto const& cs = env.getCoordinateSystem();
+
+TEST_CASE("FLUKA") {
+  //~ auto tup  = setupFluka();
+  //~ corsika::fluka::InteractionModel& flukaModel = std::get<0>(tup);
+  //~ auto const env = std::get<1>(tup);
+
+  SECTION("getMaterialIndex") {
+    REQUIRE(flukaModel.getMaterialIndex(Code::Hydrogen) > 0);
+    REQUIRE(flukaModel.getMaterialIndex(Code::Oxygen) > 0);
+    REQUIRE(flukaModel.getMaterialIndex(Code::Nitrogen) > 0);
+    REQUIRE(flukaModel.getMaterialIndex(Code::Argon) > 0);
+
+    // not initialized
+    REQUIRE(flukaModel.getMaterialIndex(Code::Uranium) < 0);
+  }
+
+  SECTION("getCrossSection") {
+    auto const projectileCode =
+        GENERATE(Code::PiMinus, Code::PiMinus, Code::PiMinus, Code::KMinus, Code::K0Long,
+                 Code::K0Short, Code::Lambda0, Code::SigmaPlus, Code::Proton,
+                 Code::AntiProton, Code::KMinus, Code::K0Long, Code::K0Short,
+                 Code::Lambda0, Code::SigmaPlus, Code::Proton, Code::AntiProton);
+
+    auto const targetCode = GENERATE(Code::Oxygen, Code::Hydrogen);
+
+    HEPEnergyType const p = 100_GeV;
+    auto const projectile4mom =
+        FourVector{calculate_total_energy(p, get_mass(projectileCode)),
+                   MomentumVector{cs, 0_eV, 0_eV, p}};
+    auto const target4mom =
+        FourVector{get_mass(targetCode), MomentumVector{cs, 0_eV, 0_eV, 0_eV}};
+
+    CHECK(flukaModel.getCrossSection(projectileCode, targetCode, projectile4mom,
+                                     target4mom) > 0_mb);
+  }
+
+  SECTION("getCrossSection invalid") {
+    auto const projectileCode = GENERATE(Code::Electron, Code::Photon);
+    auto const targetCode = GENERATE(Code::Oxygen, Code::Hydrogen);
+
+    HEPEnergyType const p = 100_GeV;
+    auto const projectile4mom =
+        FourVector{calculate_total_energy(p, get_mass(projectileCode)),
+                   MomentumVector{cs, 0_eV, 0_eV, p}};
+    auto const target4mom =
+        FourVector{get_mass(targetCode), MomentumVector{cs, 0_eV, 0_eV, 0_eV}};
+
+    CHECK(flukaModel.getCrossSection(projectileCode, targetCode, projectile4mom,
+                                     target4mom) == CrossSectionType::zero());
+  }
+
+  SECTION("doInteraction") {
+    auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Proton);
+    auto const& cs = *csPtr;
+
+    auto const projectileCode = GENERATE(Code::PiPlus, Code::PiMinus, Code::KPlus,
+                                         Code::K0Long, Code::Lambda0, Code::SigmaPlus);
+    auto const p = GENERATE(1_GeV, 20_GeV, 100_GeV, 1_TeV);
+    auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
+        Code::Hydrogen, 1_GeV, (DummyEnvironment::BaseNodeType* const)nodePtr, *csPtr);
+    { [[maybe_unused]] auto const& dummy_StackPtr = stackPtr; }
+
+    auto const targetCode = Code::Oxygen;
+    auto const projectile4mom =
+        FourVector{calculate_total_energy(p, get_mass(projectileCode)),
+                   MomentumVector{cs, 0_eV, 0_eV, p}};
+    auto const target4mom =
+        FourVector{get_mass(targetCode), MomentumVector{cs, 0_eV, 0_eV, 0_eV}};
+
+    flukaModel.doInteraction(*secViewPtr, projectileCode, targetCode, projectile4mom,
+                             target4mom);
+    auto const pSum = sumMomentum(*secViewPtr, cs);
+
+    CHECK((pSum - projectile4mom.getSpaceLikeComponents()).getNorm() / p ==
+          Approx(0).margin(1e-4));
+    CHECK((pSum.getNorm() - p) / p == Approx(0).margin(1e-4));
+    CHECK(secViewPtr->getSize() > 1);
+  }
+
+  SECTION("doInteraction-invalid-projectile") {
+    auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Proton);
+    auto const& cs = *csPtr;
+
+    auto const projectileCode = GENERATE(Code::Electron, Code::MuPlus);
+    auto const p = 50_GeV;
+    auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
+        Code::Hydrogen, 1_GeV, (DummyEnvironment::BaseNodeType* const)nodePtr, *csPtr);
+    { [[maybe_unused]] auto const& dummy_StackPtr = stackPtr; }
+
+    auto const targetCode = Code::Oxygen;
+    auto const projectile4mom =
+        FourVector{calculate_total_energy(p, get_mass(projectileCode)),
+                   MomentumVector{cs, 0_eV, 0_eV, p}};
+    auto const target4mom =
+        FourVector{get_mass(targetCode), MomentumVector{cs, 0_eV, 0_eV, 0_eV}};
+
+    REQUIRE_THROWS(flukaModel.doInteraction(*secViewPtr, projectileCode, targetCode,
+                                            projectile4mom, target4mom));
+  }
+
+  SECTION("doInteraction-invalid-target") {
+    auto [env, csPtr, nodePtr] = setup::testing::setup_environment(Code::Proton);
+    auto const& cs = *csPtr;
+
+    auto const projectileCode = Code::AntiNeutron;
+    auto const p = 50_GeV;
+    auto [stackPtr, secViewPtr] = setup::testing::setup_stack(
+        Code::Hydrogen, 1_GeV, (DummyEnvironment::BaseNodeType* const)nodePtr, *csPtr);
+    { [[maybe_unused]] auto const& dummy_StackPtr = stackPtr; }
+
+    auto const targetCode = Code::Uranium;
+    auto const projectile4mom =
+        FourVector{calculate_total_energy(p, get_mass(projectileCode)),
+                   MomentumVector{cs, 0_eV, 0_eV, p}};
+    auto const target4mom =
+        FourVector{get_mass(targetCode), MomentumVector{cs, 0_eV, 0_eV, 0_eV}};
+
+    REQUIRE_THROWS(flukaModel.doInteraction(*secViewPtr, projectileCode, targetCode,
+                                            projectile4mom, target4mom));
+  }
+}