diff --git a/corsika/detail/modules/ObservationPlane.inl b/corsika/detail/modules/ObservationPlane.inl
index eba312b29975cf5ba031e74533ea2457dad8aa5b..8f4a658b504d896f674943f4729cc2a30336329f 100644
--- a/corsika/detail/modules/ObservationPlane.inl
+++ b/corsika/detail/modules/ObservationPlane.inl
@@ -18,9 +18,7 @@ namespace corsika {
       , plane_(obsPlane)
       , xAxis_(x_axis.normalized())
       , yAxis_(obsPlane.getNormal().cross(xAxis_))
-      , deleteOnHit_(deleteOnHit)
-      , energy_ground_(0_GeV)
-      , count_ground_(0) {}
+      , deleteOnHit_(deleteOnHit) {}
 
   template <typename TTracking, typename TOutput>
   template <typename TParticle, typename TTrajectory>
@@ -60,8 +58,6 @@ namespace corsika {
     CORSIKA_LOG_TRACE("Particle detected absorbed={}", deleteOnHit_);
 
     if (deleteOnHit_) {
-      count_ground_++;
-      energy_ground_ += energy;
       return ProcessReturn::ParticleAbsorbed;
     } else {
       return ProcessReturn::Ok;
@@ -94,17 +90,6 @@ namespace corsika {
     return trajectory.getLength(fractionOfIntersection);
   }
 
-  template <typename TTracking, typename TOutput>
-  inline void ObservationPlane<TTracking, TOutput>::showResults() const {
-    CORSIKA_LOG_INFO(
-        "\n ******************************\n"
-        " ObservationPlane: \n"
-        " energy an ground (GeV)     :  {}\n"
-        " no. of particles at ground :  {}\n"
-        " ******************************",
-        energy_ground_ / 1_GeV, count_ground_);
-  }
-
   template <typename TTracking, typename TOutput>
   inline YAML::Node ObservationPlane<TTracking, TOutput>::getConfig() const {
     using namespace units::si;
@@ -148,10 +133,4 @@ namespace corsika {
     return node;
   }
 
-  template <typename TTracking, typename TOutput>
-  inline void ObservationPlane<TTracking, TOutput>::reset() {
-    energy_ground_ = 0_GeV;
-    count_ground_ = 0;
-  }
-
 } // namespace corsika
diff --git a/corsika/detail/modules/StackInspector.inl b/corsika/detail/modules/StackInspector.inl
index 06705ac8f6be0892750e8c3942e4710d273b6dc8..f801abf624705b5bb5891bd34304c3ed0570936b 100644
--- a/corsika/detail/modules/StackInspector.inl
+++ b/corsika/detail/modules/StackInspector.inl
@@ -54,20 +54,24 @@ namespace corsika {
       }
     }
 
-    auto const now = std::chrono::system_clock::now();
-    std::chrono::duration<double> const elapsed_seconds = now - StartTime_;
-    std::time_t const now_time = std::chrono::system_clock::to_time_t(now);
+    std::chrono::system_clock::time_point const now = std::chrono::system_clock::now();
+    std::chrono::duration<double> const elapsed_seconds = now - StartTime_; // seconds
     auto const dE = E0_ - Etot;
     if (dE < dE_threshold_) return;
     double const progress = dE / E0_;
 
+    // for printout
+    std::time_t const now_time = std::chrono::system_clock::to_time_t(now);
     std::time_t const start_time = std::chrono::system_clock::to_time_t(StartTime_);
 
     if (progress > 0) {
 
       double const eta_seconds = elapsed_seconds.count() / progress;
-      std::time_t const eta_time = std::chrono::system_clock::to_time_t(
-          StartTime_ + std::chrono::seconds((int)eta_seconds));
+      std::chrono::system_clock::time_point const eta =
+          StartTime_ + std::chrono::seconds((int)eta_seconds);
+
+      // for printout
+      std::time_t const eta_time = std::chrono::system_clock::to_time_t(eta);
 
       int const yday0 = std::localtime(&start_time)->tm_yday;
       int const yday1 = std::localtime(&eta_time)->tm_yday;
diff --git a/corsika/detail/modules/urqmd/UrQMD.inl b/corsika/detail/modules/urqmd/UrQMD.inl
index 9e070f1c62df8860f9bbb4d89df26f9c87728e09..1772df106dcb04d8744eac83959e833f5fa27f9a 100644
--- a/corsika/detail/modules/urqmd/UrQMD.inl
+++ b/corsika/detail/modules/urqmd/UrQMD.inl
@@ -13,6 +13,7 @@
 
 #include <corsika/framework/core/ParticleProperties.hpp>
 #include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/framework/core/EnergyMomentumOperations.hpp>
 #include <corsika/framework/geometry/QuantityVector.hpp>
 #include <corsika/framework/geometry/Vector.hpp>
 #include <corsika/framework/utility/COMBoost.hpp>
@@ -280,9 +281,11 @@ namespace corsika::urqmd {
       CORSIKA_LOG_DEBUG(" {} {} {} ", i, code, momentum.getComponents());
 
       HEPEnergyType const mass = get_mass(code);
-      HEPEnergyType Ekin = sqrt(momentum.getSquaredNorm() + mass * mass) - mass;
+      HEPEnergyType const Ekin = calculate_kinetic_energy(momentum.getNorm(), mass);
       if (Ekin <= 0_GeV) {
-        CORSIKA_LOG_WARN("Negative kinetic energy {} {}. Skipping.", code, Ekin);
+        if (Ekin < 0_GeV) {
+          CORSIKA_LOG_WARN("Negative kinetic energy {} {}. Skipping.", code, Ekin);
+        }
         view.addSecondary(
             std::make_tuple(code, 0_eV, DirectionVector{originalCS, {0, 0, 0}}));
       } else {
diff --git a/corsika/detail/modules/writers/EnergyLossWriter.inl b/corsika/detail/modules/writers/EnergyLossWriter.inl
index f4ef75adab355276b34e0e5754cf6c913c4c5eed..e4101afeb2145f70e9f3069bd43b1a7c380b4a6e 100644
--- a/corsika/detail/modules/writers/EnergyLossWriter.inl
+++ b/corsika/detail/modules/writers/EnergyLossWriter.inl
@@ -23,7 +23,8 @@ namespace corsika {
                                                      GrammageType dX,
                                                      unsigned int const nBins,
                                                      GrammageType dX_threshold)
-      : showerAxis_(axis)
+      : TOutput(dEdX_output::ProfileIndexNames)
+      , showerAxis_(axis)
       , dX_(dX)
       , nBins_(nBins)
       , dX_threshold_(dX_threshold) {}
@@ -51,9 +52,9 @@ namespace corsika {
   inline void EnergyLossWriter<TOutput>::endOfShower(unsigned int const showerId) {
 
     int iRow{0};
-    for (Profile const& row : profile_) {
+    for (dEdX_output::Profile const& row : profile_) {
       // here: write to underlying writer (e.g. parquet)
-      TOutput::write(showerId, iRow * dX_, row.at(static_cast<int>(ProfileIndex::Total)));
+      TOutput::write(showerId, iRow * dX_, row);
       iRow++;
     }
 
@@ -100,7 +101,7 @@ namespace corsika {
       CORSIKA_LOGGER_TRACE(TOutput::getLogger(),
                            "filling bin={} with weight {} : dE={} GeV ", bin, weight,
                            increment / 1_GeV);
-      profile_[bin][static_cast<int>(ProfileIndex::Total)] += increment;
+      profile_[bin][static_cast<int>(dEdX_output::ProfileIndex::Total)] += increment;
       energyCount += increment;
     };
 
@@ -129,7 +130,7 @@ namespace corsika {
     CORSIKA_LOGGER_TRACE(TOutput::getLogger(), "add local energy loss bin={} dE={} GeV ",
                          bin, dE / 1_GeV);
 
-    profile_[bin][static_cast<int>(ProfileIndex::Total)] += dE;
+    profile_[bin][static_cast<int>(dEdX_output::ProfileIndex::Total)] += dE;
   }
 
   template <typename TOutput>
@@ -141,11 +142,12 @@ namespace corsika {
 
     if (abs(bstart - floor(bstart + 0.5)) > 1e-2 ||
         abs(bend - floor(bend + 0.5)) > 1e-2 || abs(bend - bstart - 1) > 1e-2) {
-      CORSIKA_LOGGER_ERROR(TOutput::getLogger(),
-                           "CONEX and Corsika8 dX grammage binning are not the same! "
-                           "Xstart={} Xend={} dX={} g/cm2",
-                           Xstart / 1_g * square(1_cm), Xend / 1_g * square(1_cm),
-                           dX_ / 1_g * square(1_cm));
+      CORSIKA_LOGGER_ERROR(
+          TOutput::getLogger(),
+          "CascadeEquation (CONEX) and Corsika8 dX grammage binning are not the same! "
+          "Xstart={} Xend={} dX={} g/cm2",
+          Xstart / 1_g * square(1_cm), Xend / 1_g * square(1_cm),
+          dX_ / 1_g * square(1_cm));
       throw std::runtime_error(
           "CONEX and Corsika8 dX grammage binning are not the same!");
     }
@@ -154,14 +156,14 @@ namespace corsika {
     CORSIKA_LOGGER_TRACE(TOutput::getLogger(),
                          "add binned energy loss {} {} bin={} dE={} GeV ", bstart, bend,
                          bin, dE / 1_GeV);
-    profile_[bin][static_cast<int>(ProfileIndex::Total)] += dE;
+    profile_[bin][static_cast<int>(dEdX_output::ProfileIndex::Total)] += dE;
   }
 
   template <typename TOutput>
-  inline HEPEnergyType EnergyLossWriter<TOutput>::getTotal() const {
+  inline HEPEnergyType EnergyLossWriter<TOutput>::getEnergyLost() const {
     HEPEnergyType tot = HEPEnergyType::zero();
-    for (Profile const& row : profile_)
-      tot += row.at(static_cast<int>(ProfileIndex::Total));
+    for (dEdX_output::Profile const& row : profile_)
+      tot += row.at(static_cast<int>(dEdX_output::ProfileIndex::Total));
     return tot;
   }
 
@@ -177,8 +179,6 @@ namespace corsika {
     node["nbins"] = nBins_;
     node["grammage_threshold"] = dX_threshold_ / (1_g / square(1_cm));
 
-    //! \todo add shower axis to config
-
     return node;
   }
 
@@ -187,12 +187,13 @@ namespace corsika {
 
     // determined Xmax and dEdXmax from quadratic interpolation
     double maximum = 0;
-    unsigned int iMaximum = 0;
-    for (unsigned int i = 0; i < profile_.size() - 3; ++i) {
-      double value = (profile_[i + 0].at(static_cast<int>(ProfileIndex::Total)) +
-                      profile_[i + 1].at(static_cast<int>(ProfileIndex::Total)) +
-                      profile_[i + 2].at(static_cast<int>(ProfileIndex::Total))) /
-                     1_GeV;
+    size_t iMaximum = 0;
+    for (size_t i = 0; i < profile_.size() - 3; ++i) {
+      double value =
+          (profile_[i + 0].at(static_cast<int>(dEdX_output::ProfileIndex::Total)) +
+           profile_[i + 1].at(static_cast<int>(dEdX_output::ProfileIndex::Total)) +
+           profile_[i + 2].at(static_cast<int>(dEdX_output::ProfileIndex::Total))) /
+          1_GeV;
       if (value > maximum) {
         maximum = value;
         iMaximum = i;
@@ -203,12 +204,15 @@ namespace corsika {
 
     auto [Xmax, dEdXmax] = FindXmax::interpolateProfile(
         dX * (0.5 + iMaximum), dX * (1.5 + iMaximum), dX * (2.5 + iMaximum),
-        profile_[iMaximum + 0].at(static_cast<int>(ProfileIndex::Total)) / 1_GeV,
-        profile_[iMaximum + 1].at(static_cast<int>(ProfileIndex::Total)) / 1_GeV,
-        profile_[iMaximum + 2].at(static_cast<int>(ProfileIndex::Total)) / 1_GeV);
+        profile_[iMaximum + 0].at(static_cast<int>(dEdX_output::ProfileIndex::Total)) /
+            1_GeV,
+        profile_[iMaximum + 1].at(static_cast<int>(dEdX_output::ProfileIndex::Total)) /
+            1_GeV,
+        profile_[iMaximum + 2].at(static_cast<int>(dEdX_output::ProfileIndex::Total)) /
+            1_GeV);
 
     YAML::Node summary;
-    summary["total"] = getTotal() / 1_GeV;
+    summary["sum_dEdX"] = getEnergyLost() / 1_GeV;
     summary["Xmax"] = Xmax;
     summary["dEdXmax"] = dEdXmax;
     return summary;
diff --git a/corsika/detail/modules/writers/EnergyLossWriterParquet.inl b/corsika/detail/modules/writers/EnergyLossWriterParquet.inl
index 1680e421ac51725faab57a3760c43fe6c1a8b6d9..55b301f509bc094a878e529e8ebb58360dc6568a 100644
--- a/corsika/detail/modules/writers/EnergyLossWriterParquet.inl
+++ b/corsika/detail/modules/writers/EnergyLossWriterParquet.inl
@@ -18,9 +18,13 @@
 
 namespace corsika {
 
-  inline EnergyLossWriterParquet::EnergyLossWriterParquet() {}
+  template <size_t NColumns>
+  inline EnergyLossWriterParquet<NColumns>::EnergyLossWriterParquet(
+      std::array<const char*, NColumns> const& colNames)
+      : columns_(colNames) {}
 
-  inline void EnergyLossWriterParquet::startOfLibrary(
+  template <size_t NColumns>
+  inline void EnergyLossWriterParquet<NColumns>::startOfLibrary(
       boost::filesystem::path const& directory) {
 
     // setup the streamer
@@ -32,28 +36,38 @@ namespace corsika {
     // build the schema
     output_.addField("X", parquet::Repetition::REQUIRED, parquet::Type::FLOAT,
                      parquet::ConvertedType::NONE);
-    output_.addField("total", parquet::Repetition::REQUIRED, parquet::Type::FLOAT,
-                     parquet::ConvertedType::NONE);
+    for (auto const& col : columns_) {
+      output_.addField(col, parquet::Repetition::REQUIRED, parquet::Type::FLOAT,
+                       parquet::ConvertedType::NONE);
+    }
 
     // and build the streamer
     output_.buildStreamer();
   }
 
-  inline void EnergyLossWriterParquet::write(unsigned int const showerId,
-                                             GrammageType const grammage,
-                                             HEPEnergyType const total) {
-
-    double const dX = grammage / 1_g * square(1_cm); // g/cm2
+  template <size_t NColumns>
+  inline void EnergyLossWriterParquet<NColumns>::write(
+      unsigned int const showerId, GrammageType const grammage,
+      std::array<HEPEnergyType, NColumns> const& data) {
 
     // and write the data into the column
-    *(output_.getWriter()) << showerId << static_cast<float>(dX)
-                           << static_cast<float>(total / 1_GeV) << parquet::EndRow;
+    *(output_.getWriter()) << showerId
+                           << static_cast<float>(grammage / 1_g * square(1_cm));
+    for (HEPEnergyType const dedx : data) {
+      *(output_.getWriter()) << static_cast<float>(dedx / 1_GeV);
+    }
+    *(output_.getWriter()) << parquet::EndRow;
   }
 
-  inline void EnergyLossWriterParquet::startOfShower(unsigned int const) {}
+  template <size_t NColumns>
+  inline void EnergyLossWriterParquet<NColumns>::startOfShower(unsigned int const) {}
 
-  inline void EnergyLossWriterParquet::endOfShower(unsigned int const) {}
+  template <size_t NColumns>
+  inline void EnergyLossWriterParquet<NColumns>::endOfShower(unsigned int const) {}
 
-  inline void EnergyLossWriterParquet::endOfLibrary() { output_.closeStreamer(); }
+  template <size_t NColumns>
+  inline void EnergyLossWriterParquet<NColumns>::endOfLibrary() {
+    output_.closeStreamer();
+  }
 
 } // namespace corsika
diff --git a/corsika/detail/modules/writers/LongitudinalProfileWriterParquet.inl b/corsika/detail/modules/writers/LongitudinalProfileWriterParquet.inl
index 4873bc486cbcba7d307eed92183ae76d7820516c..2e5801fee1882cc0ee44c522ae63fc1b205a5f4b 100644
--- a/corsika/detail/modules/writers/LongitudinalProfileWriterParquet.inl
+++ b/corsika/detail/modules/writers/LongitudinalProfileWriterParquet.inl
@@ -19,14 +19,13 @@
 
 namespace corsika {
 
-  inline LongitudinalProfileWriterParquet::LongitudinalProfileWriterParquet(
-      ShowerAxis const& showerAxis, GrammageType const dX, unsigned int const nBins)
-      : output_()
-      , showerAxis_(showerAxis)
-      , dX_(dX) // profile binning
-      , nBins_(nBins) {}
-
-  inline void LongitudinalProfileWriterParquet::startOfLibrary(
+  template <size_t NColumns>
+  inline LongitudinalProfileWriterParquet<NColumns>::LongitudinalProfileWriterParquet(
+      std::array<const char*, NColumns> const& columns)
+      : columns_(columns) {}
+
+  template <size_t NColumns>
+  inline void LongitudinalProfileWriterParquet<NColumns>::startOfLibrary(
       boost::filesystem::path const& directory) {
     // setup the streamer
     output_.initStreamer((directory / "profile.parquet").string());
@@ -37,155 +36,40 @@ namespace corsika {
     // build the schema
     output_.addField("X", parquet::Repetition::REQUIRED, parquet::Type::FLOAT,
                      parquet::ConvertedType::NONE);
-    output_.addField("charged", parquet::Repetition::REQUIRED, parquet::Type::FLOAT,
-                     parquet::ConvertedType::NONE);
-    output_.addField("hadron", parquet::Repetition::REQUIRED, parquet::Type::FLOAT,
-                     parquet::ConvertedType::NONE);
-    output_.addField("muminus", parquet::Repetition::REQUIRED, parquet::Type::FLOAT,
-                     parquet::ConvertedType::NONE);
-    output_.addField("muplus", parquet::Repetition::REQUIRED, parquet::Type::FLOAT,
-                     parquet::ConvertedType::NONE);
-    output_.addField("photon", parquet::Repetition::REQUIRED, parquet::Type::FLOAT,
-                     parquet::ConvertedType::NONE);
-    output_.addField("electron", parquet::Repetition::REQUIRED, parquet::Type::FLOAT,
-                     parquet::ConvertedType::NONE);
-    output_.addField("positron", parquet::Repetition::REQUIRED, parquet::Type::FLOAT,
-                     parquet::ConvertedType::NONE);
+    for (auto const& col : columns_) {
+      output_.addField(col, parquet::Repetition::REQUIRED, parquet::Type::FLOAT,
+                       parquet::ConvertedType::NONE);
+    }
 
     // and build the streamer
     output_.buildStreamer();
   }
 
-  inline void LongitudinalProfileWriterParquet::startOfShower(unsigned int const) {
-    // initialize profile
-    profile_.clear();
-    profile_.resize(nBins_);
-  }
+  template <size_t NColumns>
+  inline void LongitudinalProfileWriterParquet<NColumns>::startOfShower(
+      unsigned int const) {}
 
-  inline void LongitudinalProfileWriterParquet::endOfShower(unsigned int const showerId) {
-    int iRow = 0;
-    for (ProfileData const& row : profile_) {
-
-      double const dX = dX_ / 1_g * square(1_cm); // g/cm2
-
-      // clang-format off
-      *(output_.getWriter()) << showerId << static_cast<float>(iRow * dX)
-                             << static_cast<float>(row.at(static_cast<int>(ProfileIndex::Charged)))
-                             << static_cast<float>(row.at(static_cast<int>(ProfileIndex::Hadron)))
-                             << static_cast<float>(row.at(static_cast<int>(ProfileIndex::MuMinus)))
-                             << static_cast<float>(row.at(static_cast<int>(ProfileIndex::MuPlus)))
-                             << static_cast<float>(row.at(static_cast<int>(ProfileIndex::Photon)))
-                             << static_cast<float>(row.at(static_cast<int>(ProfileIndex::Electron)))
-                             << static_cast<float>(row.at(static_cast<int>(ProfileIndex::Positron)))
-                             << parquet::EndRow;
-      // clang-format on
-      ++iRow;
-    }
-  }
+  template <size_t NColumns>
+  inline void LongitudinalProfileWriterParquet<NColumns>::endOfShower(
+      unsigned int const) {}
 
-  inline void LongitudinalProfileWriterParquet::endOfLibrary() {
+  template <size_t NColumns>
+  inline void LongitudinalProfileWriterParquet<NColumns>::endOfLibrary() {
     output_.closeStreamer();
   }
 
-  template <typename TTrack>
-  inline void LongitudinalProfileWriterParquet::write(TTrack const& track, Code const pid,
-                                                      double const weight) {
-    GrammageType const grammageStart = showerAxis_.getProjectedX(track.getPosition(0));
-    GrammageType const grammageEnd = showerAxis_.getProjectedX(track.getPosition(1));
-
-    // Note: particle may go also "upward", thus, grammageEnd<grammageStart
-    int const binStart = std::ceil(grammageStart / dX_);
-    int const binEnd = std::floor(grammageEnd / dX_);
-
-    for (int bin = binStart; bin <= binEnd; ++bin) {
-      if (pid == Code::Photon) {
-        profile_.at(bin)[static_cast<int>(ProfileIndex::Photon)] += weight;
-      } else if (pid == Code::Positron) {
-        profile_.at(bin)[static_cast<int>(ProfileIndex::Positron)] += weight;
-      } else if (pid == Code::Electron) {
-        profile_.at(bin)[static_cast<int>(ProfileIndex::Electron)] += weight;
-      } else if (pid == Code::MuPlus) {
-        profile_.at(bin)[static_cast<int>(ProfileIndex::MuPlus)] += weight;
-      } else if (pid == Code::MuMinus) {
-        profile_.at(bin)[static_cast<int>(ProfileIndex::MuMinus)] += weight;
-      } else if (is_hadron(pid)) {
-        profile_.at(bin)[static_cast<int>(ProfileIndex::Hadron)] += weight;
-      }
-      if (is_charged(pid)) {
-        profile_[bin][static_cast<int>(ProfileIndex::Charged)] += weight;
-      }
-    }
-  }
-
-  inline void LongitudinalProfileWriterParquet::write(GrammageType const Xstart,
-                                                      GrammageType const Xend,
-                                                      Code const pid,
-                                                      double const weight) {
-    double const bstart = Xstart / dX_;
-    double const bend = Xend / dX_;
-
-    if (abs(bstart - floor(bstart + 0.5)) > 1e-2 ||
-        abs(bend - floor(bend + 0.5)) > 1e-2 || abs(bend - bstart - 1) > 1e-2) {
-      CORSIKA_LOGGER_ERROR(getLogger(),
-                           "CONEX and Corsika8 dX grammage binning are not the same! "
-                           "Xstart={} Xend={} dX={}",
-                           Xstart / 1_g * square(1_cm), Xend / 1_g * square(1_cm),
-                           dX_ / 1_g * square(1_cm));
-      throw std::runtime_error(
-          "CONEX and Corsika8 dX grammage binning are not the same!");
-    }
-
-    int const bin = int((bend + bstart) / 2);
-
-    if (pid == Code::Photon) {
-      profile_.at(bin)[static_cast<int>(ProfileIndex::Photon)] += weight;
-    } else if (pid == Code::Positron) {
-      profile_.at(bin)[static_cast<int>(ProfileIndex::Positron)] += weight;
-    } else if (pid == Code::Electron) {
-      profile_.at(bin)[static_cast<int>(ProfileIndex::Electron)] += weight;
-    } else if (pid == Code::MuPlus) {
-      profile_.at(bin)[static_cast<int>(ProfileIndex::MuPlus)] += weight;
-    } else if (pid == Code::MuMinus) {
-      profile_.at(bin)[static_cast<int>(ProfileIndex::MuMinus)] += weight;
-    } else if (is_hadron(pid)) {
-      profile_.at(bin)[static_cast<int>(ProfileIndex::Hadron)] += weight;
-    }
-    if (is_charged(pid)) {
-      profile_[bin][static_cast<int>(ProfileIndex::Charged)] += weight;
-    }
-  }
+  template <size_t NColumns>
+  inline void LongitudinalProfileWriterParquet<NColumns>::write(
+      unsigned int const showerId, GrammageType const grammage,
+      std::array<double, NColumns> const& data) {
 
-  inline YAML::Node LongitudinalProfileWriterParquet::getSummary() const {
-    // determined Xmax and dEdXmax from quadratic interpolation
-
-    YAML::Node summary;
-
-    for (int index = 0; index < static_cast<int>(ProfileIndex::Entries); ++index) {
-      // first find highest 3-boxcar sum
-      double maximum = 0;
-      unsigned int iMaximum = 0;
-      for (unsigned int i = 0; i < profile_.size() - 3; ++i) {
-        double value = profile_[i + 0].at(index) + profile_[i + 1].at(index) +
-                       profile_[i + 2].at(index);
-        if (value > maximum) {
-          maximum = value;
-          iMaximum = i;
-        }
-      }
-
-      double const dX = dX_ / 1_g * square(1_cm);
-
-      // quadratic interpolation of maximum in 3 highest points
-      auto [Xmax, Nmax] = FindXmax::interpolateProfile(
-          dX * (0.5 + iMaximum), dX * (1.5 + iMaximum), dX * (2.5 + iMaximum),
-          profile_[iMaximum + 0].at(index), profile_[iMaximum + 1].at(index),
-          profile_[iMaximum + 2].at(index));
-
-      std::string const name = ProfileIndexNames[index];
-      summary[name]["Xmax"] = Xmax;
-      summary[name]["Nmax"] = Nmax;
+    // and write the data into the column
+    *(output_.getWriter()) << showerId
+                           << static_cast<float>(grammage / 1_g * square(1_cm));
+    for (double const weight : data) {
+      *(output_.getWriter()) << static_cast<float>(weight);
     }
-    return summary;
+    *(output_.getWriter()) << parquet::EndRow;
   }
 
 } // namespace corsika
diff --git a/corsika/detail/modules/writers/ParticleWriterParquet.inl b/corsika/detail/modules/writers/ParticleWriterParquet.inl
index fa26e368dc956a08878885d76b18d490041c3109..74e8b45411b3c90f563d16baf3724f1787182cb0 100644
--- a/corsika/detail/modules/writers/ParticleWriterParquet.inl
+++ b/corsika/detail/modules/writers/ParticleWriterParquet.inl
@@ -44,6 +44,10 @@ namespace corsika {
     output_.buildStreamer();
 
     showerId_ = 0;
+  }
+
+  inline void ParticleWriterParquet::startOfShower(unsigned int const showerId) {
+    showerId_ = showerId;
     totalEnergy_ = 0_eV;
     countHadrons_ = 0;
     countOthers_ = 0;
@@ -51,10 +55,6 @@ namespace corsika {
     countMuons_ = 0;
   }
 
-  inline void ParticleWriterParquet::startOfShower(unsigned int const showerId) {
-    showerId_ = showerId;
-  }
-
   inline void ParticleWriterParquet::endOfShower(unsigned int const) {}
 
   inline void ParticleWriterParquet::endOfLibrary() { output_.closeStreamer(); }
@@ -103,6 +103,7 @@ namespace corsika {
    */
   YAML::Node ParticleWriterParquet::getSummary() const {
     YAML::Node summary;
+    summary["Eground"] = totalEnergy_ / 1_GeV;
     summary["hadrons"] = countHadrons_;
     summary["muons"] = countMuons_;
     summary["em"] = countEM_;
diff --git a/corsika/modules/ObservationPlane.hpp b/corsika/modules/ObservationPlane.hpp
index aec9f663164a23bfe9216629d12c95539c44c962..21ceb790c2a8d90be35a9a6bcb6cb1078f2388cd 100644
--- a/corsika/modules/ObservationPlane.hpp
+++ b/corsika/modules/ObservationPlane.hpp
@@ -62,9 +62,6 @@ namespace corsika {
     template <typename TParticle, typename TTrajectory>
     LengthType getMaxStepLength(TParticle const&, TTrajectory const& vTrajectory);
 
-    void showResults() const;
-    void reset();
-    HEPEnergyType getEnergyGround() const { return energy_ground_; }
     YAML::Node getConfig() const;
 
   private:
@@ -72,8 +69,6 @@ namespace corsika {
     DirectionVector const xAxis_;
     DirectionVector const yAxis_;
     bool const deleteOnHit_;
-    HEPEnergyType energy_ground_;
-    unsigned int count_ground_;
   };
   //! @}
 } // namespace corsika
diff --git a/corsika/modules/StackInspector.hpp b/corsika/modules/StackInspector.hpp
index 6723ec3d9bedb0ff3097463133cc5d1a5e8dbe7c..6e723a6ced41be1e34a970991d3e55fc40c6af88 100644
--- a/corsika/modules/StackInspector.hpp
+++ b/corsika/modules/StackInspector.hpp
@@ -48,7 +48,7 @@ namespace corsika {
     bool ReportStack_;
     HEPEnergyType E0_;
     const HEPEnergyType dE_threshold_ = 1_eV;
-    decltype(std::chrono::system_clock::now()) StartTime_;
+    std::chrono::system_clock::time_point StartTime_;
   };
 
 } // namespace corsika
diff --git a/corsika/modules/writers/EnergyLossWriter.hpp b/corsika/modules/writers/EnergyLossWriter.hpp
index 5436cb5219d9538d450993f62b697cdeeaea8e43..58cc69ebcb034600ba8755a20d57d353b50af71a 100644
--- a/corsika/modules/writers/EnergyLossWriter.hpp
+++ b/corsika/modules/writers/EnergyLossWriter.hpp
@@ -18,8 +18,13 @@
 #include <vector>
 #include <array>
 
+/**
+ * @file EnergyLossWriter.hpp
+ */
+
 namespace corsika {
 
+  // clang-format-off
   /**
    * The energy loss writer can be used to pool several energy loss processes into one
    * output file/stream.
@@ -27,6 +32,7 @@ namespace corsika {
    * Typically many processes/modules can lead to energy losses in the shower. The
    * EnergyLossWriter can be used in combination with the SubWriter class to collect all
    * of them into a single output stream:
+   *
    * \code {.cpp}
    * # showerAxis must be a ShowerAxis object
    * # the X binning can be specified.
@@ -38,16 +44,68 @@ namespace corsika {
    * ...
    * \endcode
    *
+   * The EnergyLossWriter processes data on single-particle-level. The final output
+   * writer, e.g. EnergyLossWriterParquet, processes data on profile-level (bins in X).
    * The default output option is parquet format.
    *
    * @tparam TOutput
    */
+  // clang-format-on
 
-  template <typename TOutput = EnergyLossWriterParquet>
-  class EnergyLossWriter : public TOutput {
+  /**
+   * Local helper namespace to store number and names of dEdX profile columns.
+   */
+  namespace dEdX_output {
 
+    /**
+     * Definition of longitudinal profile columns.
+     */
     enum class ProfileIndex { Total, Entries };
-    typedef std::array<HEPEnergyType, static_cast<int>(ProfileIndex::Entries)> Profile;
+
+    /**
+     * Number of columns (static).
+     */
+    size_t constexpr NColumns = static_cast<int>(ProfileIndex::Entries);
+
+    /**
+     * Names of columns in output.
+     */
+    static std::array<char const*, NColumns> constexpr ProfileIndexNames{{"total"}};
+
+    /**
+     * Data type to store column data.
+     */
+    typedef std::array<HEPEnergyType, NColumns> Profile;
+
+  } // namespace dEdX_output
+
+  /**
+   * The EnergyLossWriter can be used to pool the dEdX energy loss of several
+   * processes/modules into one output file/stream.
+   *
+   * Typically several processes/modules can lead to energy losses along the shower axis
+   * in the shower. The EnergyLossWriter can be used in combination with the SubWriter
+   * class to collect all of them into a single output stream:
+   *
+   * \code {.cpp}
+   * # showerAxis must be a ShowerAxis object
+   * # the X binning can be specified.
+   * EnergyLossWriter dEdX{showerAxis, 10_g / square(1_cm), 200};
+   * # add to OutputManager:
+   * output.add("energyloss", dEdX);
+   * # add SubWriters, e.g. BetheBlochPDG, CONEX:
+   * BetheBlochPDG<SubWriter<decltype(dEdX)>> long{dEdX};
+   * CONEXhybrid<SubWriter<decltype(dEdX)>> conex{..., dEdX};
+   * ...
+   * \endcode
+   *
+   * The default output option is parquet format.
+   *
+   * @tparam TOutput
+   */
+
+  template <typename TOutput = EnergyLossWriterParquet<dEdX_output::NColumns>>
+  class EnergyLossWriter : public TOutput {
 
   public:
     /**
@@ -89,7 +147,7 @@ namespace corsika {
      *
      * @return HEPEnergyType The total energy.
      */
-    HEPEnergyType getTotal() const;
+    HEPEnergyType getEnergyLost() const;
 
     /**
      * Return a summary.
@@ -106,7 +164,7 @@ namespace corsika {
     GrammageType dX_;              ///< binning of profile.
     size_t nBins_;                 ///< number of profile bins.
     GrammageType dX_threshold_;    ///< too short tracks are discarded.
-    std::vector<Profile> profile_; // longitudinal profile
+    std::vector<dEdX_output::Profile> profile_; // longitudinal profile
 
   }; // namespace corsika
 
diff --git a/corsika/modules/writers/EnergyLossWriterParquet.hpp b/corsika/modules/writers/EnergyLossWriterParquet.hpp
index 3edc4cdd614e30e26a26135871791d88c26b874a..852696982645d57533f5fb547c0b55991a463d24 100644
--- a/corsika/modules/writers/EnergyLossWriterParquet.hpp
+++ b/corsika/modules/writers/EnergyLossWriterParquet.hpp
@@ -14,18 +14,30 @@
 #include <corsika/framework/core/PhysicalUnits.hpp>
 #include <corsika/media/ShowerAxis.hpp>
 
-#include <vector>
 #include <array>
 
 namespace corsika {
 
+  /**
+   * The actual writer to save dEdX data to disk.
+   *
+   * The purpose of this class is not to collect single-particle-level energy loss data.
+   * But to write entire binned profiles to disk at the end of a shower.
+   * To fill the shower data, you have to use EnergyLossWriter in combination with
+   * SubWriter. The EnergyLossWriterParquet is the default output mode of the
+   * EnergyLossWriter.
+   *
+   * @tparam NColumn -- the number of columns written to output. column names and data
+   * must be provided consistently.
+   */
+  template <size_t NColumns>
   class EnergyLossWriterParquet : public BaseOutput {
 
   public:
     /**
      * Construct a new writer.
      */
-    EnergyLossWriterParquet();
+    EnergyLossWriterParquet(std::array<const char*, NColumns> const& colNames);
 
     /**
      * Called at the start of each library.
@@ -54,10 +66,11 @@ namespace corsika {
      * Write energy lost to the file.
      */
     void write(unsigned int const showerId, GrammageType const grammage,
-               HEPEnergyType const total);
+               std::array<HEPEnergyType, NColumns> const& data);
 
   private:
-    ParquetStreamer output_; ///< The primary output file.
+    std::array<const char*, NColumns> columns_; ///< column names
+    ParquetStreamer output_;                    ///< The primary output file.
 
   }; // namespace corsika
 
diff --git a/corsika/modules/writers/LongitudinalProfileWriterParquet.hpp b/corsika/modules/writers/LongitudinalProfileWriterParquet.hpp
index 0c4570c71eaa7940b4d6d30a093b33704c1c39c7..bba7000ff30f73260740c66ab35a3539f97259b6 100644
--- a/corsika/modules/writers/LongitudinalProfileWriterParquet.hpp
+++ b/corsika/modules/writers/LongitudinalProfileWriterParquet.hpp
@@ -12,56 +12,44 @@
 #include <corsika/output/ParquetStreamer.hpp>
 #include <corsika/framework/core/ParticleProperties.hpp>
 #include <corsika/framework/core/PhysicalUnits.hpp>
-#include <corsika/media/ShowerAxis.hpp>
 
-#include <vector>
 #include <array>
-#include <string>
 
 namespace corsika {
 
+  /**
+   * The actual writer to save longitudinal profile data to disk.
+   *
+   * The purpose of this class is not to collect single-particle-level profile data,
+   * but to write entire binned profiles to disk at the end of a shower.
+   * To fill the shower data, you have to use ProfileWriter in combination with
+   * SubWriter. The LongitudinalProfileWriterParquet is the default output mode of the
+   * LongitudinalWriter.
+   */
+
+  template <size_t NColumns>
   class LongitudinalProfileWriterParquet : public BaseOutput {
 
-    enum class ProfileIndex {
-      Charged,
-      Hadron,
-      Photon,
-      Electron,
-      Positron,
-      MuPlus,
-      MuMinus,
-      Entries
-    };
-
-    static std::array<
-        char const*, static_cast<int>(ProfileIndex::Entries)> constexpr ProfileIndexNames{
-        {"charged", "hadron", "photon", "electron", "positron", "muplus", "muminus"}};
-
-    typedef std::array<double, static_cast<int>(ProfileIndex::Entries)> ProfileData;
-
   public:
     /**
      * Construct a new writer.
      */
-    LongitudinalProfileWriterParquet(ShowerAxis const& axis,
-                                     GrammageType dX = 10_g /
-                                                       square(1_cm),  // profile binning
-                                     unsigned int const nBins = 200); // number of bins
+    LongitudinalProfileWriterParquet(std::array<const char*, NColumns> const& colNames);
 
     /**
      * Called at the start of each library.
      */
-    void startOfLibrary(boost::filesystem::path const& directory) final override;
+    void startOfLibrary(boost::filesystem::path const& directory) override;
 
     /**
      * Called at the start of each shower.
      */
-    void startOfShower(unsigned int const showerId) final override;
+    void startOfShower(unsigned int const showerId) override;
 
     /**
      * Called at the end of each shower.
      */
-    void endOfShower(unsigned int const showerId) final override;
+    void endOfShower(unsigned int const showerId) override;
 
     /**
      * Called at the end of each library.
@@ -69,33 +57,17 @@ namespace corsika {
      * This must also increment the run number since we override
      * the default behaviour of BaseOutput.
      */
-    void endOfLibrary() final override;
-
-    /**
-     * Add continuous profile.
-     */
-    template <typename TTrack>
-    void write(TTrack const& track, Code const PID, double const weight);
+    void endOfLibrary() override;
 
     /**
-     * Add binned profile.
+     * Add profile to disk.
      */
-    void write(GrammageType const Xstart, GrammageType const Xend, Code const PID,
-               double const weight);
-
-    /**
-     * Returns a summary of this output.
-     */
-    YAML::Node getSummary() const;
+    void write(unsigned int const showerId, GrammageType const grammage,
+               std::array<double, NColumns> const& data);
 
   private:
-    ParquetStreamer output_; ///< The parquet streamer for this process.
-
-  public:
-    ShowerAxis const& showerAxis_;     ///< conversion between geometry and grammage
-    GrammageType dX_;                  ///< binning of profile.
-    unsigned int nBins_;               ///< number of profile bins.
-    std::vector<ProfileData> profile_; // longitudinal profile
+    std::array<const char*, NColumns> columns_; //!< column names
+    ParquetStreamer output_; //!< The parquet streamer for this process.
 
   }; // namespace corsika
 
diff --git a/corsika/modules/writers/ParticleWriterParquet.hpp b/corsika/modules/writers/ParticleWriterParquet.hpp
index 6fa0bdd8c5626411eef561561355ed0c58fefaa3..df4ee482b287a875913efe19020cd75e8d315521 100644
--- a/corsika/modules/writers/ParticleWriterParquet.hpp
+++ b/corsika/modules/writers/ParticleWriterParquet.hpp
@@ -69,7 +69,7 @@ namespace corsika {
     /**
      * If plane is absorbing particles: return the total energy absorbed.
      */
-    HEPEnergyType getTotalEnergy() const { return totalEnergy_; }
+    HEPEnergyType getEnergyGround() const { return totalEnergy_; }
 
   private:
     ParquetStreamer output_; ///< The primary output file.
diff --git a/examples/cascade_example.cpp b/examples/cascade_example.cpp
index d372773d9e6ce02b434e59c7dc0ec38665c2c6d5..4a33fba5047efc32ad294f501ab6392e35bac382 100644
--- a/examples/cascade_example.cpp
+++ b/examples/cascade_example.cpp
@@ -19,7 +19,6 @@
 #include <corsika/output/OutputManager.hpp>
 #include <corsika/modules/writers/SubWriter.hpp>
 #include <corsika/modules/writers/EnergyLossWriter.hpp>
-#include <corsika/modules/writers/EnergyLossWriterParquet.hpp>
 
 #include <corsika/media/Environment.hpp>
 #include <corsika/media/HomogeneousMedium.hpp>
@@ -160,15 +159,15 @@ int main() {
   EAS.run();
   output.endOfShower();
 
-  const HEPEnergyType Efinal = dEdX.getTotal();
+  const HEPEnergyType Efinal = dEdX.getEnergyLost();
   CORSIKA_LOG_INFO(
       "\n"
       "total cut energy (GeV) : {}\n"
       "relative difference (%): {}\n"
       "total dEdX energy (GeV): {}\n"
       "relative difference (%): {}\n",
-      Efinal / 1_GeV, (Efinal / E0 - 1) * 100, dEdX.getTotal() / 1_GeV,
-      dEdX.getTotal() / E0 * 100);
+      Efinal / 1_GeV, (Efinal / E0 - 1) * 100, dEdX.getEnergyLost() / 1_GeV,
+      dEdX.getEnergyLost() / E0 * 100);
 
   output.endOfLibrary();
 }
diff --git a/examples/cascade_proton_example.cpp b/examples/cascade_proton_example.cpp
index 7739e9e10cb44b4ca17109f1484eb2effd81ed82..3d878acd6c70fcac937e0352927b274198d6c269 100644
--- a/examples/cascade_proton_example.cpp
+++ b/examples/cascade_proton_example.cpp
@@ -19,7 +19,6 @@
 #include <corsika/output/OutputManager.hpp>
 #include <corsika/modules/writers/SubWriter.hpp>
 #include <corsika/modules/writers/EnergyLossWriter.hpp>
-#include <corsika/modules/writers/EnergyLossWriterParquet.hpp>
 
 #include <corsika/media/Environment.hpp>
 #include <corsika/media/HomogeneousMedium.hpp>
@@ -129,7 +128,7 @@ int main() {
   corsika::pythia8::Decay decay;
 
   ShowerAxis const showerAxis{injectionPos, Vector{rootCS, 0_m, 0_m, -100_km}, env};
-  EnergyLossWriter<EnergyLossWriterParquet> dEdX{showerAxis};
+  EnergyLossWriter dEdX{showerAxis};
   output.add("energyloss", dEdX);
 
   BetheBlochPDG<SubWriter<decltype(dEdX)>> eLoss{dEdX};
diff --git a/examples/corsika.cpp b/examples/corsika.cpp
index ffae78868fad8859d63a33cd3a250ea8d3e46329..392f7cd22eaed92a536ac4a271fa1d54491a2c7f 100644
--- a/examples/corsika.cpp
+++ b/examples/corsika.cpp
@@ -29,7 +29,7 @@
 #include <corsika/output/OutputManager.hpp>
 #include <corsika/modules/writers/SubWriter.hpp>
 #include <corsika/modules/writers/EnergyLossWriter.hpp>
-#include <corsika/modules/writers/EnergyLossWriterParquet.hpp>
+#include <corsika/modules/writers/LongitudinalWriter.hpp>
 
 #include <corsika/media/Environment.hpp>
 #include <corsika/media/FlatExponential.hpp>
@@ -64,7 +64,6 @@
 #include <CLI/Config.hpp>
 
 #include <iomanip>
-#include <iostream>
 #include <limits>
 #include <string>
 
@@ -187,8 +186,7 @@ int main(int argc, char** argv) {
   // gets all messed up
   if (app.count("--pdg") == 0) {
     if ((app.count("-A") == 0) || (app.count("-Z") == 0)) {
-      std::cerr << "If --pdg is not provided, then both -A and -Z are required."
-                << std::endl;
+      CORSIKA_LOG_ERROR("If --pdg is not provided, then both -A and -Z are required.");
       return 1;
     }
   }
@@ -246,7 +244,7 @@ int main(int argc, char** argv) {
   auto const phiRad = app["--azimuth"]->as<double>() / 180. * M_PI;
 
   // convert Elab to Plab
-  HEPMomentumType P0 = sqrt((E0 - mass) * (E0 + mass));
+  HEPMomentumType P0 = calculate_momentum(E0, mass);
 
   // convert the momentum to the zenith and azimuth angle of the primary
   auto const [px, py, pz] =
@@ -321,13 +319,13 @@ int main(int argc, char** argv) {
 
   corsika::proposal::Interaction emCascade(env);
   // NOT available for PROPOSAL due to interface trouble:
-  //  InteractionCounter emCascadeCounted(emCascade);
+  // InteractionCounter emCascadeCounted(emCascade);
   // corsika::proposal::ContinuousProcess<SubWriter<decltype(dEdX)>> emContinuous(env);
   BetheBlochPDG<SubWriter<decltype(dEdX)>> emContinuous{dEdX};
 
-  LongitudinalProfile<corsika::LongitudinalProfileWriterParquet> longprof{
-      showerAxis, 10_g / square(1_cm), 200};
-  output.add("profile", longprof);
+  LongitudinalWriter profile{showerAxis, 10_g / square(1_cm), 200};
+  output.add("profile", profile);
+  LongitudinalProfile<SubWriter<decltype(profile)>> longprof{profile};
 
   corsika::urqmd::UrQMD urqmd;
   InteractionCounter urqmdCounted(urqmd);
@@ -389,7 +387,6 @@ int main(int argc, char** argv) {
     string const outdir(app["--filename"]->as<std::string>());
     string const labHist_file = outdir + "/inthist_lab_" + to_string(i_shower) + ".npz";
     string const cMSHist_file = outdir + "/inthist_cms_" + to_string(i_shower) + ".npz";
-    string const longprof_file = outdir + "/longprof_" + to_string(i_shower) + ".txt";
 
     // setup particle stack, and add primary particle
     stack.clear();
@@ -408,10 +405,14 @@ int main(int argc, char** argv) {
     // run the shower
     EAS.run();
 
-    HEPEnergyType const Efinal = dEdX.getTotal();
-    //  +observationLevel.getTotalEnergy();
-    cout << "total cut energy (GeV): " << Efinal / 1_GeV << endl
-         << "relative difference (%): " << (Efinal / E0 - 1) * 100 << endl;
+    HEPEnergyType const Efinal =
+        dEdX.getEnergyLost() + observationLevel.getEnergyGround();
+
+    CORSIKA_LOG_INFO(
+        "total energy budget (GeV): {} (dEdX={} ground={}), "
+        "relative difference (%): {}",
+        Efinal / 1_GeV, dEdX.getEnergyLost() / 1_GeV,
+        observationLevel.getEnergyGround() / 1_GeV, (Efinal / E0 - 1) * 100);
 
     // auto const hists = heModelCounted.getHistogram() + urqmdCounted.getHistogram();
     auto const hists = sibyllCounted.getHistogram() + sibyllNucCounted.getHistogram() +
diff --git a/examples/em_shower.cpp b/examples/em_shower.cpp
index 8aa6d81daa37bb35889e035e5ec54dd2af6a5b60..ff94eae5cbb12a308f0e5f4352c558b18d3f596e 100644
--- a/examples/em_shower.cpp
+++ b/examples/em_shower.cpp
@@ -24,7 +24,7 @@
 #include <corsika/output/OutputManager.hpp>
 #include <corsika/modules/writers/SubWriter.hpp>
 #include <corsika/modules/writers/EnergyLossWriter.hpp>
-#include <corsika/modules/writers/EnergyLossWriterParquet.hpp>
+#include <corsika/modules/writers/LongitudinalWriter.hpp>
 
 #include <corsika/media/Environment.hpp>
 #include <corsika/media/LayeredSphericalAtmosphereBuilder.hpp>
@@ -163,10 +163,10 @@ int main(int argc, char** argv) {
   TrackWriter tracks;
   output.add("tracks", tracks);
 
-  // long. profile; columns for photon, e+, e- still need to be added
-  LongitudinalProfile<corsika::LongitudinalProfileWriterParquet> longprof{
-      showerAxis, 10_g / square(1_cm), 200};
-  output.add("profile", longprof);
+  // long. profile
+  LongitudinalWriter profile{showerAxis, 10_g / square(1_cm), 200};
+  output.add("profile", profile);
+  LongitudinalProfile<SubWriter<decltype(profile)>> longprof{profile};
 
   Plane const obsPlane(showerCore, DirectionVector(rootCS, {0., 0., 1.}));
   ObservationPlane<setup::Tracking, ParticleWriterParquet> observationLevel{
@@ -187,9 +187,12 @@ int main(int argc, char** argv) {
   EAS.run();
   output.endOfShower();
 
-  const HEPEnergyType Efinal = dEdX.getTotal();
-  cout << "total cut energy (GeV): " << Efinal / 1_GeV << endl
-       << "relative difference (%): " << (Efinal / E0 - 1) * 100 << endl;
+  HEPEnergyType const Efinal = dEdX.getEnergyLost() + observationLevel.getEnergyGround();
+
+  CORSIKA_LOG_INFO(
+      "total energy budget (GeV): {}, "
+      "relative difference (%): {}",
+      Efinal / 1_GeV, (Efinal / E0 - 1) * 100);
 
   output.endOfLibrary();
 }
diff --git a/examples/hybrid_MC.cpp b/examples/hybrid_MC.cpp
index 904c0d082c2a12efe17a30197bdb136a3f732d43..fa635fe380582d43ebd304b93b76e4178b8c2452 100644
--- a/examples/hybrid_MC.cpp
+++ b/examples/hybrid_MC.cpp
@@ -29,7 +29,7 @@
 #include <corsika/output/OutputManager.hpp>
 #include <corsika/modules/writers/SubWriter.hpp>
 #include <corsika/modules/writers/EnergyLossWriter.hpp>
-#include <corsika/modules/writers/EnergyLossWriterParquet.hpp>
+#include <corsika/modules/writers/LongitudinalParquet.hpp>
 
 #include <corsika/media/Environment.hpp>
 #include <corsika/media/FlatExponential.hpp>
@@ -242,11 +242,9 @@ int main(int argc, char** argv) {
   CONEXhybrid conex_model(center, showerAxis, t, injectionHeight, E0,
                           get_PDG(Code::Proton));
 
-  LongitudinalProfile<corsika::LongitudinalProfileWriterParquet> longprof{
-      showerAxis, 10_g / square(1_cm), 200};
-  output.add("profile", longprof);
-
-  LongitudinalProfile longprof(showerAxis);
+  LongitudinalWriter profile{showerAxis, 10_g / square(1_cm), 200};
+  output.add("profile", profile);
+  LongitudinalProfile<SubWriter<decltype(profile)>> longprof{profile};
 
   Plane const obsPlane(showerCore, DirectionVector(rootCS, {0., 0., 1.}));
   ObservationPlane<setup::Tracking> observationLevel(
diff --git a/examples/mars.cpp b/examples/mars.cpp
index bc7fa56e03fbadd0aef5b4639330cfbec2e57a0c..0b64ab765bf2056b12f5b753dccf6e4f1d0275ee 100644
--- a/examples/mars.cpp
+++ b/examples/mars.cpp
@@ -31,7 +31,7 @@
 #include <corsika/output/OutputManager.hpp>
 #include <corsika/modules/writers/SubWriter.hpp>
 #include <corsika/modules/writers/EnergyLossWriter.hpp>
-#include <corsika/modules/writers/EnergyLossWriterParquet.hpp>
+#include <corsika/modules/writers/LongitudinalWriter.hpp>
 
 #include <corsika/media/Environment.hpp>
 #include <corsika/media/FlatExponential.hpp>
@@ -47,7 +47,6 @@
 #include <corsika/modules/BetheBlochPDG.hpp>
 #include <corsika/modules/LongitudinalProfile.hpp>
 #include <corsika/modules/ObservationPlane.hpp>
-#include <corsika/modules/OnShellCheck.hpp>
 #include <corsika/modules/StackInspector.hpp>
 #include <corsika/modules/TrackWriter.hpp>
 #include <corsika/modules/ParticleCut.hpp>
@@ -210,8 +209,7 @@ int main(int argc, char** argv) {
   // gets all messed up
   if (app.count("--pdg") == 0) {
     if ((app.count("-A") == 0) || (app.count("-Z") == 0)) {
-      std::cerr << "If --pdg is not provided, then both -A and -Z are required."
-                << std::endl;
+      CORSIKA_LOG_ERROR("If --pdg is not provided, then both -A and -Z are required.");
       return 1;
     }
   }
@@ -314,7 +312,7 @@ int main(int argc, char** argv) {
 
   ShowerAxis const showerAxis{injectionPos, (showerCore - injectionPos) * 1.2, env};
 
-  EnergyLossWriter<EnergyLossWriterParquet> dEdX{showerAxis};
+  EnergyLossWriter dEdX{showerAxis};
   output.add("energyloss", dEdX);
 
   HEPEnergyType const emcut = 1_GeV;
@@ -359,8 +357,9 @@ int main(int argc, char** argv) {
   // corsika::proposal::ContinuousProcess emContinuous(env);
   BetheBlochPDG<SubWriter<decltype(dEdX)>> emContinuous{dEdX};
 
-  LongitudinalProfile<corsika::LongitudinalProfileWriterParquet> profile{showerAxis};
-  output.add("profile", profile);
+  LongitudinalWriter longprof{showerAxis};
+  output.add("profile", longprof);
+  LongitudinalProfile<SubWriter<decltype(longprof)>> profile{longprof};
 
   corsika::urqmd::UrQMD urqmd;
   InteractionCounter urqmdCounted{urqmd};
@@ -435,10 +434,13 @@ int main(int argc, char** argv) {
     // run the shower
     EAS.run();
 
-    const HEPEnergyType Efinal = dEdX.getTotal();
-    //  +observationLevel.getEnergyGround();
-    cout << "total cut energy (GeV): " << Efinal / 1_GeV << endl
-         << "relative difference (%): " << (Efinal / E0 - 1) * 100 << endl;
+    HEPEnergyType const Efinal =
+        dEdX.getEnergyLost() + observationLevel.getEnergyGround();
+
+    CORSIKA_LOG_INFO(
+        "total energy budget (GeV): {}, "
+        "relative difference (%): {}",
+        Efinal / 1_GeV, (Efinal / E0 - 1) * 100);
 
     auto const hists = sibyllCounted.getHistogram() + sibyllNucCounted.getHistogram() +
                        urqmdCounted.getHistogram();
diff --git a/examples/vertical_EAS.cpp b/examples/vertical_EAS.cpp
index fabcc0763f8ec290860b6f2330eda4d37adbcea1..ba69ffe2814e599a3f7bf6eb582a864beffc62c5 100644
--- a/examples/vertical_EAS.cpp
+++ b/examples/vertical_EAS.cpp
@@ -31,7 +31,7 @@
 #include <corsika/output/OutputManager.hpp>
 #include <corsika/modules/writers/SubWriter.hpp>
 #include <corsika/modules/writers/EnergyLossWriter.hpp>
-#include <corsika/modules/writers/EnergyLossWriterParquet.hpp>
+#include <corsika/modules/writers/LongitudinalWriter.hpp>
 
 #include <corsika/media/Environment.hpp>
 #include <corsika/media/FlatExponential.hpp>
@@ -198,7 +198,7 @@ int main(int argc, char** argv) {
   // register profile output
 
   // construct the overall energy loss writer and register it
-  EnergyLossWriter<EnergyLossWriterParquet> dEdX{showerAxis};
+  EnergyLossWriter dEdX{showerAxis};
   output.add("energyloss", dEdX);
 
   // construct the continuous energy loss model
@@ -208,8 +208,10 @@ int main(int argc, char** argv) {
   ParticleCut<SubWriter<decltype(dEdX)>> cut{E0, E0, 60_GeV, 60_GeV, true, dEdX};
 
   // setup longitudinal profile
-  LongitudinalProfile<corsika::LongitudinalProfileWriterParquet> profile{showerAxis};
-  output.add("profile", profile);
+  LongitudinalWriter longProf{showerAxis};
+  output.add("profile", longProf);
+
+  LongitudinalProfile<SubWriter<decltype(longProf)>> profile{longProf};
 
   // create a track writer and register it with the output manager
   TrackWriter<TrackWriterParquet> trackWriter;