]> git.mxchange.org Git - simgear.git/blob - simgear/magvar/coremag.cxx
Display a warning message for Irix users.
[simgear.git] / simgear / magvar / coremag.cxx
1 // coremag.cxx -- compute local magnetic variation given position,
2 //                altitude, and date
3 //
4 // This is an implementation of the NIMA (formerly DMA) WMM2000
5 //
6 //    http://www.nima.mil/GandG/ngdc-wmm2000.html
7 //
8 // Copyright (C) 2000  Edward A Williams <Ed_Williams@compuserve.com>
9 //
10 // Adapted from Excel 3.0 version 3/27/94 EAW
11 // Recoded in C++ by Starry Chan
12 // WMM95 added and rearranged in ANSI-C EAW 7/9/95
13 // Put shell around program and made Borland & GCC compatible EAW 11/22/95
14 // IGRF95 added 2/96 EAW
15 // WMM2000 IGR2000 added 2/00 EAW
16 // Released under GPL 3/26/00 EAW
17 // Adaptions and modifications for the SimGear project  3/27/2000 CLO
18 //
19 // Removed all pow() calls and made static roots[][] arrays to
20 // save many sqrt() calls on subsequent invocations
21 // left old code as SGMagVarOrig() for testing purposes
22 // 3/28/2000  Norman Vine -- nhv@yahoo.com
23 //
24 // Put in some bullet-proofing to handle magnetic and geographic poles.
25 // 3/28/2000 EAW
26
27 //  The routine uses a spherical harmonic expansion of the magnetic
28 // potential up to twelfth order, together with its time variation, as
29 // described in Chapter 4 of "Geomagnetism, Vol 1, Ed. J.A.Jacobs,
30 // Academic Press (London 1987)". The program first converts geodetic
31 // coordinates (lat/long on elliptic earth and altitude) to spherical
32 // geocentric (spherical lat/long and radius) coordinates. Using this,
33 // the spherical (B_r, B_theta, B_phi) magnetic field components are
34 // computed from the model. These are finally referred to surface (X, Y,
35 // Z) coordinates.
36 //
37 //   Fields are accurate to better than 200nT, variation and dip to
38 // better than 0.5 degrees, with the exception of the declination near
39 // the magnetic poles (where it is ill-defined) where the error may reach
40 // 4 degrees or more.
41 //
42 //   Variation is undefined at both the geographic and  
43 // magnetic poles, even though the field itself is well-behaved. To
44 // avoid the routine blowing up, latitude entries corresponding to
45 // the geographic poles are slightly offset. At the magnetic poles,
46 // the routine returns zero variation.
47
48
49 //
50 // This library is free software; you can redistribute it and/or
51 // modify it under the terms of the GNU Library General Public
52 // License as published by the Free Software Foundation; either
53 // version 2 of the License, or (at your option) any later version.
54 //
55 // This library is distributed in the hope that it will be useful,
56 // but WITHOUT ANY WARRANTY; without even the implied warranty of
57 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
58 // Library General Public License for more details.
59 //
60 // You should have received a copy of the GNU Library General Public
61 // License along with this library; if not, write to the
62 // Free Software Foundation, Inc., 59 Temple Place - Suite 330,
63 // Boston, MA  02111-1307, USA.
64 //
65 // $Id$
66
67
68 #include <stdio.h>
69 #include <stdlib.h>
70 #include <math.h>
71
72 #include <simgear/constants.h>
73 #include <simgear/sg_inlines.h>
74
75 #include "coremag.hxx"
76
77
78 static const double pi = 3.14159265358979;
79 static const double a = 6378.16;        /* major radius (km) IAU66 ellipsoid */
80 static const double f = 1.0 / 298.25;   /* inverse flattening IAU66 ellipsoid */
81 static const double b = 6378.16 * (1.0 -1.0 / 298.25 );
82 /* minor radius b=a*(1-f) */
83 static const double r_0 = 6371.2;       /* "mean radius" for spherical harmonic expansion */
84
85 static double gnm_wmm2000[13][13] =
86 {
87     {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
88     {-29616.0, -1722.7, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
89     {-2266.7, 3070.2, 1677.6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
90     {1322.4, -2291.5, 1255.9, 724.8, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
91     {932.1, 786.3, 250.6, -401.5, 106.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
92     {-211.9, 351.6, 220.8, -134.5, -168.8, -13.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
93     {73.8, 68.2, 74.1, -163.5, -3.8, 17.1, -85.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
94     {77.4, -73.9, 2.2, 35.7, 7.3, 5.2, 8.4, -1.5, 0.0, 0.0, 0.0, 0.0, 0.0},
95     {23.3, 7.3, -8.5, -6.6, -16.9, 8.6, 4.9, -7.8, -7.6, 0.0, 0.0, 0.0, 0.0},
96     {5.7, 8.5, 2.0, -9.8, 7.6, -7.0, -2.0, 9.2, -2.2, -6.6, 0.0, 0.0, 0.0},
97     {-2.2, -5.7, 1.6, -3.7, -0.6, 4.1, 2.2, 2.2, 4.6, 2.3, 0.1, 0.0, 0.0},
98     {3.3, -1.1, -2.4, 2.6, -1.3, -1.7, -0.6, 0.4, 0.7, -0.3, 2.3, 4.2, 0.0},
99     {-1.5, -0.2, -0.3, 0.5, 0.2, 0.9, -1.4, 0.6, -0.6, -1.0, -0.3, 0.3, 0.4},
100 };
101
102 static double hnm_wmm2000[13][13]=
103 {
104     {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
105     {0.0, 5194.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
106     {0.0, -2484.8, -467.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
107     {0.0, -224.7, 293.0, -486.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
108     {0.0, 273.3, -227.9, 120.9, -302.7, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
109     {0.0, 42.0, 173.8, -135.0, -38.6, 105.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
110     {0.0, -17.4, 61.2, 63.2, -62.9, 0.2, 43.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
111     {0.0, -62.3, -24.5, 8.9, 23.4, 15.0, -27.6, -7.8, 0.0, 0.0, 0.0, 0.0, 0.0},
112     {0.0, 12.4, -20.8, 8.4, -21.2, 15.5, 9.1, -15.5, -5.4, 0.0, 0.0, 0.0, 0.0},
113     {0.0, -20.4, 13.9, 12.0, -6.2, -8.6, 9.4, 5.0, -8.4, 3.2, 0.0, 0.0, 0.0},
114     {0.0, 0.9, -0.7, 3.9, 4.8, -5.3, -1.0, -2.4, 1.3, -2.3, -6.4, 0.0, 0.0},
115     {0.0, -1.5, 0.7, -1.1, -2.3, 1.3, -0.6, -2.8, -1.6, -0.1, -1.9, 1.4, 0.0},
116     {0.0, -1.0, 0.7, 2.2, -2.5, -0.2, 0.0, -0.2, 0.0, 0.2, -0.9, -0.2, 1.0},
117 };
118
119 static double gtnm_wmm2000[13][13]=
120 {
121     {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
122     {14.7, 11.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
123     {-13.6, -0.7, -1.8, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
124     {0.3, -4.3, 0.9, -8.4, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
125     {-1.6, 0.9, -7.6, 2.2, -3.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
126     {-0.9, -0.2, -2.5, -2.7, -0.9, 1.7, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
127     {1.2, 0.2, 1.7, 1.6, -0.1, -0.3, 0.8, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
128     {-0.4, -0.8, -0.2, 1.1, 0.4, 0.0, -0.2, -0.2, 0.0, 0.0, 0.0, 0.0, 0.0},
129     {-0.3, 0.6, -0.8, 0.3, -0.2, 0.5, 0.0, -0.6, 0.1, 0.0, 0.0, 0.0, 0.0},
130     {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
131     {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
132     {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
133     {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
134 };
135
136 static double htnm_wmm2000[13][13]=
137 {
138     {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
139     {0.0, -20.4, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
140     {0.0, -21.5, -9.6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
141     {0.0, 6.4, -1.3, -13.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
142     {0.0, 2.3, 0.7, 3.7, -0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
143     {0.0, 0.0, 2.1, 2.3, 3.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
144     {0.0, -0.3, -1.7, -0.9, -1.0, -0.1, 1.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
145     {0.0, 1.4, 0.2, 0.7, 0.4, -0.3, -0.8, -0.1, 0.0, 0.0, 0.0, 0.0, 0.0},
146     {0.0, -0.5, 0.1, -0.2, 0.0, 0.1, -0.1, 0.3, 0.2, 0.0, 0.0, 0.0, 0.0},
147     {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
148     {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
149     {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
150     {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
151 };
152
153 static const int nmax = 12;
154
155 static double P[13][13];
156 static double DP[13][13];
157 static double gnm[13][13];
158 static double hnm[13][13];
159 static double sm[13];
160 static double cm[13];
161
162 static double root[13];
163 static double roots[13][13][2];
164
165 /* Convert date to Julian day    1950-2049 */
166 unsigned long int yymmdd_to_julian_days( int yy, int mm, int dd )
167 {
168     unsigned long jd;
169  
170     yy = (yy < 50) ? (2000 + yy) : (1900 + yy);
171     jd = dd - 32075L + 1461L * (yy + 4800L + (mm - 14) / 12 ) / 4;
172     jd = jd + 367L * (mm - 2 - (mm - 14) / 12*12) / 12;
173     jd = jd - 3 * ((yy + 4900L + (mm - 14) / 12) / 100) / 4;
174
175     /* printf("julian date = %d\n", jd ); */
176     return jd;
177
178
179
180 /*
181  * return variation (in radians) given geodetic latitude (radians),
182  * longitude(radians), height (km) and (Julian) date
183  * N and E lat and long are positive, S and W negative
184 */
185
186 double calc_magvar( double lat, double lon, double h, long dat, double* field )
187 {
188     /* output field B_r,B_th,B_phi,B_x,B_y,B_z */
189     int n,m;
190     /* reference dates */
191     long date0_wmm2000 = yymmdd_to_julian_days(0,1,1);
192
193     double yearfrac,sr,r,theta,c,s,psi,fn,fn_0,B_r,B_theta,B_phi,X,Y,Z;
194     double sinpsi, cospsi, inv_s;
195
196     static int been_here = 0;
197
198     double sinlat = sin(lat);
199     double coslat = cos(lat);
200
201     /* convert to geocentric coords: */
202     // sr = sqrt(pow(a*coslat,2.0)+pow(b*sinlat,2.0));
203     sr = sqrt(a*a*coslat*coslat + b*b*sinlat*sinlat);
204     /* sr is effective radius */
205     theta = atan2(coslat * (h*sr + a*a),
206                   sinlat * (h*sr + b*b));
207     /* theta is geocentric co-latitude */
208
209     r = h*h + 2.0*h * sr +
210         (a*a*a*a - ( a*a*a*a - b*b*b*b ) * sinlat*sinlat ) / 
211         (a*a - (a*a - b*b) * sinlat*sinlat );
212
213     r = sqrt(r);
214
215     /* r is geocentric radial distance */
216     c = cos(theta);
217     s = sin(theta);
218     /* protect against zero divide at geographic poles */
219     inv_s =  1.0 / (s + (s == 0.)*1.0e-8); 
220
221     /* zero out arrays */
222     for ( n = 0; n <= nmax; n++ ) {
223         for ( m = 0; m <= n; m++ ) {
224             P[n][m] = 0;
225             DP[n][m] = 0;
226         }
227     }
228
229     /* diagonal elements */
230     P[0][0] = 1;
231     P[1][1] = s;
232     DP[0][0] = 0;
233     DP[1][1] = c;
234     P[1][0] = c ;
235     DP[1][0] = -s;
236
237     // these values will not change for subsequent function calls
238     if( !been_here ) {
239         for ( n = 2; n <= nmax; n++ ) {
240             root[n] = sqrt((2.0*n-1) / (2.0*n));
241         }
242
243         for ( m = 0; m <= nmax; m++ ) {
244             double mm = m*m;
245             for ( n = SG_MAX2(m + 1, 2); n <= nmax; n++ ) {
246                 roots[m][n][0] = sqrt((n-1)*(n-1) - mm);
247                 roots[m][n][1] = 1.0 / sqrt( n*n - mm);
248             }
249         }
250         been_here = 1;
251     }
252
253     for ( n=2; n <= nmax; n++ ) {
254         // double root = sqrt((2.0*n-1) / (2.0*n));
255         P[n][n] = P[n-1][n-1] * s * root[n];
256         DP[n][n] = (DP[n-1][n-1] * s + P[n-1][n-1] * c) *
257             root[n];
258     }
259
260     /* lower triangle */
261     for ( m = 0; m <= nmax; m++ ) {
262         // double mm = m*m;
263         for ( n = SG_MAX2(m + 1, 2); n <= nmax; n++ ) {
264             // double root1 = sqrt((n-1)*(n-1) - mm);
265             // double root2 = 1.0 / sqrt( n*n - mm);
266             P[n][m] = (P[n-1][m] * c * (2.0*n-1) -
267                        P[n-2][m] * roots[m][n][0]) *
268                 roots[m][n][1];
269
270             DP[n][m] = ((DP[n-1][m] * c - P[n-1][m] * s) *
271                         (2.0*n-1) - DP[n-2][m] * roots[m][n][0]) *
272                 roots[m][n][1];
273         }
274     }
275
276     /* compute gnm, hnm at dat */
277     /* WMM2000 */
278     yearfrac = (dat - date0_wmm2000) / 365.25;
279     for ( n = 1; n <= nmax; n++ ) {
280         for ( m = 0; m <= nmax; m++ ) {
281             gnm[n][m] = gnm_wmm2000[n][m] + yearfrac * gtnm_wmm2000[n][m];
282             hnm[n][m] = hnm_wmm2000[n][m] + yearfrac * htnm_wmm2000[n][m];
283         }
284     }
285
286     /* compute sm (sin(m lon) and cm (cos(m lon)) */
287     for ( m = 0; m <= nmax; m++ ) {
288         sm[m] = sin(m * lon);
289         cm[m] = cos(m * lon);
290     }
291
292     /* compute B fields */
293     B_r = 0.0;
294     B_theta = 0.0;
295     B_phi = 0.0;
296     fn_0 = r_0/r;
297     fn = fn_0 * fn_0;
298
299     for ( n = 1; n <= nmax; n++ ) {
300         double c1_n=0;
301         double c2_n=0;
302         double c3_n=0;
303         for ( m = 0; m <= n; m++ ) {
304             double tmp = (gnm[n][m] * cm[m] + hnm[n][m] * sm[m]); 
305             c1_n=c1_n + tmp * P[n][m];
306             c2_n=c2_n + tmp * DP[n][m];
307             c3_n=c3_n + m * (gnm[n][m] * sm[m] - hnm[n][m] * cm[m]) * P[n][m];
308         }
309         // fn=pow(r_0/r,n+2.0);
310         fn *= fn_0;
311         B_r = B_r + (n + 1) * c1_n * fn;
312         B_theta = B_theta - c2_n * fn;
313         B_phi = B_phi + c3_n * fn * inv_s;
314     }
315
316     /* Find geodetic field components: */
317     psi = theta - ((pi / 2.0) - lat);
318     sinpsi = sin(psi);
319     cospsi = cos(psi);
320     X = -B_theta * cospsi - B_r * sinpsi;
321     Y = B_phi;
322     Z = B_theta * sinpsi - B_r * cospsi;
323
324     field[0]=B_r;
325     field[1]=B_theta;
326     field[2]=B_phi;
327     field[3]=X;
328     field[4]=Y;
329     field[5]=Z;   /* output fields */
330
331     /* find variation in radians */
332     /* return zero variation at magnetic pole X=Y=0. */
333     /* E is positive */
334     return (X != 0. || Y != 0.) ? atan2(Y, X) : (double) 0.;  
335 }
336
337
338 #ifdef TEST_NHV_HACKS
339 double SGMagVarOrig( double lat, double lon, double h, long dat, double* field )
340 {
341     /* output field B_r,B_th,B_phi,B_x,B_y,B_z */
342     int n,m;
343     /* reference dates */
344     long date0_wmm2000 = yymmdd_to_julian_days(0,1,1);
345
346     double yearfrac,sr,r,theta,c,s,psi,fn,B_r,B_theta,B_phi,X,Y,Z;
347
348     /* convert to geocentric coords: */
349     sr = sqrt(pow(a*cos(lat),2.0)+pow(b*sin(lat),2.0));
350     /* sr is effective radius */
351     theta = atan2(cos(lat) * (h * sr + a * a),
352                   sin(lat) * (h * sr + b * b));
353     /* theta is geocentric co-latitude */
354
355     r = h * h + 2.0*h * sr +
356         (pow(a,4.0) - (pow(a,4.0) - pow(b,4.0)) * pow(sin(lat),2.0)) / 
357         (a * a - (a * a - b * b) * pow(sin(lat),2.0));
358
359     r = sqrt(r);
360
361     /* r is geocentric radial distance */
362     c = cos(theta);
363     s = sin(theta);
364
365     /* zero out arrays */
366     for ( n = 0; n <= nmax; n++ ) {
367         for ( m = 0; m <= n; m++ ) {
368             P[n][m] = 0;
369             DP[n][m] = 0;
370         }
371     }
372
373     /* diagonal elements */
374     P[0][0] = 1;
375     P[1][1] = s;
376     DP[0][0] = 0;
377     DP[1][1] = c;
378     P[1][0] = c ;
379     DP[1][0] = -s;
380
381     for ( n = 2; n <= nmax; n++ ) {
382         P[n][n] = P[n-1][n-1] * s * sqrt((2.0*n-1) / (2.0*n));
383         DP[n][n] = (DP[n-1][n-1] * s + P[n-1][n-1] * c) *
384             sqrt((2.0*n-1) / (2.0*n));
385     }
386
387     /* lower triangle */
388     for ( m = 0; m <= nmax; m++ ) {
389         for ( n = SG_MAX2(m + 1, 2); n <= nmax; n++ ) {
390             P[n][m] = (P[n-1][m] * c * (2.0*n-1) - P[n-2][m] *
391                        sqrt(1.0*(n-1)*(n-1) - m * m)) /
392                 sqrt(1.0* n * n - m * m);
393             DP[n][m] = ((DP[n-1][m] * c - P[n-1][m] * s) *
394                         (2.0*n-1) - DP[n-2][m] *
395                         sqrt(1.0*(n-1) * (n-1) - m * m)) /
396                 sqrt(1.0* n * n - m * m);
397         }
398     }
399
400     /* compute gnm, hnm at dat */
401     /* WMM2000 */
402     yearfrac = (dat - date0_wmm2000) / 365.25;
403     for ( n = 1; n <= nmax; n++ ) {
404         for ( m = 0; m <= nmax; m++ ) {
405             gnm[n][m] = gnm_wmm2000[n][m] + yearfrac * gtnm_wmm2000[n][m];
406             hnm[n][m] = hnm_wmm2000[n][m] + yearfrac * htnm_wmm2000[n][m];
407         }
408     }
409
410     /* compute sm (sin(m lon) and cm (cos(m lon)) */
411     for ( m = 0; m <= nmax; m++ ) {
412         sm[m] = sin(m * lon);
413         cm[m] = cos(m * lon);
414     }
415
416     /* compute B fields */
417     B_r = 0.0;
418     B_theta = 0.0;
419     B_phi = 0.0;
420
421     for ( n = 1; n <= nmax; n++ ) {
422         double c1_n=0;
423         double c2_n=0;
424         double c3_n=0;
425         for ( m = 0; m <= n; m++ ) {
426             c1_n=c1_n + (gnm[n][m] * cm[m] + hnm[n][m] * sm[m]) * P[n][m];
427             c2_n=c2_n + (gnm[n][m] * cm[m] + hnm[n][m] * sm[m]) * DP[n][m];
428             c3_n=c3_n + m * (gnm[n][m] * sm[m] - hnm[n][m] * cm[m]) * P[n][m];
429         }
430         fn=pow(r_0/r,n+2.0);
431         B_r = B_r + (n + 1) * c1_n * fn;
432         B_theta = B_theta - c2_n * fn;
433         B_phi = B_phi + c3_n * fn / s;
434     }
435
436     /* Find geodetic field components: */
437     psi = theta - (pi / 2.0 - lat);
438     X = -B_theta * cos(psi) - B_r * sin(psi);
439     Y = B_phi;
440     Z = B_theta * sin(psi) - B_r * cos(psi);
441
442     field[0]=B_r;
443     field[1]=B_theta;
444     field[2]=B_phi;
445     field[3]=X;
446     field[4]=Y;
447     field[5]=Z;   /* output fields */
448
449     /* find variation, leave in radians! */
450     return atan2(Y, X);  /* E is positive */
451 }
452 #endif // TEST_NHV_HACKS