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.troposphere;
18  
19  import java.util.Collections;
20  import java.util.List;
21  import java.util.regex.Pattern;
22  
23  import org.hipparchus.Field;
24  import org.hipparchus.CalculusFieldElement;
25  import org.hipparchus.analysis.interpolation.BilinearInterpolatingFunction;
26  import org.hipparchus.analysis.interpolation.LinearInterpolator;
27  import org.hipparchus.analysis.polynomials.PolynomialFunction;
28  import org.hipparchus.analysis.polynomials.PolynomialSplineFunction;
29  import org.hipparchus.util.FastMath;
30  import org.orekit.annotation.DefaultDataContext;
31  import org.orekit.bodies.FieldGeodeticPoint;
32  import org.orekit.bodies.GeodeticPoint;
33  import org.orekit.data.DataContext;
34  import org.orekit.data.DataProvidersManager;
35  import org.orekit.errors.OrekitException;
36  import org.orekit.errors.OrekitMessages;
37  import org.orekit.time.AbsoluteDate;
38  import org.orekit.time.FieldAbsoluteDate;
39  import org.orekit.utils.InterpolationTableLoader;
40  import org.orekit.utils.ParameterDriver;
41  
42  /** The modified Saastamoinen model. Estimates the path delay imposed to
43   * electro-magnetic signals by the troposphere according to the formula:
44   * <pre>
45   * δ = 2.277e-3 / cos z * (P + (1255 / T + 0.05) * e - B * tan²
46   * z) + δR
47   * </pre>
48   * with the following input data provided to the model:
49   * <ul>
50   * <li>z: zenith angle</li>
51   * <li>P: atmospheric pressure</li>
52   * <li>T: temperature</li>
53   * <li>e: partial pressure of water vapour</li>
54   * <li>B, δR: correction terms</li>
55   * </ul>
56   * <p>
57   * The model supports custom δR correction terms to be read from a
58   * configuration file (saastamoinen-correction.txt) via the
59   * {@link DataProvidersManager}.
60   * </p>
61   * @author Thomas Neidhart
62   * @see "Guochang Xu, GPS - Theory, Algorithms and Applications, Springer, 2007"
63   */
64  public class SaastamoinenModel implements DiscreteTroposphericModel {
65  
66      /** Default file name for δR correction term table. */
67      public static final String DELTA_R_FILE_NAME = "^saastamoinen-correction\\.txt$";
68  
69      /** Default lowest acceptable elevation angle [rad]. */
70      public static final double DEFAULT_LOW_ELEVATION_THRESHOLD = 0.05;
71  
72      /** First pattern for δR correction term table. */
73      private static final Pattern FIRST_DELTA_R_PATTERN = Pattern.compile("^\\^");
74  
75      /** Second pattern for δR correction term table. */
76      private static final Pattern SECOND_DELTA_R_PATTERN = Pattern.compile("\\$$");
77  
78      /** X values for the B function. */
79      private static final double[] X_VALUES_FOR_B = {
80          0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0
81      };
82  
83      /** E values for the B function. */
84      private static final double[] Y_VALUES_FOR_B = {
85          1.156, 1.079, 1.006, 0.938, 0.874, 0.813, 0.757, 0.654, 0.563
86      };
87  
88      /** Coefficients for the partial pressure of water vapor polynomial. */
89      private static final double[] E_COEFFICIENTS = {
90          -37.2465, 0.213166, -0.000256908
91      };
92  
93      /** Interpolation function for the B correction term. */
94      private final PolynomialSplineFunction bFunction;
95  
96      /** Polynomial function for the e term. */
97      private final PolynomialFunction eFunction;
98  
99      /** Interpolation function for the delta R correction term. */
100     private final BilinearInterpolatingFunction deltaRFunction;
101 
102     /** The temperature at the station [K]. */
103     private double t0;
104 
105     /** The atmospheric pressure [mbar]. */
106     private double p0;
107 
108     /** The humidity [percent]. */
109     private double r0;
110 
111     /** Lowest acceptable elevation angle [rad]. */
112     private double lowElevationThreshold;
113 
114     /**
115      * Create a new Saastamoinen model for the troposphere using the given environmental
116      * conditions and table from the reference book.
117      *
118      * @param t0 the temperature at the station [K]
119      * @param p0 the atmospheric pressure at the station [mbar]
120      * @param r0 the humidity at the station [fraction] (50% -&gt; 0.5)
121      * @see #SaastamoinenModel(double, double, double, String, DataProvidersManager)
122      * @since 10.1
123      */
124     public SaastamoinenModel(final double t0, final double p0, final double r0) {
125         this(t0, p0, r0, defaultDeltaR());
126     }
127 
128     /** Create a new Saastamoinen model for the troposphere using the given
129      * environmental conditions. This constructor uses the {@link DataContext#getDefault()
130      * default data context} if {@code deltaRFileName != null}.
131      *
132      * @param t0 the temperature at the station [K]
133      * @param p0 the atmospheric pressure at the station [mbar]
134      * @param r0 the humidity at the station [fraction] (50% -&gt; 0.5)
135      * @param deltaRFileName regular expression for filename containing δR
136      * correction term table (typically {@link #DELTA_R_FILE_NAME}), if null
137      * default values from the reference book are used
138      * @since 7.1
139      * @see #SaastamoinenModel(double, double, double, String, DataProvidersManager)
140      */
141     @DefaultDataContext
142     public SaastamoinenModel(final double t0, final double p0, final double r0,
143                              final String deltaRFileName) {
144         this(t0, p0, r0, deltaRFileName,
145                 DataContext.getDefault().getDataProvidersManager());
146     }
147 
148     /** Create a new Saastamoinen model for the troposphere using the given
149      * environmental conditions. This constructor allows the user to specify the source of
150      * of the δR file.
151      *
152      * @param t0 the temperature at the station [K]
153      * @param p0 the atmospheric pressure at the station [mbar]
154      * @param r0 the humidity at the station [fraction] (50% -&gt; 0.5)
155      * @param deltaRFileName regular expression for filename containing δR
156      * correction term table (typically {@link #DELTA_R_FILE_NAME}), if null
157      * default values from the reference book are used
158      * @param dataProvidersManager provides access to auxiliary data.
159      * @since 10.1
160      */
161     public SaastamoinenModel(final double t0,
162                              final double p0,
163                              final double r0,
164                              final String deltaRFileName,
165                              final DataProvidersManager dataProvidersManager) {
166         this(t0, p0, r0,
167              deltaRFileName == null ?
168                      defaultDeltaR() :
169                      loadDeltaR(deltaRFileName, dataProvidersManager));
170     }
171 
172     /** Create a new Saastamoinen model.
173      *
174      * @param t0 the temperature at the station [K]
175      * @param p0 the atmospheric pressure at the station [mbar]
176      * @param r0 the humidity at the station [fraction] (50% -> 0.5)
177      * @param deltaR δR correction term function
178      * @since 7.1
179      */
180     private SaastamoinenModel(final double t0, final double p0, final double r0,
181                               final BilinearInterpolatingFunction deltaR) {
182         this.t0             = t0;
183         this.p0             = p0;
184         this.r0             = r0;
185         this.bFunction      = new LinearInterpolator().interpolate(X_VALUES_FOR_B, Y_VALUES_FOR_B);
186         this.eFunction      = new PolynomialFunction(E_COEFFICIENTS);
187         this.deltaRFunction = deltaR;
188         this.lowElevationThreshold = DEFAULT_LOW_ELEVATION_THRESHOLD;
189     }
190 
191     /** Create a new Saastamoinen model using a standard atmosphere model.
192      *
193      * <ul>
194      * <li>temperature: 18 degree Celsius
195      * <li>pressure: 1013.25 mbar
196      * <li>humidity: 50%
197      * </ul>
198      *
199      * @return a Saastamoinen model with standard environmental values
200      */
201     public static SaastamoinenModel getStandardModel() {
202         return new SaastamoinenModel(273.16 + 18, 1013.25, 0.5);
203     }
204 
205     /** {@inheritDoc}
206      * <p>
207      * The Saastamoinen model is not defined for altitudes below 0.0. for continuity
208      * reasons, we use the value for h = 0 when altitude is negative.
209      * </p>
210      * <p>
211      * There are also numerical issues for elevation angles close to zero. For continuity reasons,
212      * elevations lower than a threshold will use the value obtained
213      * for the threshold itself.
214      * </p>
215      * @see #getLowElevationThreshold()
216      * @see #setLowElevationThreshold(double)
217      */
218     @Override
219     public double pathDelay(final double elevation, final GeodeticPoint point,
220                             final double[] parameters, final AbsoluteDate date) {
221 
222         // there are no data in the model for negative altitudes and altitude bigger than 5000 m
223         // limit the height to a range of [0, 5000] m
224         final double fixedHeight = FastMath.min(FastMath.max(0, point.getAltitude()), 5000);
225 
226         // the corrected temperature using a temperature gradient of -6.5 K/km
227         final double T = t0 - 6.5e-3 * fixedHeight;
228         // the corrected pressure
229         final double P = p0 * FastMath.pow(1.0 - 2.26e-5 * fixedHeight, 5.225);
230         // the corrected humidity
231         final double R = r0 * FastMath.exp(-6.396e-4 * fixedHeight);
232 
233         // interpolate the b correction term
234         final double B = bFunction.value(fixedHeight / 1e3);
235         // calculate e
236         final double e = R * FastMath.exp(eFunction.value(T));
237 
238         // calculate the zenith angle from the elevation
239         final double z = FastMath.abs(0.5 * FastMath.PI - FastMath.max(elevation, lowElevationThreshold));
240 
241         // get correction factor
242         final double deltaR = getDeltaR(fixedHeight, z);
243 
244         // calculate the path delay in m
245         final double tan = FastMath.tan(z);
246         final double delta = 2.277e-3 / FastMath.cos(z) *
247                              (P + (1255d / T + 5e-2) * e - B * tan * tan) + deltaR;
248 
249         return delta;
250     }
251 
252     /** {@inheritDoc}
253      * <p>
254      * The Saastamoinen model is not defined for altitudes below 0.0. for continuity
255      * reasons, we use the value for h = 0 when altitude is negative.
256      * </p>
257      * <p>
258      * There are also numerical issues for elevation angles close to zero. For continuity reasons,
259      * elevations lower than a threshold will use the value obtained
260      * for the threshold itself.
261      * </p>
262      * @see #getLowElevationThreshold()
263      * @see #setLowElevationThreshold(double)
264      */
265     @Override
266     public <T extends CalculusFieldElement<T>> T pathDelay(final T elevation, final FieldGeodeticPoint<T> point,
267                                                        final T[] parameters, final FieldAbsoluteDate<T> date) {
268 
269         final Field<T> field = date.getField();
270         final T zero = field.getZero();
271         // there are no data in the model for negative altitudes and altitude bigger than 5000 m
272         // limit the height to a range of [0, 5000] m
273         final T fixedHeight = FastMath.min(FastMath.max(zero, point.getAltitude()), zero.add(5000));
274 
275         // the corrected temperature using a temperature gradient of -6.5 K/km
276         final T T = fixedHeight.multiply(6.5e-3).negate().add(t0);
277         // the corrected pressure
278         final T P = fixedHeight.multiply(2.26e-5).negate().add(1.0).pow(5.225).multiply(p0);
279         // the corrected humidity
280         final T R = FastMath.exp(fixedHeight.multiply(-6.396e-4)).multiply(r0);
281 
282         // interpolate the b correction term
283         final T B = bFunction.value(fixedHeight.divide(1e3));
284         // calculate e
285         final T e = R.multiply(FastMath.exp(eFunction.value(T)));
286 
287         // calculate the zenith angle from the elevation
288         final T z = FastMath.abs(FastMath.max(elevation, zero.add(lowElevationThreshold)).negate().add(zero.getPi().multiply(0.5)));
289 
290         // get correction factor
291         final T deltaR = getDeltaR(fixedHeight, z, field);
292 
293         // calculate the path delay in m
294         final T tan = FastMath.tan(z);
295         final T delta = FastMath.cos(z).divide(2.277e-3).reciprocal().
296                         multiply(P.add(T.divide(1255d).reciprocal().add(5e-2).multiply(e)).subtract(B.multiply(tan).multiply(tan))).add(deltaR);
297 
298         return delta;
299     }
300 
301     /** Calculates the delta R correction term using linear interpolation.
302      * @param height the height of the station in m
303      * @param zenith the zenith angle of the satellite
304      * @return the delta R correction term in m
305      */
306     private double getDeltaR(final double height, final double zenith) {
307         // limit the height to a range of [0, 5000] m
308         final double h = FastMath.min(FastMath.max(0, height), 5000);
309         // limit the zenith angle to 90 degree
310         // Note: the function is symmetric for negative zenith angles
311         final double z = FastMath.min(Math.abs(zenith), 0.5 * FastMath.PI);
312         return deltaRFunction.value(h, z);
313     }
314 
315     /** Calculates the delta R correction term using linear interpolation.
316      * @param <T> type of the elements
317      * @param height the height of the station in m
318      * @param zenith the zenith angle of the satellite
319      * @param field field used by default
320      * @return the delta R correction term in m
321      */
322     private  <T extends CalculusFieldElement<T>> T getDeltaR(final T height, final T zenith,
323                                                          final Field<T> field) {
324         final T zero = field.getZero();
325         // limit the height to a range of [0, 5000] m
326         final T h = FastMath.min(FastMath.max(zero, height), zero.add(5000));
327         // limit the zenith angle to 90 degree
328         // Note: the function is symmetric for negative zenith angles
329         final T z = FastMath.min(zenith.abs(), zero.getPi().multiply(0.5));
330         return deltaRFunction.value(h, z);
331     }
332 
333     /** Load δR function.
334      * @param deltaRFileName regular expression for filename containing δR
335      * correction term table
336      * @param dataProvidersManager provides access to auxiliary data.
337      * @return δR function
338      */
339     private static BilinearInterpolatingFunction loadDeltaR(
340             final String deltaRFileName,
341             final DataProvidersManager dataProvidersManager) {
342 
343         // read the δR interpolation function from the config file
344         final InterpolationTableLoader loader = new InterpolationTableLoader();
345         dataProvidersManager.feed(deltaRFileName, loader);
346         if (!loader.stillAcceptsData()) {
347             final double[] elevations = loader.getOrdinateGrid();
348             for (int i = 0; i < elevations.length; ++i) {
349                 elevations[i] = FastMath.toRadians(elevations[i]);
350             }
351             return new BilinearInterpolatingFunction(loader.getAbscissaGrid(), elevations,
352                                                      loader.getValuesSamples());
353         }
354         throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE,
355                                   SECOND_DELTA_R_PATTERN.
356                                   matcher(FIRST_DELTA_R_PATTERN.matcher(deltaRFileName).replaceAll("")).
357                                   replaceAll(""));
358     }
359 
360     /** Create the default δR function.
361      * @return δR function
362      */
363     private static BilinearInterpolatingFunction defaultDeltaR() {
364 
365         // the correction table in the referenced book only contains values for an angle of 60 - 80
366         // degree, thus for 0 degree, the correction term is assumed to be 0, for degrees > 80 it
367         // is assumed to be the same value as for 80.
368 
369         // the height in m
370         final double[] xValForR = {
371             0, 500, 1000, 1500, 2000, 3000, 4000, 5000
372         };
373 
374         // the zenith angle
375         final double[] yValForR = {
376             FastMath.toRadians( 0.00), FastMath.toRadians(60.00), FastMath.toRadians(66.00), FastMath.toRadians(70.00),
377             FastMath.toRadians(73.00), FastMath.toRadians(75.00), FastMath.toRadians(76.00), FastMath.toRadians(77.00),
378             FastMath.toRadians(78.00), FastMath.toRadians(78.50), FastMath.toRadians(79.00), FastMath.toRadians(79.50),
379             FastMath.toRadians(79.75), FastMath.toRadians(80.00), FastMath.toRadians(90.00)
380         };
381 
382         final double[][] fval = new double[][] {
383             {
384                 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
385             }, {
386                 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
387             }, {
388                 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
389             }, {
390                 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
391             }, {
392                 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
393             }, {
394                 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
395             }, {
396                 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
397             }, {
398                 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
399             }
400         };
401 
402         // the actual delta R is interpolated using a a bilinear interpolator
403         return new BilinearInterpolatingFunction(xValForR, yValForR, fval);
404 
405     }
406 
407     /** {@inheritDoc} */
408     @Override
409     public List<ParameterDriver> getParametersDrivers() {
410         return Collections.emptyList();
411     }
412 
413     /** Get the low elevation threshold value for path delay computation.
414      * @return low elevation threshold, in rad.
415      * @see #pathDelay(double, GeodeticPoint, double[], AbsoluteDate)
416      * @see #pathDelay(CalculusFieldElement, FieldGeodeticPoint, CalculusFieldElement[], FieldAbsoluteDate)
417      * @since 10.2
418      */
419     public double getLowElevationThreshold() {
420         return lowElevationThreshold;
421     }
422 
423     /** Set the low elevation threshold value for path delay computation.
424      * @param lowElevationThreshold The new value for the threshold [rad]
425      * @see #pathDelay(double, GeodeticPoint, double[], AbsoluteDate)
426      * @see #pathDelay(CalculusFieldElement, FieldGeodeticPoint, CalculusFieldElement[], FieldAbsoluteDate)
427      * @since 10.2
428      */
429     public void setLowElevationThreshold(final double lowElevationThreshold) {
430         this.lowElevationThreshold = lowElevationThreshold;
431     }
432 }
433