diff --git a/corsika/detail/media/WMM.inl b/corsika/detail/media/WMM.inl deleted file mode 100644 index 3eecac4800bd830af5fceadfd8ce43d476d384d9..0000000000000000000000000000000000000000 --- a/corsika/detail/media/WMM.inl +++ /dev/null @@ -1,121 +0,0 @@ -/* - * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu - * - * This software is distributed under the terms of the GNU General Public - * Licence version 3 (GPL Version 3). See file LICENSE for a full version of - * the license. - */ - -#pragma once - -#include <boost/math/special_functions/factorials.hpp> -#include <boost/math/special_functions/legendre.hpp> - -#include <cmath> -#include <corsika/framework/core/Logging.hpp> -#include <corsika/framework/utility/CorsikaData.hpp> -#include <fstream> - -namespace corsika { - inline MagneticFieldVector get_wmm(const CoordinateSystemPtr Cs, const double year, - const LengthType altitude, const double latitude, - const double longitude) { - if (year < 2020 || year > 2025) { - CORSIKA_LOG_WARN("Year has to be between 2020 and 2025."); - } - if (altitude < -1_km || altitude > 850_km) { - CORSIKA_LOG_WARN("Altitude should be between -1_km and 850_km."); - } - if (latitude < -90 || latitude > 90) { - CORSIKA_LOG_ERROR("Latitude has to be between -90 and 90 degree."); - abort(); - } else if (latitude < -89.992 || latitude > 89.992) { - CORSIKA_LOG_WARN("Latitude is close to the poles."); - } - if (longitude < -180 || longitude > 180) { - CORSIKA_LOG_WARN("Longitude should be between -180 and 180 degree."); - } - - const double lat_geo = latitude * constants::pi / 180; - const double lon = longitude * constants::pi / 180; - - // Transform into spherical coordinates - const double f = 1 / 298.257223563; - const double e_squared = f * (2 - f); - LengthType R_c = - constants::EarthRadius::Equatorial / sqrt(1 - e_squared * pow(sin(lat_geo), 2)); - LengthType p = (R_c + altitude) * cos(lat_geo); - LengthType z = sin(lat_geo) * (altitude + R_c * (1 - e_squared)); - LengthType r = sqrt(p * p + z * z); - double lat_sph = asin(z / r); - - const int length = 90; - double epoch, g[length], h[length], g_dot[length], h_dot[length]; - std::string model_name; - std::string release_date; - int n[length], m[length]; - - // Read in coefficients - boost::filesystem::path const path = corsika::corsika_data("GeoMag/WMM.COF"); - boost::filesystem::ifstream file(path, std::ios::in); - // Exit if file opening failed - if (!file.is_open()) { - CORSIKA_LOG_ERROR("Failed opening WMM.COF."); - abort(); - } - - file >> epoch >> model_name >> release_date; - for (int i = 0; i < length; i++) { - file >> n[i] >> m[i] >> g[i] >> h[i] >> g_dot[i] >> h_dot[i]; - // Time interpolation - g[i] = g[i] + (year - epoch) * g_dot[i]; - h[i] = h[i] + (year - epoch) * h_dot[i]; - } - file.close(); - - double legendre, next_legendre, derivate_legendre; - double magneticfield[3] = {0, 0, 0}; - - for (int j = 0; j < length; j++) { - legendre = boost::math::legendre_p(n[j], m[j], sin(lat_sph)); - next_legendre = boost::math::legendre_p(n[j] + 1, m[j], sin(lat_sph)); - - // Schmidt semi-normalization and Condon-Shortley phase term - if (m[j] > 0) { - legendre *= sqrt(2 * boost::math::factorial<double>(n[j] - m[j]) / - boost::math::factorial<double>(n[j] + m[j])) * - pow(-1, m[j]); - next_legendre *= sqrt(2 * boost::math::factorial<double>(n[j] + 1 - m[j]) / - boost::math::factorial<double>(n[j] + 1 + m[j])) * - pow(-1, m[j]); - } - derivate_legendre = - (n[j] + 1) * tan(lat_sph) * legendre - - sqrt(pow(n[j] + 1, 2) - pow(m[j], 2)) / cos(lat_sph) * next_legendre; - - magneticfield[0] += - pow(constants::EarthRadius::Geomagnetic_reference / r, n[j] + 2) * - (g[j] * cos(m[j] * lon) + h[j] * sin(m[j] * lon)) * derivate_legendre; - magneticfield[1] += - pow(constants::EarthRadius::Geomagnetic_reference / r, n[j] + 2) * m[j] * - (g[j] * sin(m[j] * lon) - h[j] * cos(m[j] * lon)) * legendre; - magneticfield[2] += - (n[j] + 1) * pow(constants::EarthRadius::Geomagnetic_reference / r, n[j] + 2) * - (g[j] * cos(m[j] * lon) + h[j] * sin(m[j] * lon)) * legendre; - } - magneticfield[0] *= -1; - magneticfield[1] /= cos(lat_sph); - magneticfield[2] *= -1; - - // Transform back into geodetic coordinates - double magneticfield_geo[3]; - magneticfield_geo[0] = magneticfield[0] * cos(lat_sph - lat_geo) - - magneticfield[2] * sin(lat_sph - lat_geo); - magneticfield_geo[1] = magneticfield[1]; - magneticfield_geo[2] = magneticfield[0] * sin(lat_sph - lat_geo) + - magneticfield[2] * cos(lat_sph - lat_geo); - - return MagneticFieldVector{Cs, magneticfield_geo[0] * 1_nT, - magneticfield_geo[1] * 1_nT, magneticfield_geo[2] * -1_nT}; - } -} // namespace corsika \ No newline at end of file diff --git a/corsika/detail/media/WorldMagneticModel.inl b/corsika/detail/media/WorldMagneticModel.inl new file mode 100644 index 0000000000000000000000000000000000000000..ce1365320bc23ade677f4b4b1023c926bbf182c9 --- /dev/null +++ b/corsika/detail/media/WorldMagneticModel.inl @@ -0,0 +1,190 @@ +#include <corsika/framework/core/Logging.hpp> +#include <corsika/framework/utility/CorsikaData.hpp> + +#include <boost/filesystem.hpp> +#include <boost/math/special_functions/factorials.hpp> +#include <boost/math/special_functions/legendre.hpp> + +#include <stdexcept> +#include <string> +#include <cmath> + +namespace corsika { + + inline WorldMagneticModel::WorldMagneticModel(Point const& center, + std::string const& dataFile) + : center_(center) { + + // Read in coefficients + boost::filesystem::path const path = corsika::corsika_data(dataFile); + boost::filesystem::ifstream file(path, std::ios::in); + + // Exit if file opening failed + if (!file.is_open()) { + CORSIKA_LOG_ERROR("Failed opening data file {}", dataFile); + throw std::runtime_error("Cannot load WorldMagneticModel data."); + } + + // WorldMagneticModel supports two types of input data: WMM.COF and IGRF.COF + // They have only slightly different format and content and can be easily + // differentiated here. + + std::string line; + while (getline(file >> std::ws, line)) { + + double epoch; + std::string model_name; + std::string release_date; // just for WMM + int nMax = 12; // the spherical max n (l) shell (for IGRF), for WMM this is 12 + // Note that n=l=0 is the monopole and is not included in the model. + int dummyInt; + double dummyDouble; + + std::istringstream firstLine(line); + + // check comments and ignore: + if (firstLine.peek() == '#' || // normal comment + line.size() == 0 || // empty line + line.find("9999999999999999999999999") == 0) { // crazy WMM comment + continue; + } + + // check IGRF format: + if (firstLine >> model_name >> epoch >> nMax >> dummyInt >> dummyInt >> + dummyDouble >> dummyDouble >> dummyDouble >> dummyDouble >> model_name >> + dummyInt) { + static bool info = false; + if (!info) { + CORSIKA_LOG_INFO("Reading IGRF input data format."); + info = true; + } + } else { + // check WMM format: + firstLine.clear(); + firstLine.seekg(0, std::ios::beg); + if (firstLine >> epoch >> model_name >> release_date) { + CORSIKA_LOG_INFO("Reading WMM input data format."); + } else { + CORSIKA_LOG_ERROR("line: {}", line); + throw std::runtime_error("Incompatible input data for WorldMagneticModel"); + } + } + + int nPar = 0; + for (int i = 0; i < nMax; ++i) { nPar += i + 2; } + int iEpoch = int(epoch); + + if (parameters_.count(iEpoch) != 0) { + throw std::runtime_error( + "WorldMagneticModel input file has duplicate Epoch. Fix."); + } + parameters_[iEpoch] = std::vector<ParameterLine>(nPar); + + for (int i = 0; i < nPar; i++) { + file >> parameters_[iEpoch][i].n >> parameters_[iEpoch][i].m >> + parameters_[iEpoch][i].g >> parameters_[iEpoch][i].h >> + parameters_[iEpoch][i].dg >> parameters_[iEpoch][i].dh; + file.ignore(9999999, '\n'); + } + } + file.close(); + } + + inline MagneticFieldVector WorldMagneticModel::getField(double const year, + LengthType const altitude, + double const latitude, + double const longitude) { + + int iYear = int(year); + int iEpoch = 0; + for (auto parIt = parameters_.rbegin(); parIt != parameters_.rend(); ++parIt) { + if (parIt->first <= iYear) { + iEpoch = parIt->first; + break; + } + } + double epoch = double(iEpoch); + CORSIKA_LOG_DEBUG("Found Epoch {} for year {}", iEpoch, year); + if (iEpoch == 0) { + CORSIKA_LOG_WARN("Year {} is before first EPOCH. Results unclear.", year); + } + if (altitude < -1_km || altitude > 850_km) { + CORSIKA_LOG_WARN("Altitude should be between -1_km and 850_km."); + } + if (latitude <= -90 || latitude >= 90) { + CORSIKA_LOG_ERROR("Latitude has to be between -90 and 90 degree."); + throw std::runtime_error("Latitude has to be between -90 and 90 degree."); + } else if (latitude < -89.992 || latitude > 89.992) { + CORSIKA_LOG_WARN("Latitude is close to the poles."); + } + if (longitude < -180 || longitude > 180) { + CORSIKA_LOG_WARN("Longitude should be between -180 and 180 degree."); + } + + const double lat_geo = latitude * constants::pi / 180; + const double lon = longitude * constants::pi / 180; + + // Transform into spherical coordinates + const double f = 1 / 298.257223563; + const double e_squared = f * (2 - f); + LengthType R_c = + constants::EarthRadius::Equatorial / sqrt(1 - e_squared * pow(sin(lat_geo), 2)); + LengthType p = (R_c + altitude) * cos(lat_geo); + LengthType z = sin(lat_geo) * (altitude + R_c * (1 - e_squared)); + LengthType r = sqrt(p * p + z * z); + double lat_sph = asin(z / r); + + double legendre, next_legendre, derivate_legendre; + double magneticfield[3] = {0, 0, 0}; + + for (size_t j = 0; j < parameters_[iEpoch].size(); j++) { + + ParameterLine p = parameters_[iEpoch][j]; + + // Time interpolation + p.g = p.g + (year - epoch) * p.dg; + p.h = p.h + (year - epoch) * p.dh; + + legendre = boost::math::legendre_p(p.n, p.m, sin(lat_sph)); + next_legendre = boost::math::legendre_p(p.n + 1, p.m, sin(lat_sph)); + + // Schmidt semi-normalization and Condon-Shortley phase term + if (p.m > 0) { + legendre *= sqrt(2 * boost::math::factorial<double>(p.n - p.m) / + boost::math::factorial<double>(p.n + p.m)) * + pow(-1, p.m); + next_legendre *= sqrt(2 * boost::math::factorial<double>(p.n + 1 - p.m) / + boost::math::factorial<double>(p.n + 1 + p.m)) * + pow(-1, p.m); + } + derivate_legendre = + (p.n + 1) * tan(lat_sph) * legendre - + sqrt(pow(p.n + 1, 2) - pow(p.m, 2)) / cos(lat_sph) * next_legendre; + + magneticfield[0] += + pow(constants::EarthRadius::Geomagnetic_reference / r, p.n + 2) * + (p.g * cos(p.m * lon) + p.h * sin(p.m * lon)) * derivate_legendre; + magneticfield[1] += + pow(constants::EarthRadius::Geomagnetic_reference / r, p.n + 2) * p.m * + (p.g * sin(p.m * lon) - p.h * cos(p.m * lon)) * legendre; + magneticfield[2] += + (p.n + 1) * pow(constants::EarthRadius::Geomagnetic_reference / r, p.n + 2) * + (p.g * cos(p.m * lon) + p.h * sin(p.m * lon)) * legendre; + } + magneticfield[0] *= -1; + magneticfield[1] /= cos(lat_sph); + magneticfield[2] *= -1; + + // Transform back into geodetic coordinates + double magneticfield_geo[3]; + magneticfield_geo[0] = magneticfield[0] * cos(lat_sph - lat_geo) - + magneticfield[2] * sin(lat_sph - lat_geo); + magneticfield_geo[1] = magneticfield[1]; + magneticfield_geo[2] = magneticfield[0] * sin(lat_sph - lat_geo) + + magneticfield[2] * cos(lat_sph - lat_geo); + + return MagneticFieldVector{center_.getCoordinateSystem(), magneticfield_geo[0] * 1_nT, + magneticfield_geo[1] * 1_nT, magneticfield_geo[2] * -1_nT}; + } + +} // namespace corsika \ No newline at end of file diff --git a/corsika/media/WMM.hpp b/corsika/media/WMM.hpp deleted file mode 100644 index 9eb93b9d55854a88e059a3e0623a5509aadf0868..0000000000000000000000000000000000000000 --- a/corsika/media/WMM.hpp +++ /dev/null @@ -1,40 +0,0 @@ -/* - * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu - * - * This software is distributed under the terms of the GNU General Public - * Licence version 3 (GPL Version 3). See file LICENSE for a full version of - * the license. - */ - -#include <boost/filesystem.hpp> -#include <boost/math/special_functions/factorials.hpp> -#include <boost/math/special_functions/legendre.hpp> - -#include <cmath> -#include <corsika/framework/core/Logging.hpp> -#include <corsika/framework/utility/CorsikaData.hpp> -#include <fstream> - -namespace corsika { - /** - * A magnetic field calculated with the WMM model - * - * @param year Year of the evaluation, between 2020 and 2025. - * @param altitude Height of the location to evaluate the field at, - in km between -1 and 850. - * @param latitude Latitude of the location to evaluate the field at, - in degrees between -90 and 90 (negative for southern hemisphere). - * @param longitute Longitude of the location to evaluate the field at, - in degrees between -180 and 180 (negative for western - hemisphere). - * - * @returns The magnetic field vector in nT. - * - */ - - inline MagneticFieldVector get_wmm(const CoordinateSystemPtr Cs, const double year, - const LengthType altitude, const double latitude, - const double longitude); -} // namespace corsika - -#include <corsika/detail/media/WMM.inl> diff --git a/corsika/media/WorldMagneticModel.hpp b/corsika/media/WorldMagneticModel.hpp new file mode 100644 index 0000000000000000000000000000000000000000..642e25e73c34f64ddd7ac02e04f988957a1f4418 --- /dev/null +++ b/corsika/media/WorldMagneticModel.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include <corsika/framework/core/PhysicalUnits.hpp> +#include <corsika/framework/geometry/PhysicalGeometry.hpp> + +#include <fstream> +#include <string> + +namespace corsika { + + /** + * A magnetic field calculated with the WMM or IGRF model. + */ + + class WorldMagneticModel { + + /** + * Internal data structure for a single shell of the spherical harmonic + * parameterization. + */ + struct ParameterLine { + int n; + int m; + double g; + double h; + double dg; //< time dependence of g in "per year" + double dh; //< time dependence of h in "per year" + }; + + public: + /** + * Construct a new World Magnetic Model object. + * + * @param center Center of Earth. + * @param data Data table to read. + */ + WorldMagneticModel(Point const& center, std::string const& data = "GeoMag/WMM.COF"); + + /** + * Calculates the value of the magnetic field. + * + * @param year Year of the evaluation, between 2020 and 2025. + * @param altitude Height of the location to evaluate the field at, + * in km between -1 and 850. + * @param latitude Latitude of the location to evaluate the field at, + * in degrees between -90 and 90 (negative for southern hemisphere). + * @param longitute Longitude of the location to evaluate the field at, + * in degrees between -180 and 180 (negative for western + * hemisphere). + * + * @returns The magnetic field vector in nT. + */ + MagneticFieldVector getField(double const year, LengthType const altitude, + double const latitude, double const longitude); + + private: + Point center_; //< Center of Earth. + std::map<int, std::vector<ParameterLine>> parameters_; //< epoch to Parameters map + }; +} // namespace corsika + +#include <corsika/detail/media/WorldMagneticModel.inl> \ No newline at end of file diff --git a/tests/media/testMagneticField.cpp b/tests/media/testMagneticField.cpp index b792b7b16f4da697d096d6fae21361c8114e771a..27ee637bcde3bc27acabc9d9094a0217d51ae193 100644 --- a/tests/media/testMagneticField.cpp +++ b/tests/media/testMagneticField.cpp @@ -14,7 +14,7 @@ #include <corsika/media/UniformMagneticField.hpp> #include <corsika/media/IMagneticFieldModel.hpp> #include <corsika/media/VolumeTreeNode.hpp> -#include <corsika/media/WMM.hpp> +#include <corsika/media/WorldMagneticModel.hpp> #include <SetupTestTrajectory.hpp> #include <corsika/setup/SetupTrajectory.hpp> @@ -25,7 +25,7 @@ using namespace corsika; TEST_CASE("UniformMagneticField w/ Homogeneous Medium") { - logging::set_level(logging::level::info); + logging::set_level(logging::level::trace); CoordinateSystemPtr const& gCS = get_root_CoordinateSystem(); Point const gOrigin(gCS, {0_m, 0_m, 0_m}); @@ -74,18 +74,42 @@ TEST_CASE("UniformMagneticField w/ Homogeneous Medium") { } SECTION("WorldMagneticModel") { - // create earth magnetic field vector - MagneticFieldVector Earth_B_1 = get_wmm(gCS, 2022.5, 100_km, -80, -120); - MagneticFieldVector Earth_B_2 = get_wmm(gCS, 2022.5, 0_km, 0, 120); - MagneticFieldVector Earth_B_3 = get_wmm(gCS, 2020, 100_km, 80, 0); - CHECK(Earth_B_1.getX(gCS) / 1_nT == Approx(5815).margin(0.03)); - CHECK(Earth_B_1.getY(gCS) / 1_nT == Approx(14803).margin(0.03)); - CHECK(Earth_B_1.getZ(gCS) / 1_nT == Approx(49755.3).margin(0.03)); - CHECK(Earth_B_2.getX(gCS) / 1_nT == Approx(39684.7).margin(0.03)); - CHECK(Earth_B_2.getY(gCS) / 1_nT == Approx(-42.2).margin(0.03)); - CHECK(Earth_B_2.getZ(gCS) / 1_nT == Approx(10809.5).margin(0.03)); - CHECK(Earth_B_3.getX(gCS) / 1_nT == Approx(6261.8).margin(0.03)); - CHECK(Earth_B_3.getY(gCS) / 1_nT == Approx(-185.5).margin(0.03)); - CHECK(Earth_B_3.getZ(gCS) / 1_nT == Approx(-52429.1).margin(0.03)); + + CHECK_THROWS(WorldMagneticModel(gOrigin, "GeoMag/IDoNotExist.COF")); + + { + WorldMagneticModel wmm(gOrigin, "GeoMag/WMM.COF"); + + // create earth magnetic field vector + MagneticFieldVector Earth_B_1 = wmm.getField(2022.5, 100_km, -80, -120); + MagneticFieldVector Earth_B_2 = wmm.getField(2022.5, 0_km, 0, 120); + MagneticFieldVector Earth_B_3 = wmm.getField(2020, 100_km, 80, 0); + CHECK(Earth_B_1.getX(gCS) / 1_nT == Approx(5815).margin(0.03)); + CHECK(Earth_B_1.getY(gCS) / 1_nT == Approx(14803).margin(0.03)); + CHECK(Earth_B_1.getZ(gCS) / 1_nT == Approx(49755.3).margin(0.03)); + CHECK(Earth_B_2.getX(gCS) / 1_nT == Approx(39684.7).margin(0.03)); + CHECK(Earth_B_2.getY(gCS) / 1_nT == Approx(-42.2).margin(0.03)); + CHECK(Earth_B_2.getZ(gCS) / 1_nT == Approx(10809.5).margin(0.03)); + CHECK(Earth_B_3.getX(gCS) / 1_nT == Approx(6261.8).margin(0.03)); + CHECK(Earth_B_3.getY(gCS) / 1_nT == Approx(-185.5).margin(0.03)); + CHECK(Earth_B_3.getZ(gCS) / 1_nT == Approx(-52429.1).margin(0.03)); + } + { + WorldMagneticModel wmm(gOrigin, "GeoMag/IGRF13.COF"); + + // create earth magnetic field vector + MagneticFieldVector Earth_B_1 = wmm.getField(2022.5, 100_km, -80, -120); + MagneticFieldVector Earth_B_2 = wmm.getField(2022.5, 0_km, 0, 120); + MagneticFieldVector Earth_B_3 = wmm.getField(2020, 100_km, 80, 0); + CHECK(Earth_B_1.getX(gCS) / 1_nT == Approx(5815).margin(0.03)); + CHECK(Earth_B_1.getY(gCS) / 1_nT == Approx(14803).margin(0.03)); + CHECK(Earth_B_1.getZ(gCS) / 1_nT == Approx(49755.3).margin(0.03)); + CHECK(Earth_B_2.getX(gCS) / 1_nT == Approx(39684.7).margin(0.03)); + CHECK(Earth_B_2.getY(gCS) / 1_nT == Approx(-42.2).margin(0.03)); + CHECK(Earth_B_2.getZ(gCS) / 1_nT == Approx(10809.5).margin(0.03)); + CHECK(Earth_B_3.getX(gCS) / 1_nT == Approx(6261.8).margin(0.03)); + CHECK(Earth_B_3.getY(gCS) / 1_nT == Approx(-185.5).margin(0.03)); + CHECK(Earth_B_3.getZ(gCS) / 1_nT == Approx(-52429.1).margin(0.03)); + } } }