From f945e4ea55171ce97ea01a45ac3fef54abbcd943 Mon Sep 17 00:00:00 2001
From: Remy Prechelt <prechelt@hawaii.edu>
Date: Mon, 10 May 2021 16:07:36 -1000
Subject: [PATCH] Ported radio branch to new output architecture.

---
 corsika/modules/radio/CoREAS.hpp              |  11 +-
 corsika/modules/radio/RadioProcess.hpp        | 190 +++++++++-----
 corsika/modules/radio/ZHS.hpp                 | 242 +++++++++---------
 corsika/modules/radio/antennas/Antenna.hpp    |  87 +++++--
 .../radio/antennas/TimeDomainAntenna.hpp      | 240 ++++++++---------
 tests/modules/testRadio.cpp                   |  12 +-
 6 files changed, 465 insertions(+), 317 deletions(-)

diff --git a/corsika/modules/radio/CoREAS.hpp b/corsika/modules/radio/CoREAS.hpp
index 67af0a70b..a7933810e 100755
--- a/corsika/modules/radio/CoREAS.hpp
+++ b/corsika/modules/radio/CoREAS.hpp
@@ -23,7 +23,7 @@ namespace corsika {
   class CoREAS final : public RadioProcess<TRadioDetector, CoREAS<TRadioDetector, TPropagator>, TPropagator> {
 
     using Base = RadioProcess<TRadioDetector, CoREAS<TRadioDetector, TPropagator>, TPropagator>;
-    using Base::detector_;
+    using Base::antennas_;
 
   public:
     using ElectricFieldVector =
@@ -36,8 +36,9 @@ namespace corsika {
      *
      */
     template <typename... TArgs>
-    CoREAS(TRadioDetector& detector, TArgs&&... args)
-        : RadioProcess<TRadioDetector, CoREAS, TPropagator>(detector, args...) {}
+    CoREAS(std::string const& name,
+           TRadioDetector& detector, TArgs&&... args)
+        : RadioProcess<TRadioDetector, CoREAS, TPropagator>(name, detector, args...) {}
 
     /**
      * Simulate the radio emission from a particle across a track.
@@ -77,7 +78,7 @@ namespace corsika {
       const double approxThreshold_{1.0e-3};
 
       // loop over each antenna in the antenna collection (detector)
-      for (auto& antenna : detector_.getAntennas()) {
+      for (auto& antenna : antennas_.getAntennas()) {
 
         // check with which antenna we work in this loop
         std::cout << "ANTENNA: " << antenna.getName() << std::endl;
@@ -382,4 +383,4 @@ namespace corsika {
 
   }; // END: class CoREAS
 
-} // namespace corsika
\ No newline at end of file
+} // namespace corsika
diff --git a/corsika/modules/radio/RadioProcess.hpp b/corsika/modules/radio/RadioProcess.hpp
index ecc0943e2..527837657 100644
--- a/corsika/modules/radio/RadioProcess.hpp
+++ b/corsika/modules/radio/RadioProcess.hpp
@@ -10,34 +10,34 @@
 #include <istream>
 #include <fstream>
 #include <iostream>
-#include <xtensor/xcsv.hpp>
 #include <xtensor/xtensor.hpp>
 #include <string>
+#include <corsika/output/BaseOutput.hpp>
 #include <corsika/framework/process/ContinuousProcess.hpp>
 #include <corsika/setup/SetupStack.hpp>
 #include <corsika/setup/SetupTrajectory.hpp>
 
-
 namespace corsika {
 
   /**
    * The base interface for radio emission processes.
    *
    * TRadioImpl is the concrete implementation of the radio algorithm.
-   * TRadioDetector is the detector instance that stores antennas
+   * TAntennaCollection is the detector instance that stores antennas
    * and is responsible for managing the output writing.
    */
-  template <typename TRadioDetector, typename TRadioImpl, typename TPropagator>
+  template <typename TAntennaCollection, typename TRadioImpl, typename TPropagator>
   class RadioProcess : public ContinuousProcess<
-      RadioProcess<TRadioDetector, TRadioImpl, TPropagator>> {
+                           RadioProcess<TAntennaCollection, TRadioImpl, TPropagator>>,
+                       public BaseOutput {
 
-//    using ParticleType = corsika::setup::Stack::particle_type;
-//    using TrackType = corsika::LeapFrogTrajectory;
+    //    using ParticleType = corsika::setup::Stack::particle_type;
+    //    using TrackType = corsika::LeapFrogTrajectory;
 
     /**
      * A collection of filter objects for deciding on valid particles and tracks.
      */
-    //std::vector<std::function<bool(ParticleType&, TrackType const&)>> filters_;
+    // std::vector<std::function<bool(ParticleType&, TrackType const&)>> filters_;
 
     /**
      * Get a reference to the underlying radio implementation.
@@ -52,16 +52,19 @@ namespace corsika {
     }
 
   protected:
-    TRadioDetector& detector_; ///< The radio detector we store into.
-    TPropagator propagator_;   ///< The propagator implementation.
+    std::string const name_; ///< The name of this radio process.
+    TAntennaCollection& antennas_; ///< The radio antennas we store into.
+    TPropagator propagator_;       ///< The propagator implementation.
+    int event_{0}; ///< The current event ID.
 
   public:
     /**
      * Construct a new RadioProcess.
      */
     template <typename... TArgs>
-    RadioProcess(TRadioDetector& detector, TArgs&&... args)
-        : detector_(detector)
+    RadioProcess(std::string const& name, TAntennaCollection& antennas, TArgs&&... args)
+        : name_(name)
+        , antennas_(antennas)
         , propagator_(args...) {}
 
     /**
@@ -75,14 +78,14 @@ namespace corsika {
      */
     template <typename Particle, typename Track>
     ProcessReturn doContinuous(Particle& particle, Track const& track, bool const) {
-      //we want the following particles:
+      // we want the following particles:
       // Code::Electron & Code::Positron & Code::Gamma
 
       // we wrap Simulate() in doContinuous as the plan is to add particle level
       // filtering or thinning for calculation of the radio emission. This is
       // important for controlling the runtime of radio (by ignoring particles
       // that aren't going to contribute i.e. heavy hadrons)
-      //if (valid(particle, track)) {
+      // if (valid(particle, track)) {
       if (particle.getPID() == Code::Electron || particle.getPID() == Code::Positron) {
         return this->implementation().simulate(particle, track);
       }
@@ -92,62 +95,131 @@ namespace corsika {
     /**
      * Decide whether this particle and track is valid for radio emission.
      */
-//    template <typename Particle, typename Track>
-//    auto valid(Particle& particle, Track const& track) const {
-//
-//      // loop over the filters in the our collection
-//      for (auto& filter : filters_) {
-//        // evaluate the filter. If the filter returns false,
-//        // then this track is not valid for radio emission.
-//        if (!filter(particle, track)) return false;
-//      }
-//    }
-
-//    template <typename Particle, typename Track>
-//    void addFilter(const std::function<bool(Particle&, Track const&)> filter) {
-//      filters_.push_back(filter);
-//    }
-
-    /**
-       * TODO: This is placeholder so we can use text output while
-       * we wait for the true output formatting to be ready.
-       **/
-    bool writeOutput() const {
-      // this for loop still has some issues
-      int i = 1;
-      for (auto& antenna : detector_.getAntennas()) {
-
-        auto [t,E] = antenna.getWaveform();
-        auto c = xt::hstack(xt::xtuple(t,E));
-        std::ofstream out_file ("antenna" + to_string(i) + "_output.csv");
-        xt::dump_csv(out_file, c);
-        out_file.close();
-        ++i;
-
-      }
+    //    template <typename Particle, typename Track>
+    //    auto valid(Particle& particle, Track const& track) const {
+    //
+    //      // loop over the filters in the our collection
+    //      for (auto& filter : filters_) {
+    //        // evaluate the filter. If the filter returns false,
+    //        // then this track is not valid for radio emission.
+    //        if (!filter(particle, track)) return false;
+    //      }
+    //    }
+
+    //    template <typename Particle, typename Track>
+    //    void addFilter(const std::function<bool(Particle&, Track const&)> filter) {
+    //      filters_.push_back(filter);
+    //    }
+
+    // /**
+    //  * TODO: This is placeholder so we can use text output while
+    //  * we wait for the true output formatting to be ready.
+    //  **/
+    // bool writeOutput() const {
+    //   // this for loop still has some issues
+    //   int i = 1;
+    //   for (auto& antenna : antennas_.getAntennas()) {
+
+    //     auto [t, E] = antenna.getWaveform();
+    //     auto c = xt::hstack(xt::xtuple(t, E));
+    //     std::ofstream out_file("antenna" + to_string(i) + "_output.csv");
+    //     xt::dump_csv(out_file, c);
+    //     out_file.close();
+    //     ++i;
+    //   }
       // how this method should work:
       // 1. Loop over the antennas in the collection
       // 2. Get their waveforms
       // 3. Create a text file for each antenna
       // 4. and write out two columns, time and field.
-
-    }
+    // }
 
     /**
-   * Return the maximum step length for this particle and track.
-   *
-   * This must be provided by the TRadioImpl.
-   *
-   * @param particle    The current particle.
-   * @param track       The current track.
-   *
-   * @returns The maximum length of this track.
-   */
+     * Return the maximum step length for this particle and track.
+     *
+     * This must be provided by the TRadioImpl.
+     *
+     * @param particle    The current particle.
+     * @param track       The current track.
+     *
+     * @returns The maximum length of this track.
+     */
     LengthType getMaxStepLength(setup::Stack::particle_type const& vParticle,
                                 setup::Trajectory const& vTrack) const {
       return meter * std::numeric_limits<double>::infinity();
     }
 
+    /**
+     * Called at the start of each library.
+     */
+    void startOfLibrary(boost::filesystem::path const& directory) final override {
+
+      // loop over every antenna and set the initial path
+      // this also writes the time-bins to disk.
+      for (auto& antenna : antennas_.getAntennas()) {
+        antenna.startOfLibrary(directory);
+      }
+    }
+
+    /**
+     * Called at the end of each shower.
+     */
+    void endOfShower() final override {
+
+      // loop over every antenna and instruct them to
+      // flush data to disk, and then reset the antenna
+      // before the next event
+      for (auto& antenna : antennas_.getAntennas()) {
+        antenna.endOfShower(event_);
+        antenna.reset();
+      }
+
+      // increment our event counter
+      event_++;
+
+    }
+
+    /**
+     * Called at the end of each library.
+     *
+     * This must also increment the run number since we override
+     * the default behaviour of BaseOutput.
+     */
+    void endOfLibrary() final override;
+
+    /**
+     * Get the configuration of this output.
+     */
+    YAML::Node getConfig() const final {
+
+      // top-level YAML node
+      YAML::Node config;
+
+      // fill in some basics
+      config["type"] = "RadioProcess";
+      config["units"]["time"] = "ns";
+      config["units"]["frequency"] = "GHz";
+      config["units"]["electric field"] = "V/m";
+      config["units"]["distance"] = "m";
+
+      for (auto& antenna : antennas_.getAntennas()) {
+        // get the name/location of this antenna
+        auto name = antenna.getName();
+        auto location = antenna.getLocation().getCoordinates();
+
+        // get the antennas config
+        config["antennas"][name] = antenna.getConfig();
+
+        // write the location of this antenna
+        config["antennas"][name]["location"].push_back(location.getX() / 1_m);
+        config["antennas"][name]["location"].push_back(location.getY() / 1_m);
+        config["antennas"][name]["location"].push_back(location.getZ() / 1_m);
+
+      }
+
+      return config;
+    }
+
   }; // END: class RadioProcess
 
-} // namespace corsika
\ No newline at end of file
+} // namespace corsika
diff --git a/corsika/modules/radio/ZHS.hpp b/corsika/modules/radio/ZHS.hpp
index c4a7ab282..f5bbb0049 100644
--- a/corsika/modules/radio/ZHS.hpp
+++ b/corsika/modules/radio/ZHS.hpp
@@ -20,13 +20,15 @@ namespace corsika {
    * A concrete implementation of the ZHS algorithm.
    */
   template <typename TRadioDetector, typename TPropagator>
-  class ZHS final : public RadioProcess<TRadioDetector, ZHS<TRadioDetector, TPropagator>, TPropagator> {
+  class ZHS final : public RadioProcess<TRadioDetector, ZHS<TRadioDetector, TPropagator>,
+                                        TPropagator> {
 
-    using Base = RadioProcess<TRadioDetector, ZHS<TRadioDetector, TPropagator>, TPropagator>;
-    using Base::detector_;
+    using Base =
+        RadioProcess<TRadioDetector, ZHS<TRadioDetector, TPropagator>, TPropagator>;
+    using Base::antennas_;
 
   public:
-//    using PotentialVector = QuantityVector<PotentialVectorType::dimension_type>;
+    //    using PotentialVector = QuantityVector<PotentialVectorType::dimension_type>;
     using ElectricFieldVector = QuantityVector<ElectricFieldType::dimension_type>;
     /**
      * Construct a new ZHS instance.
@@ -36,8 +38,8 @@ namespace corsika {
      *
      */
     template <typename... TArgs>
-    ZHS(TRadioDetector& detector, TArgs&&... args)
-        : RadioProcess<TRadioDetector, ZHS, TPropagator>(detector, args...){}
+    ZHS(std::string const& name, TRadioDetector& detector, TArgs&&... args)
+        : RadioProcess<TRadioDetector, ZHS, TPropagator>(name, detector, args...) {}
 
     /**
      * Simulate the radio emission from a particle across a track.
@@ -51,129 +53,141 @@ namespace corsika {
     template <typename Particle, typename Track>
     ProcessReturn simulate(Particle& particle, Track const& track) const {
 
-      // TODO: think if we reuse these variables for the case of not being in the Fraunhoffer approx.
-      auto const startTime_ {particle.getTime()
-                       - track.getDuration()}; // time at start point of track.
-      auto const endTime_ {particle.getTime()}; // time at end point of track.
+      // TODO: think if we reuse these variables for the case of not being in the
+      // Fraunhoffer approx.
+      auto const startTime_{particle.getTime() -
+                            track.getDuration()}; // time at start point of track.
+      auto const endTime_{particle.getTime()};    // time at end point of track.
 
       auto const startPoint_{track.getPosition(0)};
       auto const endPoint_{track.getPosition(1)};
-      LengthType trackLength_ {(startPoint_ - endPoint_).getNorm()};
+      LengthType trackLength_{(startPoint_ - endPoint_).getNorm()};
 
       // track velocity
-      auto const trackVelocity_ {(track.getVelocity(0) + track.getVelocity(1)) / 2};
+      auto const trackVelocity_{(track.getVelocity(0) + track.getVelocity(1)) / 2};
 
       // beta is defined as velocity / speed of light
-      auto const beta_ { trackVelocity_ / constants::c};
+      auto const beta_{trackVelocity_ / constants::c};
 
       // get particle charge
-      auto const charge_ {get_charge(particle.getPID())};
+      auto const charge_{get_charge(particle.getPID())};
 
       // get "mid" position of the track geometrically
-      auto const midVector_ { (startPoint_ - endPoint_) / 2 };
-      auto const midPoint_ {Point(midVector_.getCoordinateSystem(), midVector_.getComponents().getX(),
-                            midVector_.getComponents().getY(), midVector_.getComponents().getZ())};
-      //changed if deltaT1_ > deltaT2_, so not const
-      auto constants {charge_/ (4 * M_PI)/ (constants::epsilonZero) / constants::c};
+      auto const midVector_{(startPoint_ - endPoint_) / 2};
+      auto const midPoint_{
+          Point(midVector_.getCoordinateSystem(), midVector_.getComponents().getX(),
+                midVector_.getComponents().getY(), midVector_.getComponents().getZ())};
+      // changed if deltaT1_ > deltaT2_, so not const
+      auto constants{charge_ / (4 * M_PI) / (constants::epsilonZero) / constants::c};
       // we loop over each antenna in the collection
-      for (auto& antenna : detector_.getAntennas()) {
+      for (auto& antenna : antennas_.getAntennas()) {
         // Probably will be reused in the case of not being in Fraunhoffer.
         auto midPaths{this->propagator_.propagate(midPoint_, antenna.getLocation(), 1_m)};
-        //Loop over midPaths to first check Fraunhoffer limit
-        for (size_t i{0}; i < midPaths.size(); i++){
-            //Maybe I can recalculate fraunhLimit without Midpaths to avoid calculating this third
-            //path which is something really slow, and only use path1 and path2.
-            double const betaTimesK {beta_.dot(midPaths[i].emit_)/beta_.getNorm()};
-            double const slitSize_ {1. - betaTimesK * betaTimesK * trackLength_ * trackLength_ /1_m /1_m};
-            //Parameter that determines the limit for the Fraunhoffer limit (probably related to antenna sampling rate
-            double const lambda {0.1};
-            double const fraunhLimit {slitSize_/midPaths[i].R_distance_/lambda * 1_m};
-            if (fraunhLimit > 1.) // Checks if we are in fraunhoffer domain (maybe it should be less?)
-            {
-                ///code for dividing track and calculating field.
-                std::cout << "This code hasnt been implemented!" << std::endl;
+        // Loop over midPaths to first check Fraunhoffer limit
+        for (size_t i{0}; i < midPaths.size(); i++) {
+          // Maybe I can recalculate fraunhLimit without Midpaths to avoid calculating
+          // this third path which is something really slow, and only use path1 and path2.
+          double const betaTimesK{beta_.dot(midPaths[i].emit_) / beta_.getNorm()};
+          double const slitSize_{1. - betaTimesK * betaTimesK * trackLength_ *
+                                          trackLength_ / 1_m / 1_m};
+          // Parameter that determines the limit for the Fraunhoffer limit (probably
+          // related to antenna sampling rate
+          double const lambda{0.1};
+          double const fraunhLimit{slitSize_ / midPaths[i].R_distance_ / lambda * 1_m};
+          if (fraunhLimit >
+              1.) // Checks if we are in fraunhoffer domain (maybe it should be less?)
+          {
+            /// code for dividing track and calculating field.
+            std::cout << "This code hasnt been implemented!" << std::endl;
+          } else // Calculate vector potential of whole track
+          {
+            // paths from start of track to antenna
+            auto paths1{
+                this->propagator_.propagate(startPoint_, antenna.getLocation(), 1_m)};
+
+            // paths from end of track to antenna
+            auto paths2{
+                this->propagator_.propagate(endPoint_, antenna.getLocation(), 1_m)};
+            // First implementation, need to check if there are the same number of paths,
+            // but once its working.
+
+            // modified later if detectionTime1_ > detectionTime2_
+            TimeType detectionTime1_{startTime_ + paths1[i].propagation_time_};
+            TimeType detectionTime2_{endTime_ + paths2[i].propagation_time_};
+
+            // make detectionTime1_ be the smallest time => changes step function order so
+            // constants is changed to account for it
+            if (detectionTime1_ > detectionTime2_) {
+              detectionTime1_ = endTime_ + paths2[i].propagation_time_;
+              detectionTime2_ = startTime_ + paths1[i].propagation_time_;
+              constants = -constants;
             }
-            else // Calculate vector potential of whole track
-            {
-                // paths from start of track to antenna
-                auto paths1{this->propagator_.propagate(startPoint_, antenna.getLocation(), 1_m)};
-
-                // paths from end of track to antenna
-                auto paths2{this->propagator_.propagate(endPoint_, antenna.getLocation(), 1_m)};
-                // First implementation, need to check if there are the same number of paths, but once its working.
-
-                //modified later if detectionTime1_ > detectionTime2_
-                TimeType detectionTime1_ {startTime_ + paths1[i].propagation_time_};
-                TimeType detectionTime2_ {endTime_ + paths2[i].propagation_time_};
-
-                //make detectionTime1_ be the smallest time => changes step function order so
-                // constants is changed to account for it
-                if (detectionTime1_ > detectionTime2_)
-                {
-                    detectionTime1_ = endTime_ + paths2[i].propagation_time_;
-                    detectionTime2_ = startTime_ + paths1[i].propagation_time_;
-                    constants = - constants;
-                }
-
-                double const startBin {std::floor(detectionTime1_ * antenna.sample_rate_)};
-                double const endBin {std::floor(detectionTime2_ * antenna.sample_rate_)};
-
-                auto const betaPerp_ {midPaths[i].emit_.cross(beta_.cross(midPaths[i].emit_))};
-                double const denominator {1 - midPaths[i].refractive_index_source_ *
-                                              beta_.dot(midPaths[i].emit_)};
-
-
-                //IMPORTANT!!!!! Using ElectricFieldVector instead of PotentialVector until antenna is adapted
-                if (startBin == endBin) {
-                //track contained in bin
-                  //if not in Cerenkov angle then
-                  if (std::fabs(denominator)>1.e-15L) {
-                    double const f {std::fabs((detectionTime2_ * antenna.sample_rate_ - detectionTime1_ * antenna.sample_rate_) )};
-                    //should be PotentialVector const Vp_ = betaPerp_.getComponents()/denominator/
-                    //                                    midPaths[i].R_distance_ * constants * f;
-                    // but to make it compile until antenna is adapted it stays like that to test it.
-                    ElectricFieldVector const Vp_ = betaPerp_.getComponents()/denominator/
-                                                    midPaths[i].R_distance_ * constants * f / 1_s;
-                    antenna.receive(detectionTime2_, betaPerp_, Vp_);
-                  }
-                  else {//If emission in Cerenkov angle => approximation
-                    double const f {(detectionTime2_ - detectionTime1_) * antenna.sample_rate_};
-                    ElectricFieldVector const Vp_ = betaPerp_.getComponents()/
-                                                    midPaths[i].R_distance_ * constants * f / 1_s;
-                    antenna.receive(detectionTime2_, betaPerp_, Vp_);
-                  }//end if Cerenkov angle approx
-                } else {
-                  /*Track is contained in more than one bin*/
-                  int const numberOfBins {static_cast<int>(endBin - startBin)};
-                  //TODO: should we check for Cerenkov angle?
-                  //first contribution
-                  double f{std::fabs(startBin + 1. - detectionTime1_ * antenna.sample_rate_)};
-                  ElectricFieldVector Vp_ = betaPerp_.getComponents() * f *
-                                                  constants / denominator /
-                                                  midPaths[i].R_distance_ / 1_s;
-                  antenna.receive(detectionTime1_, betaPerp_, Vp_);
-                  //intermidiate contributions
-                  for (int it{1}; it < numberOfBins; ++it) {
-                    Vp_ = betaPerp_.getComponents() * constants / denominator /
-                                                    midPaths[i].R_distance_ / 1_s;
-                    antenna.receive(detectionTime1_ + static_cast<double>(it) / antenna.sample_rate_,
-                                    betaPerp_, Vp_);
-                  }// end loop over bins in which potential vector is not zero
-                  //final contribution
-                  f = std::fabs(detectionTime2_ * antenna.sample_rate_ - endBin);
-                  Vp_ = betaPerp_.getComponents() * f * constants / denominator /
-                                                    midPaths[i].R_distance_ / 1_s;
-                  antenna.receive(detectionTime2_, betaPerp_, Vp_);
-                }//end if statement for track in multiple bins
-
-            }//finish if statement of track in fraunhoffer or not
-
-        }//end loop over mid paths
-
-        } // END: loop over antennas
-        return ProcessReturn::Ok;
-      } //end simulate
+
+            double const startBin{std::floor(detectionTime1_ * antenna.sample_rate_)};
+            double const endBin{std::floor(detectionTime2_ * antenna.sample_rate_)};
+
+            auto const betaPerp_{midPaths[i].emit_.cross(beta_.cross(midPaths[i].emit_))};
+            double const denominator{1 - midPaths[i].refractive_index_source_ *
+                                             beta_.dot(midPaths[i].emit_)};
+
+            // IMPORTANT!!!!! Using ElectricFieldVector instead of PotentialVector until
+            // antenna is adapted
+            if (startBin == endBin) {
+              // track contained in bin
+              // if not in Cerenkov angle then
+              if (std::fabs(denominator) > 1.e-15L) {
+                double const f{std::fabs((detectionTime2_ * antenna.sample_rate_ -
+                                          detectionTime1_ * antenna.sample_rate_))};
+                // should be PotentialVector const Vp_ =
+                // betaPerp_.getComponents()/denominator/
+                //                                    midPaths[i].R_distance_ * constants
+                //                                    * f;
+                // but to make it compile until antenna is adapted it stays like that to
+                // test it.
+                ElectricFieldVector const Vp_ = betaPerp_.getComponents() / denominator /
+                                                midPaths[i].R_distance_ * constants * f /
+                                                1_s;
+                antenna.receive(detectionTime2_, betaPerp_, Vp_);
+              } else { // If emission in Cerenkov angle => approximation
+                double const f{(detectionTime2_ - detectionTime1_) *
+                               antenna.sample_rate_};
+                ElectricFieldVector const Vp_ = betaPerp_.getComponents() /
+                                                midPaths[i].R_distance_ * constants * f /
+                                                1_s;
+                antenna.receive(detectionTime2_, betaPerp_, Vp_);
+              } // end if Cerenkov angle approx
+            } else {
+              /*Track is contained in more than one bin*/
+              int const numberOfBins{static_cast<int>(endBin - startBin)};
+              // TODO: should we check for Cerenkov angle?
+              // first contribution
+              double f{std::fabs(startBin + 1. - detectionTime1_ * antenna.sample_rate_)};
+              ElectricFieldVector Vp_ = betaPerp_.getComponents() * f * constants /
+                                        denominator / midPaths[i].R_distance_ / 1_s;
+              antenna.receive(detectionTime1_, betaPerp_, Vp_);
+              // intermidiate contributions
+              for (int it{1}; it < numberOfBins; ++it) {
+                Vp_ = betaPerp_.getComponents() * constants / denominator /
+                      midPaths[i].R_distance_ / 1_s;
+                antenna.receive(
+                    detectionTime1_ + static_cast<double>(it) / antenna.sample_rate_,
+                    betaPerp_, Vp_);
+              } // end loop over bins in which potential vector is not zero
+              // final contribution
+              f = std::fabs(detectionTime2_ * antenna.sample_rate_ - endBin);
+              Vp_ = betaPerp_.getComponents() * f * constants / denominator /
+                    midPaths[i].R_distance_ / 1_s;
+              antenna.receive(detectionTime2_, betaPerp_, Vp_);
+            } // end if statement for track in multiple bins
+
+          } // finish if statement of track in fraunhoffer or not
+
+        } // end loop over mid paths
+
+      } // END: loop over antennas
+      return ProcessReturn::Ok;
+    } // end simulate
 
   }; // END: class ZHS
 
