GeoMagneticField.java

  1. /* Copyright 2011-2012 Space Applications Services
  2.  * Licensed to CS Communication & Systèmes (CS) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * CS licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *   http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.orekit.models.earth;

  18. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  19. import org.hipparchus.util.FastMath;
  20. import org.orekit.bodies.GeodeticPoint;
  21. import org.orekit.errors.OrekitException;
  22. import org.orekit.errors.OrekitMessages;
  23. import org.orekit.utils.Constants;

  24. /** Used to calculate the geomagnetic field at a given geodetic point on earth.
  25.  * The calculation is estimated using spherical harmonic expansion of the
  26.  * geomagnetic potential with coefficients provided by an actual geomagnetic
  27.  * field model (e.g. IGRF, WMM).
  28.  * <p>
  29.  * Based on original software written by Manoj Nair from the National
  30.  * Geophysical Data Center, NOAA, as part of the WMM 2010 software release
  31.  * (WMM_SubLibrary.c)
  32.  * </p>
  33.  * @see <a href="http://www.ngdc.noaa.gov/geomag/WMM/DoDWMM.shtml">World Magnetic Model Overview</a>
  34.  * @see <a href="http://www.ngdc.noaa.gov/geomag/WMM/soft.shtml">WMM Software Downloads</a>
  35.  * @author Thomas Neidhart
  36.  */
  37. public class GeoMagneticField {

  38.     /** Semi major-axis of WGS-84 ellipsoid in km. */
  39.     private static double a = Constants.WGS84_EARTH_EQUATORIAL_RADIUS / 1000d;

  40.     /** The first eccentricity squared. */
  41.     private static double epssq = 0.0066943799901413169961;

  42.     /** Mean radius of IAU-66 ellipsoid, in km. */
  43.     private static double ellipsoidRadius = 6371.2;

  44.     /** The model name. */
  45.     private String modelName;

  46.     /** Base time of magnetic field model epoch (yrs). */
  47.     private double epoch;

  48.     /** C - Gauss coefficients of main geomagnetic model (nT). */
  49.     private double[] g;

  50.     /** C - Gauss coefficients of main geomagnetic model (nT). */
  51.     private double[] h;

  52.     /** CD - Gauss coefficients of secular geomagnetic model (nT/yr). */
  53.     private double[] dg;

  54.     /** CD - Gauss coefficients of secular geomagnetic model (nT/yr). */
  55.     private double[] dh;

  56.     /** maximum degree of spherical harmonic model. */
  57.     private int maxN;

  58.     /** maximum degree of spherical harmonic secular variations. */
  59.     private int maxNSec;

  60.     /** the validity start of this magnetic field model. */
  61.     private double validityStart;
  62.     /** the validity end of this magnetic field model. */
  63.     private double validityEnd;

  64.     /** Pre-calculated ratio between gauss-normalized and schmidt quasi-normalized
  65.      * associated Legendre functions.
  66.      */
  67.     private double[] schmidtQuasiNorm;

  68.     /** Create a new geomagnetic field model with the given parameters. Internal
  69.      * structures are initialized according to the specified degrees of the main
  70.      * and secular variations.
  71.      * @param modelName the model name
  72.      * @param epoch the epoch of the model
  73.      * @param maxN the maximum degree of the main model
  74.      * @param maxNSec the maximum degree of the secular variations
  75.      * @param validityStart validity start of this model
  76.      * @param validityEnd validity end of this model
  77.      */
  78.     protected GeoMagneticField(final String modelName, final double epoch,
  79.                                final int maxN, final int maxNSec,
  80.                                final double validityStart, final double validityEnd) {

  81.         this.modelName = modelName;
  82.         this.epoch = epoch;
  83.         this.maxN = maxN;
  84.         this.maxNSec = maxNSec;

  85.         this.validityStart = validityStart;
  86.         this.validityEnd = validityEnd;

  87.         // initialize main and secular field coefficient arrays
  88.         final int maxMainFieldTerms = (maxN + 1) * (maxN + 2) / 2;
  89.         g = new double[maxMainFieldTerms];
  90.         h = new double[maxMainFieldTerms];

  91.         final int maxSecularFieldTerms = (maxNSec + 1) * (maxNSec + 2) / 2;
  92.         dg = new double[maxSecularFieldTerms];
  93.         dh = new double[maxSecularFieldTerms];

  94.         // pre-calculate the ratio between gauss-normalized and schmidt quasi-normalized
  95.         // associated Legendre functions as they depend only on the degree of the model.

  96.         schmidtQuasiNorm = new double[maxMainFieldTerms + 1];
  97.         schmidtQuasiNorm[0] = 1.0;

  98.         int index;
  99.         int index1;
  100.         for (int n = 1; n <= maxN; n++) {
  101.             index = n * (n + 1) / 2;
  102.             index1 = (n - 1) * n / 2;

  103.             // for m = 0
  104.             schmidtQuasiNorm[index] =
  105.                 schmidtQuasiNorm[index1] * (double) (2 * n - 1) / (double) n;

  106.             for (int m = 1; m <= n; m++) {
  107.                 index = n * (n + 1) / 2 + m;
  108.                 index1 = n * (n + 1) / 2 + m - 1;
  109.                 schmidtQuasiNorm[index] =
  110.                     schmidtQuasiNorm[index1] *
  111.                     FastMath.sqrt((double) ((n - m + 1) * (m == 1 ? 2 : 1)) / (double) (n + m));
  112.             }
  113.         }
  114.     }

  115.     /** Returns the epoch for this magnetic field model.
  116.      * @return the epoch
  117.      */
  118.     public double getEpoch() {
  119.         return epoch;
  120.     }

  121.     /** Returns the model name.
  122.      * @return the model name
  123.      */
  124.     public String getModelName() {
  125.         return modelName;
  126.     }

  127.     /** Returns the start of the validity period for this model.
  128.      * @return the validity start as decimal year
  129.      */
  130.     public double validFrom() {
  131.         return validityStart;
  132.     }

  133.     /** Returns the end of the validity period for this model.
  134.      * @return the validity end as decimal year
  135.      */
  136.     public double validTo() {
  137.         return validityEnd;
  138.     }

  139.     /** Indicates whether this model supports time transformation or not.
  140.      * @return <code>true</code> if this model can be transformed within its
  141.      *         validity period, <code>false</code> otherwise
  142.      */
  143.     public boolean supportsTimeTransform() {
  144.         return maxNSec > 0;
  145.     }

  146.     /** Set the given main field coefficients.
  147.      * @param n the n index
  148.      * @param m the m index
  149.      * @param gnm the g coefficient at position n,m
  150.      * @param hnm the h coefficient at position n,m
  151.      */
  152.     protected void setMainFieldCoefficients(final int n, final int m,
  153.                                          final double gnm, final double hnm) {
  154.         final int index = n * (n + 1) / 2 + m;
  155.         g[index] = gnm;
  156.         h[index] = hnm;
  157.     }

  158.     /** Set the given secular variation coefficients.
  159.      * @param n the n index
  160.      * @param m the m index
  161.      * @param dgnm the dg coefficient at position n,m
  162.      * @param dhnm the dh coefficient at position n,m
  163.      */
  164.     protected void setSecularVariationCoefficients(final int n, final int m,
  165.                                                 final double dgnm, final double dhnm) {
  166.         final int index = n * (n + 1) / 2 + m;
  167.         dg[index] = dgnm;
  168.         dh[index] = dhnm;
  169.     }

  170.     /** Calculate the magnetic field at the specified geodetic point identified
  171.      * by latitude, longitude and altitude.
  172.      * @param latitude the WGS84 latitude in decimal degrees
  173.      * @param longitude the WGS84 longitude in decimal degrees
  174.      * @param height the height above the WGS84 ellipsoid in kilometers
  175.      * @return the {@link GeoMagneticElements} at the given geodetic point
  176.      */
  177.     public GeoMagneticElements calculateField(final double latitude,
  178.                                               final double longitude,
  179.                                               final double height) {

  180.         final GeodeticPoint gp = new GeodeticPoint(FastMath.toRadians(latitude),
  181.                                                    FastMath.toRadians(longitude),
  182.                                                    height * 1000d);

  183.         final SphericalCoordinates sph = transformToSpherical(gp);
  184.         final SphericalHarmonicVars vars = new SphericalHarmonicVars(sph);
  185.         final LegendreFunction legendre = new LegendreFunction(FastMath.sin(sph.phi));

  186.         // sum up the magnetic field vector components
  187.         final Vector3D magFieldSph = summation(sph, vars, legendre);
  188.         // rotate the field to geodetic coordinates
  189.         final Vector3D magFieldGeo = rotateMagneticVector(sph, gp, magFieldSph);
  190.         // return the magnetic elements
  191.         return new GeoMagneticElements(magFieldGeo);
  192.     }

  193.     /** Time transform the model coefficients from the base year of the model
  194.      * using secular variation coefficients.
  195.      * @param year the year to which the model shall be transformed
  196.      * @return a time-transformed magnetic field model
  197.      */
  198.     public GeoMagneticField transformModel(final double year) {

  199.         if (!supportsTimeTransform()) {
  200.             throw new OrekitException(OrekitMessages.UNSUPPORTED_TIME_TRANSFORM, modelName, String.valueOf(epoch));
  201.         }

  202.         // the model can only be transformed within its validity period
  203.         if (year < validityStart || year > validityEnd) {
  204.             throw new OrekitException(OrekitMessages.OUT_OF_RANGE_TIME_TRANSFORM,
  205.                                       modelName, String.valueOf(epoch), year, validityStart, validityEnd);
  206.         }

  207.         final double dt = year - epoch;
  208.         final int maxSecIndex = maxNSec * (maxNSec + 1) / 2 + maxNSec;

  209.         final GeoMagneticField transformed = new GeoMagneticField(modelName, year, maxN, maxNSec,
  210.                                                                   validityStart, validityEnd);

  211.         for (int n = 1; n <= maxN; n++) {
  212.             for (int m = 0; m <= n; m++) {
  213.                 final int index = n * (n + 1) / 2 + m;
  214.                 if (index <= maxSecIndex) {
  215.                     transformed.h[index] = h[index] + dt * dh[index];
  216.                     transformed.g[index] = g[index] + dt * dg[index];
  217.                     // we need a copy of the secular var coef to calculate secular change
  218.                     transformed.dh[index] = dh[index];
  219.                     transformed.dg[index] = dg[index];
  220.                 } else {
  221.                     // just copy the parts that do not have corresponding secular variation coefficients
  222.                     transformed.h[index] = h[index];
  223.                     transformed.g[index] = g[index];
  224.                 }
  225.             }
  226.         }

  227.         return transformed;
  228.     }

  229.     /** Time transform the model coefficients from the base year of the model
  230.      * using a linear interpolation with a second model. The second model is
  231.      * required to have an adjacent validity period.
  232.      * @param otherModel the other magnetic field model
  233.      * @param year the year to which the model shall be transformed
  234.      * @return a time-transformed magnetic field model
  235.      */
  236.     public GeoMagneticField transformModel(final GeoMagneticField otherModel, final double year) {

  237.         // the model can only be transformed within its validity period
  238.         if (year < validityStart || year > validityEnd) {
  239.             throw new OrekitException(OrekitMessages.OUT_OF_RANGE_TIME_TRANSFORM,
  240.                                       modelName, String.valueOf(epoch), year, validityStart, validityEnd);
  241.         }

  242.         final double factor = (year - epoch) / (otherModel.epoch - epoch);
  243.         final int maxNCommon = FastMath.min(maxN, otherModel.maxN);
  244.         final int maxNCommonIndex = maxNCommon * (maxNCommon + 1) / 2 + maxNCommon;

  245.         final int newMaxN = FastMath.max(maxN, otherModel.maxN);

  246.         final GeoMagneticField transformed = new GeoMagneticField(modelName, year, newMaxN, 0,
  247.                                                                   validityStart, validityEnd);

  248.         for (int n = 1; n <= newMaxN; n++) {
  249.             for (int m = 0; m <= n; m++) {
  250.                 final int index = n * (n + 1) / 2 + m;
  251.                 if (index <= maxNCommonIndex) {
  252.                     transformed.h[index] = h[index] + factor * (otherModel.h[index] - h[index]);
  253.                     transformed.g[index] = g[index] + factor * (otherModel.g[index] - g[index]);
  254.                 } else {
  255.                     if (maxN < otherModel.maxN) {
  256.                         transformed.h[index] = factor * otherModel.h[index];
  257.                         transformed.g[index] = factor * otherModel.g[index];
  258.                     } else {
  259.                         transformed.h[index] = h[index] + factor * -h[index];
  260.                         transformed.g[index] = g[index] + factor * -g[index];
  261.                     }
  262.                 }
  263.             }
  264.         }

  265.         return transformed;
  266.     }

  267.     /** Utility function to get a decimal year for a given day.
  268.      * @param day the day (1-31)
  269.      * @param month the month (1-12)
  270.      * @param year the year
  271.      * @return the decimal year represented by the given day
  272.      */
  273.     public static double getDecimalYear(final int day, final int month, final int year) {
  274.         final int[] days = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
  275.         final int leapYear = (((year % 4) == 0) && (((year % 100) != 0) || ((year % 400) == 0))) ? 1 : 0;

  276.         final double dayInYear = days[month - 1] + (day - 1) + (month > 2 ? leapYear : 0);
  277.         return (double) year + (dayInYear / (365.0d + leapYear));
  278.     }

  279.     /** Transform geodetic coordinates to spherical coordinates.
  280.      * @param gp the geodetic point
  281.      * @return the spherical coordinates wrt to the reference ellipsoid of the model
  282.      */
  283.     private SphericalCoordinates transformToSpherical(final GeodeticPoint gp) {

  284.         // Convert geodetic coordinates (defined by the WGS-84 reference ellipsoid)
  285.         // to Earth Centered Earth Fixed Cartesian coordinates, and then to spherical coordinates.

  286.         final double lat = gp.getLatitude();
  287.         final double heightAboveEllipsoid = gp.getAltitude() / 1000d;
  288.         final double sinLat = FastMath.sin(lat);

  289.         // compute the local radius of curvature on the reference ellipsoid
  290.         final double rc = a / FastMath.sqrt(1.0d - epssq * sinLat * sinLat);

  291.         // compute ECEF Cartesian coordinates of specified point (for longitude=0)
  292.         final double xp = (rc + heightAboveEllipsoid) * FastMath.cos(lat);
  293.         final double zp = (rc * (1.0d - epssq) + heightAboveEllipsoid) * sinLat;

  294.         // compute spherical radius and angle lambda and phi of specified point
  295.         final double r = FastMath.hypot(xp, zp);
  296.         return new SphericalCoordinates(r, gp.getLongitude(), FastMath.asin(zp / r));
  297.     }

  298.     /** Rotate the magnetic vectors to geodetic coordinates.
  299.      * @param sph the spherical coordinates
  300.      * @param gp the geodetic point
  301.      * @param field the magnetic field in spherical coordinates
  302.      * @return the magnetic field in geodetic coordinates
  303.      */
  304.     private Vector3D rotateMagneticVector(final SphericalCoordinates sph,
  305.                                           final GeodeticPoint gp,
  306.                                           final Vector3D field) {

  307.         // difference between the spherical and geodetic latitudes
  308.         final double psi = sph.phi - gp.getLatitude();

  309.         // rotate spherical field components to the geodetic system
  310.         final double Bz = field.getX() * FastMath.sin(psi) + field.getZ() * FastMath.cos(psi);
  311.         final double Bx = field.getX() * FastMath.cos(psi) - field.getZ() * FastMath.sin(psi);
  312.         final double By = field.getY();

  313.         return new Vector3D(Bx, By, Bz);
  314.     }

  315.     /** Computes Geomagnetic Field Elements X, Y and Z in spherical coordinate
  316.      * system using spherical harmonic summation.
  317.      * The vector Magnetic field is given by -grad V, where V is geomagnetic
  318.      * scalar potential. The gradient in spherical coordinates is given by:
  319.      * <pre>
  320.      *          dV ^   1 dV ^       1    dV ^
  321.      * grad V = -- r + - -- t + -------- -- p
  322.      *          dr     r dt     r sin(t) dp
  323.      * </pre>
  324.      * @param sph the spherical coordinates
  325.      * @param vars the spherical harmonic variables
  326.      * @param legendre the legendre function
  327.      * @return the magnetic field vector in spherical coordinates
  328.      */
  329.     private Vector3D summation(final SphericalCoordinates sph, final SphericalHarmonicVars vars,
  330.                                final LegendreFunction legendre) {

  331.         int index;
  332.         double Bx = 0.0;
  333.         double By = 0.0;
  334.         double Bz = 0.0;

  335.         for (int n = 1; n <= maxN; n++) {
  336.             for (int m = 0; m <= n; m++) {
  337.                 index = n * (n + 1) / 2 + m;

  338.                 /**
  339.                  * <pre>
  340.                  *       nMax               (n+2)   n    m            m           m
  341.                  * Bz = -SUM (n + 1) * (a/r)     * SUM [g cos(m p) + h sin(m p)] P (sin(phi))
  342.                  *       n=1                       m=0   n            n           n
  343.                  * </pre>
  344.                  * Equation 12 in the WMM Technical report. Derivative with respect to radius.
  345.                  */
  346.                 Bz -= vars.relativeRadiusPower[n] *
  347.                       (g[index] * vars.cmLambda[m] + h[index] * vars.smLambda[m]) * (1d + n) * legendre.mP[index];

  348.                 /**
  349.                  * <pre>
  350.                  *      nMax     (n+2)   n    m            m            m
  351.                  * By = SUM (a/r)     * SUM [g cos(m p) + h sin(m p)] dP (sin(phi))
  352.                  *      n=1             m=0   n            n            n
  353.                  * </pre>
  354.                  * Equation 11 in the WMM Technical report. Derivative with respect to longitude, divided by radius.
  355.                  */
  356.                 By += vars.relativeRadiusPower[n] *
  357.                       (g[index] * vars.smLambda[m] - h[index] * vars.cmLambda[m]) * (double) m * legendre.mP[index];
  358.                 /**
  359.                  * <pre>
  360.                  *        nMax     (n+2)   n    m            m            m
  361.                  * Bx = - SUM (a/r)     * SUM [g cos(m p) + h sin(m p)] dP (sin(phi))
  362.                  *        n=1             m=0   n            n            n
  363.                  * </pre>
  364.                  * Equation 10 in the WMM Technical report. Derivative with respect to latitude, divided by radius.
  365.                  */
  366.                 Bx -= vars.relativeRadiusPower[n] *
  367.                       (g[index] * vars.cmLambda[m] + h[index] * vars.smLambda[m]) * legendre.mPDeriv[index];
  368.             }
  369.         }

  370.         final double cosPhi = FastMath.cos(sph.phi);
  371.         if (FastMath.abs(cosPhi) > 1.0e-10) {
  372.             By = By / cosPhi;
  373.         } else {
  374.             // special calculation for component - By - at geographic poles.
  375.             // To avoid using this function, make sure that the latitude is not
  376.             // exactly +/-90.
  377.             By = summationSpecial(sph, vars);
  378.         }

  379.         return new Vector3D(Bx, By, Bz);
  380.     }

  381.     /** Special calculation for the component By at geographic poles.
  382.      * @param sph the spherical coordinates
  383.      * @param vars the spherical harmonic variables
  384.      * @return the By component of the magnetic field
  385.      */
  386.     private double summationSpecial(final SphericalCoordinates sph, final SphericalHarmonicVars vars) {

  387.         double k;
  388.         final double sinPhi = FastMath.sin(sph.phi);
  389.         final double[] mPcupS = new double[maxN + 1];
  390.         mPcupS[0] = 1;
  391.         double By = 0.0;

  392.         for (int n = 1; n <= maxN; n++) {
  393.             final int index = n * (n + 1) / 2 + 1;
  394.             if (n == 1) {
  395.                 mPcupS[n] = mPcupS[n - 1];
  396.             } else {
  397.                 k = (double) (((n - 1) * (n - 1)) - 1) / (double) ((2 * n - 1) * (2 * n - 3));
  398.                 mPcupS[n] = sinPhi * mPcupS[n - 1] - k * mPcupS[n - 2];
  399.             }

  400.             /**
  401.              * <pre>
  402.              *      nMax     (n+2)   n    m            m            m
  403.              * By = SUM (a/r)     * SUM [g cos(m p) + h sin(m p)] dP (sin(phi))
  404.              *      n=1             m=0   n            n            n
  405.              * </pre>
  406.              * Equation 11 in the WMM Technical report. Derivative with respect to longitude, divided by radius.
  407.              */
  408.             By += vars.relativeRadiusPower[n] *
  409.                   (g[index] * vars.smLambda[1] - h[index] * vars.cmLambda[1]) * mPcupS[n] * schmidtQuasiNorm[index];
  410.         }

  411.         return By;
  412.     }

  413.     /** Utility class to hold spherical coordinates. */
  414.     private static class SphericalCoordinates {

  415.         /** the radius. */
  416.         private double r;

  417.         /** the azimuth angle. */
  418.         private double lambda;

  419.         /** the polar angle. */
  420.         private double phi;

  421.         /** Create a new spherical coordinate object.
  422.          * @param r the radius
  423.          * @param lambda the lambda angle
  424.          * @param phi the phi angle
  425.          */
  426.         private SphericalCoordinates(final double r, final double lambda, final double phi) {
  427.             this.r = r;
  428.             this.lambda = lambda;
  429.             this.phi = phi;
  430.         }
  431.     }

  432.     /** Utility class to compute certain variables for magnetic field summation. */
  433.     private class SphericalHarmonicVars {

  434.         /** (Radius of Earth / Spherical radius r)^(n+2). */
  435.         private double[] relativeRadiusPower;

  436.         /** cos(m*lambda). */
  437.         private double[] cmLambda;

  438.         /** sin(m*lambda). */
  439.         private double[] smLambda;

  440.         /** Calculates the spherical harmonic variables for a given spherical coordinate.
  441.          * @param sph the spherical coordinate
  442.          */
  443.         private SphericalHarmonicVars(final SphericalCoordinates sph) {

  444.             relativeRadiusPower = new double[maxN + 1];

  445.             // Compute a table of (EARTH_REFERENCE_RADIUS_KM / radius)^n for i in
  446.             // 0 .. maxN (this is much faster than calling FastMath.pow maxN+1 times).

  447.             final double p = ellipsoidRadius / sph.r;
  448.             relativeRadiusPower[0] = p * p;
  449.             for (int n = 1; n <= maxN; n++) {
  450.                 relativeRadiusPower[n] = relativeRadiusPower[n - 1] * (ellipsoidRadius / sph.r);
  451.             }

  452.             // Compute tables of sin(lon * m) and cos(lon * m) for m = 0 .. maxN
  453.             // this is much faster than calling FastMath.sin and FastMath.cos maxN+1 times.

  454.             cmLambda = new double[maxN + 1];
  455.             smLambda = new double[maxN + 1];

  456.             cmLambda[0] = 1.0d;
  457.             smLambda[0] = 0.0d;

  458.             final double cosLambda = FastMath.cos(sph.lambda);
  459.             final double sinLambda = FastMath.sin(sph.lambda);
  460.             cmLambda[1] = cosLambda;
  461.             smLambda[1] = sinLambda;

  462.             for (int m = 2; m <= maxN; m++) {
  463.                 cmLambda[m] = cmLambda[m - 1] * cosLambda - smLambda[m - 1] * sinLambda;
  464.                 smLambda[m] = cmLambda[m - 1] * sinLambda + smLambda[m - 1] * cosLambda;
  465.             }
  466.         }
  467.     }

  468.     /** Utility class to compute a table of Schmidt-semi normalized associated Legendre functions. */
  469.     private class LegendreFunction {

  470.         /** the vector of all associated Legendre polynomials. */
  471.         private double[] mP;

  472.         /** the vector of derivatives of the Legendre polynomials wrt latitude. */
  473.         private double[] mPDeriv;

  474.         /** Calculate the Schmidt-semi normalized Legendre function.
  475.          * <p>
  476.          * <b>Note:</b> In geomagnetism, the derivatives of ALF are usually
  477.          * found with respect to the colatitudes. Here the derivatives are found
  478.          * with respect to the latitude. The difference is a sign reversal for
  479.          * the derivative of the Associated Legendre Functions.
  480.          * </p>
  481.          * @param x sinus of the spherical latitude (or cosinus of the spherical colatitude)
  482.          */
  483.         private LegendreFunction(final double x) {

  484.             final int numTerms = (maxN + 1) * (maxN + 2) / 2;

  485.             mP = new double[numTerms + 1];
  486.             mPDeriv = new double[numTerms + 1];

  487.             mP[0] = 1.0;
  488.             mPDeriv[0] = 0.0;

  489.             // sin (geocentric latitude) - sin_phi
  490.             final double z = FastMath.sqrt((1.0d - x) * (1.0d + x));

  491.             int index;
  492.             int index1;
  493.             int index2;

  494.             // First, compute the Gauss-normalized associated Legendre functions
  495.             for (int n = 1; n <= maxN; n++) {
  496.                 for (int m = 0; m <= n; m++) {
  497.                     index = n * (n + 1) / 2 + m;
  498.                     if (n == m) {
  499.                         index1 = (n - 1) * n / 2 + m - 1;
  500.                         mP[index] = z * mP[index1];
  501.                         mPDeriv[index] = z * mPDeriv[index1] + x * mP[index1];
  502.                     } else if (n == 1 && m == 0) {
  503.                         index1 = (n - 1) * n / 2 + m;
  504.                         mP[index] = x * mP[index1];
  505.                         mPDeriv[index] = x * mPDeriv[index1] - z * mP[index1];
  506.                     } else if (n > 1 && n != m) {
  507.                         index1 = (n - 2) * (n - 1) / 2 + m;
  508.                         index2 = (n - 1) * n / 2 + m;
  509.                         if (m > n - 2) {
  510.                             mP[index] = x * mP[index2];
  511.                             mPDeriv[index] = x * mPDeriv[index2] - z * mP[index2];
  512.                         } else {
  513.                             final double k = (double) ((n - 1) * (n - 1) - (m * m)) /
  514.                                              (double) ((2 * n - 1) * (2 * n - 3));

  515.                             mP[index] = x * mP[index2] - k * mP[index1];
  516.                             mPDeriv[index] = x * mPDeriv[index2] - z * mP[index2] - k * mPDeriv[index1];
  517.                         }
  518.                     }

  519.                 }
  520.             }

  521.             // Converts the Gauss-normalized associated Legendre functions to the Schmidt quasi-normalized
  522.             // version using pre-computed relation stored in the variable schmidtQuasiNorm

  523.             for (int n = 1; n <= maxN; n++) {
  524.                 for (int m = 0; m <= n; m++) {
  525.                     index = n * (n + 1) / 2 + m;

  526.                     mP[index] = mP[index] * schmidtQuasiNorm[index];
  527.                     // The sign is changed since the new WMM routines use derivative with
  528.                     // respect to latitude instead of co-latitude
  529.                     mPDeriv[index] = -mPDeriv[index] * schmidtQuasiNorm[index];
  530.                 }
  531.             }
  532.         }
  533.     }
  534. }