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% -> 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% -> 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% -> 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