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