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  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.InputStreamReader;
23  import java.nio.charset.StandardCharsets;
24  import java.text.ParseException;
25  import java.util.ArrayList;
26  import java.util.List;
27  import java.util.SortedSet;
28  import java.util.TreeSet;
29  import java.util.concurrent.atomic.AtomicReference;
30  import java.util.function.ToDoubleFunction;
31  
32  import org.hipparchus.analysis.interpolation.BilinearInterpolatingFunction;
33  import org.hipparchus.util.FastMath;
34  import org.hipparchus.util.MathUtils;
35  import org.hipparchus.util.SinCos;
36  import org.orekit.data.DataLoader;
37  import org.orekit.data.DataProvidersManager;
38  import org.orekit.errors.OrekitException;
39  import org.orekit.errors.OrekitMessages;
40  import org.orekit.time.AbsoluteDate;
41  import org.orekit.time.TimeScalesFactory;
42  import org.orekit.utils.Constants;
43  
44  /** The Global Pressure and Temperature 2 (GPT2) model.
45   * This model is an empirical model that provides the temperature, the pressure and the water vapor pressure
46   * of a site depending its latitude and  longitude. This model also provides the a<sub>h</sub>
47   * and a<sub>w</sub> coefficients used for the {@link ViennaOneModel Vienna 1} model.
48   * <p>
49   * The requisite coefficients for the computation of the weather parameters are provided by the
50   * Department of Geodesy and Geoinformation of the Vienna University. They are based on an
51   * external grid file like "gpt2_1.grd" (1° x 1°) or "gpt2_5.grd" (5° x 5°) available at:
52   * <a href="http://vmf.geo.tuwien.ac.at/codes/"> link</a>
53   * </p>
54   * <p>
55   * A bilinear interpolation is performed in order to obtained the correct values of the weather parameters.
56   * </p>
57   * <p>
58   * The format is always the same, with and example shown below for the pressure and the temperature.
59   * <p>
60   * Example:
61   * </p>
62   * <pre>
63   * %  lat    lon   p:a0    A1   B1   A2   B2  T:a0    A1   B1   A2   B2
64   *   87.5    2.5 101421    21  409 -217 -122 259.2 -13.2 -6.1  2.6  0.3
65   *   87.5    7.5 101416    21  411 -213 -120 259.3 -13.1 -6.1  2.6  0.3
66   *   87.5   12.5 101411    22  413 -209 -118 259.3 -13.1 -6.1  2.6  0.3
67   *   87.5   17.5 101407    23  415 -205 -116 259.4 -13.0 -6.1  2.6  0.3
68   *   ...
69   * </pre>
70   *
71   * @see K. Lagler, M. Schindelegger, J. Böhm, H. Krasna, T. Nilsson (2013),
72   * GPT2: empirical slant delay model for radio space geodetic techniques. Geophys
73   * Res Lett 40(6):1069–1073. doi:10.1002/grl.50288
74   *
75   * @author Bryan Cazabonne
76   *
77   */
78  public class GlobalPressureTemperature2Model implements WeatherModel {
79  
80      /** Default supported files name pattern. */
81      public static final String DEFAULT_SUPPORTED_NAMES = "gpt2_\\d+.grd";
82  
83      /** Standard gravity constant [m/s²]. */
84      private static final double G = Constants.G0_STANDARD_GRAVITY;
85  
86      /** Ideal gas constant for dry air [J/kg/K]. */
87      private static final double R = 287.0;
88  
89      /** Conversion factor from degrees to mill arcseconds. */
90      private static final int DEG_TO_MAS = 3600000;
91  
92      /** Shared lazily loaded grid. */
93      private static final AtomicReference<Grid> SHARED_GRID = new AtomicReference<>(null);
94  
95      /** South-West grid entry. */
96      private final GridEntry southWest;
97  
98      /** South-East grid entry. */
99      private final GridEntry southEast;
100 
101     /** North-West grid entry. */
102     private final GridEntry northWest;
103 
104     /** North-East grid entry. */
105     private final GridEntry northEast;
106 
107     /** The hydrostatic and wet a coefficients loaded. */
108     private double[] coefficientsA;
109 
110     /** Geodetic site latitude, radians.*/
111     private double latitude;
112 
113     /** Geodetic site longitude, radians.*/
114     private double longitude;
115 
116     /** Temperature site, in kelvins. */
117     private double temperature;
118 
119     /** Pressure site, in hPa. */
120     private double pressure;
121 
122     /** water vapour pressure, in hPa. */
123     private double e0;
124 
125     /** The height of the station in m. */
126     private double height;
127 
128     /** Geoid used to compute the undulations. */
129     private final Geoid geoid;
130 
131     /** Current date. */
132     private AbsoluteDate date;
133 
134     /** Constructor with supported names given by user.
135      * @param supportedNames supported names
136      * @param latitude geodetic latitude of the station, in radians
137      * @param longitude longitude geodetic longitude of the station, in radians
138      * @param geoid level surface of the gravity potential of a body
139      */
140     public GlobalPressureTemperature2Model(final String supportedNames, final double latitude,
141                                            final double longitude, final Geoid geoid) {
142         this.coefficientsA = null;
143         this.temperature   = Double.NaN;
144         this.pressure      = Double.NaN;
145         this.e0            = Double.NaN;
146         this.geoid         = geoid;
147         this.latitude      = latitude;
148 
149         // get the lazily loaded shared grid
150         Grid grid = SHARED_GRID.get();
151         if (grid == null) {
152             // this is the first instance we create, we need to load the grid data
153             final Parser parser = new Parser();
154             DataProvidersManager.getInstance().feed(supportedNames, parser);
155             SHARED_GRID.compareAndSet(null, parser.grid);
156             grid = parser.grid;
157         }
158 
159         // Normalize longitude according to the grid
160         this.longitude = MathUtils.normalizeAngle(longitude, grid.entries[0][0].longitude + FastMath.PI);
161 
162         final int southIndex = grid.getSouthIndex(this.latitude);
163         final int westIndex  = grid.getWestIndex(this.longitude);
164         this.southWest = grid.entries[southIndex    ][westIndex    ];
165         this.southEast = grid.entries[southIndex    ][westIndex + 1];
166         this.northWest = grid.entries[southIndex + 1][westIndex    ];
167         this.northEast = grid.entries[southIndex + 1][westIndex + 1];
168 
169     }
170 
171     /** Constructor with default supported names.
172      * @param latitude geodetic latitude of the station, in radians
173      * @param longitude geodetic latitude of the station, in radians
174      * @param geoid level surface of the gravity potential of a body
175      */
176     public GlobalPressureTemperature2Model(final double latitude, final double longitude, final Geoid geoid) {
177         this(DEFAULT_SUPPORTED_NAMES, latitude, longitude, geoid);
178     }
179 
180     /** Returns the a coefficients array.
181      * <ul>
182      * <li>double[0] = a<sub>h</sub>
183      * <li>double[1] = a<sub>w</sub>
184      * </ul>
185      * @return the a coefficients array
186      */
187     public double[] getA() {
188         return coefficientsA.clone();
189     }
190 
191     /** Returns the temperature at the station [K].
192      * @return the temperature at the station [K]
193      */
194     public double getTemperature() {
195         return temperature;
196     }
197 
198     /** Returns the pressure at the station [hPa].
199      * @return the pressure at the station [hPa]
200      */
201     public double getPressure() {
202         return pressure;
203     }
204 
205     /** Returns the water vapor pressure at the station [hPa].
206      * @return the water vapor pressure at the station [hPa]
207      */
208     public double getWaterVaporPressure() {
209         return e0;
210     }
211 
212     @Override
213     public void weatherParameters(final double stationHeight, final AbsoluteDate currentDate) {
214 
215         this.date      = currentDate;
216         this.height    = stationHeight;
217 
218         final int dayOfYear = currentDate.getComponents(TimeScalesFactory.getUTC()).getDate().getDayOfYear();
219 
220         // ah and aw coefficients
221         coefficientsA = new double[] {
222             interpolate(e -> evaluate(dayOfYear, e.ah)) * 0.001,
223             interpolate(e -> evaluate(dayOfYear, e.aw)) * 0.001
224         };
225 
226         // Corrected height (can be negative)
227         final double undu            = geoid.getUndulation(latitude, longitude, date);
228         final double correctedheight = height - undu - interpolate(e -> e.hS);
229 
230         // Temperature gradient [K/m]
231         final double dTdH = interpolate(e -> evaluate(dayOfYear, e.dT)) * 0.001;
232 
233         // Specific humidity
234         final double qv = interpolate(e -> evaluate(dayOfYear, e.qv0)) * 0.001;
235 
236         // For the computation of the temperature and the pressure, we use
237         // the standard ICAO atmosphere formulas.
238 
239         // Temperature [K]
240         final double t0 = interpolate(e -> evaluate(dayOfYear, e.temperature0));
241         this.temperature = t0 + dTdH * correctedheight;
242 
243         // Pressure [hPa]
244         final double p0 = interpolate(e -> evaluate(dayOfYear, e.pressure0));
245         final double exponent = G / (dTdH * R);
246         this.pressure = p0 * FastMath.pow(1 - (dTdH / t0) * correctedheight, exponent) * 0.01;
247 
248         // Water vapor pressure [hPa]
249         this.e0 = qv * pressure / (0.622 + 0.378 * qv);
250 
251     }
252 
253     /** Interpolate a grid function.
254      * @param gridGetter getter for the grid function
255      * @return interpolated function"
256      */
257     private double interpolate(final ToDoubleFunction<GridEntry> gridGetter) {
258 
259         // cell surrounding the point
260         final double[] xVal = new double[] {
261             southWest.longitude, southEast.longitude
262         };
263         final double[] yVal = new double[] {
264             southWest.latitude, northWest.latitude
265         };
266 
267         // evaluate grid points at specified day
268         final double[][] fval = new double[][] {
269             {
270                 gridGetter.applyAsDouble(southWest),
271                 gridGetter.applyAsDouble(northWest)
272             }, {
273                 gridGetter.applyAsDouble(southEast),
274                 gridGetter.applyAsDouble(northEast)
275             }
276         };
277 
278         // perform interpolation in the grid
279         return new BilinearInterpolatingFunction(xVal, yVal, fval).value(longitude, latitude);
280 
281     }
282 
283     /** Evaluate a model for some day.
284      * @param dayOfYear day to evaluate
285      * @param model model array
286      * @return model value at specified day
287      */
288     private double evaluate(final int dayOfYear, final double[] model) {
289 
290         final double coef = (dayOfYear / 365.25) * 2 * FastMath.PI;
291         final SinCos sc1  = FastMath.sinCos(coef);
292         final SinCos sc2  = FastMath.sinCos(2.0 * coef);
293 
294         return model[0] +
295                model[1] * sc1.cos() + model[2] * sc1.sin() +
296                model[3] * sc2.cos() + model[4] * sc2.sin();
297 
298     }
299 
300     /** Parser for GPT2 grid files. */
301     private static class Parser implements DataLoader {
302 
303         /** Grid entries. */
304         private Grid grid;
305 
306         @Override
307         public boolean stillAcceptsData() {
308             return grid == null;
309         }
310 
311         @Override
312         public void loadData(final InputStream input, final String name)
313             throws IOException, ParseException {
314 
315             final SortedSet<Integer> latSample = new TreeSet<>();
316             final SortedSet<Integer> lonSample = new TreeSet<>();
317             final List<GridEntry>    entries   = new ArrayList<>();
318 
319             // Open stream and parse data
320             int   lineNumber = 0;
321             String line      = null;
322             try (InputStreamReader isr = new InputStreamReader(input, StandardCharsets.UTF_8);
323                  BufferedReader    br = new BufferedReader(isr)) {
324                 final String splitter = "\\s+";
325 
326                 for (line = br.readLine(); line != null; line = br.readLine()) {
327                     ++lineNumber;
328                     line = line.trim();
329 
330                     // read grid data
331                     if (line.length() > 0 && !line.startsWith("%")) {
332                         final GridEntry entry = new GridEntry(line.split(splitter));
333                         latSample.add(entry.latKey);
334                         lonSample.add(entry.lonKey);
335                         entries.add(entry);
336                     }
337 
338                 }
339             } catch (NumberFormatException nfe) {
340                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
341                                           lineNumber, name, line);
342             }
343 
344             // organize entries in a grid that wraps arouns Earth in longitude
345             grid = new Grid(latSample, lonSample, entries, name);
346 
347         }
348 
349     }
350 
351     /** Container for complete grid. */
352     private static class Grid {
353 
354         /** Latitude sample. */
355         private final SortedSet<Integer> latitudeSample;
356 
357         /** Longitude sample. */
358         private final SortedSet<Integer> longitudeSample;
359 
360         /** Grid entries. */
361         private final GridEntry[][] entries;
362 
363         /** Simple constructor.
364          * @param latitudeSample latitude sample
365          * @param longitudeSample longitude sample
366          * @param loadedEntries loaded entries, organized as a simple list
367          * @param name file name
368          */
369         Grid(final SortedSet<Integer> latitudeSample, final SortedSet<Integer> longitudeSample,
370              final List<GridEntry> loadedEntries, final String name) {
371 
372             final int nA         = latitudeSample.size();
373             final int nO         = longitudeSample.size() + 1; // we add one here for wrapping the grid
374             this.entries         = new GridEntry[nA][nO];
375             this.latitudeSample  = latitudeSample;
376             this.longitudeSample = longitudeSample;
377 
378             // organize entries in the regular grid
379             for (final GridEntry entry : loadedEntries) {
380                 final int latitudeIndex  = latitudeSample.headSet(entry.latKey + 1).size() - 1;
381                 final int longitudeIndex = longitudeSample.headSet(entry.lonKey + 1).size() - 1;
382                 entries[latitudeIndex][longitudeIndex] = entry;
383             }
384 
385             // finalize the grid
386             for (final GridEntry[] row : entries) {
387 
388                 // check for missing entries
389                 for (int longitudeIndex = 0; longitudeIndex < nO - 1; ++longitudeIndex) {
390                     if (row[longitudeIndex] == null) {
391                         throw new OrekitException(OrekitMessages.IRREGULAR_OR_INCOMPLETE_GRID, name);
392                     }
393                 }
394 
395                 // wrap the grid around the Earth in longitude
396                 row[nO - 1] = new GridEntry(row[0].latitude, row[0].latKey,
397                                             row[0].longitude + 2 * FastMath.PI,
398                                             row[0].lonKey + DEG_TO_MAS * 360,
399                                             row[0].hS, row[0].pressure0, row[0].temperature0,
400                                             row[0].qv0, row[0].dT, row[0].ah, row[0].aw);
401 
402             }
403 
404         }
405 
406         /** Get index of South entries in the grid.
407          * @param latitude latitude to locate (radians)
408          * @return index of South entries in the grid
409          */
410         public int getSouthIndex(final double latitude) {
411 
412             final int latKey = (int) FastMath.rint(FastMath.toDegrees(latitude) * DEG_TO_MAS);
413             final int index  = latitudeSample.headSet(latKey + 1).size() - 1;
414 
415             // make sure we have at least one point remaining on North by clipping to size - 2
416             return FastMath.min(index, latitudeSample.size() - 2);
417 
418         }
419 
420         /** Get index of West entries in the grid.
421          * @param longitude longitude to locate (radians)
422          * @return index of West entries in the grid
423          */
424         public int getWestIndex(final double longitude) {
425 
426             final int lonKey = (int) FastMath.rint(FastMath.toDegrees(longitude) * DEG_TO_MAS);
427             final int index  = longitudeSample.headSet(lonKey + 1).size() - 1;
428 
429             // we don't do clipping in longitude because we have added a row to wrap around the Earth
430             return index;
431 
432         }
433 
434     }
435 
436     /** Container for grid entries. */
437     private static class GridEntry {
438 
439         /** Latitude (radian). */
440         private final double latitude;
441 
442         /** Latitude key (mas). */
443         private final int latKey;
444 
445         /** Longitude (radian). */
446         private final double longitude;
447 
448         /** Longitude key (mas). */
449         private final int lonKey;
450 
451         /** Height correction. */
452         private final double hS;
453 
454         /** Pressure model. */
455         private final double[] pressure0;
456 
457         /** Temperature model. */
458         private final double[] temperature0;
459 
460         /** Specific humidity model. */
461         private final double[] qv0;
462 
463         /** Temperature gradient model. */
464         private final double[] dT;
465 
466         /** ah coefficient model. */
467         private final double[] ah;
468 
469         /** aw coefficient model. */
470         private final double[] aw;
471 
472         /** Build an entry from a parsed line.
473          * @param fields line fields
474          */
475         GridEntry(final String[] fields) {
476 
477             final double latDegree = Double.parseDouble(fields[0]);
478             final double lonDegree = Double.parseDouble(fields[1]);
479             latitude     = FastMath.toRadians(latDegree);
480             longitude    = FastMath.toRadians(lonDegree);
481             latKey       = (int) FastMath.rint(latDegree * DEG_TO_MAS);
482             lonKey       = (int) FastMath.rint(lonDegree * DEG_TO_MAS);
483 
484             hS           = Double.valueOf(fields[23]);
485 
486             pressure0    = createModel(fields, 2);
487             temperature0 = createModel(fields, 7);
488             qv0          = createModel(fields, 12);
489             dT           = createModel(fields, 17);
490             ah           = createModel(fields, 24);
491             aw           = createModel(fields, 29);
492 
493         }
494 
495         /** Build an entry from its components.
496          * @param latitude latitude (radian)
497          * @param latKey latitude key (mas)
498          * @param longitude longitude (radian)
499          * @param lonKey longitude key (mas)
500          * @param hS height correction
501          * @param pressure0 pressure model
502          * @param temperature0 temperature model
503          * @param qv0 specific humidity model
504          * @param dT temperature gradient model
505          * @param ah ah coefficient model
506          * @param aw aw coefficient model
507          */
508         GridEntry(final double latitude, final int latKey, final double longitude, final int lonKey,
509                   final double hS, final double[] pressure0, final double[] temperature0,
510                   final double[] qv0, final double[] dT, final double[] ah, final double[] aw) {
511 
512             this.latitude     = latitude;
513             this.latKey       = latKey;
514             this.longitude    = longitude;
515             this.lonKey       = lonKey;
516             this.hS           = hS;
517             this.pressure0    = pressure0.clone();
518             this.temperature0 = temperature0.clone();
519             this.qv0          = qv0.clone();
520             this.dT           = dT.clone();
521             this.ah           = ah.clone();
522             this.aw           = aw.clone();
523         }
524 
525         /** Create a time model array.
526          * @param fields line fields
527          * @param first index of the first component of the model
528          * @return time model array
529          */
530         private double[] createModel(final String[] fields, final int first) {
531             return new double[] {
532                 Double.parseDouble(fields[first    ]),
533                 Double.parseDouble(fields[first + 1]),
534                 Double.parseDouble(fields[first + 2]),
535                 Double.parseDouble(fields[first + 3]),
536                 Double.parseDouble(fields[first + 4])
537             };
538         }
539 
540     }
541 
542 }