diff --git a/corsika/detail/modules/TimeCut.inl b/corsika/detail/modules/TimeCut.inl new file mode 100644 index 0000000000000000000000000000000000000000..3dbdecb7f28dd0c7804308f2841b5e7ae78ebed1 --- /dev/null +++ b/corsika/detail/modules/TimeCut.inl @@ -0,0 +1,28 @@ +/* + * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ + +#pragma once + +#include <corsika/modules/TimeCut.hpp> + +namespace corsika { + + inline TimeCut::TimeCut(const TimeType time) + : time_(time) {} + + template <typename Particle> + inline ProcessReturn TimeCut::doContinuous(Step<Particle> const& step, bool const) { + CORSIKA_LOG_TRACE("TimeCut::doContinuous"); + if (step.getTimePost() >= time_) { + CORSIKA_LOG_TRACE("stopping continuous process"); + return ProcessReturn::ParticleAbsorbed; + } + return ProcessReturn::Ok; + } + +} // namespace corsika diff --git a/corsika/detail/modules/proposal/ContinuousProcess.inl b/corsika/detail/modules/proposal/ContinuousProcess.inl index 7d0cb7eb56876bcff01b675a9b0430835fa8b82b..9ffb5d8fde6798f9adfcb2d21bc7a21edb2ea840 100644 --- a/corsika/detail/modules/proposal/ContinuousProcess.inl +++ b/corsika/detail/modules/proposal/ContinuousProcess.inl @@ -35,7 +35,7 @@ namespace corsika::proposal { // Use higland multiple scattering and deactivate stochastic deflection by // passing an empty vector - static constexpr auto ms_type = PROPOSAL::MultipleScatteringType::Highland; + static constexpr auto ms_type = PROPOSAL::MultipleScatteringType::Moliere; auto s_type = std::vector<PROPOSAL::InteractionType>(); // Build displacement integral and scattering object and interpolate them too and diff --git a/corsika/detail/modules/radio/CoREAS.inl b/corsika/detail/modules/radio/CoREAS.inl new file mode 100644 index 0000000000000000000000000000000000000000..9f25ae97265f97cddf955f19ad2eee0c199c01b8 --- /dev/null +++ b/corsika/detail/modules/radio/CoREAS.inl @@ -0,0 +1,388 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ + +#pragma once + +#include <corsika/modules/radio/CoREAS.hpp> + +namespace corsika { + + template <typename TRadioDetector, typename TPropagator> + template <typename Particle> + inline ProcessReturn CoREAS<TRadioDetector, TPropagator>::simulate( + Step<Particle> const& step) { + + // get the global simulation time for that track. + auto const startTime_{ + step.getTimePre()}; // time at the start point of the track. I + // should use something similar to fCoreHitTime (?) + auto const endTime_{step.getTimePost()}; // time at end point of track. + + // LCOV_EXCL_START + if (startTime_ == endTime_) { + CORSIKA_LOG_ERROR("Time at the start and end of the track coincides! - radio"); + return ProcessReturn::Ok; + // LCOV_EXCL_STOP + } else { + + // get start and end position of the track + Point const startPoint_{step.getPositionPre()}; + Point const endPoint_{step.getPositionPost()}; + // get the coordinate system of the startpoint and hence the track + auto const cs_{startPoint_.getCoordinateSystem()}; + + auto const currDirection{(endPoint_ - startPoint_).normalized()}; + // calculate the track length + auto const tracklength_{(endPoint_ - startPoint_).getNorm()}; + + // beta is velocity / speed of light. Start & end should be the same in endpoints! + auto const corrBetaValue{(endPoint_ - startPoint_).getNorm() / + (constants::c * (endTime_ - startTime_))}; + auto const beta_{currDirection * corrBetaValue}; + + // get particle charge + auto const charge_{get_charge(step.getParticlePre().getPID())}; + + // constants for electric field vector calculation + auto const constants_{charge_ / (4 * M_PI) / (constants::epsilonZero) / + constants::c}; + + // set threshold for application of ZHS-like approximation. + const double approxThreshold_{1.0e-3}; + + // loop over each antenna in the antenna collection (detector) + for (auto& antenna : antennas_.getAntennas()) { + + // get the SignalPathCollection (path1) from the start "endpoint" to the antenna. + auto paths1{this->propagator_.propagate( + startPoint_, antenna.getLocation(), + 1_m)}; // TODO: Add the stepsize to .propagate() at some point + + // get the SignalPathCollection (path2) from the end "endpoint" to the antenna. + auto paths2{this->propagator_.propagate(endPoint_, antenna.getLocation(), 1_m)}; + + // throw an exception if path sizes don't match + try { + // loop over both paths at once and directly compare 'start' and 'end' + // attributes + for (size_t i = (paths1.size() == paths2.size()) ? 0 : throw(i = paths1.size()); + (i < paths1.size() && i < paths2.size()); i++) { + + // calculate preDoppler factor + double preDoppler_{1.0 - paths1[i].refractive_index_source_ * + beta_.dot(paths1[i].emit_)}; + + // check if preDoppler has become zero in case of refractive index of unity + // because of numerical limitations here you might need std::fabs(preDoppler) + // in the if statement - same with post & mid + // LCOV_EXCL_START + if (preDoppler_ == 0) { + CORSIKA_LOG_WARN("preDoppler factor numerically zero in COREAS"); + // redo calculation with higher precision + auto const& beta_components_{beta_.getComponents(cs_)}; + auto const& emit_components_{paths1[i].emit_.getComponents(cs_)}; + long double const indexL_{paths1[i].refractive_index_source_}; + long double const betaX_{static_cast<double>(beta_components_.getX())}; + long double const betaY_{static_cast<double>(beta_components_.getY())}; + long double const betaZ_{static_cast<double>(beta_components_.getZ())}; + long double const startX_{static_cast<double>(emit_components_.getX())}; + long double const startY_{static_cast<double>(emit_components_.getY())}; + long double const startZ_{static_cast<double>(emit_components_.getZ())}; + long double const doppler = + 1.0l - + indexL_ * (betaX_ * startX_ + betaY_ * startY_ + betaZ_ * startZ_); + preDoppler_ = doppler; + } + // LCOV_EXCL_STOP + + // calculate postDoppler factor + double postDoppler_{1.0 - paths2[i].refractive_index_source_ * + beta_.dot(paths2[i].emit_)}; + + // check if postDoppler has become zero in case of refractive index of unity + // because of numerical limitations + // LCOV_EXCL_START + if (postDoppler_ == 0) { + CORSIKA_LOG_WARN("postDoppler factor numerically zero in CoREAS"); + // redo calculation with higher precision + auto const& beta_components_{beta_.getComponents(cs_)}; + auto const& emit_components_{paths2[i].emit_.getComponents(cs_)}; + long double const indexL_{paths2[i].refractive_index_source_}; + long double const betaX_{static_cast<double>(beta_components_.getX())}; + long double const betaY_{static_cast<double>(beta_components_.getY())}; + long double const betaZ_{static_cast<double>(beta_components_.getZ())}; + long double const endX_{static_cast<double>(emit_components_.getX())}; + long double const endY_{static_cast<double>(emit_components_.getY())}; + long double const endZ_{static_cast<double>(emit_components_.getZ())}; + long double const doppler = + 1.0l - indexL_ * (betaX_ * endX_ + betaY_ * endY_ + betaZ_ * endZ_); + postDoppler_ = doppler; + } + // LCOV_EXCL_STOP + + // calculate receive time for startpoint (aka time delay) + auto startPointReceiveTime_{ + startTime_ + + paths1[i].propagation_time_}; // TODO: time 0 is when the imaginary + // primary hits the ground + + // calculate receive time for endpoint + auto endPointReceiveTime_{endTime_ + paths2[i].propagation_time_}; + + // get unit vector for startpoint at antenna location + auto ReceiveVectorStart_{paths1[i].receive_}; + + // get unit vector for endpoint at antenna location + auto ReceiveVectorEnd_{paths2[i].receive_}; + + // perform ZHS-like calculation close to Cherenkov angle and for refractive + // index at antenna location greater than 1 + if ((paths1[i].refractive_index_destination_ > 1) && + ((std::fabs(preDoppler_) < approxThreshold_) || + (std::fabs(postDoppler_) < approxThreshold_))) { + CORSIKA_LOG_DEBUG("Used ZHS-like approximation in CoREAS - radio"); + + // clear the existing paths for this particle and track, since we don't need + // them anymore + paths1.clear(); + paths2.clear(); + + auto const halfVector_{(startPoint_ - endPoint_) * 0.5}; + auto const midPoint_{endPoint_ + halfVector_}; + + // get global simulation time for the middle point of that track. + TimeType const midTime_{(startTime_ + endTime_) * 0.5}; + + // get the SignalPathCollection (path3) from the middle "endpoint" to the + // antenna. + auto paths3{ + this->propagator_.propagate(midPoint_, antenna.getLocation(), 1_m)}; + + // now loop over the paths for endpoint that we got above + for (auto const& path : paths3) { + + auto const midPointReceiveTime_{midTime_ + path.propagation_time_}; + double midDoppler_{1.0 - + path.refractive_index_source_ * beta_.dot(path.emit_)}; + + // check if midDoppler has become zero because of numerical limitations + // LCOV_EXCL_START + if (midDoppler_ == 0) { + CORSIKA_LOG_WARN("midDoppler factor numerically zero in COREAS"); + // redo calculation with higher precision + auto const& beta_components_{beta_.getComponents(cs_)}; + auto const& emit_components_{path.emit_.getComponents(cs_)}; + long double const indexL_{path.refractive_index_source_}; + long double const betaX_{static_cast<double>(beta_components_.getX())}; + long double const betaY_{static_cast<double>(beta_components_.getY())}; + long double const betaZ_{static_cast<double>(beta_components_.getZ())}; + long double const midX_{static_cast<double>(emit_components_.getX())}; + long double const midY_{static_cast<double>(emit_components_.getY())}; + long double const midZ_{static_cast<double>(emit_components_.getZ())}; + long double const doppler = + 1.0l - indexL_ * (betaX_ * midX_ + betaY_ * midY_ + betaZ_ * midZ_); + midDoppler_ = doppler; + } + // LCOV_EXCL_STOP + + // change the values of the receive unit vectors of start and end + ReceiveVectorStart_ = path.receive_; + ReceiveVectorEnd_ = path.receive_; + + // CoREAS calculation -> get ElectricFieldVector for "midPoint" + ElectricFieldVector EVmid_ = (path.emit_.cross(path.emit_.cross(beta_))) / + midDoppler_ / path.R_distance_ * constants_ * + antenna.getSampleRate(); + + ElectricFieldVector EV1_{EVmid_}; + ElectricFieldVector EV2_{EVmid_ * (-1.0)}; + + TimeType deltaT_{tracklength_ / (constants::c * corrBetaValue) * + std::fabs(midDoppler_)}; // TODO: Caution with this! + + if (startPointReceiveTime_ < + endPointReceiveTime_) // EVstart_ arrives earlier + { + startPointReceiveTime_ = midPointReceiveTime_ - 0.5 * deltaT_; + endPointReceiveTime_ = midPointReceiveTime_ + 0.5 * deltaT_; + } else // EVend_ arrives earlier + { + startPointReceiveTime_ = midPointReceiveTime_ + 0.5 * deltaT_; + endPointReceiveTime_ = midPointReceiveTime_ - 0.5 * deltaT_; + } + + TimeType const gridResolution_{1 / antenna.getSampleRate()}; + deltaT_ = endPointReceiveTime_ - startPointReceiveTime_; + + // redistribute contributions over time scale defined by the observation + // time resolution + if (abs(deltaT_) < (gridResolution_)) { + + EV1_ *= std::fabs((deltaT_ / gridResolution_)); + EV2_ *= std::fabs((deltaT_ / gridResolution_)); + + // ToDO: be careful with times in C8!!! where is the zero (time). Is it + // close-by? + long const startBin = static_cast<long>( + std::floor(startPointReceiveTime_ / gridResolution_ + 0.5l)); + long const endBin = static_cast<long>( + std::floor(endPointReceiveTime_ / gridResolution_ + 0.5l)); + double const startBinFraction = + (startPointReceiveTime_ / gridResolution_) - + std::floor(startPointReceiveTime_ / gridResolution_); + double const endBinFraction = + (endPointReceiveTime_ / gridResolution_) - + std::floor(endPointReceiveTime_ / gridResolution_); + + // only do timing modification if contributions would land in same bin + if (startBin == endBin) { + + // if startE arrives before endE + if ((deltaT_) >= 0_s) { + if ((startBinFraction >= 0.5) && + (endBinFraction >= 0.5)) // both points left of bin center + { + startPointReceiveTime_ -= + gridResolution_; // shift EV1_ to previous gridpoint + } else if ((startBinFraction < 0.5) && + (endBinFraction < + 0.5)) // both points right of bin center + { + endPointReceiveTime_ += + gridResolution_; // shift EV2_ to next gridpoint + } else // points on both sides of bin center + { + double const leftDist = 1.0 - startBinFraction; + double const rightDist = endBinFraction; + // check if asymmetry to right or left + if (rightDist >= leftDist) { + endPointReceiveTime_ += + gridResolution_; // shift EV2_ to next gridpoint + } else { + startPointReceiveTime_ -= + gridResolution_; // shift EV1_ to previous gridpoint + } + } + } else // if endE arrives before startE + { + if ((startBinFraction >= 0.5) && + (endBinFraction >= 0.5)) // both points left of bin center + { + endPointReceiveTime_ -= + gridResolution_; // shift EV2_ to previous gridpoint + } else if ((startBinFraction < 0.5) && + (endBinFraction < + 0.5)) // both points right of bin center + { + startPointReceiveTime_ += + gridResolution_; // shift EV1_ to next gridpoint + } else // points on both sides of bin center + { + double const leftDist = 1.0 - endBinFraction; + double const rightDist = startBinFraction; + // check if asymmetry to right or left + if (rightDist >= leftDist) { + startPointReceiveTime_ += + gridResolution_; // shift EV1_ to next gridpoint + } else { + endPointReceiveTime_ -= + gridResolution_; // shift EV2_ to previous gridpoint + } + } + } // End of else statement + } // End of if for startbin == endbin + } // End of if deltaT < gridresolution + + // TODO: Be very careful with this. Maybe the EVs should be fed after the + // for loop of paths3 + antenna.receive(startPointReceiveTime_, ReceiveVectorStart_, EV1_); + antenna.receive(endPointReceiveTime_, ReceiveVectorEnd_, EV2_); + } // End of looping over paths3 + + } // end of ZHS-like approximation + else { + + // calculate electric field vector for startpoint + ElectricFieldVector EV1_ = + (paths1[i].emit_.cross(paths1[i].emit_.cross(beta_))) / preDoppler_ / + paths1[i].R_distance_ * constants_ * antenna.getSampleRate(); + + // calculate electric field vector for endpoint + ElectricFieldVector EV2_ = + (paths2[i].emit_.cross(paths2[i].emit_.cross(beta_))) / postDoppler_ / + paths2[i].R_distance_ * constants_ * (-1.0) * antenna.getSampleRate(); + + if ((preDoppler_ < 1.e-9) || (postDoppler_ < 1.e-9)) { + + TimeType const gridResolution_{1 / antenna.getSampleRate()}; + TimeType deltaT_{endPointReceiveTime_ - startPointReceiveTime_}; + + if (abs(deltaT_) < (gridResolution_)) { + + EV1_ *= std::fabs(deltaT_ / gridResolution_); // Todo: rename EV1 and 2 + EV2_ *= std::fabs(deltaT_ / gridResolution_); + + long const startBin = static_cast<long>( + std::floor(startPointReceiveTime_ / gridResolution_ + 0.5l)); + long const endBin = static_cast<long>( + std::floor(endPointReceiveTime_ / gridResolution_ + 0.5l)); + double const startBinFraction = + (startPointReceiveTime_ / gridResolution_) - + std::floor(startPointReceiveTime_ / gridResolution_); + double const endBinFraction = + (endPointReceiveTime_ / gridResolution_) - + std::floor(endPointReceiveTime_ / gridResolution_); + + // only do timing modification if contributions would land in same bin + if (startBin == endBin) { + + if ((startBinFraction >= 0.5) && + (endBinFraction >= 0.5)) // both points left of bin center + { + startPointReceiveTime_ -= + gridResolution_; // shift EV1_ to previous gridpoint + } else if ((startBinFraction < 0.5) && + (endBinFraction < 0.5)) // both points right of bin center + { + endPointReceiveTime_ += + gridResolution_; // shift EV2_ to next gridpoint + } else // points on both sides of bin center + { + double const leftDist = 1.0 - startBinFraction; + double const rightDist = endBinFraction; + // check if asymmetry to right or left + if (rightDist >= leftDist) { + endPointReceiveTime_ += + gridResolution_; // shift EV2_ to next gridpoint + } else { + startPointReceiveTime_ -= + gridResolution_; // shift EV1_ to previous gridpoint + } + } + + } // End of if for startbin == endbin + } // End of if deltaT < gridresolution + } // End of if that checks small doppler factors + antenna.receive(startPointReceiveTime_, ReceiveVectorStart_, EV1_); + antenna.receive(endPointReceiveTime_, ReceiveVectorEnd_, EV2_); + } // End of else that does not perform ZHS-like approximation + + } // End of loop over both paths to get signal info + } // End of try block + // LCOV_EXCL_START + catch (size_t i) { + CORSIKA_LOG_ERROR("Signal Paths do not have the same size in CoREAS"); + } + // LCOV_EXCL_STOP + } // End of looping over antennas + + return ProcessReturn::Ok; + } + } // End of simulate method + +} // namespace corsika \ No newline at end of file diff --git a/corsika/detail/modules/radio/RadioProcess.inl b/corsika/detail/modules/radio/RadioProcess.inl new file mode 100644 index 0000000000000000000000000000000000000000..0548b71a4226e554ddb20b59ed4fa13b0b69ff2a --- /dev/null +++ b/corsika/detail/modules/radio/RadioProcess.inl @@ -0,0 +1,128 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <corsika/modules/radio/RadioProcess.hpp> + +namespace corsika { + + template <typename TAntennaCollection, typename TRadioImpl, typename TPropagator> + inline TRadioImpl& + RadioProcess<TAntennaCollection, TRadioImpl, TPropagator>::implementation() { + return static_cast<TRadioImpl&>(*this); + } + + template <typename TAntennaCollection, typename TRadioImpl, typename TPropagator> + inline TRadioImpl const& + RadioProcess<TAntennaCollection, TRadioImpl, TPropagator>::implementation() const { + return static_cast<TRadioImpl const&>(*this); + } + + template <typename TAntennaCollection, typename TRadioImpl, typename TPropagator> + template <typename... TArgs> + inline RadioProcess<TAntennaCollection, TRadioImpl, TPropagator>::RadioProcess( + TAntennaCollection& antennas, TArgs&&... args) + : antennas_(antennas) + , propagator_(args...) {} + + template <typename TAntennaCollection, typename TRadioImpl, typename TPropagator> + template <typename Particle> + inline ProcessReturn RadioProcess<TAntennaCollection, TRadioImpl, + TPropagator>::doContinuous(const Step<Particle>& step, + const bool) { + // 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)) { + auto const particleID_{step.getParticlePre().getPID()}; + if ((particleID_ == Code::Electron) || (particleID_ == Code::Positron)) { + CORSIKA_LOG_DEBUG("Particle for radio calculation: {} ", particleID_); + return this->implementation().simulate(step); + } else { + CORSIKA_LOG_DEBUG("Particle {} is irrelevant for radio", particleID_); + return ProcessReturn::Ok; + } + //} + } + + template <typename TAntennaCollection, typename TRadioImpl, typename TPropagator> + template <typename Particle, typename Track> + inline LengthType + RadioProcess<TAntennaCollection, TRadioImpl, TPropagator>::getMaxStepLength( + const Particle& vParticle, const Track& vTrack) const { + return meter * std::numeric_limits<double>::infinity(); + } + + // this should all be moved at a separate radio output function + // LCOV_EXCL_START + template <typename TAntennaCollection, typename TRadioImpl, typename TPropagator> + inline void RadioProcess<TAntennaCollection, TRadioImpl, TPropagator>::startOfLibrary( + const boost::filesystem::path& directory) { + + // 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, this->implementation().algorithm); + } + } + + template <typename TAntennaCollection, typename TRadioImpl, typename TPropagator> + inline void RadioProcess<TAntennaCollection, TRadioImpl, TPropagator>::endOfShower( + const unsigned int) { + + // 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_, this->implementation().algorithm, + antenna.getSampleRate() * 1_s); + antenna.reset(); + } + + // increment our event counter + event_++; + } + + template <typename TAntennaCollection, typename TRadioImpl, typename TPropagator> + inline YAML::Node RadioProcess<TAntennaCollection, TRadioImpl, TPropagator>::getConfig() + const { + + // top-level YAML node + YAML::Node config; + + // fill in some basics + config["type"] = "RadioProcess"; + config["algorithm"] = this->implementation().algorithm; + 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; + } + // LCOV_EXCL_STOP + +} // namespace corsika \ No newline at end of file diff --git a/corsika/detail/modules/radio/ZHS.inl b/corsika/detail/modules/radio/ZHS.inl new file mode 100644 index 0000000000000000000000000000000000000000..5c9e4aacc824b06499b65b988883d9bc1f3cbb00 --- /dev/null +++ b/corsika/detail/modules/radio/ZHS.inl @@ -0,0 +1,242 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ + +#pragma once + +#include <corsika/modules/radio/ZHS.hpp> + +namespace corsika { + + template <typename TRadioDetector, typename TPropagator> + template <typename Particle> + inline ProcessReturn ZHS<TRadioDetector, TPropagator>::simulate( + Step<Particle> const& step) const { + auto const startTime{step.getTimePre()}; + auto const endTime{step.getTimePost()}; + + // LCOV_EXCL_START + if (startTime == endTime) { + CORSIKA_LOG_ERROR("Time at the start and end of the track coincides! - radio"); + return ProcessReturn::Ok; + // LCOV_EXCL_STOP + } else { + + auto const startPoint{step.getPositionPre()}; + auto const endPoint{step.getPositionPost()}; + LengthType const trackLength{(startPoint - endPoint).getNorm()}; + + auto const betaModule{(endPoint - startPoint).getNorm() / + (constants::c * (endTime - startTime))}; + auto const beta{(endPoint - startPoint).normalized() * betaModule}; + + auto const charge{get_charge(step.getParticlePre().getPID())}; + + // // get "mid" position of the track geometrically + + auto const halfVector{(startPoint - endPoint) / 2}; + auto const midPoint{endPoint + halfVector}; + + auto const constants{charge / (4 * M_PI) / (constants::epsilonZero) / constants::c}; + + // we loop over each antenna in the collection + for (auto& antenna : antennas_.getAntennas()) { + auto midPaths{this->propagator_.propagate(midPoint, antenna.getLocation(), 1_m)}; + // Loop over midPaths, first check Fraunhoffer limit + for (size_t i{0}; i < midPaths.size(); i++) { + double const uTimesK{beta.dot(midPaths[i].emit_) / betaModule}; + double const sinTheta2{1. - uTimesK * uTimesK}; + LengthType const lambda{constants::c / antenna.getSampleRate()}; + double const fraunhLimit{sinTheta2 * trackLength * trackLength / + midPaths[i].R_distance_ / lambda * 2 * M_PI}; + // Checks if we are in fraunhoffer domain + if (fraunhLimit > 1.0) { + /// code for dividing track and calculating field. + double const nSubTracks{sqrt(fraunhLimit) + 1}; + auto const step_{(endPoint - startPoint) / nSubTracks}; + TimeType const timeStep{(endTime - startTime) / nSubTracks}; + // energy should be divided up when it is possible to get the energy at end of + // track!!!! + auto point1{startPoint}; + TimeType time1{startTime}; + for (int j{0}; j < nSubTracks; j++) { + auto const point2{point1 + step_}; + TimeType const time2{time1 + timeStep}; + auto const newHalfVector{(point1 - point2) / 2.}; + auto const newMidPoint{point2 + newHalfVector}; + auto const newMidPaths{ + this->propagator_.propagate(newMidPoint, antenna.getLocation(), 1_m)}; + // A function for calculating the field should be made since it is repeated + // later + for (size_t k{0}; k < newMidPaths.size(); k++) { + + double const n_source{newMidPaths[k].refractive_index_source_}; + + double const betaTimesK{beta.dot(newMidPaths[k].emit_)}; + TimeType const midTime{(time1 + time2) / 2.}; + TimeType detectionTime1{time1 + newMidPaths[k].propagation_time_ - + n_source * betaTimesK * (time1 - midTime)}; + TimeType detectionTime2{time2 + newMidPaths[k].propagation_time_ - + n_source * betaTimesK * (time2 - midTime)}; + + // make detectionTime1_ be the smallest time => + // changes step function order so sign is changed to account for it + double sign = 1.; + if (detectionTime1 > detectionTime2) { + detectionTime1 = time2 + newMidPaths[k].propagation_time_ - + n_source * betaTimesK * (time2 - midTime); + detectionTime2 = time1 + newMidPaths[k].propagation_time_ - + n_source * betaTimesK * (time1 - midTime); + sign = -1.; + } // end if statement for time structure + + double const startBin{std::floor( + (detectionTime1 - antenna.getStartTime()) * antenna.getSampleRate() + + 0.5)}; + double const endBin{std::floor((detectionTime2 - antenna.getStartTime()) * + antenna.getSampleRate() + + 0.5)}; + + auto const betaPerp{ + newMidPaths[k].emit_.cross(beta.cross(newMidPaths[k].emit_))}; + double const denominator{1. - n_source * betaTimesK}; + + if (startBin == endBin) { + // track contained in bin + // if not in Cerenkov angle then + if (std::fabs(denominator) > 1.e-15) { + double const f{std::fabs((detectionTime2 * antenna.getSampleRate() - + detectionTime1 * antenna.getSampleRate()))}; + VectorPotential const Vp = betaPerp * sign * constants * f / + denominator / newMidPaths[k].R_distance_; + antenna.receive(detectionTime2, betaPerp, Vp); + } else { // If emission in Cerenkov angle => approximation + double const f{time2 * antenna.getSampleRate() - + time1 * antenna.getSampleRate()}; + VectorPotential const Vp = + betaPerp * sign * constants * f / newMidPaths[k].R_distance_; + 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)}; + // first contribution/ plus 1 bin minus 0.5 from new antenna ruonding + double f{std::fabs(startBin + 0.5 - + (detectionTime1 - antenna.getStartTime()) * + antenna.getSampleRate())}; + VectorPotential Vp = betaPerp * sign * constants * f / denominator / + newMidPaths[k].R_distance_; + antenna.receive(detectionTime1, betaPerp, Vp); + // intermidiate contributions + for (int it{1}; it < numberOfBins; ++it) { + Vp = betaPerp * constants / denominator / newMidPaths[k].R_distance_; + antenna.receive(detectionTime1 + + static_cast<double>(it) / antenna.getSampleRate(), + betaPerp, Vp); + } // end loop over bins in which potential vector is not zero + // final contribution// f +0.5 from new antenna rounding + f = std::fabs((detectionTime2 - antenna.getStartTime()) * + antenna.getSampleRate() + + 0.5 - endBin); + Vp = betaPerp * sign * constants * f / denominator / + newMidPaths[k].R_distance_; + antenna.receive(detectionTime2, betaPerp, Vp); + } // end if statement for track in multiple bins + + } // end of loop over newMidPaths + // update points for next sub track + point1 = point1 + step_; + time1 = time1 + timeStep; + } + + } else // Calculate vector potential of whole track + { + double const n_source{midPaths[i].refractive_index_source_}; + + double const betaTimesK{beta.dot(midPaths[i].emit_)}; + TimeType const midTime{(startTime + endTime) / 2}; + TimeType detectionTime1{startTime + midPaths[i].propagation_time_ - + n_source * betaTimesK * (startTime - midTime)}; + TimeType detectionTime2{endTime + midPaths[i].propagation_time_ - + n_source * betaTimesK * (endTime - midTime)}; + + // make detectionTime1_ be the smallest time => + // changes step function order so sign is changed to account for it + double sign = 1.; + if (detectionTime1 > detectionTime2) { + detectionTime1 = endTime + midPaths[i].propagation_time_ - + n_source * betaTimesK * (endTime - midTime); + detectionTime2 = startTime + midPaths[i].propagation_time_ - + n_source * betaTimesK * (startTime - midTime); + sign = -1.; + } // end if statement for time structure + + double const startBin{std::floor((detectionTime1 - antenna.getStartTime()) * + antenna.getSampleRate() + + 0.5)}; + double const endBin{std::floor((detectionTime2 - antenna.getStartTime()) * + antenna.getSampleRate() + + 0.5)}; + + auto const betaPerp{midPaths[i].emit_.cross(beta.cross(midPaths[i].emit_))}; + double const denominator{1. - + midPaths[i].refractive_index_source_ * betaTimesK}; + + if (startBin == endBin) { + // track contained in bin + // if not in Cerenkov angle then + if (std::fabs(denominator) > 1.e-15) { + double const f{std::fabs((detectionTime2 * antenna.getSampleRate() - + detectionTime1 * antenna.getSampleRate()))}; + + VectorPotential const Vp = betaPerp * sign * constants * f / denominator / + midPaths[i].R_distance_; + antenna.receive(detectionTime2, betaPerp, Vp); + } else { // If emission in Cerenkov angle => approximation + double const f{endTime * antenna.getSampleRate() - + startTime * antenna.getSampleRate()}; + VectorPotential const Vp = + betaPerp * sign * constants * f / midPaths[i].R_distance_; + 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 + 0.5 - + (detectionTime1 - antenna.getStartTime()) * + antenna.getSampleRate())}; + VectorPotential Vp = + betaPerp * sign * constants * f / denominator / midPaths[i].R_distance_; + antenna.receive(detectionTime1, betaPerp, Vp); + // intermidiate contributions + for (int it{1}; it < numberOfBins; ++it) { + Vp = betaPerp * sign * constants / denominator / midPaths[i].R_distance_; + antenna.receive( + detectionTime1 + static_cast<double>(it) / antenna.getSampleRate(), + betaPerp, Vp); + } // end loop over bins in which potential vector is not zero + // final contribution + f = std::fabs((detectionTime2 - antenna.getStartTime()) * + antenna.getSampleRate() + + 0.5 - endBin); + Vp = + betaPerp * sign * constants * f / denominator / midPaths[i].R_distance_; + 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 + +} // namespace corsika \ No newline at end of file diff --git a/corsika/detail/modules/radio/antennas/Antenna.inl b/corsika/detail/modules/radio/antennas/Antenna.inl new file mode 100644 index 0000000000000000000000000000000000000000..0b97560aaefc129779fdf1e39dfdfe8f68e9e61b --- /dev/null +++ b/corsika/detail/modules/radio/antennas/Antenna.inl @@ -0,0 +1,115 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <corsika/modules/radio/antennas/Antenna.hpp> + +namespace corsika { + + template <typename TAntennaImpl> + inline Antenna<TAntennaImpl>::Antenna(std::string const& name, Point const& location, + CoordinateSystemPtr const& coordinateSystem) + : name_(name) + , location_(location) + , coordinateSystem_(coordinateSystem){}; + + template <typename TAntennaImpl> + inline Point const& Antenna<TAntennaImpl>::getLocation() const { + return location_; + } + + template <typename TAntennaImpl> + inline std::string const& Antenna<TAntennaImpl>::getName() const { + return name_; + } + + template <typename TAntennaImpl> + inline void Antenna<TAntennaImpl>::startOfLibrary( + const boost::filesystem::path& directory, const std::string radioImplementation) { + + // calculate and save our filename + filename_ = (directory / this->getName()).string() + ".npz"; + + // get the axis labels for this antenna and write the first row. + axistype axis = 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"; + } + + if (radioImplementation == "ZHS" && TAntennaImpl::is_time_domain) { + for (size_t i = 0; i < axis.size() - 1; i++) { + axis.at(i) = (axis.at(i + 1) + axis.at(i)) / 2.; + } + // explicitly convert the arrays to the needed type for cnpy + axis.pop_back(); + long double const* raw_data = axis.data(); + std::vector<size_t> N = { + axis.size()}; // cnpy needs a vector here -- this should be axis.size() - 1 -- + // write the labels to the first row of the NumPy file + cnpy::npz_save(filename_, label, raw_data, N, "w"); + } else { + // explicitly convert the arrays to the needed type for cnpy + long double 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"); + } + } + + template <typename TAntennaImpl> + inline void Antenna<TAntennaImpl>::endOfShower(const int event, + std::string const& radioImplementation, + const double sampleRate) { + + // get the copy of the waveform data for this event + // we transpose it so that we can match dimensions with the + // time array that is already in the output file + std::vector<double> const& dataX = this->implementation().getWaveformX(); + std::vector<double> const& dataY = this->implementation().getWaveformY(); + std::vector<double> const& dataZ = this->implementation().getWaveformZ(); + + if (radioImplementation == "ZHS") { + std::vector<double> electricFieldX(dataX.size() - 1, + 0); // num_bins_, std::vector<double>(3, 0) + std::vector<double> electricFieldY(dataY.size() - 1, 0); + std::vector<double> electricFieldZ(dataZ.size() - 1, 0); + for (size_t i = 0; i < electricFieldX.size(); i++) { + electricFieldX.at(i) = -(dataX.at(i + 1) - dataX.at(i)) * sampleRate; + electricFieldY.at(i) = -(dataY.at(i + 1) - dataY.at(i)) * sampleRate; + electricFieldZ.at(i) = -(dataZ.at(i + 1) - dataZ.at(i)) * sampleRate; + } + // cnpy needs a vector for the shape + cnpy::npz_save(filename_, std::to_string(event) + "X", electricFieldX.data(), + {electricFieldX.size()}, "a"); + cnpy::npz_save(filename_, std::to_string(event) + "Y", electricFieldY.data(), + {electricFieldY.size()}, "a"); + cnpy::npz_save(filename_, std::to_string(event) + "Z", electricFieldZ.data(), + {electricFieldZ.size()}, "a"); + } else { + // cnpy needs a vector for the shape + // and write this event to the .npz archive + cnpy::npz_save(filename_, std::to_string(event) + "X", dataX.data(), {dataX.size()}, + "a"); + cnpy::npz_save(filename_, std::to_string(event) + "Y", dataY.data(), {dataY.size()}, + "a"); + cnpy::npz_save(filename_, std::to_string(event) + "Z", dataZ.data(), {dataZ.size()}, + "a"); + } + } + + template <typename TAntennaImpl> + inline TAntennaImpl& Antenna<TAntennaImpl>::implementation() { + return static_cast<TAntennaImpl&>(*this); + } + +} // namespace corsika diff --git a/corsika/detail/modules/radio/antennas/TimeDomainAntenna.inl b/corsika/detail/modules/radio/antennas/TimeDomainAntenna.inl new file mode 100644 index 0000000000000000000000000000000000000000..920b303b9d63281e708ad59b278b8089a4a98cf8 --- /dev/null +++ b/corsika/detail/modules/radio/antennas/TimeDomainAntenna.inl @@ -0,0 +1,130 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <corsika/modules/radio/antennas/TimeDomainAntenna.hpp> + +namespace corsika { + + inline TimeDomainAntenna::TimeDomainAntenna(std::string const& name, + Point const& location, + CoordinateSystemPtr coordinateSystem, + TimeType const& start_time, + TimeType const& duration, + InverseTimeType const& sample_rate, + TimeType const ground_hit_time) + : Antenna(name, location, coordinateSystem) + , start_time_(start_time) + , duration_(duration) + , sample_rate_(sample_rate) + , ground_hit_time_(ground_hit_time) + , num_bins_(static_cast<std::size_t>(duration * sample_rate + 1.5l)) + , waveformEX_(num_bins_, 0) + , waveformEY_(num_bins_, 0) + , waveformEZ_(num_bins_, 0) + , time_axis_(createTimeAxis()){}; + + inline void TimeDomainAntenna::receive(const TimeType time, + const Vector<dimensionless_d>& receive_vector, + const ElectricFieldVector& 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>( + std::floor((time - start_time_) * sample_rate_ + 0.5l))}; + + // store the x,y,z electric field components. + auto const& Electric_field_components{efield.getComponents(coordinateSystem_)}; + waveformEX_.at(timebin_) += (Electric_field_components.getX() * (1_m / 1_V)); + waveformEY_.at(timebin_) += (Electric_field_components.getY() * (1_m / 1_V)); + waveformEZ_.at(timebin_) += (Electric_field_components.getZ() * (1_m / 1_V)); + // TODO: Check how they are stored in memory, row-wise or column-wise? Probably use + // a 3D object + } + } + + inline void TimeDomainAntenna::receive(const TimeType time, + const Vector<dimensionless_d>& receive_vector, + const VectorPotential& vectorP) { + + 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>( + std::floor((time - start_time_) * sample_rate_ + 0.5l))}; + + // store the x,y,z electric field components. + auto const& Vector_potential_components{vectorP.getComponents(coordinateSystem_)}; + waveformEX_.at(timebin_) += + (Vector_potential_components.getX() * (1_m / (1_V * 1_s))); + waveformEY_.at(timebin_) += + (Vector_potential_components.getY() * (1_m / (1_V * 1_s))); + waveformEZ_.at(timebin_) += + (Vector_potential_components.getZ() * (1_m / (1_V * 1_s))); + // TODO: Check how they are stored in memory, row-wise or column-wise? Probably use + // a 3D object + } + } + + inline auto const& TimeDomainAntenna::getWaveformX() const { return waveformEX_; } + + inline auto const& TimeDomainAntenna::getWaveformY() const { return waveformEY_; } + + inline auto const& TimeDomainAntenna::getWaveformZ() const { return waveformEZ_; } + + inline std::vector<long double> TimeDomainAntenna::createTimeAxis() const { + + // create a 1-D xtensor to store time values so we can print them later. + std::vector<long double> times(num_bins_, 0); + + // calculate the sample_period + auto sample_period{1 / sample_rate_}; + + // fill in every time-value + for (std::size_t i = 0; i < num_bins_; i++) { + // create the current time in nanoseconds + times.at(i) = static_cast<long double>( + ((start_time_ - ground_hit_time_) + i * sample_period) / 1_ns); + } + + return times; + } + + inline auto const& TimeDomainAntenna::getAxis() const { return time_axis_; } + + inline InverseTimeType const& TimeDomainAntenna::getSampleRate() const { + return sample_rate_; + } + + inline TimeType const& TimeDomainAntenna::getStartTime() const { return start_time_; } + + inline void TimeDomainAntenna::reset() { + std::fill(waveformEX_.begin(), waveformEX_.end(), 0); + std::fill(waveformEY_.begin(), waveformEY_.end(), 0); + std::fill(waveformEZ_.begin(), waveformEZ_.end(), 0); + } + + inline YAML::Node TimeDomainAntenna::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; + } + +} // namespace corsika diff --git a/corsika/detail/modules/radio/detectors/AntennaCollection.inl b/corsika/detail/modules/radio/detectors/AntennaCollection.inl new file mode 100644 index 0000000000000000000000000000000000000000..d985bb98027ccd9f89b202c8352355dccd3da35f --- /dev/null +++ b/corsika/detail/modules/radio/detectors/AntennaCollection.inl @@ -0,0 +1,45 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <corsika/modules/radio/detectors/AntennaCollection.hpp> + +namespace corsika { + + template <typename TAntennaImpl> + inline void AntennaCollection<TAntennaImpl>::addAntenna(TAntennaImpl const& antenna) { + antennas_.push_back(antenna); + } + + template <typename TAntennaImpl> + inline TAntennaImpl& AntennaCollection<TAntennaImpl>::at(std::size_t const i) { + return antennas_.at(i); + } + + template <typename TAntennaImpl> + inline TAntennaImpl const& AntennaCollection<TAntennaImpl>::at( + std::size_t const i) const { + return antennas_.at(i); + } + + template <typename TAntennaImpl> + inline int AntennaCollection<TAntennaImpl>::size() const { + return antennas_.size(); + } + + template <typename TAntennaImpl> + inline std::vector<TAntennaImpl>& AntennaCollection<TAntennaImpl>::getAntennas() { + return antennas_; + } + + template <typename TAntennaImpl> + inline void AntennaCollection<TAntennaImpl>::reset() { + std::for_each(antennas_.begin(), antennas_.end(), std::mem_fn(&TAntennaImpl::reset)); + } + +} // namespace corsika diff --git a/corsika/detail/modules/radio/propagators/RadioPropagator.inl b/corsika/detail/modules/radio/propagators/RadioPropagator.inl new file mode 100644 index 0000000000000000000000000000000000000000..e5c65f97ef3518a5bf47467c8b3650956fcf3f65 --- /dev/null +++ b/corsika/detail/modules/radio/propagators/RadioPropagator.inl @@ -0,0 +1,18 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <corsika/modules/radio/propagators/RadioPropagator.hpp> + +namespace corsika { + + template <typename TImpl, typename TEnvironment> + inline RadioPropagator<TImpl, TEnvironment>::RadioPropagator(TEnvironment const& env) + : env_(env) {} + +} // namespace corsika diff --git a/corsika/detail/modules/radio/propagators/SignalPath.inl b/corsika/detail/modules/radio/propagators/SignalPath.inl new file mode 100644 index 0000000000000000000000000000000000000000..7de9c8419a9e75bde2fd556d717e1135028fa0cb --- /dev/null +++ b/corsika/detail/modules/radio/propagators/SignalPath.inl @@ -0,0 +1,29 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ + +#pragma once + +#include <corsika/modules/radio/propagators/SignalPath.hpp> + +namespace corsika { + + inline SignalPath::SignalPath( + TimeType const propagation_time, double const average_refractive_index, + double const refractive_index_source, double const refractive_index_destination, + Vector<dimensionless_d> const& emit, Vector<dimensionless_d> const& receive, + LengthType const R_distance, std::deque<Point> const& points) + : Path(points) + , propagation_time_(propagation_time) + , average_refractive_index_(average_refractive_index) + , refractive_index_source_(refractive_index_source) + , refractive_index_destination_(refractive_index_destination) + , emit_(emit) + , receive_(receive) + , R_distance_(R_distance) {} + +} // namespace corsika \ No newline at end of file diff --git a/corsika/detail/modules/radio/propagators/SimplePropagator.inl b/corsika/detail/modules/radio/propagators/SimplePropagator.inl new file mode 100644 index 0000000000000000000000000000000000000000..4101878983cb7f3060e0743392a08caca9689508 --- /dev/null +++ b/corsika/detail/modules/radio/propagators/SimplePropagator.inl @@ -0,0 +1,70 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <corsika/modules/radio/propagators/SimplePropagator.hpp> + +namespace corsika { + + template <typename TEnvironment> + inline SimplePropagator<TEnvironment>::SimplePropagator(TEnvironment const& env) + : RadioPropagator<SimplePropagator, TEnvironment>(env){}; + + template <typename TEnvironment> + inline typename SimplePropagator<TEnvironment>::SignalPathCollection + SimplePropagator<TEnvironment>::propagate(Point const& source, Point const& destination, + LengthType const stepsize) const { + + /** + * This is the simplest case of straight propagator + * where no integration takes place. + * This can be used for fast tests and checks of the radio module. + * + */ + + // these are used for the direction of emission and reception of signal at the antenna + auto const emit_{(destination - source).normalized()}; + auto const receive_{-emit_}; + + // the geometrical distance from the point of emission to an observer + auto const distance_{(destination - source).getNorm()}; + + // get the universe for this environment + auto const* const universe{Base::env_.getUniverse().get()}; + + // the points that consist the signal path (source & destination). + std::deque<Point> points; + + // store value of the refractive index at points. + std::vector<double> rindex; + rindex.reserve(2); + + // get and store the refractive index of the first point 'source'. + auto const* const nodeSource{universe->getContainingNode(source)}; + auto const ri_source{nodeSource->getModelProperties().getRefractiveIndex(source)}; + rindex.push_back(ri_source); + points.push_back(source); + + // add the refractive index of last point 'destination' and store it. + auto const* const node{universe->getContainingNode(destination)}; + auto const ri_destination{node->getModelProperties().getRefractiveIndex(destination)}; + rindex.push_back(ri_destination); + points.push_back(destination); + + // compute the average refractive index. + auto const averageRefractiveIndex_ = (ri_source + ri_destination) / 2; + + // compute the total time delay. + TimeType const time = averageRefractiveIndex_ * (distance_ / constants::c); + + return {SignalPath(time, averageRefractiveIndex_, ri_source, ri_destination, emit_, + receive_, distance_, points)}; + + } // END: propagate() + +} // namespace corsika diff --git a/corsika/detail/modules/radio/propagators/StraightPropagator.inl b/corsika/detail/modules/radio/propagators/StraightPropagator.inl new file mode 100644 index 0000000000000000000000000000000000000000..9962fc133003d855ab682aa15b12c275e225c035 --- /dev/null +++ b/corsika/detail/modules/radio/propagators/StraightPropagator.inl @@ -0,0 +1,169 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <corsika/modules/radio/propagators/StraightPropagator.hpp> + +namespace corsika { + + template <typename TEnvironment> + // TODO: maybe the constructor doesn't take any arguments for the environment (?) + inline StraightPropagator<TEnvironment>::StraightPropagator(TEnvironment const& env) + : RadioPropagator<StraightPropagator, TEnvironment>(env){}; + + template <typename TEnvironment> + inline typename StraightPropagator<TEnvironment>::SignalPathCollection + StraightPropagator<TEnvironment>::propagate(Point const& source, + Point const& destination, + LengthType const stepsize) const { + + /* + * get the normalized (unit) vector from `source` to `destination'. + * this is also the `emit` and `receive` vectors in the SignalPath class. + * in this case emit and receive unit vectors should be the same + * so they are both called direction + */ + + // these are used for the direction of emission and reception of signal at the antenna + auto const emit_{(destination - source).normalized()}; + auto const receive_{-emit_}; + + // the distance from the point of emission to an observer + auto const distance_{(destination - source).getNorm()}; + + try { + if (stepsize <= 0.5 * distance_) { + + // "step" is the direction vector with length `stepsize` + auto const step{emit_ * stepsize}; + + // calculate the number of points (roughly) for the numerical integration + auto const n_points{(destination - source).getNorm() / stepsize}; + + // get the universe for this environment + auto const* const universe{Base::env_.getUniverse().get()}; + + // the points that we build along the way for the numerical integration + std::deque<Point> points; + + // store value of the refractive index at points + std::vector<double> rindex; + rindex.reserve(n_points); + + // get and store the refractive index of the first point 'source' + auto const* const nodeSource{universe->getContainingNode(source)}; + auto const ri_source{nodeSource->getModelProperties().getRefractiveIndex(source)}; + rindex.push_back(ri_source); + points.push_back(source); + + // loop from `source` to `destination` to store values before Simpson's rule. + // this loop skips the last point 'destination' and "misses" the extra point + for (auto point = source + step; (point - destination).getNorm() > 0.6 * stepsize; + point = point + step) { + + // get the environment node at this specific 'point' + auto const* const node{universe->getContainingNode(point)}; + + // get the associated refractivity at 'point' + auto const refractive_index{ + node->getModelProperties().getRefractiveIndex(point)}; + // auto const refractive_index{1.000327}; + rindex.push_back(refractive_index); + + // add this 'point' to our deque collection + points.push_back(point); + } + + // Get the extra points that the for loop misses until the destination + auto const extrapoint_{points.back() + step}; + + // add the refractive index of last point 'destination' and store it + auto const* const node{universe->getContainingNode(destination)}; + auto const ri_destination{ + node->getModelProperties().getRefractiveIndex(destination)}; + // auto const ri_destination{1.000327}; + rindex.push_back(ri_destination); + points.push_back(destination); + + auto N = rindex.size(); + std::size_t index = 0; + double sum = rindex.at(index); + auto refra_ = rindex.at(index); + TimeType time{0_s}; + + if ((N - 1) % 2 == 0) { + // Apply the standard Simpson's rule + auto const h = ((destination - source).getNorm()) / (N - 1); + + for (std::size_t index = 1; index < (N - 1); index += 2) { + sum += 4 * rindex.at(index); + refra_ += rindex.at(index); + } + for (std::size_t index = 2; index < (N - 1); index += 2) { + sum += 2 * rindex.at(index); + refra_ += rindex.at(index); + } + index = N - 1; + sum = sum + rindex.at(index); + refra_ += rindex.at(index); + + // compute the total time delay. + time = sum * (h / (3 * constants::c)); + } else { + // Apply Simpson's rule for one "extra" point and then subtract the difference + points.pop_back(); + rindex.pop_back(); + auto const* const node{universe->getContainingNode(extrapoint_)}; + auto const ri_extrapoint{ + node->getModelProperties().getRefractiveIndex(extrapoint_)}; + rindex.push_back(ri_extrapoint); + points.push_back(extrapoint_); + auto const extrapoint2_{extrapoint_ + step}; + auto const* const node2{universe->getContainingNode(extrapoint2_)}; + auto const ri_extrapoint2{ + node2->getModelProperties().getRefractiveIndex(extrapoint2_)}; + rindex.push_back(ri_extrapoint2); + points.push_back(extrapoint2_); + N = rindex.size(); + auto const h = ((extrapoint2_ - source).getNorm()) / (N - 1); + for (std::size_t index = 1; index < (N - 1); index += 2) { + sum += 4 * rindex.at(index); + refra_ += rindex.at(index); + } + for (std::size_t index = 2; index < (N - 1); index += 2) { + sum += 2 * rindex.at(index); + refra_ += rindex.at(index); + } + index = N - 1; + sum = sum + rindex.at(index); + refra_ += rindex.at(index); + + // compute the total time delay including the correction + time = + sum * (h / (3 * constants::c)) - + (ri_extrapoint2 * ((extrapoint2_ - destination).getNorm()) / constants::c); + } + + // uncomment the following if you want to skip the integration for fast tests + // TimeType time = ri_destination * (distance_ / constants::c); + + // compute the average refractive index. + auto const averageRefractiveIndex_ = refra_ / N; + + return {SignalPath(time, averageRefractiveIndex_, ri_source, ri_destination, + emit_, receive_, distance_, points)}; + } else { + throw stepsize; + } + } catch (const LengthType& s) { + CORSIKA_LOG_ERROR("Please choose a smaller stepsize for the numerical integration"); + } + + } // END: propagate() + +} // namespace corsika diff --git a/corsika/framework/core/PhysicalConstants.hpp b/corsika/framework/core/PhysicalConstants.hpp index 92df20c2018ef382aac191517116d98363155f25..5fd079bd2985f9ece15619964da7a2483ee9576f 100644 --- a/corsika/framework/core/PhysicalConstants.hpp +++ b/corsika/framework/core/PhysicalConstants.hpp @@ -35,6 +35,10 @@ namespace corsika::constants { // elementary charge constexpr quantity<electric_charge_d> e{Rep(1.6021766208e-19L) * coulomb}; + // vacuum permittivity + constexpr quantity<dimensions<-3, -1, 4, 2>> epsilonZero{Rep(8.8541878128e-12L) * + farad / meter}; + // electronvolt // constexpr quantity<hepenergy_d> eV{e / coulomb * joule}; diff --git a/corsika/framework/core/PhysicalGeometry.hpp b/corsika/framework/core/PhysicalGeometry.hpp index 6a1227de457dc5febde7f4d730ca74893972fdd6..e0ea84b10d38899161b8dade986618311060e648 100644 --- a/corsika/framework/core/PhysicalGeometry.hpp +++ b/corsika/framework/core/PhysicalGeometry.hpp @@ -43,4 +43,14 @@ namespace corsika { **/ using LengthVector = Vector<length_d>; + /** + * A 3D vector defined in a specific coordinate system with units ElectricFieldType + **/ + typedef Vector<ElectricFieldType::dimension_type> ElectricFieldVector; + + /** + * A 3D vector defined in a specific coordinate system with units VectorPotentialType + **/ + typedef Vector<VectorPotentialType::dimension_type> VectorPotential; + } // namespace corsika diff --git a/corsika/framework/core/PhysicalUnits.hpp b/corsika/framework/core/PhysicalUnits.hpp index 82373acc9057817ccde077ad93adac8f3584360b..ba04c819969448fc586733c8e642cdc5a6b680d2 100644 --- a/corsika/framework/core/PhysicalUnits.hpp +++ b/corsika/framework/core/PhysicalUnits.hpp @@ -96,6 +96,10 @@ namespace corsika::units::si { phys::units::quantity<phys::units::dimensions<2, -1, 0>, double>; using MagneticFluxType = phys::units::quantity<phys::units::magnetic_flux_density_d, double>; + using ElectricFieldType = + phys::units::quantity<phys::units::dimensions<1, 1, -3, -1>, double>; + using VectorPotentialType = + phys::units::quantity<phys::units::dimensions<1, 1, -2, -1>, double>; template <typename DimFrom, typename DimTo> auto constexpr conversion_factor_HEP_to_SI() { diff --git a/corsika/modules/TimeCut.hpp b/corsika/modules/TimeCut.hpp new file mode 100644 index 0000000000000000000000000000000000000000..2c846ad0ca5dbb92266321469a46b79789ec79c7 --- /dev/null +++ b/corsika/modules/TimeCut.hpp @@ -0,0 +1,44 @@ +/* + * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ + +#pragma once + +#include <corsika/framework/core/ParticleProperties.hpp> +#include <corsika/framework/core/PhysicalUnits.hpp> +#include <corsika/framework/process/ContinuousProcess.hpp> +#include <corsika/framework/core/Step.hpp> + +#include <corsika/setup/SetupStack.hpp> +#include <corsika/setup/SetupTrajectory.hpp> + +namespace corsika { + + /** + * Simple TimeCut process, removes particles older than specified cut time. + */ + class TimeCut : public ContinuousProcess<TimeCut> { + + public: + TimeCut(TimeType const time); + + template <typename Particle> + ProcessReturn doContinuous( + Step<Particle> const& step, + const bool limitFlag = false); // this is not used for TimeCut + template <typename Particle, typename Track> + LengthType getMaxStepLength(Particle const&, Track const&) { + return meter * std::numeric_limits<double>::infinity(); + } + + private: + TimeType time_; + }; + +} // namespace corsika + +#include <corsika/detail/modules/TimeCut.inl> diff --git a/corsika/modules/radio/CoREAS.hpp b/corsika/modules/radio/CoREAS.hpp new file mode 100644 index 0000000000000000000000000000000000000000..0756a78af2b932093312fd3416780929c319379b --- /dev/null +++ b/corsika/modules/radio/CoREAS.hpp @@ -0,0 +1,59 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <corsika/modules/radio/RadioProcess.hpp> +#include <corsika/modules/radio/propagators/StraightPropagator.hpp> +#include <corsika/framework/geometry/QuantityVector.hpp> +#include <corsika/framework/core/PhysicalUnits.hpp> +#include <corsika/modules/radio/propagators/SignalPath.hpp> +#include <cmath> + +namespace corsika { + + template <typename TRadioDetector, typename TPropagator> + class CoREAS final + : public RadioProcess<TRadioDetector, CoREAS<TRadioDetector, TPropagator>, + TPropagator> { + + public: + // an identifier for which algorithm was used + static constexpr auto algorithm = "CoREAS"; + + /** + * Construct a new CoREAS instance. + * + * This forwards the detector and other arguments to + * the RadioProcess parent. + * + */ + template <typename... TArgs> + CoREAS(TRadioDetector& detector, TArgs&&... args) + : RadioProcess<TRadioDetector, CoREAS, TPropagator>(detector, args...){}; + + /** + * Simulate the radio emission from a particle across a track. + * + * This must be provided by the TRadioImpl. + * + * @param particle The current particle. + * @param track The current track. + * + */ + template <typename Particle> + ProcessReturn simulate(Step<Particle> const& step); + + using Base = + RadioProcess<TRadioDetector, CoREAS<TRadioDetector, TPropagator>, TPropagator>; + using Base::antennas_; + + }; // end of class CoREAS + +} // namespace corsika + +#include <corsika/detail/modules/radio/CoREAS.inl> \ No newline at end of file diff --git a/corsika/modules/radio/RadioProcess.hpp b/corsika/modules/radio/RadioProcess.hpp new file mode 100644 index 0000000000000000000000000000000000000000..9ae9087bde8e81b06c143f9fff320cacd7fe279f --- /dev/null +++ b/corsika/modules/radio/RadioProcess.hpp @@ -0,0 +1,111 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <istream> +#include <fstream> +#include <iostream> +#include <string> +#include <corsika/output/BaseOutput.hpp> +#include <corsika/framework/process/ContinuousProcess.hpp> +#include <corsika/framework/core/Step.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. + * TAntennaCollection is the detector instance that stores antennas + * and is responsible for managing the output writing. + */ + template <typename TAntennaCollection, typename TRadioImpl, typename TPropagator> + class RadioProcess : public ContinuousProcess< + RadioProcess<TAntennaCollection, TRadioImpl, TPropagator>>, + public BaseOutput { + + /* + * A collection of filter objects for deciding on valid particles and tracks. + */ + // std::vector<std::function<bool(ParticleType&, TrackType const&)>> filters_; + + /** + * Get a reference to the underlying radio implementation. + */ + TRadioImpl& implementation(); + + /** + * Get a const reference to the underlying implementation. + */ + TRadioImpl const& implementation() const; + + protected: + 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(TAntennaCollection& antennas, TArgs&&... args); + + /** + * Perform the continuous process (radio emission). + * + * This handles filtering individual particle tracks + * before passing them to `Simulate`.` + * + * @param particle The current particle. + * @param track The current track. + */ + template <typename Particle> + ProcessReturn doContinuous(Step<Particle> const& step, bool const); + + /** + * 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. + */ + template <typename Particle, typename Track> + LengthType getMaxStepLength(Particle const& vParticle, Track const& vTrack) const; + + /** + * Called at the start of each library. + */ + void startOfLibrary(boost::filesystem::path const& directory) final override; + + /** + * Called at the end of each shower. + */ + virtual void endOfShower(unsigned int const) final override; + + /** + * Called at the end of each library. + * + */ + void endOfLibrary() final override {} + + /** + * Get the configuration of this output. + */ + YAML::Node getConfig() const final; + + }; // END: class RadioProcess + +} // namespace corsika + +#include <corsika/detail/modules/radio/RadioProcess.inl> \ No newline at end of file diff --git a/corsika/modules/radio/ZHS.hpp b/corsika/modules/radio/ZHS.hpp new file mode 100644 index 0000000000000000000000000000000000000000..5ec38895b8a8c75a9cb6974b996253a430de1f3e --- /dev/null +++ b/corsika/modules/radio/ZHS.hpp @@ -0,0 +1,62 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <corsika/modules/radio/RadioProcess.hpp> +#include <corsika/modules/radio/propagators/StraightPropagator.hpp> +#include <corsika/framework/geometry/QuantityVector.hpp> +#include <corsika/framework/core/PhysicalUnits.hpp> +#include <corsika/modules/radio/propagators/SignalPath.hpp> +#include <cmath> + +namespace corsika { + + /** + * A concrete implementation of the ZHS algorithm. + */ + template <typename TRadioDetector, typename TPropagator> + class ZHS final : public RadioProcess<TRadioDetector, ZHS<TRadioDetector, TPropagator>, + TPropagator> { + + public: + // an identifier for which algorithm was used + static constexpr auto algorithm = "ZHS"; + + /** + * Construct a new ZHS instance. + * + * This forwards the detector and other arguments to + * the RadioProcess parent. + * + */ + template <typename... TArgs> + ZHS(TRadioDetector& detector, TArgs&&... args) + : RadioProcess<TRadioDetector, ZHS, TPropagator>(detector, args...){}; + + /** + * Simulate the radio emission from a particle across a track. + * + * This must be provided by the TRadioImpl. + * + * @param particle The current particle. + * @param track The current track. + * + */ + template <typename Particle> + ProcessReturn simulate(Step<Particle> const& step) const; + + private: + using Base = + RadioProcess<TRadioDetector, ZHS<TRadioDetector, TPropagator>, TPropagator>; + using Base::antennas_; + + }; // END: class ZHS + +} // namespace corsika + +#include <corsika/detail/modules/radio/ZHS.inl> \ No newline at end of file diff --git a/corsika/modules/radio/antennas/Antenna.hpp b/corsika/modules/radio/antennas/Antenna.hpp new file mode 100644 index 0000000000000000000000000000000000000000..b5aeb8fb99be1dd69e0ce57536a2e0e731921d09 --- /dev/null +++ b/corsika/modules/radio/antennas/Antenna.hpp @@ -0,0 +1,127 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <cnpy.hpp> +#include <boost/filesystem.hpp> +#include <corsika/framework/geometry/Point.hpp> +#include <corsika/framework/core/PhysicalGeometry.hpp> + +namespace corsika { + + /** + * A common abstract interface for radio antennas. + * + * All concrete antenna implementations should be of + * type Antenna<T> where T is a concrete antenna implementation. + * + */ + template <typename TAntennaImpl> + class Antenna { + + protected: + std::string const name_; ///< The name/identifier of this antenna. + Point const location_; ///< The location of this antenna. + CoordinateSystemPtr const coordinateSystem_; ///< The coordinate system of the antenna + std::string filename_ = ""; ///< The filename for the output file for this antenna. + + public: + using axistype = std::vector<long double>; + + /** + * \brief Construct a base antenna instance. + * + * @param name A name for this antenna. + * @param location The location of this antenna. + * + */ + Antenna(std::string const& name, Point const& location, + CoordinateSystemPtr const& coordinateSystem); + + /** + * Receive a signal at this antenna. + * + * This is a general implementation call that must be specialized + * for the particular antenna implementation and usage. + * + */ + template <typename... TVArgs> + void receive(TVArgs&&... args); + + /** + * Get the location of this antenna. + */ + Point const& getLocation() const; + + /** + * Get the name of this name antenna. + * + * This is used in producing the output data file. + */ + std::string const& getName() const; + + /** + * Reset the antenna before starting a new simulation. + */ + 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. + */ + axistype getAxis() const; + + /** + * Return a reference to the underlying waveform data for X polarization. + * + * This is used when writing the antenna information to disk + * and will be converted to a 32-bit float before writing. + */ + std::vector<double> const& getWaveformX() const; + + /** + * Return a reference to the underlying waveform data for Y polarization. + * + * This is used when writing the antenna information to disk + * and will be converted to a 32-bit float before writing. + */ + std::vector<double> const& getWaveformY() const; + + /** + * Return a reference to the underlying waveform data for Z polarization. + * + * This is used when writing the antenna information to disk + * and will be converted to a 32-bit float before writing. + */ + std::vector<double> const& getWaveformZ() const; + + /** + * Prepare for the start of the library. + */ + void startOfLibrary(boost::filesystem::path const& directory, + std::string const radioImplementation); + + /** + * Flush the data from this shower to disk. + */ + void endOfShower(int const event, std::string const& radioImplementation, + double const sampleRate); + + protected: + /** + * Get a reference to the underlying radio implementation. + */ + TAntennaImpl& implementation(); + + }; // END: class Antenna final + +} // namespace corsika + +#include <corsika/detail/modules/radio/antennas/Antenna.inl> diff --git a/corsika/modules/radio/antennas/TimeDomainAntenna.hpp b/corsika/modules/radio/antennas/TimeDomainAntenna.hpp new file mode 100644 index 0000000000000000000000000000000000000000..801c529b87a035a021ab1c3aeb52cd86adf9e14d --- /dev/null +++ b/corsika/modules/radio/antennas/TimeDomainAntenna.hpp @@ -0,0 +1,138 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <corsika/modules/radio/antennas/Antenna.hpp> +#include <vector> + +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> { + + 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 const num_bins_; ///< The number of bins used. + std::vector<double> waveformEX_; ///< EX polarization. + std::vector<double> waveformEY_; ///< EY polarization. + std::vector<double> waveformEZ_; ///< EZ polarization. + TimeType const ground_hit_time_; ///< The time the primary particle hits the ground. + std::vector<long double> const + time_axis_; ///< The time axis corresponding to the electric field. + + public: + // import the methods from the antenna + + // label this as a time-domain antenna. + static constexpr bool is_time_domain{true}; + + 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 coordinateSystem The coordinate system 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 ground_hit_time The time the primary particle hits the ground on a + * straight vertical line. + * + */ + TimeDomainAntenna(std::string const& name, Point const& location, + CoordinateSystemPtr coordinateSystem, TimeType const& start_time, + TimeType const& duration, InverseTimeType const& sample_rate, + TimeType const ground_hit_time); + + /** + * 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); + + void receive(TimeType const time, Vector<dimensionless_d> const& receive_vector, + VectorPotential const& vectorP); + + /** + * Return the time-units of each waveform for X polarization + * + * This returns them in nanoseconds for ease of use. + */ + auto const& getWaveformX() const; + + /** + * Return the time-units of each waveform for Y polarization + * + * This returns them in nanoseconds for ease of use. + */ + auto const& getWaveformY() const; + + /** + * Return the time-units of each waveform for Z polarization + * + * This returns them in nanoseconds for ease of use. + */ + auto const& getWaveformZ() const; + + /** + * Creates time-units of each waveform. + * + * It creates them in nanoseconds for ease of use. + */ + std::vector<long double> createTimeAxis() const; + + /** + * Return the time-units of each waveform. + * + * This returns them in nanoseconds for ease of use. + */ + auto const& getAxis() const; + + /** + * Returns the sampling rate of the time domain antenna. + */ + InverseTimeType const& getSampleRate() const; + + /** + * Returns the start time of detection for the time domain antenna. + */ + TimeType const& getStartTime() const; + + /** + * Reset the antenna before starting a new simulation. + */ + void reset(); + + /** + * Return a YAML configuration for this antenna. + */ + YAML::Node getConfig() const; + + }; // END: class TimeDomainAntenna + +} // namespace corsika + +#include <corsika/detail/modules/radio/antennas/TimeDomainAntenna.inl> diff --git a/corsika/modules/radio/detectors/AntennaCollection.hpp b/corsika/modules/radio/detectors/AntennaCollection.hpp new file mode 100644 index 0000000000000000000000000000000000000000..368be18d934d2a4093a9871c6e51e4157523c058 --- /dev/null +++ b/corsika/modules/radio/detectors/AntennaCollection.hpp @@ -0,0 +1,69 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +namespace corsika { + + /** + * The base interface for radio detectors. + * At the moment it is a collection of antennas with the same implementation. + */ + + template <typename TAntennaImpl> + class AntennaCollection { + + /** + * The collection of antennas used in this simulation. + */ + std::vector<TAntennaImpl> antennas_; + + public: + /** + * Add an antenna to this radio process. + * + * @param antenna The antenna to add + */ + void addAntenna(TAntennaImpl const& antenna); + + /** + * Get the specific antenna at that place in the collection + * + * @param index in the collection + */ + TAntennaImpl& at(std::size_t const i); + + TAntennaImpl const& at(std::size_t const i) const; + + /** + * Get the number of antennas in the collection + */ + int size() const; + + /** + * Get a *non*-const reference to the collection of antennas. + * + * @returns An iterable mutable reference to the antennas. + */ + std::vector<TAntennaImpl>& getAntennas(); + + /** + * Get a const reference to the collection of antennas. + * + * @returns An iterable mutable reference to the antennas. + */ + std::vector<TAntennaImpl> const& getAntennas() const; + + /** + * Reset all the antenna waveforms. + */ + void reset(); + }; // END: class RadioDetector + +} // namespace corsika + +#include <corsika/detail/modules/radio/detectors/AntennaCollection.inl> diff --git a/corsika/modules/radio/propagators/RadioPropagator.hpp b/corsika/modules/radio/propagators/RadioPropagator.hpp new file mode 100644 index 0000000000000000000000000000000000000000..599ada07f8029beb776aeeab9ada233c9cb22f26 --- /dev/null +++ b/corsika/modules/radio/propagators/RadioPropagator.hpp @@ -0,0 +1,47 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <vector> + +#include <corsika/framework/geometry/Path.hpp> +#include <corsika/modules/radio/propagators/SignalPath.hpp> + +namespace corsika { + + /** + * Radio propagators are used to calculate the propagation + * paths from particles to antennas. Any class that wants + * to be used as a RadioPropagator must implement the + * following methods: + * + * SignalPathCollection Propagate(Point const& start, + * Point const& end, + * LengthType const stepsize); + */ + template <typename TImpl, typename TEnvironment> + class RadioPropagator { + + protected: + // Since we typically know roughly how many paths will + // be computed for a given propagator, we use a std::vector here. + using SignalPathCollection = std::vector<SignalPath> const; + + TEnvironment const& env_; ///< The environment. + + public: + /** + * Construct a new RadioPropagator instance. + */ + RadioPropagator(TEnvironment const& env); + + }; // class RadioPropagator + +} // namespace corsika + +#include <corsika/detail/modules/radio/propagators/RadioPropagator.inl> \ No newline at end of file diff --git a/corsika/modules/radio/propagators/SignalPath.hpp b/corsika/modules/radio/propagators/SignalPath.hpp new file mode 100644 index 0000000000000000000000000000000000000000..9a1239223838b5c56dd49384d9152bb42ee9032d --- /dev/null +++ b/corsika/modules/radio/propagators/SignalPath.hpp @@ -0,0 +1,51 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ + +#pragma once + +#include <corsika/framework/geometry/Path.hpp> +#include <corsika/framework/geometry/Vector.hpp> + +namespace corsika { + + /** + * Store the photon signal path between two points. + * + * This is basically a container class + */ + struct SignalPath final : private Path { + + // TODO: discuss if we need average refractivity or average refractive index + TimeType const propagation_time_; ///< The total propagation time. + double const average_refractive_index_; ///< The average refractive index. + double const refractive_index_source_; ///< The refractive index at the source. + double const + refractive_index_destination_; ///< The refractive index at the destination point. + Vector<dimensionless_d> const emit_; ///< The (unit-length) emission vector. + Vector<dimensionless_d> const receive_; ///< The (unit-length) receive vector. + std::deque<Point> const + points_; ///< A collection of points that make up the geometrical path. + LengthType const + R_distance_; ///< The distance from the point of emission to an observer. TODO: + ///< optical path, not geometrical! (probably) + + /** + * Create a new SignalPath instance. + */ + SignalPath(TimeType const propagation_time, double const average_refractive_index, + double const refractive_index_source, + double const refractive_index_destination, + Vector<dimensionless_d> const& emit, + Vector<dimensionless_d> const& receive, LengthType const R_distance, + std::deque<Point> const& points); + + }; // END: class SignalPath final + +} // namespace corsika + +#include <corsika/detail/modules/radio/propagators/SignalPath.inl> \ No newline at end of file diff --git a/corsika/modules/radio/propagators/SimplePropagator.hpp b/corsika/modules/radio/propagators/SimplePropagator.hpp new file mode 100644 index 0000000000000000000000000000000000000000..33c402774178560248a7e5949bb69f1794ca0f7f --- /dev/null +++ b/corsika/modules/radio/propagators/SimplePropagator.hpp @@ -0,0 +1,51 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <corsika/media/Environment.hpp> +#include <corsika/framework/geometry/Point.hpp> +#include <corsika/framework/geometry/Vector.hpp> +#include <corsika/framework/core/PhysicalConstants.hpp> +#include <corsika/framework/core/PhysicalUnits.hpp> +#include <corsika/modules/radio/propagators/RadioPropagator.hpp> + +namespace corsika { + + /** + * This class implements a simple propagator that uses + * the straight-line (vector) between the particle + * location and the antenna as the trajectory. + * + */ + template <typename TEnvironment> + class SimplePropagator final + : public RadioPropagator<SimplePropagator<TEnvironment>, TEnvironment> { + + using Base = RadioPropagator<SimplePropagator<TEnvironment>, TEnvironment>; + using SignalPathCollection = typename Base::SignalPathCollection; + + public: + /** + * Construct a new SimplePropagator with a given environment. + * + */ + SimplePropagator(TEnvironment const& env); + + /** + * Return the collection of paths from `source` to `destination`. + * Hence, the signal propagated from the + * emission point to the antenna location. + * + */ + SignalPathCollection propagate(Point const& source, Point const& destination, + LengthType const stepsize) const; + }; // End: SimplePropagator + +} // namespace corsika + +#include <corsika/detail/modules/radio/propagators/SimplePropagator.inl> \ No newline at end of file diff --git a/corsika/modules/radio/propagators/StraightPropagator.hpp b/corsika/modules/radio/propagators/StraightPropagator.hpp new file mode 100644 index 0000000000000000000000000000000000000000..d6c156336efadd83e94434360e14caa8ed72bb07 --- /dev/null +++ b/corsika/modules/radio/propagators/StraightPropagator.hpp @@ -0,0 +1,52 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#pragma once + +#include <corsika/media/Environment.hpp> +#include <corsika/framework/geometry/Point.hpp> +#include <corsika/framework/geometry/Vector.hpp> +#include <corsika/framework/core/PhysicalConstants.hpp> +#include <corsika/framework/core/PhysicalUnits.hpp> +#include <corsika/modules/radio/propagators/RadioPropagator.hpp> + +namespace corsika { + + /** + * This class implements a basic propagator that uses + * the straight-line (vector) between the particle + * location and the antenna as the trajectory. + * + * This is what is used in ZHAireS and CoREAS in C7. + */ + template <typename TEnvironment> + class StraightPropagator final + : public RadioPropagator<StraightPropagator<TEnvironment>, TEnvironment> { + + using Base = RadioPropagator<StraightPropagator<TEnvironment>, TEnvironment>; + using SignalPathCollection = typename Base::SignalPathCollection; + + public: + /** + * Construct a new StraightPropagator with a given environment. + * + */ + StraightPropagator(TEnvironment const& env); + + /** + * Return the collection of paths from `start` to `end`. + * or from 'source' which is the emission point to 'destination' + * which is the location of the antenna + */ + SignalPathCollection propagate(Point const& source, Point const& destination, + LengthType const stepsize) const; + + }; // End: StraightPropagator + +} // namespace corsika + +#include <corsika/detail/modules/radio/propagators/StraightPropagator.inl> \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 7b89a91772b63dfc731b7ba24d33536158d356f9..7e1df4b56ae2ab93962396f77d941166c619c43b 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -68,4 +68,20 @@ add_executable (water water.cpp) target_link_libraries (water CORSIKA8::CORSIKA8) add_executable (environment environment.cpp) + target_link_libraries (environment CORSIKA8::CORSIKA8) +add_executable (synchrotron_test_manual_tracking synchrotron_test_manual_tracking.cpp) +target_link_libraries (synchrotron_test_manual_tracking CORSIKA8::CORSIKA8) +CORSIKA_REGISTER_EXAMPLE (synchrotron_test_manual_tracking) + +add_executable (synchrotron_test_C8tracking synchrotron_test_C8tracking.cpp) +target_link_libraries (synchrotron_test_C8tracking CORSIKA8::CORSIKA8) +CORSIKA_REGISTER_EXAMPLE (synchrotron_test_C8tracking) + +add_executable (clover_leaf clover_leaf.cpp) +target_link_libraries (clover_leaf CORSIKA8::CORSIKA8) +CORSIKA_REGISTER_EXAMPLE (clover_leaf) + +add_executable (radio_em_shower radio_em_shower.cpp) +target_link_libraries (radio_em_shower CORSIKA8::CORSIKA8) +CORSIKA_REGISTER_EXAMPLE (radio_em_shower RUN_OPTIONS 10 1121673489) diff --git a/examples/boundary_example.cpp b/examples/boundary_example.cpp index 4ede912094f627f1e590996a183ba5ce4b98e5db..b9037bf019255e7d4232f3cbb4c256811a30766d 100644 --- a/examples/boundary_example.cpp +++ b/examples/boundary_example.cpp @@ -80,7 +80,7 @@ private: // int main() { - logging::set_level(logging::level::info); + logging::set_level(logging::level::warn); CORSIKA_LOG_INFO("boundary_example"); diff --git a/examples/cascade_example.cpp b/examples/cascade_example.cpp index 8f846ac8130a3336ef6ec242d8398e7f72f7c214..c27511c18ef03c561838ee45fe21f13763ed002a 100644 --- a/examples/cascade_example.cpp +++ b/examples/cascade_example.cpp @@ -57,7 +57,7 @@ using namespace std; // int main() { - logging::set_level(logging::level::info); + logging::set_level(logging::level::warn); CORSIKA_LOG_INFO("cascade_example"); diff --git a/examples/cascade_proton_example.cpp b/examples/cascade_proton_example.cpp index 62ce71079ddbeebf50e39115cfa7729ac42f9bb0..efb99770ad9e22dea7b94c71ed735a0ad1c680b4 100644 --- a/examples/cascade_proton_example.cpp +++ b/examples/cascade_proton_example.cpp @@ -60,7 +60,7 @@ using namespace std; // int main() { - logging::set_level(logging::level::info); + logging::set_level(logging::level::warn); std::cout << "cascade_proton_example" << std::endl; diff --git a/examples/clover_leaf.cpp b/examples/clover_leaf.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ccb8c7e79aed238d9bfeabe5994dac0efe41cbff --- /dev/null +++ b/examples/clover_leaf.cpp @@ -0,0 +1,228 @@ +/* + * (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ + +#include <corsika/framework/core/Cascade.hpp> +#include <corsika/framework/geometry/Sphere.hpp> +#include <corsika/framework/geometry/PhysicalGeometry.hpp> +#include <corsika/framework/process/ProcessSequence.hpp> +#include <corsika/framework/random/RNGManager.hpp> +#include <corsika/framework/core/PhysicalUnits.hpp> +#include <corsika/framework/utility/CorsikaFenv.hpp> +#include <corsika/framework/core/Logging.hpp> + +#include <corsika/output/OutputManager.hpp> + +#include <corsika/media/Environment.hpp> +#include <corsika/media/HomogeneousMedium.hpp> +#include <corsika/media/IMagneticFieldModel.hpp> +#include <corsika/media/NuclearComposition.hpp> +#include <corsika/media/MediumPropertyModel.hpp> +#include <corsika/media/UniformMagneticField.hpp> +#include <corsika/media/UniformRefractiveIndex.hpp> +#include <corsika/media/ShowerAxis.hpp> + +#include <corsika/setup/SetupStack.hpp> +#include <corsika/setup/SetupTrajectory.hpp> + +#include <corsika/modules/radio/RadioProcess.hpp> +#include <corsika/modules/radio/CoREAS.hpp> +#include <corsika/modules/radio/ZHS.hpp> +#include <corsika/modules/radio/antennas/Antenna.hpp> +#include <corsika/modules/radio/antennas/TimeDomainAntenna.hpp> +#include <corsika/modules/radio/detectors/AntennaCollection.hpp> +#include <corsika/modules/radio/propagators/StraightPropagator.hpp> +#include <corsika/modules/radio/propagators/SimplePropagator.hpp> +#include <corsika/modules/radio/propagators/SignalPath.hpp> +#include <corsika/modules/radio/propagators/RadioPropagator.hpp> +#include <corsika/modules/TrackWriter.hpp> + +#include <corsika/modules/StackInspector.hpp> +#include <corsika/modules/ParticleCut.hpp> +#include <corsika/modules/TimeCut.hpp> +//#include <corsika/modules/TrackWriter.hpp> + +/* + NOTE, WARNING, ATTENTION + + The .../Random.hpppp implement the hooks of external modules to the C8 random + number generator. It has to occur excatly ONCE per linked + executable. If you include the header below multiple times and + link this together, it will fail. + */ +#include <corsika/modules/sibyll/Random.hpp> +#include <corsika/modules/urqmd/Random.hpp> + +#include <iomanip> +#include <iostream> +#include <limits> +#include <string> +#include <typeinfo> + +using namespace corsika; +using namespace std; + +// +// A simple shower to get the electric field trace of an electron (& a positron) +// in order to reveal the "clover leaf" pattern of energy fluence to the ground. +// +int main() { + + logging::set_level(logging::level::warn); + corsika_logger->set_pattern("[%n:%^%-8l%$] custom pattern: %v"); + + CORSIKA_LOG_INFO("Synchrotron radiation"); + + feenableexcept(FE_INVALID); + RNGManager<>::getInstance().registerRandomStream("cascade"); + std::random_device rd; + auto seed = rd(); + RNGManager<>::getInstance().setSeed(seed); + + OutputManager output("1 electron - 1 positron"); + + // create a suitable environment + using IModelInterface = + IRefractiveIndexModel<IMediumPropertyModel<IMagneticFieldModel<IMediumModel>>>; + using AtmModel = UniformRefractiveIndex< + MediumPropertyModel<UniformMagneticField<HomogeneousMedium<IModelInterface>>>>; + using EnvType = Environment<AtmModel>; + EnvType env; + CoordinateSystemPtr const& rootCS = env.getCoordinateSystem(); + Point const center{rootCS, 0_m, 0_m, 0_m}; + // a refractive index for the vacuum + const double ri_{1.}; + // the composition we use for the homogeneous medium + NuclearComposition const Composition({Code::Nitrogen}, {1.}); + // create magnetic field vector + auto const Bmag{0.00005_T}; + MagneticFieldVector B(rootCS, 0_T, Bmag, 0_T); + // create a Sphere for the medium + auto world = EnvType::createNode<Sphere>(center, 150_km); + // set the environment properties + world->setModelProperties<AtmModel>(ri_, Medium::AirDry1Atm, B, + 1_kg / (1_m * 1_m * 1_m), Composition); + // bind things together + env.getUniverse()->addChild(std::move(world)); + + Point injectionPos(rootCS, 0_m, 0_m, 5_km); + + auto const centerX{center.getCoordinates().getX()}; + auto const centerY{center.getCoordinates().getY()}; + auto const centerZ{center.getCoordinates().getZ()}; + auto const injectionPosX_{injectionPos.getCoordinates().getX()}; + auto const injectionPosY_{injectionPos.getCoordinates().getY()}; + auto const injectionPosZ_{injectionPos.getCoordinates().getZ()}; + auto const triggerpoint_{Point(rootCS, injectionPosX_, injectionPosY_, injectionPosZ_)}; + std::cout << "Trigger Point is: " << triggerpoint_ << std::endl; + + // the antenna characteristics + const TimeType duration_{2e-6_s}; // 0.8e-4_s + const InverseTimeType sampleRate_{1e+11_Hz}; // 1e+9_Hz + + // the detectors + AntennaCollection<TimeDomainAntenna> detectorCoREAS; + // AntennaCollection<TimeDomainAntenna> detectorZHS; + + std::string name_center = "CoREAS_R=0_m--Phi=0degrees"; + auto triggertime_center{((triggerpoint_ - center).getNorm() / constants::c) - 500_ns}; + TimeDomainAntenna antenna_center(name_center, center, rootCS, triggertime_center, + duration_, sampleRate_, triggertime_center); + detectorCoREAS.addAntenna(antenna_center); + + for (auto radius_1 = 25_m; radius_1 <= 1000_m; radius_1 += 25_m) { + for (auto phi_1 = 0; phi_1 <= 315; phi_1 += 45) { + auto phiRad_1 = phi_1 / 180. * M_PI; + auto rr_1 = static_cast<int>(radius_1 / 1_m); + auto const point_1{Point(rootCS, centerX + radius_1 * cos(phiRad_1), + centerY + radius_1 * sin(phiRad_1), centerZ)}; + std::cout << "Antenna point: " << point_1 << std::endl; + auto triggertime_1{((triggerpoint_ - point_1).getNorm() / constants::c) - 500_ns}; + std::string name_1 = "CoREAS_R=" + std::to_string(rr_1) + + "_m--Phi=" + std::to_string(phi_1) + "degrees"; + TimeDomainAntenna antenna_1(name_1, point_1, rootCS, triggertime_1, duration_, + sampleRate_, triggertime_1); + detectorCoREAS.addAntenna(antenna_1); + } + } + + // setup particle stack, and add primary particle + setup::Stack<EnvType> stack; + stack.clear(); + + // electron + const Code beamCode = Code::Electron; + auto const charge = get_charge(beamCode); + auto const mass = get_mass(beamCode); + auto const gyroradius = 100_m; + auto const pLabMag = convert_SI_to_HEP(charge * Bmag * gyroradius); + auto const omega_inv = + convert_HEP_to_SI<MassType::dimension_type>(mass) / (abs(charge) * Bmag); + MomentumVector const plab{rootCS, 0_MeV, 0_MeV, -10_MeV}; + auto const Elab = sqrt(plab.getSquaredNorm() + static_pow<2>(mass)); + auto gamma = Elab / mass; + TimeType const period = 2 * M_PI * omega_inv * gamma; + + // positron + const Code beamCodeP = Code::Positron; + auto const chargeP = get_charge(beamCodeP); + auto const massP = get_mass(beamCodeP); + // auto const gyroradius = 100_m; + auto const pLabMagP = convert_SI_to_HEP(chargeP * Bmag * gyroradius); + auto const omega_invP = + convert_HEP_to_SI<MassType::dimension_type>(massP) / (abs(chargeP) * Bmag); + MomentumVector const plabP{rootCS, 0_MeV, 0_MeV, -10_MeV}; + auto const ElabP = sqrt(plabP.getSquaredNorm() + static_pow<2>(massP)); + auto gammaP = ElabP / massP; + TimeType const periodP = 2 * M_PI * omega_invP * gammaP; + + stack.addParticle(std::make_tuple(beamCode, + calculate_kinetic_energy(plab.getNorm(), mass), + plab.normalized(), injectionPos, 0_ns)); + stack.addParticle(std::make_tuple(beamCodeP, + calculate_kinetic_energy(plabP.getNorm(), massP), + plabP.normalized(), injectionPos, 0_ns)); + + // setup relevant processes + setup::Tracking tracking; + + // put radio processes here + RadioProcess<decltype(detectorCoREAS), + CoREAS<decltype(detectorCoREAS), decltype(SimplePropagator(env))>, + decltype(SimplePropagator(env))> + coreas(detectorCoREAS, env); + output.add("CoREAS", coreas); + + // RadioProcess<decltype(detectorZHS), ZHS<decltype(detectorZHS), + // decltype(SimplePropagator(env))>, decltype(SimplePropagator(env))> + // zhs(detectorZHS, env); + // output.add("ZHS", zhs); + + TimeCut cut(period / 4); + + // TrackWriter trackWriter; + // output.add("tracks", trackWriter); // register TrackWriter + + // assemble all processes into an ordered process list + auto sequence = make_sequence(coreas, cut); + + // define air shower object, run simulation + Cascade EAS(env, tracking, sequence, output, stack); + output.startOfShower(); + EAS.run(); + output.endOfShower(); + + CORSIKA_LOG_INFO("|p| electron = {} and E electron = {}", plab.getNorm(), Elab); + CORSIKA_LOG_INFO("period: {}", period); + CORSIKA_LOG_INFO("gamma: {}", gamma); + + CORSIKA_LOG_INFO("|p| positron = {} and E positron = {}", plabP.getNorm(), ElabP); + CORSIKA_LOG_INFO("period: {}", periodP); + CORSIKA_LOG_INFO("gamma: {}", gammaP); + + output.endOfLibrary(); +} diff --git a/examples/em_shower.cpp b/examples/em_shower.cpp index 1516bac2046e818d33e457ba61442db7ac0331cf..0c8e4c448cdf1d878e9a300bf5d8e0475af2ba56 100644 --- a/examples/em_shower.cpp +++ b/examples/em_shower.cpp @@ -81,7 +81,7 @@ using MyExtraEnv = MediumPropertyModel<UniformMagneticField<T>>; int main(int argc, char** argv) { - logging::set_level(logging::level::info); + logging::set_level(logging::level::warn); if (!(argc == 2 || argc == 3)) { std::cerr << "usage: em_shower <energy/GeV> [seed]" << std::endl diff --git a/examples/geometry_example.cpp b/examples/geometry_example.cpp index 596e6c1842011d56d8db27dc6e02fe81ab8bdbb9..059f6d849c5d6758b6fd5ffbdb99ddfdd71bd0e0 100644 --- a/examples/geometry_example.cpp +++ b/examples/geometry_example.cpp @@ -20,7 +20,7 @@ using namespace corsika; int main() { - logging::set_level(logging::level::info); + logging::set_level(logging::level::warn); CORSIKA_LOG_INFO("geometry_example"); diff --git a/examples/helix_example.cpp b/examples/helix_example.cpp index e980931b74ace074bf6d0459cf69e6d158f32b91..9ce484b67636bb68a693e5451e969ace248983df 100644 --- a/examples/helix_example.cpp +++ b/examples/helix_example.cpp @@ -20,7 +20,7 @@ using namespace corsika; int main() { - logging::set_level(logging::level::info); + logging::set_level(logging::level::warn); CORSIKA_LOG_INFO("helix_example"); diff --git a/examples/particle_list_example.cpp b/examples/particle_list_example.cpp index 241fda60d53fbeb8d5d9e7862c3a7df51b7a1235..36020bc1d6ac4fc9f9a10e2496441c15312585e2 100644 --- a/examples/particle_list_example.cpp +++ b/examples/particle_list_example.cpp @@ -32,7 +32,7 @@ using namespace std; // int main() { - logging::set_level(logging::level::info); + logging::set_level(logging::level::warn); corsika_logger->set_pattern("[%n:%^%-8l%$] %v"); logging::info( diff --git a/examples/radio_em_shower.cpp b/examples/radio_em_shower.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f151b1e893d0444c47b54365f6145852f53dc0ff --- /dev/null +++ b/examples/radio_em_shower.cpp @@ -0,0 +1,305 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ + +#include <corsika/framework/process/ProcessSequence.hpp> +#include <corsika/framework/process/SwitchProcessSequence.hpp> +#include <corsika/framework/process/InteractionCounter.hpp> +#include <corsika/framework/core/Cascade.hpp> +#include <corsika/framework/core/PhysicalUnits.hpp> +#include <corsika/framework/core/Logging.hpp> +#include <corsika/framework/core/EnergyMomentumOperations.hpp> +#include <corsika/framework/random/RNGManager.hpp> +#include <corsika/framework/geometry/Sphere.hpp> +#include <corsika/framework/geometry/Plane.hpp> +#include <corsika/framework/geometry/Sphere.hpp> +#include <corsika/framework/geometry/PhysicalGeometry.hpp> +#include <corsika/framework/utility/CorsikaFenv.hpp> +#include <corsika/framework/utility/SaveBoostHistogram.hpp> + +#include <corsika/output/OutputManager.hpp> +#include <corsika/modules/writers/SubWriter.hpp> +#include <corsika/modules/writers/EnergyLossWriter.hpp> +#include <corsika/modules/writers/LongitudinalWriter.hpp> + +#include <corsika/media/Environment.hpp> +#include <corsika/media/LayeredSphericalAtmosphereBuilder.hpp> +#include <corsika/media/NuclearComposition.hpp> +#include <corsika/media/ShowerAxis.hpp> +#include <corsika/media/MediumPropertyModel.hpp> +#include <corsika/media/UniformMagneticField.hpp> +#include <corsika/media/UniformRefractiveIndex.hpp> +#include <corsika/media/CORSIKA7Atmospheres.hpp> + +#include <corsika/modules/LongitudinalProfile.hpp> +#include <corsika/modules/ObservationPlane.hpp> +#include <corsika/modules/ParticleCut.hpp> +#include <corsika/modules/TrackWriter.hpp> +#include <corsika/modules/Sibyll.hpp> +#include <corsika/modules/PROPOSAL.hpp> + +#include <corsika/modules/radio/RadioProcess.hpp> +#include <corsika/modules/radio/CoREAS.hpp> +#include <corsika/modules/radio/ZHS.hpp> +#include <corsika/modules/radio/antennas/Antenna.hpp> +#include <corsika/modules/radio/antennas/TimeDomainAntenna.hpp> +#include <corsika/modules/radio/detectors/AntennaCollection.hpp> +#include <corsika/modules/radio/propagators/StraightPropagator.hpp> +#include <corsika/modules/radio/propagators/SimplePropagator.hpp> +#include <corsika/modules/radio/propagators/SignalPath.hpp> +#include <corsika/modules/radio/propagators/RadioPropagator.hpp> + +#include <corsika/setup/SetupStack.hpp> +#include <corsika/setup/SetupTrajectory.hpp> + +#include <iomanip> +#include <iostream> +#include <limits> +#include <string> +#include <typeinfo> + +/* + NOTE, WARNING, ATTENTION + + The .../Random.hpppp implement the hooks of external modules to the C8 random + number generator. It has to occur exactly ONCE per linked + executable. If you include the header below multiple times and + link this together, it will fail. +*/ +#include <corsika/modules/Random.hpp> + +using namespace corsika; +using namespace std; + +void registerRandomStreams(int seed) { + RNGManager<>::getInstance().registerRandomStream("cascade"); + RNGManager<>::getInstance().registerRandomStream("proposal"); + RNGManager<>::getInstance().registerRandomStream("sibyll"); + if (seed == 0) { + std::random_device rd; + seed = rd(); + cout << "new random seed (auto) " << seed << endl; + } + RNGManager<>::getInstance().setSeed(seed); +} + +template <typename TInterface> +using MyExtraEnv = + UniformRefractiveIndex<MediumPropertyModel<UniformMagneticField<TInterface>>>; + +int main(int argc, char** argv) { + + logging::set_level(logging::level::warn); + + if (argc != 3) { + std::cerr + << "usage: radio_em_shower <energy/GeV> <seed> - put seed=0 to use random seed" + << std::endl; + return 1; + } + + int seed{static_cast<int>(std::stof(std::string(argv[2])))}; + std::cout << "Seed: " << seed << std::endl; + feenableexcept(FE_INVALID); + // initialize random number sequence(s) + registerRandomStreams(seed); + + // setup environment, geometry + using EnvironmentInterface = + IRefractiveIndexModel<IMediumPropertyModel<IMagneticFieldModel<IMediumModel>>>; + using EnvType = Environment<EnvironmentInterface>; + EnvType env; + CoordinateSystemPtr const& rootCS = env.getCoordinateSystem(); + Point const center{rootCS, 0_m, 0_m, 0_m}; + + create_5layer_atmosphere<EnvironmentInterface, MyExtraEnv>( + env, AtmosphereId::LinsleyUSStd, center, 1.000327, Medium::AirDry1Atm, + MagneticFieldVector{rootCS, 50_uT, 0_T, 0_T}); + + std::unordered_map<Code, HEPEnergyType> energy_resolution = { + {Code::Electron, 5_MeV}, + {Code::Positron, 5_MeV}, + {Code::Photon, 5_MeV}, + }; + for (auto [pcode, energy] : energy_resolution) + set_energy_production_threshold(pcode, energy); + + // setup particle stack, and add primary particle + setup::Stack<EnvType> stack; + stack.clear(); + const Code beamCode = Code::Electron; + auto const mass = get_mass(beamCode); + const HEPEnergyType E0 = 1_GeV * std::stof(std::string(argv[1])); + double theta = 0.; + auto const thetaRad = theta / 180. * M_PI; + + HEPMomentumType P0 = calculate_momentum(E0, mass); + auto momentumComponents = [](double thetaRad, HEPMomentumType ptot) { + return std::make_tuple(ptot * sin(thetaRad), 0_eV, -ptot * cos(thetaRad)); + }; + + auto const [px, py, pz] = momentumComponents(thetaRad, P0); + auto plab = MomentumVector(rootCS, {px, py, pz}); + cout << "input particle: " << beamCode << endl; + cout << "input angles: theta=" << theta << endl; + cout << "input momentum: " << plab.getComponents() / 1_GeV + << ", norm = " << plab.getNorm() << endl; + + auto const observationHeight = 1.4_km + constants::EarthRadius::Mean; + auto const injectionHeight = 112.75_km + constants::EarthRadius::Mean; + auto const t = -observationHeight * cos(thetaRad) + + sqrt(-static_pow<2>(sin(thetaRad) * observationHeight) + + static_pow<2>(injectionHeight)); + Point const showerCore{rootCS, 0_m, 0_m, observationHeight}; + Point const injectionPos = + showerCore + DirectionVector{rootCS, {-sin(thetaRad), 0, cos(thetaRad)}} * t; + + std::cout << "point of injection: " << injectionPos.getCoordinates() << std::endl; + + stack.addParticle(std::make_tuple( + beamCode, calculate_kinetic_energy(plab.getNorm(), get_mass(beamCode)), + plab.normalized(), injectionPos, 0_ns)); + + CORSIKA_LOG_INFO("shower axis length: {} ", + (showerCore - injectionPos).getNorm() * 1.02); + + ShowerAxis const showerAxis{injectionPos, (showerCore - injectionPos) * 1.02, env, + false, 1000}; + + TimeType const groundHitTime{(showerCore - injectionPos).getNorm() / constants::c}; + + std::string outname_ = "radio_em_shower_outputs"; // + std::to_string(rr_); + OutputManager output(outname_); + + // Radio antennas and relevant information + // the antenna time variables + const TimeType duration_{1e-6_s}; + const InverseTimeType sampleRate_{1e+9_Hz}; + + // the detector (aka antenna collection) for CoREAS and ZHS + AntennaCollection<TimeDomainAntenna> detectorCoREAS; + AntennaCollection<TimeDomainAntenna> detectorZHS; + + auto const showerCoreX_{showerCore.getCoordinates().getX()}; + auto const showerCoreY_{showerCore.getCoordinates().getY()}; + auto const injectionPosX_{injectionPos.getCoordinates().getX()}; + auto const injectionPosY_{injectionPos.getCoordinates().getY()}; + auto const injectionPosZ_{injectionPos.getCoordinates().getZ()}; + auto const triggerpoint_{Point(rootCS, injectionPosX_, injectionPosY_, injectionPosZ_)}; + std::cout << "Trigger Point is: " << triggerpoint_ << std::endl; + + // // setup CoREAS antennas - use the for loop for star shape pattern + // for (auto radius_1 = 25_m; radius_1 <= 500_m; radius_1 += 25_m) { + // for (auto phi_1 = 0; phi_1 <= 315; phi_1 += 45) { + auto radius_1 = 200_m; + auto phi_1 = 45; + auto phiRad_1 = phi_1 / 180. * M_PI; + auto rr_1 = static_cast<int>(radius_1 / 1_m); + auto const point_1{Point(rootCS, showerCoreX_ + radius_1 * cos(phiRad_1), + showerCoreY_ + radius_1 * sin(phiRad_1), + constants::EarthRadius::Mean)}; + std::cout << "Antenna point: " << point_1 << std::endl; + auto triggertime_1{(triggerpoint_ - point_1).getNorm() / constants::c}; + std::string name_1 = + "CoREAS_R=" + std::to_string(rr_1) + "_m--Phi=" + std::to_string(phi_1) + "degrees"; + TimeDomainAntenna antenna_1(name_1, point_1, rootCS, triggertime_1, duration_, + sampleRate_, triggertime_1); + detectorCoREAS.addAntenna(antenna_1); + // } + // } + + // // setup ZHS antennas - use the for loop for star shape pattern + // for (auto radius_ = 25_m; radius_ <= 500_m; radius_ += 25_m) { + // for (auto phi_ = 0; phi_ <= 315; phi_ += 45) { + auto radius_ = 200_m; + auto phi_ = 45; + auto phiRad_ = phi_ / 180. * M_PI; + auto rr_ = static_cast<int>(radius_ / 1_m); + auto const point_{Point(rootCS, showerCoreX_ + radius_ * cos(phiRad_), + showerCoreY_ + radius_ * sin(phiRad_), + constants::EarthRadius::Mean)}; + auto triggertime_{(triggerpoint_ - point_).getNorm() / constants::c}; + std::string name_ = + "ZHS_R=" + std::to_string(rr_) + "_m--Phi=" + std::to_string(phi_) + "degrees"; + TimeDomainAntenna antenna_(name_, point_, rootCS, triggertime_, duration_, sampleRate_, + triggertime_); + detectorZHS.addAntenna(antenna_); + // } + // } + + // setup processes, decays and interactions + + EnergyLossWriter dEdX{showerAxis, 10_g / square(1_cm), 200}; + // register energy losses as output + output.add("dEdX", dEdX); + + ParticleCut<SubWriter<decltype(dEdX)>> cut(5_MeV, 5_MeV, 100_GeV, 100_GeV, true, dEdX); + + corsika::sibyll::Interaction sibyll{env}; + HEPEnergyType heThresholdNN = 80_GeV; + corsika::proposal::Interaction emCascade(env, sibyll.getHadronInteractionModel(), + heThresholdNN); + corsika::proposal::ContinuousProcess<SubWriter<decltype(dEdX)>> emContinuous(env, dEdX); + // BetheBlochPDG<SubWriter<decltype(dEdX)>> emContinuous{dEdX}; + + // NOT possible right now, due to interface differenc in PROPOSAL + // InteractionCounter emCascadeCounted(emCascade); + + TrackWriter tracks; + output.add("tracks", tracks); + + // long. profile + LongitudinalWriter profile{showerAxis, 10_g / square(1_cm)}; + output.add("profile", profile); + LongitudinalProfile<SubWriter<decltype(profile)>> longprof{profile}; + + // initiate CoREAS + RadioProcess<decltype(detectorCoREAS), + CoREAS<decltype(detectorCoREAS), decltype(SimplePropagator(env))>, + decltype(SimplePropagator(env))> + coreas(detectorCoREAS, env); + + // register CoREAS with the output manager + output.add("CoREAS", coreas); + + // initiate ZHS + RadioProcess<decltype(detectorZHS), + ZHS<decltype(detectorZHS), decltype(SimplePropagator(env))>, + decltype(SimplePropagator(env))> + zhs(detectorZHS, env); + + // // register ZHS with the output manager + output.add("ZHS", zhs); + + Plane const obsPlane(showerCore, DirectionVector(rootCS, {0., 0., 1.})); + ObservationPlane<setup::Tracking, ParticleWriterParquet> observationLevel{ + obsPlane, DirectionVector(rootCS, {1., 0., 0.})}; + output.add("particles", observationLevel); + + // auto sequence = make_sequence(emCascade, emContinuous, longprof, cut, coreas, zhs); + auto sequence = make_sequence(emCascade, emContinuous, longprof, cut, coreas, zhs, + observationLevel, tracks); + // define air shower object, run simulation + setup::Tracking tracking; + + output.startOfLibrary(); + Cascade EAS(env, tracking, sequence, output, stack); + + // to fix the point of first interaction, uncomment the following two lines: + // EAS.forceInteraction(); + + EAS.run(); + + 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(); +} \ No newline at end of file diff --git a/examples/stack_example.cpp b/examples/stack_example.cpp index da97201afad3001f6a4b9e5cec6f2701df227091..8f7f6cdeb9a5e24e2547c309b69506836a4fc615 100644 --- a/examples/stack_example.cpp +++ b/examples/stack_example.cpp @@ -43,7 +43,7 @@ void read(VectorStack& s) { int main() { - logging::set_level(logging::level::info); + logging::set_level(logging::level::warn); CORSIKA_LOG_INFO("stack_example"); VectorStack s; diff --git a/examples/stopping_power.cpp b/examples/stopping_power.cpp index 4aa5a0f5a504aa4a6baf5ebb2bffd14f87c01c26..8b39786e1380f7f25c0821635218f220e472094f 100644 --- a/examples/stopping_power.cpp +++ b/examples/stopping_power.cpp @@ -30,7 +30,7 @@ using namespace std; // int main() { - logging::set_level(logging::level::info); + logging::set_level(logging::level::warn); CORSIKA_LOG_INFO("stopping_power"); diff --git a/examples/synchrotron_test_C8tracking.cpp b/examples/synchrotron_test_C8tracking.cpp new file mode 100644 index 0000000000000000000000000000000000000000..cf1792188e4e463c33c2f5a9078b8121bdfb9ef3 --- /dev/null +++ b/examples/synchrotron_test_C8tracking.cpp @@ -0,0 +1,172 @@ +/* + * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ + +#include <corsika/framework/core/Cascade.hpp> +#include <corsika/framework/geometry/Sphere.hpp> +#include <corsika/framework/geometry/PhysicalGeometry.hpp> +#include <corsika/framework/process/ProcessSequence.hpp> +#include <corsika/framework/random/RNGManager.hpp> +#include <corsika/framework/core/PhysicalUnits.hpp> +#include <corsika/framework/utility/CorsikaFenv.hpp> +#include <corsika/framework/core/Logging.hpp> + +#include <corsika/output/OutputManager.hpp> + +#include <corsika/media/Environment.hpp> +#include <corsika/media/HomogeneousMedium.hpp> +#include <corsika/media/IMagneticFieldModel.hpp> +#include <corsika/media/NuclearComposition.hpp> +#include <corsika/media/MediumPropertyModel.hpp> +#include <corsika/media/UniformMagneticField.hpp> +#include <corsika/media/UniformRefractiveIndex.hpp> + +#include <corsika/setup/SetupStack.hpp> +#include <corsika/setup/SetupTrajectory.hpp> + +#include <corsika/modules/radio/RadioProcess.hpp> +#include <corsika/modules/radio/CoREAS.hpp> +#include <corsika/modules/radio/ZHS.hpp> +#include <corsika/modules/radio/antennas/TimeDomainAntenna.hpp> +#include <corsika/modules/radio/detectors/AntennaCollection.hpp> +#include <corsika/modules/radio/propagators/SimplePropagator.hpp> + +#include <corsika/modules/TimeCut.hpp> + +/* + NOTE, WARNING, ATTENTION + + The .../Random.hpppp implement the hooks of external modules to the C8 random + number generator. It has to occur excatly ONCE per linked + executable. If you include the header below multiple times and + link this together, it will fail. + */ +#include <corsika/modules/sibyll/Random.hpp> +#include <corsika/modules/urqmd/Random.hpp> + +#include <iomanip> +#include <iostream> +#include <limits> +#include <string> +#include <typeinfo> + +using namespace corsika; +using namespace std; + +// +// A simple shower to get the electric field trace of an electron using C8 tracking +// +int main() { + + logging::set_level(logging::level::warn); + corsika_logger->set_pattern("[%n:%^%-8l%$] custom pattern: %v"); + + CORSIKA_LOG_INFO("Synchrotron radiation"); + + feenableexcept(FE_INVALID); + RNGManager<>::getInstance().registerRandomStream("cascade"); + std::random_device rd; + auto seed = rd(); + RNGManager<>::getInstance().setSeed(seed); + + OutputManager output("synchrotron_radiation_C8tracking-output"); + + // set up the environment + using EnvironmentInterface = + IRefractiveIndexModel<IMediumPropertyModel<IMagneticFieldModel<IMediumModel>>>; + using EnvType = Environment<EnvironmentInterface>; + EnvType env; + auto& universe = *(env.getUniverse()); + CoordinateSystemPtr const& rootCS = env.getCoordinateSystem(); + + auto world = EnvType::createNode<Sphere>(Point{rootCS, 0_m, 0_m, 0_m}, 150_km); + + using MyHomogeneousModel = UniformRefractiveIndex< + MediumPropertyModel<UniformMagneticField<HomogeneousMedium<EnvironmentInterface>>>>; + + auto const Bmag{0.0003809_T}; + MagneticFieldVector B{rootCS, 0_T, 0_T, Bmag}; + + // the composition we use for the homogeneous medium + NuclearComposition const nitrogenComposition({Code::Nitrogen}, {1.}); + + world->setModelProperties<MyHomogeneousModel>( + 1, Medium::AirDry1Atm, B, 1_kg / (1_m * 1_m * 1_m), nitrogenComposition); + + universe.addChild(std::move(world)); + + // the antenna locations + const auto point1{Point(rootCS, 30000_m, 0_m, 0_m)}; + + // the antenna time variables + const TimeType t1{0.994e-4_s}; + const TimeType t2{1.07e-4_s - 0.994e-4_s}; + const InverseTimeType t3{5e+11_Hz}; + + // the antennas + TimeDomainAntenna ant1("antenna CoREAS", point1, rootCS, t1, t2, t3, t1); + TimeDomainAntenna ant2("antenna ZHS", point1, rootCS, t1, t2, t3, t1); + + // the detectors + AntennaCollection<TimeDomainAntenna> detectorCoREAS; + AntennaCollection<TimeDomainAntenna> detectorZHS; + detectorCoREAS.addAntenna(ant1); + detectorZHS.addAntenna(ant2); + + // setup particle stack, and add primary particle + setup::Stack<EnvType> stack; + stack.clear(); + const Code beamCode = Code::Electron; + auto const charge = get_charge(beamCode); + auto const mass = get_mass(beamCode); + auto const gyroradius = 100_m; + auto const pLabMag = convert_SI_to_HEP(charge * Bmag * gyroradius); + auto const omega_inv = + convert_HEP_to_SI<MassType::dimension_type>(mass) / (abs(charge) * Bmag); + MomentumVector const plab{rootCS, pLabMag, 0_MeV, 0_MeV}; + auto const Elab = sqrt(plab.getSquaredNorm() + static_pow<2>(mass)); + auto gamma = Elab / mass; + TimeType const period = 2 * M_PI * omega_inv * gamma; + + Point injectionPos(rootCS, 0_m, 100_m, 0_m); + stack.addParticle(std::make_tuple( + beamCode, calculate_kinetic_energy(plab.getNorm(), get_mass(beamCode)), + plab.normalized(), injectionPos, 0_ns)); + + // setup relevant processes + setup::Tracking tracking; + + // put radio processes here + RadioProcess<decltype(detectorCoREAS), + CoREAS<decltype(detectorCoREAS), decltype(SimplePropagator(env))>, + decltype(SimplePropagator(env))> + coreas(detectorCoREAS, env); + output.add("CoREAS", coreas); + + RadioProcess<decltype(detectorZHS), + ZHS<decltype(detectorZHS), decltype(SimplePropagator(env))>, + decltype(SimplePropagator(env))> + zhs(detectorZHS, env); + output.add("ZHS", zhs); + + TimeCut cut(period); + + // assemble all processes into an ordered process list + auto sequence = make_sequence(coreas, zhs, cut); + + // define air shower object, run simulation + Cascade EAS(env, tracking, sequence, output, stack); + output.startOfShower(); + EAS.run(); + output.endOfShower(); + + CORSIKA_LOG_INFO("|p| = {} and E = {}", plab.getNorm(), Elab); + CORSIKA_LOG_INFO("period: {}", period); + CORSIKA_LOG_INFO("gamma: {}", gamma); + + output.endOfLibrary(); +} \ No newline at end of file diff --git a/examples/synchrotron_test_manual_tracking.cpp b/examples/synchrotron_test_manual_tracking.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8921329f5d365d7f0e54003b7ed51e9417435315 --- /dev/null +++ b/examples/synchrotron_test_manual_tracking.cpp @@ -0,0 +1,176 @@ +/* + * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ + +#include <corsika/framework/geometry/Sphere.hpp> +#include <corsika/framework/geometry/PhysicalGeometry.hpp> +#include <corsika/framework/process/ProcessSequence.hpp> +#include <corsika/framework/core/PhysicalUnits.hpp> + +#include <corsika/output/OutputManager.hpp> + +#include <corsika/media/Environment.hpp> +#include <corsika/media/HomogeneousMedium.hpp> +#include <corsika/media/IMagneticFieldModel.hpp> +#include <corsika/media/NuclearComposition.hpp> +#include <corsika/media/MediumPropertyModel.hpp> +#include <corsika/media/UniformMagneticField.hpp> +#include <corsika/media/UniformRefractiveIndex.hpp> + +#include <corsika/setup/SetupStack.hpp> +#include <corsika/setup/SetupTrajectory.hpp> + +#include <corsika/modules/radio/RadioProcess.hpp> +#include <corsika/modules/radio/CoREAS.hpp> +#include <corsika/modules/radio/ZHS.hpp> +#include <corsika/modules/radio/antennas/TimeDomainAntenna.hpp> +#include <corsika/modules/radio/detectors/AntennaCollection.hpp> +#include <corsika/modules/radio/propagators/SimplePropagator.hpp> + +/* + NOTE, WARNING, ATTENTION + + The .../Random.hpppp implement the hooks of external modules to the C8 random + number generator. It has to occur excatly ONCE per linked + executable. If you include the header below multiple times and + link this together, it will fail. + */ +#include <corsika/modules/sibyll/Random.hpp> +#include <corsika/modules/urqmd/Random.hpp> + +#include <iomanip> +#include <iostream> +#include <limits> +#include <string> + +using namespace corsika; +using namespace std; + +// +// A simple example to get the electric field trace of an electron using manual tracking +// +int main() { + + // create a suitable environment + using IModelInterface = + IRefractiveIndexModel<IMediumPropertyModel<IMagneticFieldModel<IMediumModel>>>; + using AtmModel = UniformRefractiveIndex< + MediumPropertyModel<UniformMagneticField<HomogeneousMedium<IModelInterface>>>>; + using EnvType = Environment<AtmModel>; + EnvType env; + CoordinateSystemPtr const& rootCS = env.getCoordinateSystem(); + Point const center{rootCS, 0_m, 0_m, 0_m}; + // a refractive index for the vacuum + const double ri_{1}; + // the constant density + const auto density{19.2_g / cube(1_cm)}; + // the composition we use for the homogeneous medium + NuclearComposition const Composition({Code::Nitrogen}, {1.}); + // create magnetic field vector + Vector B1(rootCS, 0_T, 0_T, 0.3809_T); + // create a Sphere for the medium + auto Medium = + EnvType::createNode<Sphere>(center, 1_km * std::numeric_limits<double>::infinity()); + // set the environment properties + auto const props = Medium->setModelProperties<AtmModel>(ri_, Medium::AirDry1Atm, B1, + density, Composition); + // bind things together + env.getUniverse()->addChild(std::move(Medium)); + + // the antennas location + const auto point1{Point(rootCS, 30000_m, 0_m, 0_m)}; + + // create times for the antenna + const TimeType start{0.994e-4_s}; + const TimeType duration{1.07e-4_s - 0.994e-4_s}; + // 3 km antenna + // const TimeType start{0.994e-5_s}; + // const TimeType duration{1.7e-5_s - 0.994e-5_s}; + const InverseTimeType sampleRate_{5e+11_Hz}; + + std::cout << "number of points in time: " << duration * sampleRate_ << std::endl; + + // create 2 antennas + TimeDomainAntenna ant1("CoREAS antenna", point1, rootCS, start, duration, sampleRate_, + start); + TimeDomainAntenna ant2("ZHS antenna", point1, rootCS, start, duration, sampleRate_, + start); + + // construct a radio detector instance to store our antennas + AntennaCollection<TimeDomainAntenna> detectorCoREAS; + AntennaCollection<TimeDomainAntenna> detectorZHS; + + // add the antennas to the detector + detectorCoREAS.addAntenna(ant1); + detectorZHS.addAntenna(ant2); + + // create a new stack for each trial + setup::Stack<EnvType> stack; + stack.clear(); + + const Code particle{Code::Electron}; + const HEPMassType pmass{get_mass(particle)}; + + // construct an energy // move in the for loop + const HEPEnergyType E0{11.4_MeV}; + + // construct the output manager + OutputManager outputs("synchrotron_radiation_manual_tracking-output"); + + // create a radio process instance using CoREAS + RadioProcess< + AntennaCollection<TimeDomainAntenna>, + CoREAS<AntennaCollection<TimeDomainAntenna>, decltype(SimplePropagator(env))>, + decltype(SimplePropagator(env))> + coreas(detectorCoREAS, env); + // register CoREAS to the output manager + outputs.add("CoREAS", coreas); + + // create a radio process instance using ZHS + RadioProcess<AntennaCollection<TimeDomainAntenna>, + ZHS<AntennaCollection<TimeDomainAntenna>, decltype(SimplePropagator(env))>, + decltype(SimplePropagator(env))> + zhs(detectorZHS, env); + // register ZHS to the output manager + outputs.add("ZHS", zhs); + + // trigger the start of the library and the first event + outputs.startOfLibrary(); + outputs.startOfShower(); + + // the number of points that make up the circle + int const n_points{100000}; + LengthType const radius{100_m}; + TimeType timeCounter{0._s}; + + // loop over all the tracks twice (this produces 2 pulses) + for (size_t i = 0; i <= (n_points)*2; i++) { + Point const point_1(rootCS, {radius * cos(M_PI * 2 * i / n_points), + radius * sin(M_PI * 2 * i / n_points), 0_m}); + Point const point_2(rootCS, {radius * cos(M_PI * 2 * (i + 1) / n_points), + radius * sin(M_PI * 2 * (i + 1) / n_points), 0_m}); + TimeType t{(point_2 - point_1).getNorm() / (0.999 * constants::c)}; + timeCounter = timeCounter + t; + VelocityVector v{(point_2 - point_1) / t}; + auto beta{v / constants::c}; + auto gamma{E0 / pmass}; + auto plab{beta * pmass * gamma}; + Line l{point_1, v}; + StraightTrajectory track{l, t}; + auto particle1{stack.addParticle(std::make_tuple( + particle, calculate_kinetic_energy(plab.getNorm(), get_mass(particle)), + plab.normalized(), point_1, timeCounter))}; + Step step(particle1, track); + coreas.doContinuous(step, true); + zhs.doContinuous(step, true); + stack.clear(); + } + + // trigger the manager to write the data to disk + outputs.endOfShower(); + outputs.endOfLibrary(); +} \ No newline at end of file diff --git a/examples/vertical_EAS.cpp b/examples/vertical_EAS.cpp index 1a24ab355ae5180cebf04fc709f37e243e089e4b..e24a632fadef6f9f399d6e3a7604909e50a4156f 100644 --- a/examples/vertical_EAS.cpp +++ b/examples/vertical_EAS.cpp @@ -105,7 +105,7 @@ using MyExtraEnv = MediumPropertyModel<UniformMagneticField<T>>; int main(int argc, char** argv) { - logging::set_level(logging::level::info); + logging::set_level(logging::level::warn); CORSIKA_LOG_INFO("vertical_EAS"); diff --git a/python/corsika/io/library.py b/python/corsika/io/library.py index 236fc97ac972a11c0fb366876bd92cae20d6ab18..1c2ff38e025e9a728bde4b9c7a5e0cad5982deb1 100644 --- a/python/corsika/io/library.py +++ b/python/corsika/io/library.py @@ -10,8 +10,7 @@ import logging import os import os.path as op -import re -from typing import Any, Dict, Optional, List +from typing import Any, Dict, List, Optional import yaml diff --git a/python/corsika/io/outputs/__init__.py b/python/corsika/io/outputs/__init__.py index 63b76b533d41085aa77af57df5439c5dd3c68335..7678ab32b2fa79be594ca0b34e2846a166c933c4 100644 --- a/python/corsika/io/outputs/__init__.py +++ b/python/corsika/io/outputs/__init__.py @@ -14,6 +14,7 @@ from .bethe_bloch import BetheBlochPDG from .particle_cut import ParticleCut from .energy_loss import EnergyLoss from .output import Output +from .radio_process import RadioProcess __all__ = [ "Output", @@ -23,4 +24,5 @@ __all__ = [ "BetheBlochPDG", "ParticleCut", "EnergyLoss" + "RadioProcess", ] diff --git a/python/corsika/io/outputs/output.py b/python/corsika/io/outputs/output.py index fed4368aac5d1cb8be7c1cbe6a955f1f799df4df..5b59f1099261f10d7270622655d4829c41b32201 100644 --- a/python/corsika/io/outputs/output.py +++ b/python/corsika/io/outputs/output.py @@ -114,7 +114,7 @@ class Output(ABC): Any: The data in its default format. """ - return self.astype() + return self.astype() # type: ignore @staticmethod def load_config(path: str) -> Dict[str, Any]: diff --git a/python/corsika/io/outputs/radio_process.py b/python/corsika/io/outputs/radio_process.py new file mode 100644 index 0000000000000000000000000000000000000000..4936f92e8a25cfbce852c046ee0ec34b39d6eb89 --- /dev/null +++ b/python/corsika/io/outputs/radio_process.py @@ -0,0 +1,157 @@ +""" + Read data written by a RadioProcess + + (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu + + This software is distributed under the terms of the GNU General Public + Licence version 3 (GPL Version 3). See file LICENSE for a full version of + the license. +""" +import logging +import os.path as op +from typing import Any + +import numpy as np +import xarray as xr + +from .output import Output + + +class RadioProcess(Output): + """ + Read particle data from an RadioProcess. + + This *currently* can be used to read data written by ZHS + or CoREAS implementations of the RadioProcess. + """ + + def __init__(self, path: str): + """ + Initialize this radio process reader (and load the data). + + Since each antenna can have a different sample rate and duration, + we can't load them into a shared array. Therefore, we load each + of the antennas into an XArray dataset. + + Parameters + ---------- + path: str + The path to the directory containing this output. + """ + super().__init__(path) + + # try and load our data + try: + self.__data = self.load_data(path) + except Exception as e: + logging.getLogger("corsika").warn( + f"An error occured loading a RadioProcess: {e}" + ) + + def load_data(self, path: str) -> xr.Dataset: + """ + Load the data associated with this radio process. + + Parameters + ---------- + path: str + The path to the directory containing this output. + + """ + + # get the list of antenna names + antennas = list(self.config["antennas"].keys()) + + # if there are no antennas, + if len(antennas) == 0: + logging.warn(f"No antennas were found for {self.config['name']}") + + # we build up the XArray Dataset in this dictionary + dataset = {} + + # loop over each of the antennas + for iant, name in enumerate(antennas): + + # load the data file associated with this antenna + try: + data = np.load(f"{op.join(path, name)}.npz") + except Exception as e: + raise RuntimeError( + ( + f"Unable to open file for antenna {name}" + f"in {self.config['name']} as {e}" + ) + ) + + # if we get here, we have successfully loaded the antennas data file + + # extract the sample times (in ns) + times = data["Time"] + + # calculate the number of showers for this antenna + nshowers = len(list(data.keys())) - 1 + + # check that we got some events + if nshowers == 0: + logging.warn(f"Antenna {name} contains data for 0 showers.") + + # create the array to store the waveforms for all the events + waveforms = np.zeros((nshowers, *data["0"].shape), dtype=np.float32) + + # fill in the 'waveforms' array + for iev in np.arange(nshowers): + waveforms[iev, ...] = data[str(iev)] + + # create the data array + showers = xr.DataArray( + waveforms, + coords=(np.arange(nshowers), ["x", "y", "z"], times), # type: ignore + dims=["shower", "pol", "time"], + ) + + # save this data array + dataset[name] = showers + + return xr.Dataset(dataset) + + def is_good(self) -> bool: + """ + Returns true if this output has been read successfully + and has the correct files/state/etc. + + Returns + ------- + bool: + True if this is a good output. + """ + return self.__data is not None + + def astype(self, dtype: str = "xarray", **kwargs: Any) -> Any: + """ + Load the antenna data from this process. + + Parameters + ---------- + dtype: str + The data format to return the data in (i.e. numpy, pandas, etc.) + + Returns + ------- + Any: + The return type of this method is determined by `dtype`. + """ + if dtype == "xarray": + return self.__data + else: + raise ValueError( + ( + f"Unknown format '{dtype}' for RadioProcess. " + "We currently only support ['xarray']." + ) + ) + + def __repr__(self) -> str: + """ + Return a string representation of this class. + """ + return f"RadioProcess('{self.config['name']}', '{self.config['algorithm']}')" diff --git a/python/setup.cfg b/python/setup.cfg index f58d04eeb061f000b2d890aa26f542f70f3aa0c0..2c8d668ffdf9a3a8135dc52fc6e4c79b66c06e72 100644 --- a/python/setup.cfg +++ b/python/setup.cfg @@ -1,6 +1,6 @@ [flake8] # use a slightly longer line to be consistent with black -max-line-length = 88 +max-line-length = 90 # E231 is missing whitespace after a comma # this contradicts the black formatting rules diff --git a/python/setup.py b/python/setup.py index 4bc73b3260b617ac3c55b2273d399b546ffdd3f6..0e7b1babd8d98fc1228627dd3f70dcb7910af8fc 100644 --- a/python/setup.py +++ b/python/setup.py @@ -32,7 +32,7 @@ setup( keywords=["cosmic ray", "physics", "air shower", "simulation"], packages=find_packages(), python_requires=">=3.6*, <4", - install_requires=["numpy", "pyyaml", "pyarrow", "boost_histogram"], + install_requires=["numpy", "pyyaml", "pyarrow", "boost_histogram", "xarray"], extras_require={ "test": [ "pytest", diff --git a/python/tests/io/test_hist.py b/python/tests/io/test_hist.py index 7cf5b38fb7798c81680cf14d108e3b31dd3dcaf1..3bd11003ca663337f322ed9fea4a7f7559e75d03 100644 --- a/python/tests/io/test_hist.py +++ b/python/tests/io/test_hist.py @@ -16,7 +16,7 @@ import corsika from .. import build_directory # the directory containing 'testSaveBoostHistogram' -bindir = op.join(build_directory, "Framework", "Utilities") +bindir = op.join(build_directory, "bin") def generate_hist() -> str: @@ -32,14 +32,14 @@ def generate_hist() -> str: """ # we construct the name of the bin - bin = op.join(bindir, "testSaveBoostHistogram") + bin = op.join(bindir, "testFramework") # check if a histogram already exists if op.exists(op.join(bin, "hist.npz")): return op.join(bin, "hist.npz"), False else: # run the program - this generates "hist.npz" in the CWD - subprocess.call(bin) + subprocess.call([bin, "saveHistogram"]) return op.join(os.getcwd(), "hist.npz"), True diff --git a/tests/modules/CMakeLists.txt b/tests/modules/CMakeLists.txt index 2073a7d50f44a6647163cd40618fd73b8293d95f..78e3d5faff9e66ccd4a2d9720fc294717e1cd850 100644 --- a/tests/modules/CMakeLists.txt +++ b/tests/modules/CMakeLists.txt @@ -12,6 +12,7 @@ set (test_modules_sources testParticleCut.cpp testSibyll.cpp testEpos.cpp + testRadio.cpp ) CORSIKA_ADD_TEST (testModules SOURCES ${test_modules_sources}) diff --git a/tests/modules/testRadio.cpp b/tests/modules/testRadio.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4d019d9ecad783ee3e501d5b22a8130ca73026d2 --- /dev/null +++ b/tests/modules/testRadio.cpp @@ -0,0 +1,800 @@ +/* + * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu + * + * This software is distributed under the terms of the GNU General Public + * Licence version 3 (GPL Version 3). See file LICENSE for a full version of + * the license. + */ +#include <catch2/catch.hpp> + +#include <corsika/modules/radio/ZHS.hpp> +#include <corsika/modules/radio/CoREAS.hpp> +#include <corsika/modules/radio/antennas/TimeDomainAntenna.hpp> +#include <corsika/modules/radio/detectors/AntennaCollection.hpp> +#include <corsika/modules/radio/propagators/StraightPropagator.hpp> +#include <corsika/modules/radio/propagators/SimplePropagator.hpp> +#include <corsika/modules/radio/propagators/SignalPath.hpp> +#include <corsika/modules/radio/propagators/RadioPropagator.hpp> + +#include <vector> +#include <istream> +#include <fstream> +#include <iostream> + +#include <corsika/media/Environment.hpp> +#include <corsika/media/FlatExponential.hpp> +#include <corsika/media/HomogeneousMedium.hpp> +#include <corsika/media/IMagneticFieldModel.hpp> +#include <corsika/media/LayeredSphericalAtmosphereBuilder.hpp> +#include <corsika/media/NuclearComposition.hpp> +#include <corsika/media/MediumPropertyModel.hpp> +#include <corsika/media/UniformMagneticField.hpp> +#include <corsika/media/SlidingPlanarExponential.hpp> +#include <corsika/media/IMediumModel.hpp> +#include <corsika/media/IRefractiveIndexModel.hpp> +#include <corsika/media/UniformRefractiveIndex.hpp> +#include <corsika/media/ExponentialRefractiveIndex.hpp> +#include <corsika/media/VolumeTreeNode.hpp> +#include <corsika/media/CORSIKA7Atmospheres.hpp> + +#include <corsika/framework/geometry/CoordinateSystem.hpp> +#include <corsika/framework/geometry/Line.hpp> +#include <corsika/framework/geometry/Point.hpp> +#include <corsika/framework/geometry/RootCoordinateSystem.hpp> +#include <corsika/framework/geometry/Vector.hpp> +#include <corsika/setup/SetupStack.hpp> +#include <corsika/setup/SetupTrajectory.hpp> +#include <corsika/framework/core/PhysicalUnits.hpp> +#include <corsika/framework/core/PhysicalConstants.hpp> + +#include <corsika/output/OutputManager.hpp> + +using namespace corsika; + +double constexpr absMargin = 1.0e-7; + +template <typename TInterface> +using MyExtraEnv = + UniformRefractiveIndex<MediumPropertyModel<UniformMagneticField<TInterface>>>; + +TEST_CASE("Radio", "[processes]") { + + logging::set_level(logging::level::debug); + + SECTION("CoREAS process") { + + // This serves as a compiler test for any changes in the CoREAS algorithm + // Environment + using EnvironmentInterface = + IRefractiveIndexModel<IMediumPropertyModel<IMagneticFieldModel<IMediumModel>>>; + + // using EnvType = setup::Environment; + using EnvType = Environment<EnvironmentInterface>; + EnvType envCoREAS; + CoordinateSystemPtr const& rootCSCoREAS = envCoREAS.getCoordinateSystem(); + Point const center{rootCSCoREAS, 0_m, 0_m, 0_m}; + + // 1.000327, + create_5layer_atmosphere<EnvironmentInterface, MyExtraEnv>( + envCoREAS, AtmosphereId::LinsleyUSStd, center, 1.000327, Medium::AirDry1Atm, + MagneticFieldVector{rootCSCoREAS, 0_T, 50_uT, 0_T}); + + // now create antennas and detectors + // the antennas location + const auto point1{Point(envCoREAS.getCoordinateSystem(), 100_m, 2_m, 3_m)}; + const auto point2{Point(envCoREAS.getCoordinateSystem(), 4_m, 80_m, 6_m)}; + const auto point3{Point(envCoREAS.getCoordinateSystem(), 7_m, 8_m, 9_m)}; + const auto point4{Point(envCoREAS.getCoordinateSystem(), 5_m, 5_m, 10_m)}; + + // create times for the antenna + const TimeType t1{0_s}; + const TimeType t2{10_s}; + const InverseTimeType t3{1e+3_Hz}; + const TimeType t4{11_s}; + + // check that I can create an antenna at (1, 2, 3) + TimeDomainAntenna ant1("antenna_name", point1, rootCSCoREAS, t1, t2, t3, t1); + TimeDomainAntenna ant2("antenna_name2", point2, rootCSCoREAS, t1, t2, t3, t1); + + // construct a radio detector instance to store our antennas + AntennaCollection<TimeDomainAntenna> detector; + + // add the antennas to the detector + detector.addAntenna(ant1); + detector.addAntenna(ant2); + + // create a particle + const Code particle{Code::Electron}; + // const Code particle{Code::Proton}; + const auto pmass{get_mass(particle)}; + + VelocityVector v0(rootCSCoREAS, {5e+2_m / second, 5e+2_m / second, 5e+2_m / second}); + + Vector B0(rootCSCoREAS, 5_T, 5_T, 5_T); + + Line const line(point3, v0); + + auto const k{1_m * ((1_m) / ((1_s * 1_s) * 1_V))}; + + auto const t = 1e-12_s; + LeapFrogTrajectory base(point4, v0, B0, k, t); + // std::cout << "Leap Frog Trajectory is: " << base << std::endl; + + // create a new stack for each trial + setup::Stack<EnvType> stack; + + // construct an energy + const HEPEnergyType E0{1_TeV}; + + // compute the necessary momentumn + const HEPMomentumType P0{sqrt(E0 * E0 - pmass * pmass)}; + + // and create the momentum vector + const auto plab{MomentumVector(rootCSCoREAS, {0_GeV, 0_GeV, P0})}; + + // and create the location of the particle in this coordinate system + const Point pos(rootCSCoREAS, 50_m, 10_m, 80_m); + + // add the particle to the stack + auto const particle1{stack.addParticle(std::make_tuple( + particle, calculate_kinetic_energy(plab.getNorm(), get_mass(particle)), + plab.normalized(), pos, 0_ns))}; + + auto const charge_{get_charge(particle1.getPID())}; + + // create a radio process instance using CoREAS + RadioProcess<decltype(detector), + CoREAS<decltype(detector), decltype(StraightPropagator(envCoREAS))>, + decltype(StraightPropagator(envCoREAS))> + coreas(detector, envCoREAS); + + Step step(particle1, base); + // check doContinuous and simulate methods + coreas.doContinuous(step, true); + } // END: SECTION("CoREAS process") + + SECTION("ZHS process") { + + // This section serves as a compiler test for any changes in the ZHS algorithm + // Environment + using IModelInterface = + IRefractiveIndexModel<IMediumPropertyModel<IMagneticFieldModel<IMediumModel>>>; + using AtmModel = UniformRefractiveIndex< + MediumPropertyModel<UniformMagneticField<HomogeneousMedium<IModelInterface>>>>; + using EnvType = Environment<AtmModel>; + EnvType envZHS; + CoordinateSystemPtr const& rootCSZHS = envZHS.getCoordinateSystem(); + // get the center point + Point const center{rootCSZHS, 0_m, 0_m, 0_m}; + // a refractive index + const double ri_{1.000327}; + + // the constant density + const auto density{19.2_g / cube(1_cm)}; + + // the composition we use for the homogeneous medium + NuclearComposition const protonComposition({Code::Proton}, {1.}); + + // create magnetic field vector + Vector B1(rootCSZHS, 0_T, 0_T, 1_T); + + auto Medium = EnvType::createNode<Sphere>( + center, 1_km * std::numeric_limits<double>::infinity()); + + auto const props = Medium->setModelProperties<AtmModel>(ri_, Medium::AirDry1Atm, B1, + density, protonComposition); + envZHS.getUniverse()->addChild(std::move(Medium)); + + // the antennas location + const auto point1{Point(envZHS.getCoordinateSystem(), 100_m, 2_m, 3_m)}; + const auto point2{Point(envZHS.getCoordinateSystem(), 4_m, 80_m, 6_m)}; + const auto point3{Point(envZHS.getCoordinateSystem(), 7_m, 8_m, 9_m)}; + const auto point4{Point(envZHS.getCoordinateSystem(), 5_m, 5_m, 10_m)}; + + // create times for the antenna + const TimeType t1{0_s}; + const TimeType t2{10_s}; + const InverseTimeType t3{1e+3_Hz}; + const TimeType t4{11_s}; + + // check that I can create an antenna at (1, 2, 3) + TimeDomainAntenna ant1("antenna_zhs", point1, rootCSZHS, t1, t2, t3, t1); + TimeDomainAntenna ant2("antenna_zhs2", point2, rootCSZHS, t1, t2, t3, t1); + + // construct a radio detector instance to store our antennas + AntennaCollection<TimeDomainAntenna> detector; + + // add the antennas to the detector + detector.addAntenna(ant1); + detector.addAntenna(ant2); + + // create a particle + auto const particle{Code::Electron}; + const auto pmass{get_mass(particle)}; + + VelocityVector v0(rootCSZHS, {5e+2_m / second, 5e+2_m / second, 5e+2_m / second}); + + Vector B0(rootCSZHS, 5_T, 5_T, 5_T); + + Line const line(point3, v0); + + auto const k{1_m * ((1_m) / ((1_s * 1_s) * 1_V))}; + + auto const t = 1e-12_s; + LeapFrogTrajectory base(point4, v0, B0, k, t); + // std::cout << "Leap Frog Trajectory is: " << base << std::endl; + + // create a new stack for each trial + setup::Stack<EnvType> stack; + + // construct an energy + const HEPEnergyType E0{1_TeV}; + + // compute the necessary momentum + const HEPMomentumType P0{sqrt(E0 * E0 - pmass * pmass)}; + + // and create the momentum vector + const auto plab{MomentumVector(rootCSZHS, {0_GeV, 0_GeV, P0})}; + + // and create the location of the particle in this coordinate system + const Point pos(rootCSZHS, 50_m, 10_m, 80_m); + + // add the particle to the stack + auto const particle1{stack.addParticle(std::make_tuple( + particle, calculate_kinetic_energy(plab.getNorm(), get_mass(particle)), + plab.normalized(), pos, 0_ns))}; + + auto const charge_{get_charge(particle1.getPID())}; + + // create a radio process instance using ZHS + RadioProcess< + AntennaCollection<TimeDomainAntenna>, + ZHS<AntennaCollection<TimeDomainAntenna>, decltype(StraightPropagator(envZHS))>, + decltype(StraightPropagator(envZHS))> + zhs(detector, envZHS); + + Step step(particle1, base); + // check doContinuous and simulate methods + zhs.doContinuous(step, true); + + } // END: SECTION("ZHS process") + + SECTION("Radio extreme cases") { + + // Environment + using EnvironmentInterface = + IRefractiveIndexModel<IMediumPropertyModel<IMagneticFieldModel<IMediumModel>>>; + + using EnvType = Environment<EnvironmentInterface>; + EnvType envRadio; + CoordinateSystemPtr const& rootCSRadio = envRadio.getCoordinateSystem(); + Point const center{rootCSRadio, 0_m, 0_m, 0_m}; + + create_5layer_atmosphere<EnvironmentInterface, MyExtraEnv>( + envRadio, AtmosphereId::LinsleyUSStd, center, 1.415, Medium::AirDry1Atm, + MagneticFieldVector{rootCSRadio, 0_T, 50_uT, 0_T}); + + // now create antennas and detectors + // the antennas location + const auto point1{Point(envRadio.getCoordinateSystem(), 0_m, 0_m, 0_m)}; + + // track points + Point const point_1(rootCSRadio, {1_m, 1_m, 0_m}); + Point const point_2(rootCSRadio, {2_km, 1_km, 0_m}); + Point const point_4(rootCSRadio, {0_m, 1_m, 0_m}); + + // create times for the antenna + const TimeType start{0_s}; + const TimeType duration{100_ns}; + const InverseTimeType sample{1e+12_Hz}; + const TimeType duration_dummy{2_s}; + const InverseTimeType sample_dummy{1_Hz}; + + // check that I can create an antenna at (1, 2, 3) + TimeDomainAntenna ant1("antenna_name", point1, rootCSRadio, start, duration, sample, + start); + TimeDomainAntenna ant2("dummy", point1, rootCSRadio, start, duration_dummy, + sample_dummy, start); + // construct a radio detector instance to store our antennas + AntennaCollection<TimeDomainAntenna> detector; + AntennaCollection<TimeDomainAntenna> detector_dummy; + // add the antennas to the detector + detector.addAntenna(ant1); + detector_dummy.addAntenna(ant2); + + // create a new stack for each trial + setup::Stack<EnvType> stack; + // create a particle + const Code particle{Code::Electron}; + const Code particle2{Code::Proton}; + + const auto pmass{get_mass(particle)}; + const auto pmass2{get_mass(particle2)}; + // construct an energy + const HEPEnergyType E0{1_TeV}; + // compute the necessary momentumn + const HEPMomentumType P0{sqrt(E0 * E0 - pmass * pmass)}; + // and create the momentum vector + const auto plab{MomentumVector(rootCSRadio, {P0, 0_GeV, 0_GeV})}; + // add the particle to the stack + auto const particle_stack{stack.addParticle(std::make_tuple( + particle, calculate_kinetic_energy(plab.getNorm(), get_mass(particle)), + plab.normalized(), point_1, 0_ns))}; + + // particle stack with proton + auto const particle_stack_proton{stack.addParticle(std::make_tuple( + particle2, calculate_kinetic_energy(plab.getNorm(), get_mass(particle2)), + plab.normalized(), point_1, 0_ns))}; + + // feed radio with a proton track to check that it skips that track. + TimeType tp{(point_2 - point_1).getNorm() / (0.999 * constants::c)}; + VelocityVector vp{(point_2 - point_1) / tp}; + Line lp{point_1, vp}; + StraightTrajectory track_p{lp, tp}; + Step step_proton(particle_stack_proton, track_p); + + // feed radio with a track that triggers zhs like approx in coreas and fraunhofer + // limit check for zhs + TimeType th{(point_4 - point1).getNorm() / constants::c}; + VelocityVector vh{(point_4 - point1) / th}; + Line lh{point1, vh}; + StraightTrajectory track_h{lh, th}; + Step step_h(particle_stack, track_h); + + // create radio process instances + RadioProcess<decltype(detector), + CoREAS<decltype(detector), decltype(SimplePropagator(envRadio))>, + decltype(SimplePropagator(envRadio))> + coreas(detector, envRadio); + + RadioProcess<decltype(detector), + ZHS<decltype(detector), decltype(SimplePropagator(envRadio))>, + decltype(SimplePropagator(envRadio))> + zhs(detector, envRadio); + + coreas.doContinuous(step_proton, true); + zhs.doContinuous(step_proton, true); + coreas.doContinuous(step_h, true); + zhs.doContinuous(step_h, true); + + // create radio processes with "dummy" antenna to trigger extreme time-binning + RadioProcess<decltype(detector_dummy), + CoREAS<decltype(detector_dummy), decltype(SimplePropagator(envRadio))>, + decltype(SimplePropagator(envRadio))> + coreas_dummy(detector_dummy, envRadio); + + RadioProcess<decltype(detector_dummy), + ZHS<decltype(detector_dummy), decltype(SimplePropagator(envRadio))>, + decltype(SimplePropagator(envRadio))> + zhs_dummy(detector_dummy, envRadio); + + coreas_dummy.doContinuous(step_h, true); + zhs_dummy.doContinuous(step_h, true); + + } // END: SECTION("Radio extreme cases") + +} // END: TEST_CASE("Radio", "[processes]") + +TEST_CASE("Antennas") { + + SECTION("TimeDomainAntenna") { + + // create an environment so we can get a coordinate system + using EnvType = Environment<IRefractiveIndexModel<IMediumModel>>; + EnvType env6; + + using UniRIndex = + UniformRefractiveIndex<HomogeneousMedium<IRefractiveIndexModel<IMediumModel>>>; + + // the antenna location + const auto point1{Point(env6.getCoordinateSystem(), 1_m, 2_m, 3_m)}; + const auto point2{Point(env6.getCoordinateSystem(), 4_m, 5_m, 6_m)}; + + // get a coordinate system + const CoordinateSystemPtr rootCS6 = env6.getCoordinateSystem(); + + auto Medium6 = EnvType::createNode<Sphere>( + Point{rootCS6, 0_m, 0_m, 0_m}, 1_km * std::numeric_limits<double>::infinity()); + + auto const props6 = Medium6->setModelProperties<UniRIndex>( + 1, 1_kg / (1_m * 1_m * 1_m), NuclearComposition({Code::Nitrogen}, {1.})); + + env6.getUniverse()->addChild(std::move(Medium6)); + + // create times for the antenna + const TimeType t1{10_s}; + const TimeType t2{10_s}; + const InverseTimeType t3{1 / 1_s}; + const TimeType t4{11_s}; + + // check that I can create an antenna at (1, 2, 3) + TimeDomainAntenna ant1("antenna_name", point1, rootCS6, t1, t2, t3, t1); + TimeDomainAntenna ant2("antenna_name2", point2, rootCS6, t4, t2, t3, t4); + + // assert that the antenna name is correct + REQUIRE(ant1.getName() == "antenna_name"); + REQUIRE(ant2.getName() == "antenna_name2"); + + // and check that the antenna is at the right location + REQUIRE((ant1.getLocation() - point1).getNorm() < 1e-12 * 1_m); + REQUIRE((ant2.getLocation() - point2).getNorm() < 1e-12 * 1_m); + + // construct a radio detector instance to store our antennas + AntennaCollection<TimeDomainAntenna> detector; + + // add this antenna to the process + detector.addAntenna(ant1); + detector.addAntenna(ant2); + CHECK(detector.size() == 2); + + // get a unit vector + Vector<dimensionless_d> v1(rootCS6, {0, 0, 1}); + Vector<ElectricFieldType::dimension_type> v11(rootCS6, + {10_V / 1_m, 10_V / 1_m, 10_V / 1_m}); + + Vector<dimensionless_d> v2(rootCS6, {0, 1, 0}); + Vector<ElectricFieldType::dimension_type> v22(rootCS6, + {20_V / 1_m, 20_V / 1_m, 20_V / 1_m}); + + // use receive methods + ant1.receive(15_s, v1, v11); + ant2.receive(16_s, v2, v22); + + // use getDataX,Y,Z() and getAxis() methods + auto Ex = ant1.getWaveformX(); + CHECK(Ex[5] - 10 == 0); + auto tx = ant1.getAxis(); + CHECK(tx[5] - 5 * 1_s / 1_ns == Approx(0.0)); + auto Ey = ant1.getWaveformY(); + CHECK(Ey[5] - 10 == 0); + auto Ez = ant1.getWaveformZ(); + CHECK(Ez[5] - 10 == 0); + auto ty = ant1.getAxis(); + auto tz = ant1.getAxis(); + CHECK(tx[5] - ty[5] == 0); + CHECK(ty[5] - tz[5] == 0); + auto Ex2 = ant2.getWaveformX(); + CHECK(Ex2[5] - 20 == 0); + auto Ey2 = ant2.getWaveformY(); + CHECK(Ey2[5] - 20 == 0); + auto Ez2 = ant2.getWaveformZ(); + CHECK(Ez2[5] - 20 == 0); + + // the following creates a star-shaped pattern of antennas in the ground + AntennaCollection<TimeDomainAntenna> detector__; + const auto point11{Point(env6.getCoordinateSystem(), 1000_m, 20_m, 30_m)}; + const TimeType t2222{1e-6_s}; + const InverseTimeType t3333{1e+9_Hz}; + + std::vector<std::string> antenna_names; + std::vector<Point> antenna_locations; + for (auto radius_ = 100_m; radius_ <= 200_m; radius_ += 100_m) { + for (auto phi_ = 0; phi_ <= 315; phi_ += 45) { + auto phiRad_ = phi_ / 180. * M_PI; + auto const point_{Point(env6.getCoordinateSystem(), radius_ * cos(phiRad_), + radius_ * sin(phiRad_), 0_m)}; + antenna_locations.push_back(point_); + auto time__{(point11 - point_).getNorm() / constants::c}; + const int rr_ = static_cast<int>(radius_ / 1_m); + std::string var_ = "antenna_R=" + std::to_string(rr_) + + "_m-Phi=" + std::to_string(phi_) + "degrees"; + antenna_names.push_back(var_); + TimeDomainAntenna ant111(var_, point_, rootCS6, time__, t2222, t3333, time__); + detector__.addAntenna(ant111); + } + } + + CHECK(detector__.size() == 16); + CHECK(detector__.getAntennas().size() == 16); + int i = 0; + // this prints out the antenna names and locations + for (auto const antenna : detector__.getAntennas()) { + CHECK(antenna.getName() == antenna_names[i]); + CHECK(distance(antenna.getLocation(), antenna_locations[i]) / 1_m == 0); + i++; + } + + // Check the .at() method for radio detectors + for (size_t i = 0; i <= (detector__.size() - 1); i++) { + CHECK(detector__.at(i).getName() == antenna_names[i]); + CHECK(distance(detector__.at(i).getLocation(), antenna_locations[i]) / 1_m == 0); + } + + } // END: SECTION("TimeDomainAntenna") + +} // END: TEST_CASE("Antennas") + +TEST_CASE("Propagators") { + + SECTION("Simple Propagator w/ Uniform Refractive Index") { + + // create a suitable environment + using IModelInterface = + IRefractiveIndexModel<IMediumPropertyModel<IMagneticFieldModel<IMediumModel>>>; + using AtmModel = UniformRefractiveIndex< + MediumPropertyModel<UniformMagneticField<HomogeneousMedium<IModelInterface>>>>; + using EnvType = Environment<AtmModel>; + EnvType env; + CoordinateSystemPtr const& rootCS = env.getCoordinateSystem(); + // get the center point + Point const center{rootCS, 0_m, 0_m, 0_m}; + // a refractive index for the vacuum + const double ri_{1}; + // the constant density + const auto density{19.2_g / cube(1_cm)}; + // the composition we use for the homogeneous medium + NuclearComposition const Composition({Code::Nitrogen}, {1.}); + // create magnetic field vector + Vector B1(rootCS, 0_T, 0_T, 0.3809_T); + // create a Sphere for the medium + auto Medium = EnvType::createNode<Sphere>( + center, 1_km * std::numeric_limits<double>::infinity()); + // set the environment properties + auto const props = Medium->setModelProperties<AtmModel>(ri_, Medium::AirDry1Atm, B1, + density, Composition); + // bind things together + env.getUniverse()->addChild(std::move(Medium)); + + // get some points + Point p0(rootCS, {0_m, 0_m, 0_m}); + Point p10(rootCS, {0_m, 0_m, 10_m}); + + // get a unit vector + Vector<dimensionless_d> v1(rootCS, {0, 0, 1}); + Vector<dimensionless_d> v2(rootCS, {0, 0, -1}); + + // get a geometrical path of points + Path P1({p0, p10}); + + // construct a Straight Propagator given the uniform refractive index environment + SimplePropagator SP(env); + + // store the outcome of the Propagate method to paths_ + auto const paths_ = SP.propagate(p0, p10, 1_m); + + // perform checks to paths_ components + for (auto const& path : paths_) { + CHECK((path.propagation_time_ / 1_s) - + (((p10 - p0).getNorm() / constants::c) / 1_s) == + Approx(0)); + CHECK(path.average_refractive_index_ == Approx(1)); + CHECK(path.refractive_index_source_ == Approx(1)); + CHECK(path.refractive_index_destination_ == Approx(1)); + CHECK(path.emit_.getComponents() == v1.getComponents()); + CHECK(path.receive_.getComponents() == v2.getComponents()); + CHECK(path.R_distance_ == 10_m); + CHECK(std::equal(P1.begin(), P1.end(), Path(path.points_).begin(), + [](Point a, Point b) { return (a - b).getNorm() / 1_m < 1e-5; })); + } + + } // END: SECTION("Simple Propagator w/ Uniform Refractive Index") + + // check that I can create working Straight Propagators in different environments + SECTION("Straight Propagator w/ Uniform Refractive Index") { + + // create a suitable environment + using IModelInterface = + IRefractiveIndexModel<IMediumPropertyModel<IMagneticFieldModel<IMediumModel>>>; + using AtmModel = UniformRefractiveIndex< + MediumPropertyModel<UniformMagneticField<HomogeneousMedium<IModelInterface>>>>; + using EnvType = Environment<AtmModel>; + EnvType env; + CoordinateSystemPtr const& rootCS = env.getCoordinateSystem(); + // get the center point + Point const center{rootCS, 0_m, 0_m, 0_m}; + // a refractive index for the vacuum + const double ri_{1}; + // the constant density + const auto density{19.2_g / cube(1_cm)}; + // the composition we use for the homogeneous medium + NuclearComposition const Composition({Code::Nitrogen}, {1.}); + // create magnetic field vector + Vector B1(rootCS, 0_T, 0_T, 0.3809_T); + // create a Sphere for the medium + auto Medium = EnvType::createNode<Sphere>( + center, 1_km * std::numeric_limits<double>::infinity()); + // set the environment properties + auto const props = Medium->setModelProperties<AtmModel>(ri_, Medium::AirDry1Atm, B1, + density, Composition); + // bind things together + env.getUniverse()->addChild(std::move(Medium)); + + // get some points + Point p0(rootCS, {0_m, 0_m, 0_m}); + Point p1(rootCS, {0_m, 0_m, 1_m}); + Point p2(rootCS, {0_m, 0_m, 2_m}); + Point p3(rootCS, {0_m, 0_m, 3_m}); + Point p4(rootCS, {0_m, 0_m, 4_m}); + Point p5(rootCS, {0_m, 0_m, 5_m}); + Point p6(rootCS, {0_m, 0_m, 6_m}); + Point p7(rootCS, {0_m, 0_m, 7_m}); + Point p8(rootCS, {0_m, 0_m, 8_m}); + Point p9(rootCS, {0_m, 0_m, 9_m}); + Point p10(rootCS, {0_m, 0_m, 10_m}); + Point p30(rootCS, {0_m, 0_m, 30000_m}); + + // get a unit vector + Vector<dimensionless_d> v1(rootCS, {0, 0, 1}); + Vector<dimensionless_d> v2(rootCS, {0, 0, -1}); + + // get a geometrical path of points + Path P1({p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10}); + + // construct a Straight Propagator given the uniform refractive index environment + StraightPropagator SP(env); + + // store the outcome of the Propagate method to paths_ + auto const paths_ = SP.propagate(p0, p10, 1_m); + + // perform checks to paths_ components + for (auto const& path : paths_) { + CHECK((path.propagation_time_ / 1_s) - + (((p10 - p0).getNorm() / constants::c) / 1_s) == + Approx(0).margin(absMargin)); + CHECK(path.average_refractive_index_ == Approx(1)); + CHECK(path.refractive_index_source_ == Approx(1)); + CHECK(path.refractive_index_destination_ == Approx(1)); + CHECK(path.emit_.getComponents() == v1.getComponents()); + CHECK(path.receive_.getComponents() == v2.getComponents()); + CHECK(path.R_distance_ == 10_m); + CHECK(std::equal(P1.begin(), P1.end(), Path(path.points_).begin(), + [](Point a, Point b) { return (a - b).getNorm() / 1_m < 1e-5; })); + } + + // get another path to different points + auto const paths2_{SP.propagate(p0, p30, 909_m)}; + + for (auto const& path : paths2_) { + CHECK((path.propagation_time_ / 1_s) - + (((p30 - p0).getNorm() / constants::c) / 1_s) == + Approx(0).margin(absMargin)); + CHECK(path.average_refractive_index_ == Approx(1)); + CHECK(path.refractive_index_source_ == Approx(1)); + CHECK(path.refractive_index_destination_ == Approx(1)); + CHECK(path.R_distance_ == 30000_m); + } + + // get a third path using a weird stepsize + auto const paths3_{SP.propagate(p0, p30, 731.89_m)}; + + for (auto const& path : paths3_) { + CHECK((path.propagation_time_ / 1_s) - + (((p30 - p0).getNorm() / constants::c) / 1_s) == + Approx(0).margin(absMargin)); + CHECK(path.average_refractive_index_ == Approx(1)); + CHECK(path.refractive_index_source_ == Approx(1)); + CHECK(path.refractive_index_destination_ == Approx(1)); + CHECK(path.R_distance_ == 30000_m); + } + + CHECK(paths_.size() == 1); + CHECK(paths2_.size() == 1); + CHECK(paths3_.size() == 1); + } // END: SECTION("Straight Propagator w/ Uniform Refractive Index") + + SECTION("Straight Propagator w/ Exponential Refractive Index") { + + // create an environment with exponential refractive index (n_0 = 1 & lambda = 0) + using ExpoRIndex = ExponentialRefractiveIndex< + HomogeneousMedium<IRefractiveIndexModel<IMediumModel>>>; + + using EnvType = Environment<IRefractiveIndexModel<IMediumModel>>; + EnvType env1; + + // get another coordinate system + const CoordinateSystemPtr rootCS1 = env1.getCoordinateSystem(); + + // the center of the earth + Point const center1_{rootCS1, 0_m, 0_m, 0_m}; + LengthType const radius_{0_m}; + + auto Medium1 = EnvType::createNode<Sphere>( + Point{rootCS1, 0_m, 0_m, 0_m}, 1_km * std::numeric_limits<double>::infinity()); + + auto const props1 = Medium1->setModelProperties<ExpoRIndex>( + 1, 0 / 1_m, center1_, radius_, 1_kg / (1_m * 1_m * 1_m), + NuclearComposition({Code::Nitrogen}, {1.})); + + env1.getUniverse()->addChild(std::move(Medium1)); + + // get some points + Point pp0(rootCS1, {0_m, 0_m, 0_m}); + Point pp1(rootCS1, {0_m, 0_m, 1_m}); + Point pp2(rootCS1, {0_m, 0_m, 2_m}); + Point pp3(rootCS1, {0_m, 0_m, 3_m}); + Point pp4(rootCS1, {0_m, 0_m, 4_m}); + Point pp5(rootCS1, {0_m, 0_m, 5_m}); + Point pp6(rootCS1, {0_m, 0_m, 6_m}); + Point pp7(rootCS1, {0_m, 0_m, 7_m}); + Point pp8(rootCS1, {0_m, 0_m, 8_m}); + Point pp9(rootCS1, {0_m, 0_m, 9_m}); + Point pp10(rootCS1, {0_m, 0_m, 10_m}); + + // get a unit vector + Vector<dimensionless_d> vv1(rootCS1, {0, 0, 1}); + Vector<dimensionless_d> vv2(rootCS1, {0, 0, -1}); + + // get a geometrical path of points + Path PP1({pp0, pp1, pp2, pp3, pp4, pp5, pp6, pp7, pp8, pp9, pp10}); + + // construct a Straight Propagator given the exponential refractive index environment + StraightPropagator SP1(env1); + + // store the outcome of Propagate method to paths1_ + auto const paths1_ = SP1.propagate(pp0, pp10, 1_m); + + // perform checks to paths1_ components (this is just a sketch for now) + for (auto const& path : paths1_) { + CHECK((path.propagation_time_ / 1_s) - + (((pp10 - pp0).getNorm() / constants::c) / 1_s) == + Approx(0).margin(absMargin)); + CHECK(path.average_refractive_index_ == Approx(1)); + CHECK(path.refractive_index_source_ == Approx(1)); + CHECK(path.refractive_index_destination_ == Approx(1)); + CHECK(path.emit_.getComponents() == vv1.getComponents()); + CHECK(path.receive_.getComponents() == vv2.getComponents()); + CHECK(path.R_distance_ == 10_m); + CHECK(std::equal(PP1.begin(), PP1.end(), Path(path.points_).begin(), + [](Point a, Point b) { return (a - b).getNorm() / 1_m < 1e-5; })); + } + + CHECK(paths1_.size() == 1); + + /* + * A second environment with another exponential refractive index + */ + + // create an environment with exponential refractive index (n_0 = 2 & lambda = 2) + using ExpoRIndex = ExponentialRefractiveIndex< + HomogeneousMedium<IRefractiveIndexModel<IMediumModel>>>; + + using EnvType = Environment<IRefractiveIndexModel<IMediumModel>>; + EnvType env2; + + // get another coordinate system + const CoordinateSystemPtr rootCS2 = env2.getCoordinateSystem(); + + // the center of the earth + Point const center2_{rootCS2, 0_m, 0_m, 0_m}; + + auto Medium2 = EnvType::createNode<Sphere>( + Point{rootCS2, 0_m, 0_m, 0_m}, 1_km * std::numeric_limits<double>::infinity()); + + auto const props2 = Medium2->setModelProperties<ExpoRIndex>( + 2, 2 / 1_m, center2_, radius_, 1_kg / (1_m * 1_m * 1_m), + NuclearComposition({Code::Nitrogen}, {1.})); + + env2.getUniverse()->addChild(std::move(Medium2)); + + // get some points + Point ppp0(rootCS2, {0_m, 0_m, 0_m}); + Point ppp10(rootCS2, {0_m, 0_m, 10_m}); + + // get a unit vector + Vector<dimensionless_d> vvv1(rootCS2, {0, 0, 1}); + Vector<dimensionless_d> vvv2(rootCS2, {0, 0, -1}); + + // construct a Straight Propagator given the exponential refractive index environment + StraightPropagator SP2(env2); + + // store the outcome of Propagate method to paths1_ + auto const paths2_ = SP2.propagate(ppp0, ppp10, 1_m); + + // perform checks to paths1_ components (this is just a sketch for now) + for (auto const& path : paths2_) { + CHECK((path.propagation_time_ / 1_s) - + ((3.177511688_m / (3 * constants::c)) / 1_s) == + Approx(0).margin(absMargin)); + CHECK(path.average_refractive_index_ == Approx(0.210275935)); + CHECK(path.refractive_index_source_ == Approx(2)); + // CHECK(path.refractive_index_destination_ == Approx(0.0000000041)); + CHECK(path.emit_.getComponents() == vvv1.getComponents()); + CHECK(path.receive_.getComponents() == vvv2.getComponents()); + CHECK(path.R_distance_ == 10_m); + } + + CHECK(paths2_.size() == 1); + + } // END: SECTION("Straight Propagator w/ Exponential Refractive Index") + +} // END: TEST_CASE("Propagators")