GlobalPressureTemperature2Model.java

  1. /* Copyright 2002-2019 CS Systèmes d'Information
  2.  * Licensed to CS Systèmes d'Information (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.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.InputStreamReader;
  22. import java.nio.charset.StandardCharsets;
  23. import java.text.ParseException;
  24. import java.util.ArrayList;
  25. import java.util.List;
  26. import java.util.SortedSet;
  27. import java.util.TreeSet;
  28. import java.util.concurrent.atomic.AtomicReference;
  29. import java.util.function.ToDoubleFunction;

  30. import org.hipparchus.analysis.interpolation.BilinearInterpolatingFunction;
  31. import org.hipparchus.util.FastMath;
  32. import org.hipparchus.util.MathUtils;
  33. import org.hipparchus.util.SinCos;
  34. import org.orekit.data.DataLoader;
  35. import org.orekit.data.DataProvidersManager;
  36. import org.orekit.errors.OrekitException;
  37. import org.orekit.errors.OrekitMessages;
  38. import org.orekit.time.AbsoluteDate;
  39. import org.orekit.time.TimeScalesFactory;
  40. import org.orekit.utils.Constants;

  41. /** The Global Pressure and Temperature 2 (GPT2) model.
  42.  * This model is an empirical model that provides the temperature, the pressure and the water vapor pressure
  43.  * of a site depending its latitude and  longitude. This model also provides the a<sub>h</sub>
  44.  * and a<sub>w</sub> coefficients used for the {@link ViennaOneModel Vienna 1} model.
  45.  * <p>
  46.  * The requisite coefficients for the computation of the weather parameters are provided by the
  47.  * Department of Geodesy and Geoinformation of the Vienna University. They are based on an
  48.  * external grid file like "gpt2_1.grd" (1° x 1°) or "gpt2_5.grd" (5° x 5°) available at:
  49.  * <a href="http://vmf.geo.tuwien.ac.at/codes/"> link</a>
  50.  * </p>
  51.  * <p>
  52.  * A bilinear interpolation is performed in order to obtained the correct values of the weather parameters.
  53.  * </p>
  54.  * <p>
  55.  * The format is always the same, with and example shown below for the pressure and the temperature.
  56.  * <p>
  57.  * Example:
  58.  * </p>
  59.  * <pre>
  60.  * %  lat    lon   p:a0    A1   B1   A2   B2  T:a0    A1   B1   A2   B2
  61.  *   87.5    2.5 101421    21  409 -217 -122 259.2 -13.2 -6.1  2.6  0.3
  62.  *   87.5    7.5 101416    21  411 -213 -120 259.3 -13.1 -6.1  2.6  0.3
  63.  *   87.5   12.5 101411    22  413 -209 -118 259.3 -13.1 -6.1  2.6  0.3
  64.  *   87.5   17.5 101407    23  415 -205 -116 259.4 -13.0 -6.1  2.6  0.3
  65.  *   ...
  66.  * </pre>
  67.  *
  68.  * @see K. Lagler, M. Schindelegger, J. Böhm, H. Krasna, T. Nilsson (2013),
  69.  * GPT2: empirical slant delay model for radio space geodetic techniques. Geophys
  70.  * Res Lett 40(6):1069–1073. doi:10.1002/grl.50288
  71.  *
  72.  * @author Bryan Cazabonne
  73.  *
  74.  */
  75. public class GlobalPressureTemperature2Model implements WeatherModel {

  76.     /** Default supported files name pattern. */
  77.     public static final String DEFAULT_SUPPORTED_NAMES = "gpt2_\\d+.grd";

  78.     /** Standard gravity constant [m/s²]. */
  79.     private static final double G = Constants.G0_STANDARD_GRAVITY;

  80.     /** Ideal gas constant for dry air [J/kg/K]. */
  81.     private static final double R = 287.0;

  82.     /** Conversion factor from degrees to mill arcseconds. */
  83.     private static final int DEG_TO_MAS = 3600000;

  84.     /** Shared lazily loaded grid. */
  85.     private static final AtomicReference<Grid> SHARED_GRID = new AtomicReference<>(null);

  86.     /** South-West grid entry. */
  87.     private final GridEntry southWest;

  88.     /** South-East grid entry. */
  89.     private final GridEntry southEast;

  90.     /** North-West grid entry. */
  91.     private final GridEntry northWest;

  92.     /** North-East grid entry. */
  93.     private final GridEntry northEast;

  94.     /** The hydrostatic and wet a coefficients loaded. */
  95.     private double[] coefficientsA;

  96.     /** Geodetic site latitude, radians.*/
  97.     private double latitude;

  98.     /** Geodetic site longitude, radians.*/
  99.     private double longitude;

  100.     /** Temperature site, in kelvins. */
  101.     private double temperature;

  102.     /** Pressure site, in hPa. */
  103.     private double pressure;

  104.     /** water vapour pressure, in hPa. */
  105.     private double e0;

  106.     /** The height of the station in m. */
  107.     private double height;

  108.     /** Geoid used to compute the undulations. */
  109.     private final Geoid geoid;

  110.     /** Current date. */
  111.     private AbsoluteDate date;

  112.     /** Constructor with supported names given by user.
  113.      * @param supportedNames supported names
  114.      * @param latitude geodetic latitude of the station, in radians
  115.      * @param longitude longitude geodetic longitude of the station, in radians
  116.      * @param geoid level surface of the gravity potential of a body
  117.      */
  118.     public GlobalPressureTemperature2Model(final String supportedNames, final double latitude,
  119.                                            final double longitude, final Geoid geoid) {
  120.         this.coefficientsA = null;
  121.         this.temperature   = Double.NaN;
  122.         this.pressure      = Double.NaN;
  123.         this.e0            = Double.NaN;
  124.         this.geoid         = geoid;
  125.         this.latitude      = latitude;

  126.         // get the lazily loaded shared grid
  127.         Grid grid = SHARED_GRID.get();
  128.         if (grid == null) {
  129.             // this is the first instance we create, we need to load the grid data
  130.             final Parser parser = new Parser();
  131.             DataProvidersManager.getInstance().feed(supportedNames, parser);
  132.             SHARED_GRID.compareAndSet(null, parser.grid);
  133.             grid = parser.grid;
  134.         }

  135.         // Normalize longitude according to the grid
  136.         this.longitude = MathUtils.normalizeAngle(longitude, grid.entries[0][0].longitude + FastMath.PI);

  137.         final int southIndex = grid.getSouthIndex(this.latitude);
  138.         final int westIndex  = grid.getWestIndex(this.longitude);
  139.         this.southWest = grid.entries[southIndex    ][westIndex    ];
  140.         this.southEast = grid.entries[southIndex    ][westIndex + 1];
  141.         this.northWest = grid.entries[southIndex + 1][westIndex    ];
  142.         this.northEast = grid.entries[southIndex + 1][westIndex + 1];

  143.     }

  144.     /** Constructor with default supported names.
  145.      * @param latitude geodetic latitude of the station, in radians
  146.      * @param longitude geodetic latitude of the station, in radians
  147.      * @param geoid level surface of the gravity potential of a body
  148.      */
  149.     public GlobalPressureTemperature2Model(final double latitude, final double longitude, final Geoid geoid) {
  150.         this(DEFAULT_SUPPORTED_NAMES, latitude, longitude, geoid);
  151.     }

  152.     /** Returns the a coefficients array.
  153.      * <ul>
  154.      * <li>double[0] = a<sub>h</sub>
  155.      * <li>double[1] = a<sub>w</sub>
  156.      * </ul>
  157.      * @return the a coefficients array
  158.      */
  159.     public double[] getA() {
  160.         return coefficientsA.clone();
  161.     }

  162.     /** Returns the temperature at the station [K].
  163.      * @return the temperature at the station [K]
  164.      */
  165.     public double getTemperature() {
  166.         return temperature;
  167.     }

  168.     /** Returns the pressure at the station [hPa].
  169.      * @return the pressure at the station [hPa]
  170.      */
  171.     public double getPressure() {
  172.         return pressure;
  173.     }

  174.     /** Returns the water vapor pressure at the station [hPa].
  175.      * @return the water vapor pressure at the station [hPa]
  176.      */
  177.     public double getWaterVaporPressure() {
  178.         return e0;
  179.     }

  180.     @Override
  181.     public void weatherParameters(final double stationHeight, final AbsoluteDate currentDate) {

  182.         this.date      = currentDate;
  183.         this.height    = stationHeight;

  184.         final int dayOfYear = currentDate.getComponents(TimeScalesFactory.getUTC()).getDate().getDayOfYear();

  185.         // ah and aw coefficients
  186.         coefficientsA = new double[] {
  187.             interpolate(e -> evaluate(dayOfYear, e.ah)) * 0.001,
  188.             interpolate(e -> evaluate(dayOfYear, e.aw)) * 0.001
  189.         };

  190.         // Corrected height (can be negative)
  191.         final double undu            = geoid.getUndulation(latitude, longitude, date);
  192.         final double correctedheight = height - undu - interpolate(e -> e.hS);

  193.         // Temperature gradient [K/m]
  194.         final double dTdH = interpolate(e -> evaluate(dayOfYear, e.dT)) * 0.001;

  195.         // Specific humidity
  196.         final double qv = interpolate(e -> evaluate(dayOfYear, e.qv0)) * 0.001;

  197.         // For the computation of the temperature and the pressure, we use
  198.         // the standard ICAO atmosphere formulas.

  199.         // Temperature [K]
  200.         final double t0 = interpolate(e -> evaluate(dayOfYear, e.temperature0));
  201.         this.temperature = t0 + dTdH * correctedheight;

  202.         // Pressure [hPa]
  203.         final double p0 = interpolate(e -> evaluate(dayOfYear, e.pressure0));
  204.         final double exponent = G / (dTdH * R);
  205.         this.pressure = p0 * FastMath.pow(1 - (dTdH / t0) * correctedheight, exponent) * 0.01;

  206.         // Water vapor pressure [hPa]
  207.         this.e0 = qv * pressure / (0.622 + 0.378 * qv);

  208.     }

  209.     /** Interpolate a grid function.
  210.      * @param gridGetter getter for the grid function
  211.      * @return interpolated function"
  212.      */
  213.     private double interpolate(final ToDoubleFunction<GridEntry> gridGetter) {

  214.         // cell surrounding the point
  215.         final double[] xVal = new double[] {
  216.             southWest.longitude, southEast.longitude
  217.         };
  218.         final double[] yVal = new double[] {
  219.             southWest.latitude, northWest.latitude
  220.         };

  221.         // evaluate grid points at specified day
  222.         final double[][] fval = new double[][] {
  223.             {
  224.                 gridGetter.applyAsDouble(southWest),
  225.                 gridGetter.applyAsDouble(northWest)
  226.             }, {
  227.                 gridGetter.applyAsDouble(southEast),
  228.                 gridGetter.applyAsDouble(northEast)
  229.             }
  230.         };

  231.         // perform interpolation in the grid
  232.         return new BilinearInterpolatingFunction(xVal, yVal, fval).value(longitude, latitude);

  233.     }

  234.     /** Evaluate a model for some day.
  235.      * @param dayOfYear day to evaluate
  236.      * @param model model array
  237.      * @return model value at specified day
  238.      */
  239.     private double evaluate(final int dayOfYear, final double[] model) {

  240.         final double coef = (dayOfYear / 365.25) * 2 * FastMath.PI;
  241.         final SinCos sc1  = FastMath.sinCos(coef);
  242.         final SinCos sc2  = FastMath.sinCos(2.0 * coef);

  243.         return model[0] +
  244.                model[1] * sc1.cos() + model[2] * sc1.sin() +
  245.                model[3] * sc2.cos() + model[4] * sc2.sin();

  246.     }

  247.     /** Parser for GPT2 grid files. */
  248.     private static class Parser implements DataLoader {

  249.         /** Grid entries. */
  250.         private Grid grid;

  251.         @Override
  252.         public boolean stillAcceptsData() {
  253.             return grid == null;
  254.         }

  255.         @Override
  256.         public void loadData(final InputStream input, final String name)
  257.             throws IOException, ParseException {

  258.             final SortedSet<Integer> latSample = new TreeSet<>();
  259.             final SortedSet<Integer> lonSample = new TreeSet<>();
  260.             final List<GridEntry>    entries   = new ArrayList<>();

  261.             // Open stream and parse data
  262.             int   lineNumber = 0;
  263.             String line      = null;
  264.             try (InputStreamReader isr = new InputStreamReader(input, StandardCharsets.UTF_8);
  265.                  BufferedReader    br = new BufferedReader(isr)) {
  266.                 final String splitter = "\\s+";

  267.                 for (line = br.readLine(); line != null; line = br.readLine()) {
  268.                     ++lineNumber;
  269.                     line = line.trim();

  270.                     // read grid data
  271.                     if (line.length() > 0 && !line.startsWith("%")) {
  272.                         final GridEntry entry = new GridEntry(line.split(splitter));
  273.                         latSample.add(entry.latKey);
  274.                         lonSample.add(entry.lonKey);
  275.                         entries.add(entry);
  276.                     }

  277.                 }
  278.             } catch (NumberFormatException nfe) {
  279.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  280.                                           lineNumber, name, line);
  281.             }

  282.             // organize entries in a grid that wraps arouns Earth in longitude
  283.             grid = new Grid(latSample, lonSample, entries, name);

  284.         }

  285.     }

  286.     /** Container for complete grid. */
  287.     private static class Grid {

  288.         /** Latitude sample. */
  289.         private final SortedSet<Integer> latitudeSample;

  290.         /** Longitude sample. */
  291.         private final SortedSet<Integer> longitudeSample;

  292.         /** Grid entries. */
  293.         private final GridEntry[][] entries;

  294.         /** Simple constructor.
  295.          * @param latitudeSample latitude sample
  296.          * @param longitudeSample longitude sample
  297.          * @param loadedEntries loaded entries, organized as a simple list
  298.          * @param name file name
  299.          */
  300.         Grid(final SortedSet<Integer> latitudeSample, final SortedSet<Integer> longitudeSample,
  301.              final List<GridEntry> loadedEntries, final String name) {

  302.             final int nA         = latitudeSample.size();
  303.             final int nO         = longitudeSample.size() + 1; // we add one here for wrapping the grid
  304.             this.entries         = new GridEntry[nA][nO];
  305.             this.latitudeSample  = latitudeSample;
  306.             this.longitudeSample = longitudeSample;

  307.             // organize entries in the regular grid
  308.             for (final GridEntry entry : loadedEntries) {
  309.                 final int latitudeIndex  = latitudeSample.headSet(entry.latKey + 1).size() - 1;
  310.                 final int longitudeIndex = longitudeSample.headSet(entry.lonKey + 1).size() - 1;
  311.                 entries[latitudeIndex][longitudeIndex] = entry;
  312.             }

  313.             // finalize the grid
  314.             for (final GridEntry[] row : entries) {

  315.                 // check for missing entries
  316.                 for (int longitudeIndex = 0; longitudeIndex < nO - 1; ++longitudeIndex) {
  317.                     if (row[longitudeIndex] == null) {
  318.                         throw new OrekitException(OrekitMessages.IRREGULAR_OR_INCOMPLETE_GRID, name);
  319.                     }
  320.                 }

  321.                 // wrap the grid around the Earth in longitude
  322.                 row[nO - 1] = new GridEntry(row[0].latitude, row[0].latKey,
  323.                                             row[0].longitude + 2 * FastMath.PI,
  324.                                             row[0].lonKey + DEG_TO_MAS * 360,
  325.                                             row[0].hS, row[0].pressure0, row[0].temperature0,
  326.                                             row[0].qv0, row[0].dT, row[0].ah, row[0].aw);

  327.             }

  328.         }

  329.         /** Get index of South entries in the grid.
  330.          * @param latitude latitude to locate (radians)
  331.          * @return index of South entries in the grid
  332.          */
  333.         public int getSouthIndex(final double latitude) {

  334.             final int latKey = (int) FastMath.rint(FastMath.toDegrees(latitude) * DEG_TO_MAS);
  335.             final int index  = latitudeSample.headSet(latKey + 1).size() - 1;

  336.             // make sure we have at least one point remaining on North by clipping to size - 2
  337.             return FastMath.min(index, latitudeSample.size() - 2);

  338.         }

  339.         /** Get index of West entries in the grid.
  340.          * @param longitude longitude to locate (radians)
  341.          * @return index of West entries in the grid
  342.          */
  343.         public int getWestIndex(final double longitude) {

  344.             final int lonKey = (int) FastMath.rint(FastMath.toDegrees(longitude) * DEG_TO_MAS);
  345.             final int index  = longitudeSample.headSet(lonKey + 1).size() - 1;

  346.             // we don't do clipping in longitude because we have added a row to wrap around the Earth
  347.             return index;

  348.         }

  349.     }

  350.     /** Container for grid entries. */
  351.     private static class GridEntry {

  352.         /** Latitude (radian). */
  353.         private final double latitude;

  354.         /** Latitude key (mas). */
  355.         private final int latKey;

  356.         /** Longitude (radian). */
  357.         private final double longitude;

  358.         /** Longitude key (mas). */
  359.         private final int lonKey;

  360.         /** Height correction. */
  361.         private final double hS;

  362.         /** Pressure model. */
  363.         private final double[] pressure0;

  364.         /** Temperature model. */
  365.         private final double[] temperature0;

  366.         /** Specific humidity model. */
  367.         private final double[] qv0;

  368.         /** Temperature gradient model. */
  369.         private final double[] dT;

  370.         /** ah coefficient model. */
  371.         private final double[] ah;

  372.         /** aw coefficient model. */
  373.         private final double[] aw;

  374.         /** Build an entry from a parsed line.
  375.          * @param fields line fields
  376.          */
  377.         GridEntry(final String[] fields) {

  378.             final double latDegree = Double.parseDouble(fields[0]);
  379.             final double lonDegree = Double.parseDouble(fields[1]);
  380.             latitude     = FastMath.toRadians(latDegree);
  381.             longitude    = FastMath.toRadians(lonDegree);
  382.             latKey       = (int) FastMath.rint(latDegree * DEG_TO_MAS);
  383.             lonKey       = (int) FastMath.rint(lonDegree * DEG_TO_MAS);

  384.             hS           = Double.valueOf(fields[23]);

  385.             pressure0    = createModel(fields, 2);
  386.             temperature0 = createModel(fields, 7);
  387.             qv0          = createModel(fields, 12);
  388.             dT           = createModel(fields, 17);
  389.             ah           = createModel(fields, 24);
  390.             aw           = createModel(fields, 29);

  391.         }

  392.         /** Build an entry from its components.
  393.          * @param latitude latitude (radian)
  394.          * @param latKey latitude key (mas)
  395.          * @param longitude longitude (radian)
  396.          * @param lonKey longitude key (mas)
  397.          * @param hS height correction
  398.          * @param pressure0 pressure model
  399.          * @param temperature0 temperature model
  400.          * @param qv0 specific humidity model
  401.          * @param dT temperature gradient model
  402.          * @param ah ah coefficient model
  403.          * @param aw aw coefficient model
  404.          */
  405.         GridEntry(final double latitude, final int latKey, final double longitude, final int lonKey,
  406.                   final double hS, final double[] pressure0, final double[] temperature0,
  407.                   final double[] qv0, final double[] dT, final double[] ah, final double[] aw) {

  408.             this.latitude     = latitude;
  409.             this.latKey       = latKey;
  410.             this.longitude    = longitude;
  411.             this.lonKey       = lonKey;
  412.             this.hS           = hS;
  413.             this.pressure0    = pressure0.clone();
  414.             this.temperature0 = temperature0.clone();
  415.             this.qv0          = qv0.clone();
  416.             this.dT           = dT.clone();
  417.             this.ah           = ah.clone();
  418.             this.aw           = aw.clone();
  419.         }

  420.         /** Create a time model array.
  421.          * @param fields line fields
  422.          * @param first index of the first component of the model
  423.          * @return time model array
  424.          */
  425.         private double[] createModel(final String[] fields, final int first) {
  426.             return new double[] {
  427.                 Double.parseDouble(fields[first    ]),
  428.                 Double.parseDouble(fields[first + 1]),
  429.                 Double.parseDouble(fields[first + 2]),
  430.                 Double.parseDouble(fields[first + 3]),
  431.                 Double.parseDouble(fields[first + 4])
  432.             };
  433.         }

  434.     }

  435. }