geomag_wmm/
lib.rs

1#![allow(non_snake_case)]
2#![allow(non_upper_case_globals)]
3use std::{fs, process};
4use std::io::{BufRead,BufReader};
5
6// Defining the constants used in the model
7const WMM_VALIDITY_RANGE: f64 = 5.0;
8// WGS84 ellipsoid parameters
9const A : f64 = 6378137.0; // Equation (8)
10const f : f64 = 1.0 / 298.257223563; // Equation (8)
11const e_2 : f64 = f * (2.0 - f); // Equation (8)
12// Geomagnetic parameters
13const a : f64 = 6371200.0; // Equation (4)
14
15
16/// Calculate the factorial of a number
17fn factorial(n: usize) -> f64 {
18    (1..=n).fold(1.0, |acc, x| acc * x as f64)
19}
20
21/// Calculate the Legendre polynomial
22/// 
23/// # Arguments
24/// 
25/// * `t` - The value to evaluate
26/// * `n` - The degree of the polynomial
27/// * `m` - The order of the polynomial
28/// 
29/// # Returns
30/// 
31/// * The value of the Legendre polynomial
32/// 
33/// # Reference
34/// 
35/// * Heiskanen and Moritz, 1967 (https://archive.org/details/HeiskanenMoritz1967PhysicalGeodesy/page/n33/mode/2up): equation 1-62
36fn legendre_polynomial(t: f64, n: usize, m: usize) -> f64 {
37    let mut P = 0.0;
38    let mut k = 0;
39    while k <= (n - m) / 2 {
40        let num = (-1.0 as f64).powi(k.try_into().unwrap()) * factorial(2 * n - 2 * k) * t.powi(n as i32 - m as i32 - 2 * k as i32);
41        let den = factorial(k) * factorial(n - k) * factorial(n - m - 2 * k);
42        P += num / den;
43        k += 1;
44    }
45    P *= (2.0 as f64).powi(-(n as i32)) * (1.0 - t*t).powf(m as f64 / 2.0);
46    P
47}
48
49/// Calculate the Schmidt semi-normalised associated Legendre functions
50/// for all degrees and orders up to 14
51/// 
52/// # Arguments
53/// 
54/// * `mu` - The value to evaluate
55/// 
56/// # Reference
57/// 
58/// * Equation (5) in "The US/UK Workd Magnetic Model for 2020-2025"
59fn schmidt_semi_normalised_associated_legendre(mu: f64) -> [[f64; 14]; 14] {
60    let mut Psn = [[0.0; 14]; 14];
61    for n in 0..=13 {
62        for m in 0..=n {
63            if m == 0 {
64                Psn[n][m] = legendre_polynomial(mu, n, m);
65            } else {
66                Psn[n][m] = f64::sqrt(2.0 * factorial(n - m) / factorial(n + m)) * legendre_polynomial(mu, n, m);
67            }
68        }
69    }
70    Psn
71}
72
73pub enum Error {
74    DateOutsideOfValidityRange
75}
76
77impl std::fmt::Display for Error {
78    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
79        match self {
80            Error::DateOutsideOfValidityRange => write!(formatter, "Date outside of validity range!")
81        }
82    }
83}
84
85pub struct MagneticModel {
86    from_year: f64,
87    until_year: f64,
88    g: [[f64; 13]; 13],
89    h: [[f64; 13]; 13],
90    g_sv: [[f64; 13]; 13],
91    h_sv: [[f64; 13]; 13]
92}
93
94impl MagneticModel {
95    /// Calculate the geomagnetic field components at a given location and time
96    /// 
97    /// # Arguments
98    /// 
99    /// * `h` - The height above the WGS84 ellipsoid \[metres\]
100    /// * `phi` - The latitude \[degrees\]
101    /// * `lambda` - The longitude \[degrees\]
102    /// * `year` - The year \[decimal\] e.g. 2022.5
103    /// 
104    /// # Returns
105    /// 
106    /// * `X` - The northward intensity \[nT\]
107    /// * `Y` - The eastward intensity \[nT\]
108    /// * `Z` - The downward intensity \[nT\]
109    /// * `H` - The horizontal intensity \[nT\]
110    /// * `F` - The total intensity \[nT\]
111    /// * `I` - The inclination \[degrees\]
112    /// * `D` - The declination \[degrees\]
113    /// * `X_dot` - The rate of change of the northward intensity \[nT/year\]
114    /// * `Y_dot` - The rate of change of the eastward intensity \[nT/year\]
115    /// * `Z_dot` - The rate of change of the downward intensity \[nT/year\]
116    /// * `H_dot` - The rate of change of the horizontal intensity \[nT/year\]
117    /// * `F_dot` - The rate of change of the total intensity \[nT/year\]
118    /// * `I_dot` - The rate of change of the inclination \[degrees/year\]
119    /// * `D_dot` - The rate of change of the declination \[degrees/year\]
120    /// 
121    /// # Note
122    /// 
123    /// The conversion from height above MSL to height above the WGS 84 ellipsoid has a very small effect on the
124    /// resulting magnetic field values (of the order of 1 nT or less).
125    pub fn calculate(&self, h: f64, phi: f64, lambda: f64, year: f64) -> Result<(f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64), Error> {
126        // Translation of coordinates from geodetic to geocentric
127        let lambda = lambda.to_radians();
128        let phi = phi.to_radians();
129        let _R_c = A / f64::sqrt(1.0 - e_2 * f64::sin(phi)*f64::sin(phi)); // Equation (8)
130        let _p = (_R_c + h) * f64::cos(phi); // Equation (7)
131        let _z = (_R_c * (1.0 - e_2) + h) * f64::sin(phi); // Equation (7)
132        let r = f64::sqrt(_p*_p + _z*_z); // Equation (7)
133        let phi_prime = f64::asin(_z / r); // Equation (7)
134
135        let Psn_sinphi = schmidt_semi_normalised_associated_legendre(f64::sin(phi_prime));
136
137        let mut g_t = [[0.0; 13]; 13];
138        let mut h_t = [[0.0; 13]; 13];
139        let mut dPsn_sinphi_dphi = [[0.0; 13]; 13];
140        let time_delta = year - self.from_year;
141
142        if time_delta < 0.0 || time_delta > WMM_VALIDITY_RANGE {
143            return Result::Err(Error::DateOutsideOfValidityRange);
144        }
145
146        // Prepare matrices
147        for n in 0..=12 {
148            for m in 0..=n {
149                g_t[n][m] = self.g[n][m] + time_delta * self.g_sv[n][m]; // Equation (9): calculating Gauss coefficients for the desired time
150                h_t[n][m] = self.h[n][m] + time_delta * self.h_sv[n][m]; // Equation (9): calculating Gauss coefficients for the desired time
151
152                dPsn_sinphi_dphi[n][m] = (n as f64 + 1.0) * f64::tan(phi_prime) * Psn_sinphi[n][m] - // Equation (16): used to calculate the field vector components
153                    f64::sqrt((n as f64 + 1.0).powi(2) - (m as f64).powi(2)) / f64::cos(phi_prime) * Psn_sinphi[n+1][m];
154            }
155        }
156
157        // Field vector components
158        let mut X_prime = 0.0; // will be computed using Equation (10)
159        let mut Y_prime = 0.0; // will be computed using Equation (11)
160        let mut Z_prime = 0.0; // will be computed using Equation (12)
161        let mut X_dot_prime = 0.0; // will be computed using Equation (13)
162        let mut Y_dot_prime = 0.0; // will be computed using Equation (14)
163        let mut Z_dot_prime = 0.0; // will be computed using Equation (15)
164
165        for n in 1..=12 {
166            let mut X_prime_tmp = 0.0;
167            let mut Y_prime_tmp = 0.0;
168            let mut Z_prime_tmp = 0.0;
169            let mut X_dot_prime_tmp = 0.0;
170            let mut Y_dot_prime_tmp = 0.0;
171            let mut Z_dot_prime_tmp = 0.0;
172
173            for m in 0..=n {
174                X_prime_tmp += (g_t[n][m] * f64::cos(m as f64 * lambda) + h_t[n][m] * f64::sin(m as f64 * lambda)) * dPsn_sinphi_dphi[n][m];
175                Y_prime_tmp += m as f64 * (g_t[n][m] * f64::sin(m as f64 * lambda) - h_t[n][m] * f64::cos(m as f64 * lambda)) * Psn_sinphi[n][m];
176                Z_prime_tmp += (g_t[n][m] * f64::cos(m as f64 * lambda) + h_t[n][m] * f64::sin(m as f64 * lambda)) * Psn_sinphi[n][m];
177                X_dot_prime_tmp += (self.g_sv[n][m] * f64::cos(m as f64 * lambda) + self.h_sv[n][m] * f64::sin(m as f64 * lambda)) * dPsn_sinphi_dphi[n][m];
178                Y_dot_prime_tmp += m as f64 * (self.g_sv[n][m] * f64::sin(m as f64 * lambda) - self.h_sv[n][m] * f64::cos(m as f64 * lambda)) * Psn_sinphi[n][m];
179                Z_dot_prime_tmp += (self.g_sv[n][m] * f64::cos(m as f64 * lambda) + self.h_sv[n][m] * f64::sin(m as f64 * lambda)) * Psn_sinphi[n][m];
180            }
181            let k_temp = (a / r).powi(n as i32 + 2);
182            X_prime += k_temp * X_prime_tmp;
183            Y_prime += k_temp * Y_prime_tmp;
184            Z_prime += (n as f64 + 1.0) * k_temp * Z_prime_tmp;
185            X_dot_prime += k_temp * X_dot_prime_tmp;
186            Y_dot_prime += k_temp * Y_dot_prime_tmp;
187            Z_dot_prime += (n as f64 + 1.0) * k_temp * Z_dot_prime_tmp;
188        }
189        X_prime *= -1.0;
190        Y_prime *= 1.0 / f64::cos(phi_prime);
191        Z_prime *= -1.0;
192        X_dot_prime *= -1.0;
193        Y_dot_prime *= 1.0 / f64::cos(phi_prime);
194        Z_dot_prime *= -1.0;
195
196        // Rotate the field vector components to the ellipsoidal reference frame
197        let X = X_prime * f64::cos(phi_prime - phi) - Z_prime * f64::sin(phi_prime - phi);
198        let Y = Y_prime;
199        let Z = X_prime * f64::sin(phi_prime - phi) + Z_prime * f64::cos(phi_prime - phi);
200        let X_dot = X_dot_prime * f64::cos(phi_prime - phi) - Z_dot_prime * f64::sin(phi_prime - phi);
201        let Y_dot = Y_dot_prime;
202        let Z_dot = X_dot_prime * f64::sin(phi_prime - phi) + Z_dot_prime * f64::cos(phi_prime - phi);
203
204        // Compute the magnetic elements
205        let H = f64::sqrt(X*X + Y*Y);
206        let F = f64::sqrt(H*H + Z*Z);
207        let I = f64::atan2(Z, H);
208        let D = f64::atan2(Y, X);
209        let H_dot = (X*X_dot + Y*Y_dot) / H;
210        let F_dot = (X*X_dot + Y*Y_dot + Z*Z_dot) / F;
211        let I_dot = (Z_dot*H - Z*H_dot) / (F*F);
212        let D_dot = (Y_dot*X - Y*X_dot) / (H*H);
213
214        return Result::Ok (
215            (X, Y, Z, H, F, I.to_degrees(), D.to_degrees(),
216            X_dot, Y_dot, Z_dot, H_dot, F_dot, I_dot.to_degrees(), D_dot.to_degrees())
217        );
218    }
219}
220
221/// Function to initialise the magnetic model
222/// 
223/// # Arguments
224/// 
225/// * `path` - The path to the WMM model file
226pub fn initialise_magnetic_model(path: &str) -> MagneticModel {
227    // Read WMM model file
228    let model_file = fs::File::open(path).expect("Model file not found!");
229    let mut model_file = BufReader::new(model_file).lines();
230    
231    // Parse header line
232    let from_year = model_file
233        .next().expect("Model file is empty!")
234        .expect("Error reading model file!")
235        .split_whitespace()
236        .nth(0).expect("Error parsing model file!")
237        .parse::<f64>().expect("Error parsing model file!");
238
239    // Determine the validity range of the model
240    let until_year = from_year + WMM_VALIDITY_RANGE;
241
242    // Read the rest of the file
243    let mut g = [[0.0; 13]; 13];
244    let mut h = [[0.0; 13]; 13];
245    let mut g_sv = [[0.0; 13]; 13];
246    let mut h_sv = [[0.0; 13]; 13];
247    for line in model_file {
248        match line {
249            Ok(line) => {
250                if line.starts_with("9999") {
251                    break;
252                }
253
254                let mut line = line.split_whitespace();
255
256                let n_line: i32 = line.next().expect("Error parsing model file!").parse().expect("Error parsing model file!");
257                let m_line: i32 = line.next().expect("Error parsing model file!").parse().expect("Error parsing model file!");
258                let g_line: f64 = line.next().expect("Error parsing model file!").parse().expect("Error parsing model file!");
259                let h_line: f64 = line.next().expect("Error parsing model file!").parse().expect("Error parsing model file!");
260                let g_sv_line: f64 = line.next().expect("Error parsing model file!").parse().expect("Error parsing model file!");
261                let h_sv_line: f64 = line.next().expect("Error parsing model file!").parse().expect("Error parsing model file!");
262
263                if n_line > 12 {
264                    break;
265                }
266
267                if m_line > n_line || m_line < 0 {
268                    eprintln!("Corrupt record in model file!");
269                    process::exit(1);
270                }
271
272                g[n_line as usize][m_line as usize] = g_line;
273                g_sv[n_line as usize][m_line as usize] = g_sv_line;
274
275                if m_line != 0 {
276                    h[n_line as usize][m_line as usize] = h_line;
277                    h_sv[n_line as usize][m_line as usize] = h_sv_line;
278                }
279            },
280            Err(e) => {
281                eprintln!("Error reading model file: {:?}", e);
282            }
283        }
284    }
285
286    println!("Model is valid from {:04}-01-01 to {:04}-01-01", from_year, until_year);
287
288    MagneticModel {
289        from_year,
290        until_year,// geomg1
291        g,
292        h,
293        g_sv,
294        h_sv
295    }
296}
297
298
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    fn round_to_decimal_places(value: f64, decimal_places: i32) -> f64 {
305        let multiplier = 10.0_f64.powi(decimal_places);
306        (value * multiplier).round() / multiplier
307    }
308
309    /// Test function
310    /// 
311    /// # Arguments
312    /// 
313    /// * `year` - The year
314    /// * `h` - The height above the WGS84 ellipsoid [km]
315    /// * `phi` - The latitude \[degrees\]
316    /// * `l` - The longitude \[degrees\]
317    /// * `expected_X` - The expected northward intensity \[nT\]
318    /// * `expected_Y` - The expected eastward intensity \[nT\]
319    /// * `expected_Z` - The expected downward intensity \[nT\]
320    /// * `expected_H` - The expected horizontal intensity \[nT\]
321    /// * `expected_F` - The expected total intensity \[nT\]
322    /// * `expected_I` - The expected inclination \[degrees\]
323    /// * `expected_D` - The expected declination \[degrees\]
324    /// * `expected_GV` - The expected grid variation \[degrees\]
325    /// * `expected_X_dot` - The expected rate of change of the northward intensity \[nT/year\]
326    /// * `expected_Y_dot` - The expected rate of change of the eastward intensity \[nT/year\]
327    /// * `expected_Z_dot` - The expected rate of change of the downward intensity \[nT/year\]
328    /// * `expected_H_dot` - The expected rate of change of the horizontal intensity \[nT/year\]
329    /// * `expected_F_dot` - The expected rate of change of the total intensity \[nT/year\]
330    /// * `expected_I_dot` - The expected rate of change of the inclination \[degrees/year\]
331    /// * `expected_D_dot` - The expected rate of change of the declination \[degrees/year\]
332    /// 
333    /// # Note
334    /// 
335    /// The grid variation is not tested as it is not calculated in the model (yet)
336    fn test(year: f64, h: f64, phi: f64, l: f64,
337            expected_X: f64, expected_Y: f64, expected_Z: f64, expected_H: f64, expected_F: f64, expected_I: f64, expected_D: f64, expected_GV: f64,
338            expected_X_dot: f64, expected_Y_dot: f64, expected_Z_dot: f64, expected_H_dot: f64, expected_F_dot: f64, expected_I_dot: f64, expected_D_dot: f64) {
339        let magnetic_model = initialise_magnetic_model("WMM.2020.COF");
340        let h = h * 1000.0;
341        let result = magnetic_model.calculate(h, phi, l, year);
342
343        match result {
344            Ok(results) => {
345                assert_eq!(round_to_decimal_places(results.0, 1), expected_X);
346                assert_eq!(round_to_decimal_places(results.1, 1), expected_Y);
347                assert_eq!(round_to_decimal_places(results.2, 1), expected_Z);
348                assert_eq!(round_to_decimal_places(results.3, 1), expected_H);
349                assert_eq!(round_to_decimal_places(results.4, 1), expected_F);
350                assert_eq!(round_to_decimal_places(results.5, 2), expected_I);
351                assert_eq!(round_to_decimal_places(results.6, 2), expected_D);
352                assert_eq!(round_to_decimal_places(results.7, 1), expected_X_dot);
353                assert_eq!(round_to_decimal_places(results.8, 1), expected_Y_dot);
354                assert_eq!(round_to_decimal_places(results.9, 1), expected_Z_dot);
355                assert_eq!(round_to_decimal_places(results.10, 1), expected_H_dot);
356                assert_eq!(round_to_decimal_places(results.11, 1), expected_F_dot);
357                assert!(f64::abs(results.12 - expected_I_dot) <= 0.01);
358                assert!(f64::abs(results.13 - expected_D_dot) <= 0.01);
359            },
360            Err(e) => {
361                eprintln!("Error: {}", e);
362                process::exit(1);
363            }
364        }
365    }
366
367    #[test]
368    fn wmm2020_case1() {
369        test(2020.0, 0.0, 80.0, 0.0,
370            6570.4, -146.3, 54606.0, 6572.0, 55000.1, 83.14, -1.28, -1.28,
371            -16.2, 59.0, 42.9, -17.5, 40.5, 0.02, 0.51
372            );
373    }
374
375    #[test]
376    fn wmm2020_case2() {
377        test(2020.0, 0.0, 0.0, 120.0, 39624.3, 109.9, -10932.5, 39624.4, 41104.9, -15.42, 0.16, 020.0, 24.2,  -60.8,  49.2,  24.0,  10.1,  0.08,  -0.09);
378    }
379
380    #[test]
381    fn wmm2020_case3() {
382        test(2020.0, 0.0, -80.0, 240.0, 5940.6, 15772.1, -52480.8, 16853.8, 55120.6, -72.20, 69.36, -50.64, 30.4,  1.8,  91.7,  12.4,  -83.5,  0.04,  -0.10);
383    }
384
385    #[test]
386    fn wmm2020_case4() {
387        test(2020.0, 100.0, 80.0, 0.0, 6261.8, -185.5, 52429.1, 6264.5, 52802.0, 83.19, -1.70, -1.70, -15.1,  56.4,  39.2,  -16.8,  36.9,  0.02,  0.51);
388    }
389
390    #[test]
391    fn wmm2020_case5() {
392        test(2020.0, 100.0, 0.0, 120.0, 37636.7, 104.9, -10474.8, 37636.9, 39067.3, -15.55, 0.16, 0.0, 22.9,  -56.1,  45.1,  22.8,  9.8,  0.07,  -0.09);
393    }
394
395    #[test]
396    fn wmm2020_case6() {
397        test(2020.0, 100.0, -80.0, 240.0, 5744.9, 14799.5, -49969.4, 15875.4, 52430.6, -72.37, 68.78, -51.22,  28.0,  1.4,  85.6,  11.4,  -78.1,  0.04,  -0.09);
398    }
399
400    #[test]
401    fn wmm2020_case7() {
402        test(2022.5, 0.0, 80.0, 0.0, 6529.9, 1.1, 54713.4, 6529.9, 55101.7, 83.19, 0.01, 0.01, -16.2,  59.0,  42.9,  -16.2,  40.7,  0.02,  0.52);
403    }
404
405    #[test]
406    fn wmm2020_case8() {
407        test(2022.5, 0.0, 0.0, 120.0, 39684.7, -42.2, -10809.5, 39684.7, 41130.5, -15.24, -0.06, 0.0, 24.2,  -60.8,  49.2,  24.2,  10.5,  0.08,  -0.09);
408    }
409
410    #[test]
411    fn wmm2020_case9() {
412        test(2022.5, 0.0, -80.0, 240.0, 6016.5, 15776.7, -52251.6, 16885.0, 54912.1, -72.09, 69.13, -50.87, 30.4,  1.8,  91.7,  12.6,  -83.4,  0.04,  -0.09);
413    }
414
415    #[test]
416    fn wmm2020_case10() {
417        test(2022.5, 100.0, 80.0, 0.0, 6224.0, -44.5, 52527.0, 6224.2, 52894.5, 83.24, -0.41, -0.41, -15.1,  56.4,  39.2,  -15.5,  37.1,  0.02,  0.52);
418    }
419
420    #[test]
421    fn wmm2020_case11() {
422        test(2022.5, 100.0, 0.0, 120.0, 37694.0, -35.3, -10362.0, 37694.1, 39092.4, -15.37, -0.05, 0.0, 22.9,  -56.1,  45.1,  23.0,  10.2,  0.07,  -0.09);
423    }
424
425    #[test]
426    fn wmm2020_case12() {
427        test(2022.5, 100.0, -80.0, 240.0, 5815.0, 14803.0, -49755.3, 15904.1, 52235.4, -72.27, 68.55, -51.47,  28.0,  1.4,  85.6,  11.6,  -78.0,  0.04,  -0.09);
428    }
429}