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