1 //==============================================================================
2 //
3 //  This file is part of GPSTk, the GPS Toolkit.
4 //
5 //  The GPSTk is free software; you can redistribute it and/or modify
6 //  it under the terms of the GNU Lesser General Public License as published
7 //  by the Free Software Foundation; either version 3.0 of the License, or
8 //  any later version.
9 //
10 //  The GPSTk is distributed in the hope that it will be useful,
11 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 //  GNU Lesser General Public License for more details.
14 //
15 //  You should have received a copy of the GNU Lesser General Public
16 //  License along with GPSTk; if not, write to the Free Software Foundation,
17 //  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110, USA
18 //
19 //  This software was developed by Applied Research Laboratories at the
20 //  University of Texas at Austin.
21 //  Copyright 2004-2020, The Board of Regents of The University of Texas System
22 //
23 //==============================================================================
24 
25 //==============================================================================
26 //
27 //  This software was developed by Applied Research Laboratories at the
28 //  University of Texas at Austin, under contract to an agency or agencies
29 //  within the U.S. Department of Defense. The U.S. Government retains all
30 //  rights to use, duplicate, distribute, disclose, or release this software.
31 //
32 //  Pursuant to DoD Directive 523024
33 //
34 //  DISTRIBUTION STATEMENT A: This software has been approved for public
35 //                            release, distribution is unlimited.
36 //
37 //==============================================================================
38 
39 
40 #include "YDSTime.hpp"
41 #include "SaasTropModel.hpp"
42 
43 #define THROW_IF_INVALID_DETAILED() {if (!valid) {\
44          InvalidTropModel e;                                            \
45          if(!validWeather)    e.addText("Invalid trop model: Weather"); \
46          if(!validRxHeight)   e.addText("Invalid trop model: Rx Height"); \
47          if(!validRxLatitude) e.addText("Invalid trop model: Rx Latitude"); \
48          if(!validDOY)        e.addText("Invalid trop model: day of year"); \
49          GPSTK_THROW(e);}}
50 
51 namespace gpstk
52 {
53    // Saastamoinen tropospheric model.
54    // This model needs work; it is not the Saastamoinen model, but appears to be
55    // a combination of the Neill mapping functions and an unknown delay model.
56    // Based on Saastamoinen, J., 'Atmospheric
57    // Correction for the Troposphere and Stratosphere in Radio Ranging of
58    // Satellites,' Geophysical Monograph 15, American Geophysical Union, 1972,
59    // and Ch. 9 of McCarthy, D. and Petit, G., IERS Conventions (2003), IERS
60    // Technical Note 32, IERS, 2004. The mapping functions are from
61    // Neill, A.E., 1996, 'Global Mapping Functions for the Atmosphere Delay of
62    // Radio Wavelengths,' J. Geophys. Res., 101, pp. 3227-3246 (also see IERS TN 32).
63    //
64    // This model includes a wet and dry component, and requires input of the
65    // geodetic latitude, day of year and height above the ellipsoid of the receiver.
66    //
67    // Usually, the caller will set the latitude and day of year at the same
68    // time the weather is set
69    //   SaasTropModel stm;
70    //   stm.setReceiverLatitude(lat);
71    //   stm.setDayOfYear(doy);
72    //   stm.setWeather(T,P,H);
73    // Then, when the correction (and/or delay and map) is computed, receiver height
74    // should be set before the call to correction(elevation):
75    //   stm.setReceiverHeight(height);
76    //   trop_corr = stm.correction(elevation);
77    //
78    // NB in this model, units of 'temp' are degrees Celcius and
79    // humid actually stores water vapor partial pressure in mbars
80    //
81 
82    // constants for wet mapping function
83    static const double SaasWetA[5]=
84      { 0.00058021897, 0.00056794847, 0.00058118019, 0.00059727542, 0.00061641693 };
85    static const double SaasWetB[5]=
86      { 0.0014275268, 0.0015138625, 0.0014572752, 0.0015007428, 0.0017599082 };
87    static const double SaasWetC[5]=
88      { 0.043472961, 0.046729510, 0.043908931, 0.044626982, 0.054736038 };
89 
90    // constants for dry mapping function
91    static const double SaasDryA[5]=
92      { 0.0012769934, 0.0012683230, 0.0012465397, 0.0012196049, 0.0012045996 };
93    static const double SaasDryB[5]=
94      { 0.0029153695, 0.0029152299, 0.0029288445, 0.0029022565, 0.0029024912 };
95    static const double SaasDryC[5]=
96      { 0.062610505, 0.062837393, 0.063721774, 0.063824265, 0.064258455 };
97 
98    static const double SaasDryA1[5]=
99      { 0.0, 0.000012709626, 0.000026523662, 0.000034000452, 0.000041202191 };
100    static const double SaasDryB1[5]=
101      { 0.0, 0.000021414979, 0.000030160779, 0.000072562722, 0.00011723375 };
102    static const double SaasDryC1[5]=
103      { 0.0, 0.000090128400, 0.000043497037, 0.00084795348, 0.0017037206 };
104 
105       // Default constructor
SaasTropModel(void)106    SaasTropModel::SaasTropModel(void)
107    {
108       validWeather = false;
109       validRxLatitude = false;
110       validDOY = false;
111       validRxHeight = false;
112    } // end SaasTropModel::SaasTropModel()
113 
114       // Create a trop model using the minimum information: latitude and doy.
115       // Interpolate the weather unless setWeather (optional) is called.
116       // @param lat Latitude of the receiver in degrees.
117       // @param day Day of year.
SaasTropModel(const double & lat,const int & day)118    SaasTropModel::SaasTropModel(const double& lat,
119                                 const int& day)
120    {
121       validWeather = false;
122       validRxHeight = false;
123       SaasTropModel::setReceiverLatitude(lat);
124       SaasTropModel::setDayOfYear(day);
125    } // end SaasTropModel::SaasTropModel
126 
127       // Create a trop model with weather.
128       // @param lat Latitude of the receiver in degrees.
129       // @param day Day of year.
130       // @param wx the weather to use for this correction.
SaasTropModel(const double & lat,const int & day,const WxObservation & wx)131    SaasTropModel::SaasTropModel(const double& lat,
132                                 const int& day,
133                                 const WxObservation& wx)
134    {
135       validRxHeight = false;
136       SaasTropModel::setReceiverLatitude(lat);
137       SaasTropModel::setDayOfYear(day);
138       SaasTropModel::setWeather(wx);
139    }  // end SaasTropModel::SaasTropModel(weather)
140 
141       // Create a tropospheric model from explicit weather data
142       // @param lat Latitude of the receiver in degrees.
143       // @param day Day of year.
144       // @param T temperature in degrees Celsius
145       // @param P atmospheric pressure in millibars
146       // @param H relative humidity in percent
SaasTropModel(const double & lat,const int & day,const double & T,const double & P,const double & H)147    SaasTropModel::SaasTropModel(const double& lat,
148                                 const int& day,
149                                 const double& T,
150                                 const double& P,
151                                 const double& H)
152    {
153       validRxHeight = false;
154       SaasTropModel::setReceiverLatitude(lat);
155       SaasTropModel::setDayOfYear(day);
156       SaasTropModel::setWeather(T,P,H);
157    } // end SaasTropModel::SaasTropModel()
158 
159       // re-define this to get the throws correct
correction(double elevation) const160    double SaasTropModel::correction(double elevation) const
161    {
162       if(!valid) {
163          if(!validWeather) GPSTK_THROW(
164             InvalidTropModel("Invalid Saastamoinen trop model: weather"));
165          if(!validRxLatitude) GPSTK_THROW(
166             InvalidTropModel("Invalid Saastamoinen trop model: Rx Latitude"));
167          if(!validRxHeight) GPSTK_THROW(
168             InvalidTropModel("Invalid Saastamoinen trop model: Rx Height"));
169          if(!validDOY) GPSTK_THROW(
170             InvalidTropModel("Invalid Saastamoinen trop model: day of year"));
171          GPSTK_THROW(
172             InvalidTropModel("Valid flag corrupted in Saastamoinen trop model"));
173       }
174 
175       if(elevation < 0.0) return 0.0;
176 
177       double corr=0.0;
178       try {
179          corr = (dry_zenith_delay() * dry_mapping_function(elevation)
180             + wet_zenith_delay() * wet_mapping_function(elevation));
181       }
182       catch(Exception& e) { GPSTK_RETHROW(e); }
183 
184       return corr;
185 
186    }  // end SaasTropModel::correction(elevation)
187 
188       // Compute and return the full tropospheric delay, given the positions of
189       // receiver and satellite and the time tag. This version is most useful
190       // within positioning algorithms, where the receiver position and timetag
191       // may vary; it computes the elevation (and other receiver location
192       // information) and passes them to appropriate set...() routines
193       // and the correction(elevation) routine.
194       // @param RX  Receiver position
195       // @param SV  Satellite position
196       // @param tt  Time tag of the signal
correction(const Position & RX,const Position & SV,const CommonTime & tt)197    double SaasTropModel::correction(const Position& RX,
198                                     const Position& SV,
199                                     const CommonTime& tt)
200    {
201       SaasTropModel::setReceiverHeight(RX.getHeight());
202       SaasTropModel::setReceiverLatitude(RX.getGeodeticLatitude());
203       SaasTropModel::setDayOfYear(int((static_cast<YDSTime>(tt).doy)));
204 
205       if(!valid) {
206          if(!validWeather) GPSTK_THROW(
207             InvalidTropModel("Invalid Saastamoinen trop model: weather"));
208          if(!validRxLatitude) GPSTK_THROW(
209             InvalidTropModel("Invalid Saastamoinen trop model: Rx Latitude"));
210          if(!validRxHeight) GPSTK_THROW(
211             InvalidTropModel("Invalid Saastamoinen trop model: Rx Height"));
212          if(!validDOY) GPSTK_THROW(
213             InvalidTropModel("Invalid Saastamoinen trop model: day of year"));
214          valid = true;
215       }
216 
217       double corr=0.0;
218       try {
219          corr = SaasTropModel::correction(RX.elevation(SV));
220       }
221       catch(Exception& e) { GPSTK_RETHROW(e); }
222 
223       return corr;
224 
225    }  // end SaasTropModel::correction(RX,SV,TT)
226 
correction(const Xvt & RX,const Xvt & SV,const CommonTime & tt)227    double SaasTropModel::correction(const Xvt& RX,
228                                     const Xvt& SV,
229                                     const CommonTime& tt)
230    {
231       Position R(RX),S(SV);
232       return SaasTropModel::correction(R,S,tt);
233    }
234 
235       // Compute and return the zenith delay for dry component of the troposphere
dry_zenith_delay(void) const236    double SaasTropModel::dry_zenith_delay(void) const
237    {
238       THROW_IF_INVALID_DETAILED();
239 
240       //return (0.0022768*pr/(1-0.00266*::cos(2*lat*DEG_TO_RAD)-0.00028*ht/1000.));
241       return SaasDryDelay(press,latitude,height);
242 
243    }  // end SaasTropModel::dry_zenith_delay()
244 
245       // Compute and return the zenith delay for wet component of the troposphere
wet_zenith_delay(void) const246    double SaasTropModel::wet_zenith_delay(void) const
247    {
248       THROW_IF_INVALID_DETAILED();
249 
250       double T = temp+CELSIUS_TO_KELVIN;
251 
252       // partial pressure due to water vapor. Leick 4th ed 8.2.4
253       double pwv = 0.01 * humid * ::exp(-37.2465+0.213166*T-0.000256908*T*T);
254       // IERS2003 Ch 9 pg 99 - very similar to Leick above
255       //double pwv = 0.01*humid
256       //      * 0.01*::exp(33.93711047-1.9121316e-2*T+1.2378847e-5*T*T-6.3431645e3/T)
257       //      * (1.00062+3.14e-6*press+5.6e-7*temp);
258 
259       // Saastamoinen 1973 Atmospheric correction for the troposphere and
260       // stratosphere in radio ranging of satellites. The use of artificial
261       // satellites for geodesy, Geophys. Monogr. Ser. 15, Amer. Geophys. Union,
262       // pp. 274-251, 1972, modified for gravity as in Davis etal 1985
263       return ( 0.002277*((1255/T)+0.05)*pwv /
264                   (1-0.00266*::cos(2*latitude*DEG_TO_RAD)-0.00028*height/1000.) );
265 
266    }  // end SaasTropModel::wet_zenith_delay()
267 
268       // Compute and return the mapping function for dry component of the troposphere
269       // @param elevation Elevation of satellite as seen at receiver, in degrees
dry_mapping_function(double elevation) const270    double SaasTropModel::dry_mapping_function(double elevation) const
271    {
272       THROW_IF_INVALID_DETAILED();
273       if(elevation < 0.0) return 0.0;
274 
275       double lat,t,ct;
276       lat = fabs(latitude);         // degrees
277       t = doy - 28.;                // mid-winter
278       if(latitude < 0)              // southern hemisphere
279          t += 365.25/2.;
280       t *= 360.0/365.25;            // convert to degrees
281       ct = ::cos(t*DEG_TO_RAD);
282 
283       double a,b,c;
284       if(lat < 15.) {
285          a = SaasDryA[0];
286          b = SaasDryB[0];
287          c = SaasDryC[0];
288       }
289       else if(lat < 75.) {          // coefficients are for 15,30,45,60,75 deg
290          int i=int(lat/15.0)-1;
291          double frac=(lat-15.*(i+1))/15.;
292          a = SaasDryA[i] + frac*(SaasDryA[i+1]-SaasDryA[i]);
293          b = SaasDryB[i] + frac*(SaasDryB[i+1]-SaasDryB[i]);
294          c = SaasDryC[i] + frac*(SaasDryC[i+1]-SaasDryC[i]);
295 
296          a -= ct * (SaasDryA1[i] + frac*(SaasDryA1[i+1]-SaasDryA1[i]));
297          b -= ct * (SaasDryB1[i] + frac*(SaasDryB1[i+1]-SaasDryB1[i]));
298          c -= ct * (SaasDryC1[i] + frac*(SaasDryC1[i+1]-SaasDryC1[i]));
299       }
300       else {
301          a = SaasDryA[4] - ct * SaasDryA1[4];
302          b = SaasDryB[4] - ct * SaasDryB1[4];
303          c = SaasDryC[4] - ct * SaasDryC1[4];
304       }
305 
306       double se = ::sin(elevation*DEG_TO_RAD);
307       double map = (1.+a/(1.+b/(1.+c)))/(se+a/(se+b/(se+c)));
308 
309       a = 0.0000253;
310       b = 0.00549;
311       c = 0.00114;
312       map += (height/1000.0)*(1./se-(1+a/(1.+b/(1.+c)))/(se+a/(se+b/(se+c))));
313 
314       return map;
315 
316    }  // end SaasTropModel::dry_mapping_function()
317 
318       // Compute and return the mapping function for wet component of the troposphere
319       // @param elevation Elevation of satellite as seen at receiver, in degrees.
wet_mapping_function(double elevation) const320    double SaasTropModel::wet_mapping_function(double elevation) const
321    {
322       THROW_IF_INVALID_DETAILED();
323       if(elevation < 0.0) return 0.0;
324 
325       double a,b,c,lat;
326       lat = fabs(latitude);         // degrees
327       if(lat < 15.) {
328          a = SaasWetA[0];
329          b = SaasWetB[0];
330          c = SaasWetC[0];
331       }
332       else if(lat < 75.) {          // coefficients are for 15,30,45,60,75 deg
333          int i=int(lat/15.0)-1;
334          double frac=(lat-15.*(i+1))/15.;
335          a = SaasWetA[i] + frac*(SaasWetA[i+1]-SaasWetA[i]);
336          b = SaasWetB[i] + frac*(SaasWetB[i+1]-SaasWetB[i]);
337          c = SaasWetC[i] + frac*(SaasWetC[i+1]-SaasWetC[i]);
338       }
339       else {
340          a = SaasWetA[4];
341          b = SaasWetB[4];
342          c = SaasWetC[4];
343       }
344 
345       double se = ::sin(elevation*DEG_TO_RAD);
346       double map = (1.+a/(1.+b/(1.+c)))/(se+a/(se+b/(se+c)));
347 
348       return map;
349 
350    }
351 
352       // Re-define the weather data.
353       // If called, typically called before any calls to correction().
354       // @param T temperature in degrees Celsius
355       // @param P atmospheric pressure in millibars
356       // @param H relative humidity in percent
setWeather(const double & T,const double & P,const double & H)357    void SaasTropModel::setWeather(const double& T,
358                                   const double& P,
359                                   const double& H)
360    {
361       temp = T;
362       press = P;
363          // humid actually stores water vapor partial pressure
364       double exp=7.5*T/(T+237.3);
365       humid = 6.11 * (H/100.) * std::pow(10.0,exp);
366 
367       validWeather = true;
368       valid = (validWeather && validRxHeight && validRxLatitude && validDOY);
369 
370    }
371 
372       // Re-define the tropospheric model with explicit weather data.
373       // Typically called just before correction().
374       // @param wx the weather to use for this correction
setWeather(const WxObservation & wx)375    void SaasTropModel::setWeather(const WxObservation& wx)
376    {
377       try
378       {
379          SaasTropModel::setWeather(wx.temperature,wx.pressure,wx.humidity);
380       }
381       catch(InvalidParameter& e)
382       {
383          valid = validWeather = false;
384          GPSTK_RETHROW(e);
385       }
386    }
387 
388       // Define the receiver height; this required before calling
389       // correction() or any of the zenith_delay or mapping_function routines.
setReceiverHeight(const double & ht)390    void SaasTropModel::setReceiverHeight(const double& ht)
391    {
392       height = ht;
393       validRxHeight = true;
394       valid = (validWeather && validRxHeight && validRxLatitude && validDOY);
395    }
396 
397       // Define the latitude of the receiver; this is required before calling
398       // correction() or any of the zenith_delay or mapping_function routines.
setReceiverLatitude(const double & lat)399    void SaasTropModel::setReceiverLatitude(const double& lat)
400    {
401       latitude = lat;
402       validRxLatitude = true;
403       valid = (validWeather && validRxHeight && validRxLatitude && validDOY);
404    }
405 
406       // Define the day of year; this is required before calling
407       // correction() or any of the zenith_delay or mapping_function routines.
setDayOfYear(const int & d)408    void SaasTropModel::setDayOfYear(const int& d)
409    {
410       doy = d;
411       if(doy > 0 && doy < 367) validDOY=true; else validDOY = false;
412       valid = (validWeather && validRxHeight && validRxLatitude && validDOY);
413    }
414 }
415