-} // namespace corsika
\ No newline at end of file
+} // namespace corsika
diff --git a/corsika/modules/radio/antennas/Antenna.hpp b/corsika/modules/radio/antennas/Antenna.hpp
index 8fe910cc8..2acc44b94 100644
--- a/corsika/modules/radio/antennas/Antenna.hpp
+++ b/corsika/modules/radio/antennas/Antenna.hpp
@@ -9,6 +9,9 @@
  */
 #pragma once
 
+#include <cnpy.hpp>
+#include <xtensor/xtensor.hpp>
+#include <boost/filesystem.hpp>
 #include <corsika/framework/geometry/Point.hpp>
 
 namespace corsika {
@@ -20,23 +23,22 @@ namespace corsika {
    * type Antenna<T> where T is a concrete antenna implementation.
    *
    */
-  template <typename AntennaImpl>
+  template <typename TAntennaImpl>
   class Antenna {
 
-
+  protected:
+    /**
+     * Get a reference to the underlying radio implementation.
+     */
+    TAntennaImpl& implementation() { return static_cast<TAntennaImpl&>(*this); }
 
   public:
-    std::string const name_;         ///< The name/identifier of this antenna.
-    Point const location_;           ///< The location of this antenna.
+    std::string const name_;    ///< The name/identifier of this antenna.
+    Point const location_;      ///< The location of this antenna.
+    std::string filename_ = ""; ///< The filename for the output file for this antenna.
 
     // this stores the polarization vector of an electric field
-    using ElectricFieldVector =
-    QuantityVector<ElectricFieldType::dimension_type>;
-//    using MagneticFieldVector =
-//        QuantityVector<MagneticFieldType::dimension_type>;
-
-    // a dimensionless vector used for the incident direction
-//    using Vector = QuantityVector<dimensionless_d>;
+    using ElectricFieldVector = QuantityVector<ElectricFieldType::dimension_type>;
 
     /**
      * \brief Construct a base antenna instance.
@@ -49,11 +51,6 @@ namespace corsika {
         : name_(name)
         , location_(location){};
 
-    // copy constructor
-    Antenna(const Antenna& Ant)
-        : name_(Ant.name_)
-        , location_(Ant.location_){};
-
     /**
      * Receive a signal at this antenna.
      *
@@ -81,6 +78,64 @@ namespace corsika {
      */
     void reset();
 
+    /**
+     * Return a reference to the x-axis labels (i.e. time or frequency).
+     *
+     * This should be an xtensor-convertible type with
+     * a ->data() method that converts to a raw pointer.
+     */
+    xt::xtensor<double, 1> getAxis() const;
+
+    /**
+     * Return a reference to the underlying data.
+     *
+     * This is used when writing the antenna information to disk
+     * and will be converted to a 32-bit float before writing.
+     */
+    xt::xtensor<double, 2>& getData() const;
+
+    /**
+     * Prepare for the start of the library.
+     */
+    void startOfLibrary(boost::filesystem::path const& directory) {
+
+      // calculate and save our filename
+      filename_ = (directory / this->getName()).string() + ".npz";
+
+      // get the axis labels for this antenna and write the first row.
+      xt::xtensor<float, 1> axis = xt::cast<float>(this->implementation().getAxis());
+
+      // check for the axis name
+      std::string label = "Unknown";
+      if constexpr (TAntennaImpl::is_time_domain) {
+        label = "Time";
+      } else if constexpr (TAntennaImpl::is_freq_domain) {
+        label = "Frequency";
+      }
+
+      // explicitly convert the arrays to the needed type for cnpy
+      float const* raw_data = axis.data();
+      std::vector<size_t> N = {axis.size()}; // cnpy needs a vector here
+
+      // write the labels to the first row of the NumPy file
+      cnpy::npz_save(filename_, label, raw_data, N, "w");
+    }
+
+    /**
+     * Flush the data from this shower to disk.
+     */
+    void endOfShower(int const event) {
+
+      // get the copy of the waveform data for this event
+      auto data{this->implementation().getData()};
+
+      // cnpy needs a vector for the shape
+      std::vector<size_t> shape = {data.shape()[0], data.shape()[1]};
+
+      // and write this event to the .npz archive
+      cnpy::npz_save(filename_, std::to_string(event), data.data(), shape, "a");
+    }
+
   }; // END: class Antenna final
 
 } // namespace corsika
diff --git a/corsika/modules/radio/antennas/TimeDomainAntenna.hpp b/corsika/modules/radio/antennas/TimeDomainAntenna.hpp
index e3cf1a745..879432f12 100644
--- a/corsika/modules/radio/antennas/TimeDomainAntenna.hpp
+++ b/corsika/modules/radio/antennas/TimeDomainAntenna.hpp
@@ -16,125 +16,131 @@
 
 namespace corsika {
 
+  /**
+   * An implementation of a time-domain antenna that has a customized
+   * start time, sampling rate, and waveform duration.
+   *
+   */
+  class TimeDomainAntenna : public Antenna<TimeDomainAntenna> {
+
+  public:
+    // import the methods from the antenna
+
+    // label this as a time-domain antenna.
+    static constexpr bool is_time_domain{true};
+
+    TimeType const start_time_;         ///< The start time of this waveform.
+    TimeType const duration_;           ///< The duration of this waveform.
+    InverseTimeType const sample_rate_; ///< The sampling rate of this antenna.
+    int num_bins_;                      ///< The number of bins used.
+    xt::xtensor<double, 2> waveformE_;  ///< The waveform stored by this antenna.
+
+    using Antenna<TimeDomainAntenna>::getName;
+    using Antenna<TimeDomainAntenna>::getLocation;
+
+
+    /**
+     * Construct a new TimeDomainAntenna.
+     *
+     * @param name               The name of this antenna.
+     * @param location           The location of this antenna.
+     * @param start_time         The starting time of this waveform.
+     * @param duration           The duration of this waveform.
+     * @param sample_rate        The sample rate of this waveform.
+     * @param num_bins_          The number of timebins to store E-field.
+     * @param waveformE_         The xtensor initialized to zero for E-field.
+     *
+     */
+    TimeDomainAntenna(std::string const& name, Point const& location,
+                      TimeType const& start_time, TimeType const& duration,
+                      InverseTimeType const& sample_rate)
+        : Antenna(name, location)
+        , start_time_(start_time)
+        , duration_(duration)
+        , sample_rate_(sample_rate)
+        , num_bins_(static_cast<int>(duration * sample_rate))
+        , waveformE_(xt::zeros<double>({num_bins_, 3})){};
+
     /**
-     * An implementation of a time-domain antenna that has a customized
-     * start time, sampling rate, and waveform duration.
+     * Receive an electric field at this antenna.
+     *
+     * This assumes that the antenna will receive
+     *  an *instantaneous* electric field modeled as a delta function (or timebin).
+     *
+     * @param time             The (global) time at which this signal is received.
+     * @param receive_vector   The incident unit vector. (not used at the moment)
+     * @param field            The incident electric field vector.
      *
      */
-    class TimeDomainAntenna : public Antenna<TimeDomainAntenna> {
-
-
-//  protected:
-//    // expose the CRTP interfaces constructor
-
-    public:
-        // import the methods from the antenna
-
-        TimeType const start_time_;      ///< The start time of this waveform.
-        TimeType const duration_;        ///< The duration of this waveform.
-        InverseTimeType const sample_rate_; ///< The sampling rate of this antenna.
-        int num_bins_;                   ///< The number of bins used.
-        xt::xtensor<double,2> waveformE_; ///< The waveform stored by this antenna.
-        std::pair<xt::xtensor<double, 2>,
-        xt::xtensor<double,2>> waveform_; ///< useful for .getWaveform()
-
-        using Antenna<TimeDomainAntenna>::getName;
-        using Antenna<TimeDomainAntenna>::getLocation;
-
-        /**
-         * Construct a new TimeDomainAntenna.
-         *
-         * @param name               The name of this antenna.
-         * @param location           The location of this antenna.
-         * @param start_time         The starting time of this waveform.
-         * @param duration           The duration of this waveform.
-         * @param sample_rate        The sample rate of this waveform.
-         * @param num_bins_          The number of timebins to store E-field.
-         * @param waveformE_         The xtensor initialized to zero for E-field.
-         *
-         */
-        TimeDomainAntenna(std::string const& name, Point const& location,
-                          TimeType const& start_time,
-                          TimeType const& duration,
-                          InverseTimeType const& sample_rate)
-                : Antenna(name, location)
-                , start_time_(start_time)
-                , duration_(duration)
-                , sample_rate_(sample_rate)
-                , num_bins_ (static_cast<int>(duration * sample_rate))
-                , waveformE_ (xt::zeros<double>({num_bins_, 3}))
-        {};
-
-        // copy constructor
-        TimeDomainAntenna(const TimeDomainAntenna& Tant)
-                : Antenna(Tant.name_, Tant.location_)
-                , start_time_(Tant.start_time_)
-                , duration_(Tant.duration_)
-                , sample_rate_(Tant.sample_rate_)
-                , num_bins_(Tant.num_bins_)
-                , waveformE_(Tant.waveformE_)
-        {};
-
-        /**
-         * Receive an electric field at this antenna.
-         *
-         * This assumes that the antenna will receive
-         *  an *instantaneous* electric field modeled as a delta function (or timebin).
-         *
-         * @param time             The (global) time at which this signal is received.
-         * @param receive_vector   The incident unit vector. (not used at the moment)
-         * @param field            The incident electric field vector.
-         *
-         */
-        // TODO: rethink this method a bit. If the endpoint is at the end of the antenna resolution then you get the startpoint signal but you lose the endpoint signal!
-        void receive(TimeType const time, Vector<dimensionless_d> const& receive_vector,
-                     ElectricFieldVector const& efield) {
-
-            if (time < start_time_ || time > start_time_ + duration_) {
-                return;
-            } else {
-                // figure out the correct timebin to store the E-field value.
-                // NOTE: static cast is implicitly flooring
-                auto timebin_ {static_cast<std::size_t>((time - start_time_) * sample_rate_)};
-                std::cout << "TIMEBIN IS: " << timebin_ << std::endl;
-
-                // store the x,y,z electric field components.
-                waveformE_.at(timebin_, 0) += (efield.getX() / (1_V/1_m));
-                waveformE_.at(timebin_, 1) += (efield.getY() / (1_V/1_m));
-                waveformE_.at(timebin_, 2) += (efield.getZ() / (1_V/1_m));
-                //TODO: Check how they are stored in memory, row-wise or column-wise?
-            }
-        }
-
-        /**
-         * Get the current waveform for this antenna.
-         *
-         * NOTE: Currently returns ns and V/m but this should be UNITful
-         *
-         * @returns A pair of the sample times, and the field
-         */
-        std::pair<xt::xtensor<double, 2>, xt::xtensor<double,2>> getWaveform() const {
-            // TODO: divide by Δt for CoREAS ONLY!
-
-            // create a 1-D xtensor to store time values so we can print them later.
-            xt::xtensor<double, 2> times_ (xt::zeros<double>({num_bins_, 1}));
-
-            for (int i = 0; i < num_bins_; i++) {
-                // copy here waveformE_ (maybe that solves the segmentation error)
-                times_.at(i,0) = static_cast<double>(start_time_ / 1_s + i / (sample_rate_ * 1_s));
-            }
-
-            return std::make_pair(times_, waveformE_);
-        };
-
-        /**
-         * Reset the antenna before starting a new simulation.
-         */
-        void reset() {
-            waveformE_ = xt::zeros_like(waveformE_);
-//      times_ = xt::zeros_like(times_);
-        };
-
-    }; // END: class Antenna final
+    // TODO: rethink this method a bit. If the endpoint is at the end of the antenna
+    // resolution then you get the startpoint signal but you lose the endpoint signal!
+    void receive(TimeType const time, Vector<dimensionless_d> const& receive_vector,
+                 ElectricFieldVector const& efield) {
+
+      if (time < start_time_ || time > start_time_ + duration_) {
+        return;
+      } else {
+        // figure out the correct timebin to store the E-field value.
+        // NOTE: static cast is implicitly flooring
+        auto timebin_{static_cast<std::size_t>((time - start_time_) * sample_rate_)};
+        std::cout << "TIMEBIN IS: " << timebin_ << std::endl;
+
+        // store the x,y,z electric field components.
+        waveformE_.at(timebin_, 0) += (efield.getX() / (1_V / 1_m));
+        waveformE_.at(timebin_, 1) += (efield.getY() / (1_V / 1_m));
+        waveformE_.at(timebin_, 2) += (efield.getZ() / (1_V / 1_m));
+        // TODO: Check how they are stored in memory, row-wise or column-wise?
+      }
+    }
+
+    /**
+     * Return the time-units of each waveform.
+     *
+     * This returns them in nanoseconds for ease of use.
+     */
+    auto& getData() const { return waveformE_; }
+
+    /**
+     * Return the time-units of each waveform.
+     *
+     * This returns them in nanoseconds for ease of use.
+     */
+    auto getAxis() const {
+
+      // create a 1-D xtensor to store time values so we can print them later.
+      xt::xtensor<double, 1> times(xt::zeros<double>({num_bins_}));
+
+      for (int i = 0; i < num_bins_; i++) {
+        // create the current time in nanoseconds
+        times.at(i) = static_cast<double>(start_time_ / 1_ns + i / (sample_rate_ * 1_ns));
+      }
+
+      return times;
+    }
+
+    auto getWaveform() const { return std::make_pair(getAxis(), waveformE_); }
+
+    /**
+     * Reset the antenna before starting a new simulation.
+     */
+    void reset() { waveformE_ = xt::zeros_like(waveformE_); };
+
+    /**
+     * Return a YAML configuration for this antenna.
+     */
+    YAML::Node getConfig() const {
+
+      // top-level config
+      YAML::Node config;
+
+      config["type"] = "TimeDomainAntenna";
+      config["start_time"] = start_time_ / 1_ns;
+      config["duration"] = duration_ / 1_ns;
+      config["sample_rate"] = sample_rate_ / 1_GHz;
+
+      return config;
+    }
+
+  }; // END: class Antenna final
 
 } // namespace corsika
diff --git a/tests/modules/testRadio.cpp b/tests/modules/testRadio.cpp
index a533b3357..c05f2a6b3 100644
--- a/tests/modules/testRadio.cpp
+++ b/tests/modules/testRadio.cpp
@@ -258,7 +258,7 @@ TEST_CASE("Radio", "[processes]") {
 
     // create a radio process instance using CoREAS
     RadioProcess<decltype(detector), CoREAS<decltype(detector), decltype(StraightPropagator(envCoREAS))>, decltype(StraightPropagator(envCoREAS))>
-        coreas(detector, envCoREAS);
+        coreas("CoREAS", detector, envCoREAS);
 
     // check doContinuous and simulate methods
     coreas.doContinuous(particle1, base, true);
@@ -363,7 +363,7 @@ TEST_CASE("Radio", "[processes]") {
 
     // create a radio process instance using CoREAS
     RadioProcess<decltype(detector), ZHS<decltype(detector), decltype(StraightPropagator(envZHS))>, decltype(StraightPropagator(envZHS))>
-        zhs(detector, envZHS);
+        zhs("ZHS", detector, envZHS);
 
     // check doContinuous and simulate methods
     zhs.doContinuous(particle1, base, true);
@@ -895,7 +895,7 @@ TEST_CASE("Radio", "[processes]") {
 
     // create a radio process instance using CoREAS
     RadioProcess<decltype(detector), CoREAS<decltype(detector), decltype(StraightPropagator(env))>, decltype(StraightPropagator(env))>
-        coreas(detector, env);
+        coreas("CoREAS", detector, env);
 
     TimeType timeCounter {0._s};
 
@@ -925,7 +925,7 @@ TEST_CASE("Radio", "[processes]") {
     coreas.doContinuous(particle1,track,true);
 
     // get the output
-    coreas.writeOutput();
+    // coreas.writeOutput();
 
   }
 
@@ -1006,7 +1006,7 @@ const HEPEnergyType E0{11.4_MeV};
 
 // create a radio process instance using CoREAS
 RadioProcess<decltype(detector), CoREAS<decltype(detector), decltype(StraightPropagator(env))>, decltype(StraightPropagator(env))>
-coreas(detector, env);
+coreas("CoREAS", detector, env);
 
 // loop over all the tracks except the last one
 int const n_points {100000};
@@ -1108,7 +1108,7 @@ const HEPEnergyType E0{11.4_MeV};
 
 // create a radio process instance using CoREAS or ZHS
 RadioProcess<decltype(detector), CoREAS<decltype(detector), decltype(StraightPropagator(env))>, decltype(StraightPropagator(env))>
-coreas(detector, env);
+coreas("CoREAS", detector, env);
 
 // loop over all the tracks except the last one
 int const n_points {60000};
-- 
GitLab