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.apache.commons.math3.geometry.euclidean.threed.Vector3D;
  19. import org.apache.commons.math3.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 latitude in decimal degrees
  173.      * @param longitude the longitude in decimal degrees
  174.      * @param height the altitude in kilometers above mean sea level
  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.      * @throws OrekitException if the specified year is outside the validity period of the
  198.      *                         model or the model does not support time transformations
  199.      *                         (i.e. no secular variations available)
  200.      */
  201.     public GeoMagneticField transformModel(final double year) throws OrekitException {

  202.         if (!supportsTimeTransform()) {
  203.             throw new OrekitException(OrekitMessages.UNSUPPORTED_TIME_TRANSFORM, modelName, String.valueOf(epoch));
  204.         }

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

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

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

  225.         return transformed;
  226.     }

  227.     /** Time transform the model coefficients from the base year of the model
  228.      * using a linear interpolation with a second model. The second model is
  229.      * required to have an adjacent validity period.
  230.      * @param otherModel the other magnetic field model
  231.      * @param year the year to which the model shall be transformed
  232.      * @return a time-transformed magnetic field model
  233.      * @throws OrekitException if the specified year is outside the validity period of the
  234.      *                         model or the model does not support time transformations
  235.      *                         (i.e. no secular variations available)
  236.      */
  237.     public GeoMagneticField transformModel(final GeoMagneticField otherModel, final double year)
  238.         throws OrekitException {

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

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

  247.         final int newMaxN = Math.max(maxN, otherModel.maxN);

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

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

  267.         return transformed;
  268.     }

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

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

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

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

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

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

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

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

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

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

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

  315.         return new Vector3D(Bx, By, Bz);
  316.     }

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

  333.         int index;
  334.         double Bx = 0.0;
  335.         double By = 0.0;
  336.         double Bz = 0.0;

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

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

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

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

  381.         return new Vector3D(Bx, By, Bz);
  382.     }

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

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

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

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

  413.         return By;
  414.     }

  415.     /** Utility class to hold spherical coordinates. */
  416.     private static class SphericalCoordinates {

  417.         /** the radius. */
  418.         private double r;

  419.         /** the azimuth angle. */
  420.         private double lambda;

  421.         /** the polar angle. */
  422.         private double phi;

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

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

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

  438.         /** cos(m*lambda). */
  439.         private double[] cmLambda;

  440.         /** sin(m*lambda). */
  441.         private double[] smLambda;

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

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

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

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

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

  456.             cmLambda = new double[maxN + 1];
  457.             smLambda = new double[maxN + 1];

  458.             cmLambda[0] = 1.0d;
  459.             smLambda[0] = 0.0d;

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

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

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

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

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

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

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

  487.             mP = new double[numTerms + 1];
  488.             mPDeriv = new double[numTerms + 1];

  489.             mP[0] = 1.0;
  490.             mPDeriv[0] = 0.0;

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

  493.             int index;
  494.             int index1;
  495.             int index2;

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

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

  521.                 }
  522.             }

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

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

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