SaastamoinenModel.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 java.io.Serializable;
  19. import java.util.Arrays;

  20. import org.hipparchus.analysis.BivariateFunction;
  21. import org.hipparchus.analysis.UnivariateFunction;
  22. import org.hipparchus.analysis.interpolation.LinearInterpolator;
  23. import org.hipparchus.analysis.polynomials.PolynomialFunction;
  24. import org.hipparchus.exception.LocalizedCoreFormats;
  25. import org.hipparchus.exception.MathIllegalArgumentException;
  26. import org.hipparchus.util.FastMath;
  27. import org.hipparchus.util.MathArrays;
  28. import org.orekit.data.DataProvidersManager;
  29. import org.orekit.errors.OrekitException;
  30. import org.orekit.errors.OrekitMessages;
  31. import org.orekit.utils.InterpolationTableLoader;

  32. /** The modified Saastamoinen model. Estimates the path delay imposed to
  33.  * electro-magnetic signals by the troposphere according to the formula:
  34.  * <pre>
  35.  * δ = 2.277e-3 / cos z * (P + (1255 / T + 0.05) * e - B * tan²
  36.  * z) + δR
  37.  * </pre>
  38.  * with the following input data provided to the model:
  39.  * <ul>
  40.  * <li>z: zenith angle</li>
  41.  * <li>P: atmospheric pressure</li>
  42.  * <li>T: temperature</li>
  43.  * <li>e: partial pressure of water vapour</li>
  44.  * <li>B, δR: correction terms</li>
  45.  * </ul>
  46.  * <p>
  47.  * The model supports custom δR correction terms to be read from a
  48.  * configuration file (saastamoinen-correction.txt) via the
  49.  * {@link DataProvidersManager}.
  50.  * </p>
  51.  * @author Thomas Neidhart
  52.  * @see "Guochang Xu, GPS - Theory, Algorithms and Applications, Springer, 2007"
  53.  */
  54. public class SaastamoinenModel implements TroposphericModel {

  55.     /** Default file name for δR correction term table. */
  56.     public static final String DELTA_R_FILE_NAME = "^saastamoinen-correction\\.txt$";

  57.     /** Serializable UID. */
  58.     private static final long serialVersionUID = 20160126L;

  59.     /** X values for the B function. */
  60.     private static final double[] X_VALUES_FOR_B = {
  61.         0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0
  62.     };

  63.     /** E values for the B function. */
  64.     private static final double[] Y_VALUES_FOR_B = {
  65.         1.156, 1.079, 1.006, 0.938, 0.874, 0.813, 0.757, 0.654, 0.563
  66.     };

  67.     /** Coefficients for the partial pressure of water vapor polynomial. */
  68.     private static final double[] E_COEFFICIENTS = {
  69.         -37.2465, 0.213166, -0.000256908
  70.     };

  71.     /** Interpolation function for the B correction term. */
  72.     private final transient UnivariateFunction bFunction;

  73.     /** Polynomial function for the e term. */
  74.     private final transient PolynomialFunction eFunction;

  75.     /** Interpolation function for the delta R correction term. */
  76.     private final transient BilinearInterpolatingFunction deltaRFunction;

  77.     /** The temperature at the station [K]. */
  78.     private double t0;

  79.     /** The atmospheric pressure [mbar]. */
  80.     private double p0;

  81.     /** The humidity [percent]. */
  82.     private double r0;

  83.     /** Create a new Saastamoinen model for the troposphere using the given
  84.      * environmental conditions.
  85.      * @param t0 the temperature at the station [K]
  86.      * @param p0 the atmospheric pressure at the station [mbar]
  87.      * @param r0 the humidity at the station [fraction] (50% -&gt; 0.5)
  88.      * @param deltaRFileName regular expression for filename containing δR
  89.      * correction term table (typically {@link #DELTA_R_FILE_NAME}), if null
  90.      * default values from the reference book are used
  91.           * @since 7.1
  92.      */
  93.     public SaastamoinenModel(final double t0, final double p0, final double r0,
  94.                              final String deltaRFileName) {
  95.         this(t0, p0, r0,
  96.              deltaRFileName == null ? defaultDeltaR() : loadDeltaR(deltaRFileName));
  97.     }

  98.     /** Create a new Saastamoinen model.
  99.      * @param t0 the temperature at the station [K]
  100.      * @param p0 the atmospheric pressure at the station [mbar]
  101.      * @param r0 the humidity at the station [fraction] (50% -> 0.5)
  102.      * @param deltaR δR correction term function
  103.      * @since 7.1
  104.      */
  105.     private SaastamoinenModel(final double t0, final double p0, final double r0,
  106.                               final BilinearInterpolatingFunction deltaR) {
  107.         this.t0             = t0;
  108.         this.p0             = p0;
  109.         this.r0             = r0;
  110.         this.bFunction      = new LinearInterpolator().interpolate(X_VALUES_FOR_B, Y_VALUES_FOR_B);
  111.         this.eFunction      = new PolynomialFunction(E_COEFFICIENTS);
  112.         this.deltaRFunction = deltaR;
  113.     }

  114.     /** Create a new Saastamoinen model using a standard atmosphere model.
  115.      *
  116.      * <ul>
  117.      * <li>temperature: 18 degree Celsius
  118.      * <li>pressure: 1013.25 mbar
  119.      * <li>humidity: 50%
  120.      * </ul>
  121.      *
  122.      * @return a Saastamoinen model with standard environmental values
  123.      */
  124.     public static SaastamoinenModel getStandardModel() {
  125.         return new SaastamoinenModel(273.16 + 18, 1013.25, 0.5, (String) null);
  126.     }

  127.     /** {@inheritDoc}
  128.      * <p>
  129.      * The Saastamoinen model is not defined for altitudes below 0.0. for continuity
  130.      * reasons, we use the value for h = 0 when altitude is negative.
  131.      * </p>
  132.      */
  133.     public double pathDelay(final double elevation, final double height) {

  134.         // there are no data in the model for negative altitudes
  135.         // we use the data for the lowest available altitude: 0.0
  136.         final double fixedHeight = FastMath.max(0.0, height);

  137.         // the corrected temperature using a temperature gradient of -6.5 K/km
  138.         final double T = t0 - 6.5e-3 * fixedHeight;
  139.         // the corrected pressure
  140.         final double P = p0 * FastMath.pow(1.0 - 2.26e-5 * fixedHeight, 5.225);
  141.         // the corrected humidity
  142.         final double R = r0 * FastMath.exp(-6.396e-4 * fixedHeight);

  143.         // interpolate the b correction term
  144.         final double B = bFunction.value(fixedHeight / 1e3);
  145.         // calculate e
  146.         final double e = R * FastMath.exp(eFunction.value(T));

  147.         // calculate the zenith angle from the elevation
  148.         final double z = FastMath.abs(0.5 * FastMath.PI - elevation);

  149.         // get correction factor
  150.         final double deltaR = getDeltaR(fixedHeight, z);

  151.         // calculate the path delay in m
  152.         final double tan = FastMath.tan(z);
  153.         final double delta = 2.277e-3 / FastMath.cos(z) *
  154.                              (P + (1255d / T + 5e-2) * e - B * tan * tan) + deltaR;

  155.         return delta;
  156.     }

  157.     /** Calculates the delta R correction term using linear interpolation.
  158.      * @param height the height of the station in m
  159.      * @param zenith the zenith angle of the satellite
  160.      * @return the delta R correction term in m
  161.      */
  162.     private double getDeltaR(final double height, final double zenith) {
  163.         // limit the height to a range of [0, 5000] m
  164.         final double h = FastMath.min(FastMath.max(0, height), 5000);
  165.         // limit the zenith angle to 90 degree
  166.         // Note: the function is symmetric for negative zenith angles
  167.         final double z = FastMath.min(Math.abs(zenith), 0.5 * FastMath.PI);
  168.         return deltaRFunction.value(h, z);
  169.     }

  170.     /** Load δR function.
  171.      * @param deltaRFileName regular expression for filename containing δR
  172.      * correction term table
  173.      * @return δR function
  174.      */
  175.     private static BilinearInterpolatingFunction loadDeltaR(final String deltaRFileName) {

  176.         // read the δR interpolation function from the config file
  177.         final InterpolationTableLoader loader = new InterpolationTableLoader();
  178.         DataProvidersManager.getInstance().feed(deltaRFileName, loader);
  179.         if (!loader.stillAcceptsData()) {
  180.             final double[] elevations = loader.getOrdinateGrid();
  181.             for (int i = 0; i < elevations.length; ++i) {
  182.                 elevations[i] = FastMath.toRadians(elevations[i]);
  183.             }
  184.             return new BilinearInterpolatingFunction(loader.getAbscissaGrid(), elevations,
  185.                                                      loader.getValuesSamples());
  186.         }
  187.         throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE,
  188.                                   deltaRFileName.replaceAll("^\\^", "").replaceAll("\\$$", ""));
  189.     }

  190.     /** Create the default δR function.
  191.      * @return δR function
  192.      */
  193.     private static BilinearInterpolatingFunction defaultDeltaR() {

  194.         // the correction table in the referenced book only contains values for an angle of 60 - 80
  195.         // degree, thus for 0 degree, the correction term is assumed to be 0, for degrees > 80 it
  196.         // is assumed to be the same value as for 80.

  197.         // the height in m
  198.         final double xValForR[] = {
  199.             0, 500, 1000, 1500, 2000, 3000, 4000, 5000
  200.         };

  201.         // the zenith angle
  202.         final double yValForR[] = {
  203.             FastMath.toRadians( 0.00), FastMath.toRadians(60.00), FastMath.toRadians(66.00), FastMath.toRadians(70.00),
  204.             FastMath.toRadians(73.00), FastMath.toRadians(75.00), FastMath.toRadians(76.00), FastMath.toRadians(77.00),
  205.             FastMath.toRadians(78.00), FastMath.toRadians(78.50), FastMath.toRadians(79.00), FastMath.toRadians(79.50),
  206.             FastMath.toRadians(79.75), FastMath.toRadians(80.00), FastMath.toRadians(90.00)
  207.         };

  208.         final double[][] fval = new double[][] {
  209.             {
  210.                 0.000, 0.003, 0.006, 0.012, 0.020, 0.031, 0.039, 0.050, 0.065, 0.075, 0.087, 0.102, 0.111, 0.121, 0.121
  211.             }, {
  212.                 0.000, 0.003, 0.006, 0.011, 0.018, 0.028, 0.035, 0.045, 0.059, 0.068, 0.079, 0.093, 0.101, 0.110, 0.110
  213.             }, {
  214.                 0.000, 0.002, 0.005, 0.010, 0.017, 0.025, 0.032, 0.041, 0.054, 0.062, 0.072, 0.085, 0.092, 0.100, 0.100
  215.             }, {
  216.                 0.000, 0.002, 0.005, 0.009, 0.015, 0.023, 0.029, 0.037, 0.049, 0.056, 0.065, 0.077, 0.083, 0.091, 0.091
  217.             }, {
  218.                 0.000, 0.002, 0.004, 0.008, 0.013, 0.021, 0.026, 0.033, 0.044, 0.051, 0.059, 0.070, 0.076, 0.083, 0.083
  219.             }, {
  220.                 0.000, 0.002, 0.003, 0.006, 0.011, 0.017, 0.021, 0.027, 0.036, 0.042, 0.049, 0.058, 0.063, 0.068, 0.068
  221.             }, {
  222.                 0.000, 0.001, 0.003, 0.005, 0.009, 0.014, 0.017, 0.022, 0.030, 0.034, 0.040, 0.047, 0.052, 0.056, 0.056
  223.             }, {
  224.                 0.000, 0.001, 0.002, 0.004, 0.007, 0.011, 0.014, 0.018, 0.024, 0.028, 0.033, 0.039, 0.043, 0.047, 0.047
  225.             }
  226.         };

  227.         // the actual delta R is interpolated using a a bilinear interpolator
  228.         return new BilinearInterpolatingFunction(xValForR, yValForR, fval);

  229.     }

  230.     /** Replace the instance with a data transfer object for serialization.
  231.      * @return data transfer object that will be serialized
  232.      */
  233.     private Object writeReplace() {
  234.         return new DataTransferObject(this);
  235.     }

  236.     /** Specialization of the data transfer object for serialization. */
  237.     private static class DataTransferObject implements Serializable {

  238.         /** Serializable UID. */
  239.         private static final long serialVersionUID = 20160126L;

  240.         /** The temperature at the station [K]. */
  241.         private double t0;

  242.         /** The atmospheric pressure [mbar]. */
  243.         private double p0;

  244.         /** The humidity [percent]. */
  245.         private double r0;

  246.         /** Samples x-coordinates. */
  247.         private final double[] xval;

  248.         /** Samples y-coordinates. */
  249.         private final double[] yval;

  250.         /** Set of cubic splines patching the whole data grid. */
  251.         private final double[][] fval;

  252.         /** Simple constructor.
  253.          * @param model model to serialize
  254.          */
  255.         DataTransferObject(final SaastamoinenModel model) {
  256.             this.t0 = model.t0;
  257.             this.p0 = model.p0;
  258.             this.r0 = model.r0;
  259.             this.xval = model.deltaRFunction.xval.clone();
  260.             this.yval = model.deltaRFunction.yval.clone();
  261.             this.fval = model.deltaRFunction.fval.clone();
  262.         }

  263.         /** Replace the deserialized data transfer object with a {@link SaastamoinenModel}.
  264.          * @return replacement {@link SaastamoinenModel}
  265.          */
  266.         private Object readResolve() {
  267.             return new SaastamoinenModel(t0, p0, r0,
  268.                                          new BilinearInterpolatingFunction(xval, yval, fval));
  269.         }

  270.     }

  271.     /**
  272.      * Function that implements a standard bilinear interpolation.
  273.      * The interpolation as found
  274.      * in the Wikipedia reference <a href =
  275.      * "http://en.wikipedia.org/wiki/Bilinear_interpolation">BiLinear
  276.      * Interpolation</a>. This is a stand-in until Apache Math has a
  277.      * bilinear interpolator
  278.      */
  279.     private static class BilinearInterpolatingFunction implements BivariateFunction {

  280.         /**
  281.          * The minimum number of points that are needed to compute the
  282.          * function.
  283.          */
  284.         private static final int MIN_NUM_POINTS = 2;

  285.         /** Samples x-coordinates. */
  286.         private final double[] xval;

  287.         /** Samples y-coordinates. */
  288.         private final double[] yval;

  289.         /** Set of cubic splines patching the whole data grid. */
  290.         private final double[][] fval;

  291.         /**
  292.          * @param x Sample values of the x-coordinate, in increasing order.
  293.          * @param y Sample values of the y-coordinate, in increasing order.
  294.          * @param f Values of the function on every grid point. the expected
  295.          *        number of elements.
  296.          * @throws MathIllegalArgumentException if the length of x and y don't
  297.          *         match the row, column height of f, or if any of the arguments
  298.          *         are null, or if any of the arrays has zero length, or if
  299.          *         {@code x} or {@code y} are not strictly increasing.
  300.          */
  301.         BilinearInterpolatingFunction(final double[] x, final double[] y, final double[][] f)
  302.                         throws MathIllegalArgumentException {

  303.             if (x == null || y == null || f == null || f[0] == null) {
  304.                 throw new IllegalArgumentException("All arguments must be non-null");
  305.             }

  306.             final int xLen = x.length;
  307.             final int yLen = y.length;

  308.             if (xLen == 0 || yLen == 0 || f.length == 0 || f[0].length == 0) {
  309.                 throw new MathIllegalArgumentException(LocalizedCoreFormats.NO_DATA);
  310.             }

  311.             if (xLen < MIN_NUM_POINTS || yLen < MIN_NUM_POINTS || f.length < MIN_NUM_POINTS ||
  312.                             f[0].length < MIN_NUM_POINTS) {
  313.                 throw new MathIllegalArgumentException(LocalizedCoreFormats.INSUFFICIENT_DATA);
  314.             }

  315.             if (xLen != f.length) {
  316.                 throw new MathIllegalArgumentException(LocalizedCoreFormats.DIMENSIONS_MISMATCH,
  317.                                                        xLen, f.length);
  318.             }

  319.             if (yLen != f[0].length) {
  320.                 throw new MathIllegalArgumentException(LocalizedCoreFormats.DIMENSIONS_MISMATCH,
  321.                                                        yLen, f[0].length);
  322.             }

  323.             MathArrays.checkOrder(x);
  324.             MathArrays.checkOrder(y);

  325.             xval = x.clone();
  326.             yval = y.clone();
  327.             fval = f.clone();
  328.         }

  329.         @Override
  330.         public double value(final double x, final double y) {
  331.             final int offset = 1;
  332.             final int count = offset + 1;
  333.             final int i = searchIndex(x, xval, offset, count);
  334.             final int j = searchIndex(y, yval, offset, count);

  335.             final double x1 = xval[i];
  336.             final double x2 = xval[i + 1];
  337.             final double y1 = yval[j];
  338.             final double y2 = yval[j + 1];
  339.             final double fQ11 = fval[i][j];
  340.             final double fQ21 = fval[i + 1][j];
  341.             final double fQ12 = fval[i][j + 1];
  342.             final double fQ22 = fval[i + 1][j + 1];

  343.             final double f = (fQ11 * (x2 - x)  * (y2 - y) +
  344.                             fQ21 * (x  - x1) * (y2 - y) +
  345.                             fQ12 * (x2 - x)  * (y  - y1) +
  346.                             fQ22 * (x  - x1) * (y  - y1)) /
  347.                             ((x2 - x1) * (y2 - y1));

  348.             return f;
  349.         }

  350.         /**
  351.          * @param c Coordinate.
  352.          * @param val Coordinate samples.
  353.          * @param offset how far back from found value to offset for
  354.          *        querying
  355.          * @param count total number of elements forward from beginning that
  356.          *        will be queried
  357.          * @return the index in {@code val} corresponding to the interval
  358.          *         containing {@code c}.
  359.          * @throws MathIllegalArgumentException if {@code c} is out of the range
  360.          *         defined by the boundary values of {@code val}.
  361.          */
  362.         private int searchIndex(final double c, final double[] val, final int offset, final int count)
  363.             throws MathIllegalArgumentException {
  364.             int r = Arrays.binarySearch(val, c);

  365.             if (r == -1 || r == -val.length - 1) {
  366.                 throw new MathIllegalArgumentException(LocalizedCoreFormats.OUT_OF_RANGE_SIMPLE,
  367.                                                        c, val[0], val[val.length - 1]);
  368.             }

  369.             if (r < 0) {
  370.                 // "c" in within an interpolation sub-interval, which
  371.                 // returns
  372.                 // negative
  373.                 // need to remove the negative sign for consistency
  374.                 r = -r - offset - 1;
  375.             } else {
  376.                 r -= offset;
  377.             }

  378.             if (r < 0) {
  379.                 r = 0;
  380.             }

  381.             if ((r + count) >= val.length) {
  382.                 // "c" is the last sample of the range: Return the index
  383.                 // of the sample at the lower end of the last sub-interval.
  384.                 r = val.length - count;
  385.             }

  386.             return r;
  387.         }

  388.     }

  389. }