diff --git a/corsika/detail/media/WMM.inl b/corsika/detail/media/WMM.inl new file mode 100644 index 0000000000000000000000000000000000000000..3eecac4800bd830af5fceadfd8ce43d476d384d9 --- /dev/null +++ b/corsika/detail/media/WMM.inl @@ -0,0 +1,121 @@ +/* + * (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/media/WMM.hpp b/corsika/media/WMM.hpp index a1f84351a4b51e679691c92a14aca1814a9d1798..9eb93b9d55854a88e059a3e0623a5509aadf0868 100644 --- a/corsika/media/WMM.hpp +++ b/corsika/media/WMM.hpp @@ -25,14 +25,16 @@ namespace corsika { * @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). + 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); + + 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/tests/media/testMagneticField.cpp b/tests/media/testMagneticField.cpp index 1bbe1b718a2c9ed10dc6bc256ad00a53ab70f5c9..b792b7b16f4da697d096d6fae21361c8114e771a 100644 --- a/tests/media/testMagneticField.cpp +++ b/tests/media/testMagneticField.cpp @@ -30,72 +30,62 @@ TEST_CASE("UniformMagneticField w/ Homogeneous Medium") { CoordinateSystemPtr const& gCS = get_root_CoordinateSystem(); Point const gOrigin(gCS, {0_m, 0_m, 0_m}); - // setup our interface types - using IModelInterface = IMagneticFieldModel<IMediumModel>; - using AtmModel = UniformMagneticField<HomogeneousMedium<IModelInterface>>; - - // the composition we use for the homogenous medium - NuclearComposition const protonComposition({Code::Proton}, {1.}); - - // create a magnetic field vector - Vector B0(gCS, 0_T, 0_T, 0_T); - - // the constant density - const auto density{19.2_g / cube(1_cm)}; - - // create our atmospheric model - AtmModel medium(B0, density, protonComposition); - - // and test at several locations - CHECK(B0.getComponents(gCS) == - medium.getMagneticField(Point(gCS, -10_m, 4_m, 35_km)).getComponents(gCS)); - CHECK( - B0.getComponents(gCS) == - medium.getMagneticField(Point(gCS, 1000_km, -1000_km, 1000_km)).getComponents(gCS)); - CHECK(B0.getComponents(gCS) == - medium.getMagneticField(Point(gCS, 0_m, 0_m, 0_m)).getComponents(gCS)); - - // create a new magnetic field vector - Vector B1(gCS, 23_T, 57_T, -4_T); - - // and update this atmospheric model - medium.setMagneticField(B1); - - // and test at several locations - CHECK(B1.getComponents(gCS) == - medium.getMagneticField(Point(gCS, -10_m, 4_m, 35_km)).getComponents(gCS)); - CHECK( - B1.getComponents(gCS) == - medium.getMagneticField(Point(gCS, 1000_km, -1000_km, 1000_km)).getComponents(gCS)); - CHECK(B1.getComponents(gCS) == - medium.getMagneticField(Point(gCS, 0_m, 0_m, 0_m)).getComponents(gCS)); - - // check the density and nuclear composition - CHECK(density == medium.getMassDensity(Point(gCS, 0_m, 0_m, 0_m))); - medium.getNuclearComposition(); - - // create a line of length 1 m - Line const line(gOrigin, Vector<SpeedType::dimension_type>( - gCS, {1_m / second, 0_m / second, 0_m / second})); - - // the end time of our line - auto const tEnd = 1_s; - - // and the associated trajectory - setup::Trajectory const track = - setup::testing::make_track<setup::Trajectory>(line, tEnd); - - // 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)); + SECTION("UniformMagneticField interface") { + + // setup our interface types + using IModelInterface = IMagneticFieldModel<IMediumModel>; + using AtmModel = UniformMagneticField<HomogeneousMedium<IModelInterface>>; + + // the composition we use for the homogenous medium + NuclearComposition const protonComposition({Code::Proton}, {1.}); + + // create a magnetic field vector + Vector B0(gCS, 0_T, 0_T, 0_T); + + // the constant density + const auto density{19.2_g / cube(1_cm)}; + + // create our atmospheric model + AtmModel medium(B0, density, protonComposition); + + // and test at several locations + CHECK(B0.getComponents(gCS) == + medium.getMagneticField(Point(gCS, -10_m, 4_m, 35_km)).getComponents(gCS)); + CHECK(B0.getComponents(gCS) == + medium.getMagneticField(Point(gCS, 1000_km, -1000_km, 1000_km)) + .getComponents(gCS)); + CHECK(B0.getComponents(gCS) == + medium.getMagneticField(Point(gCS, 0_m, 0_m, 0_m)).getComponents(gCS)); + + // create a new magnetic field vector + Vector B1(gCS, 23_T, 57_T, -4_T); + + // and update this atmospheric model + medium.setMagneticField(B1); + + // and test at several locations + CHECK(B1.getComponents(gCS) == + medium.getMagneticField(Point(gCS, -10_m, 4_m, 35_km)).getComponents(gCS)); + CHECK(B1.getComponents(gCS) == + medium.getMagneticField(Point(gCS, 1000_km, -1000_km, 1000_km)) + .getComponents(gCS)); + CHECK(B1.getComponents(gCS) == + medium.getMagneticField(Point(gCS, 0_m, 0_m, 0_m)).getComponents(gCS)); + } + + 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)); + } }