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.      * @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.         // the model can only be transformed within its validity period
  206.         if (year < validityStart || year > validityEnd) {
  207.             throw new OrekitException(OrekitMessages.OUT_OF_RANGE_TIME_TRANSFORM,
  208.                                       modelName, String.valueOf(epoch), year, validityStart, validityEnd);
  209.         }

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

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

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

  230.         return transformed;
  231.     }

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

  244.         // the model can only be transformed within its validity period
  245.         if (year < validityStart || year > validityEnd) {
  246.             throw new OrekitException(OrekitMessages.OUT_OF_RANGE_TIME_TRANSFORM,
  247.                                       modelName, String.valueOf(epoch), year, validityStart, validityEnd);
  248.         }

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

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

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

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

  272.         return transformed;
  273.     }

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

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

  286.     /** Transform geodetic coordinates to spherical coordinates.
  287.      * @param gp the geodetic point
  288.      * @return the spherical coordinates wrt to the reference ellipsoid of the model
  289.      */
  290.     private SphericalCoordinates transformToSpherical(final GeodeticPoint gp) {

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

  293.         final double lat = gp.getLatitude();
  294.         final double heightAboveEllipsoid = gp.getAltitude() / 1000d;
  295.         final double sinLat = FastMath.sin(lat);

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

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

  301.         // compute spherical radius and angle lambda and phi of specified point
  302.         final double r = FastMath.hypot(xp, zp);
  303.         return new SphericalCoordinates(r, gp.getLongitude(), FastMath.asin(zp / r));
  304.     }

  305.     /** Rotate the magnetic vectors to geodetic coordinates.
  306.      * @param sph the spherical coordinates
  307.      * @param gp the geodetic point
  308.      * @param field the magnetic field in spherical coordinates
  309.      * @return the magnetic field in geodetic coordinates
  310.      */
  311.     private Vector3D rotateMagneticVector(final SphericalCoordinates sph,
  312.                                           final GeodeticPoint gp,
  313.                                           final Vector3D field) {

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

  316.         // rotate spherical field components to the geodetic system
  317.         final double Bz = field.getX() * FastMath.sin(psi) + field.getZ() * FastMath.cos(psi);
  318.         final double Bx = field.getX() * FastMath.cos(psi) - field.getZ() * FastMath.sin(psi);
  319.         final double By = field.getY();

  320.         return new Vector3D(Bx, By, Bz);
  321.     }

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

  338.         int index;
  339.         double Bx = 0.0;
  340.         double By = 0.0;
  341.         double Bz = 0.0;

  342.         for (int n = 1; n <= maxN; n++) {
  343.             for (int m = 0; m <= n; m++) {
  344.                 index = n * (n + 1) / 2 + m;

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

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

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

  386.         return new Vector3D(Bx, By, Bz);
  387.     }

  388.     /** Special calculation for the component By at geographic poles.
  389.      * @param sph the spherical coordinates
  390.      * @param vars the spherical harmonic variables
  391.      * @return the By component of the magnetic field
  392.      */
  393.     private double summationSpecial(final SphericalCoordinates sph, final SphericalHarmonicVars vars) {

  394.         double k;
  395.         final double sinPhi = FastMath.sin(sph.phi);
  396.         final double[] mPcupS = new double[maxN + 1];
  397.         mPcupS[0] = 1;
  398.         double By = 0.0;

  399.         for (int n = 1; n <= maxN; n++) {
  400.             final int index = n * (n + 1) / 2 + 1;
  401.             if (n == 1) {
  402.                 mPcupS[n] = mPcupS[n - 1];
  403.             } else {
  404.                 k = (double) (((n - 1) * (n - 1)) - 1) / (double) ((2 * n - 1) * (2 * n - 3));
  405.                 mPcupS[n] = sinPhi * mPcupS[n - 1] - k * mPcupS[n - 2];
  406.             }

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

  418.         return By;
  419.     }

  420.     /** Utility class to hold spherical coordinates. */
  421.     private static class SphericalCoordinates {

  422.         /** the radius. */
  423.         private double r;

  424.         /** the azimuth angle. */
  425.         private double lambda;

  426.         /** the polar angle. */
  427.         private double phi;

  428.         /** Create a new spherical coordinate object.
  429.          * @param r the radius
  430.          * @param lambda the lambda angle
  431.          * @param phi the phi angle
  432.          */
  433.         private SphericalCoordinates(final double r, final double lambda, final double phi) {
  434.             this.r = r;
  435.             this.lambda = lambda;
  436.             this.phi = phi;
  437.         }
  438.     }

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

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

  443.         /** cos(m*lambda). */
  444.         private double[] cmLambda;

  445.         /** sin(m*lambda). */
  446.         private double[] smLambda;

  447.         /** Calculates the spherical harmonic variables for a given spherical coordinate.
  448.          * @param sph the spherical coordinate
  449.          */
  450.         private SphericalHarmonicVars(final SphericalCoordinates sph) {

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

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

  454.             final double p = ellipsoidRadius / sph.r;
  455.             relativeRadiusPower[0] = p * p;
  456.             for (int n = 1; n <= maxN; n++) {
  457.                 relativeRadiusPower[n] = relativeRadiusPower[n - 1] * (ellipsoidRadius / sph.r);
  458.             }

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

  461.             cmLambda = new double[maxN + 1];
  462.             smLambda = new double[maxN + 1];

  463.             cmLambda[0] = 1.0d;
  464.             smLambda[0] = 0.0d;

  465.             final double cosLambda = FastMath.cos(sph.lambda);
  466.             final double sinLambda = FastMath.sin(sph.lambda);
  467.             cmLambda[1] = cosLambda;
  468.             smLambda[1] = sinLambda;

  469.             for (int m = 2; m <= maxN; m++) {
  470.                 cmLambda[m] = cmLambda[m - 1] * cosLambda - smLambda[m - 1] * sinLambda;
  471.                 smLambda[m] = cmLambda[m - 1] * sinLambda + smLambda[m - 1] * cosLambda;
  472.             }
  473.         }
  474.     }

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

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

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

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

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

  492.             mP = new double[numTerms + 1];
  493.             mPDeriv = new double[numTerms + 1];

  494.             mP[0] = 1.0;
  495.             mPDeriv[0] = 0.0;

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

  498.             int index;
  499.             int index1;
  500.             int index2;

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

  522.                             mP[index] = x * mP[index2] - k * mP[index1];
  523.                             mPDeriv[index] = x * mPDeriv[index2] - z * mP[index2] - k * mPDeriv[index1];
  524.                         }
  525.                     }

  526.                 }
  527.             }

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

  530.             for (int n = 1; n <= maxN; n++) {
  531.                 for (int m = 0; m <= n; m++) {
  532.                     index = n * (n + 1) / 2 + m;

  533.                     mP[index] = mP[index] * schmidtQuasiNorm[index];
  534.                     // The sign is changed since the new WMM routines use derivative with
  535.                     // respect to latitude instead of co-latitude
  536.                     mPDeriv[index] = -mPDeriv[index] * schmidtQuasiNorm[index];
  537.                 }
  538.             }
  539.         }
  540.     }
  541. }