///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 Jon Beniston, M7RCE //
// Copyright (C) 2004 Rutherford Appleton Laboratory //
// Copyright (C) 2012 Science and Technology Facilities Council. //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see . //
///////////////////////////////////////////////////////////////////////////////////
#include
#include
#include
#include
#include "util/units.h"
#include "astronomy.h"
// Function prototypes
static void palRefz(double zu, double refa, double refb, double *zr);
static void palRefco (double hm, double tdk, double pmb, double rh,
double wl, double phi, double tlr, double eps,
double *refa, double *refb);
static void palRefro( double zobs, double hm, double tdk, double pmb,
double rh, double wl, double phi, double tlr,
double eps, double * ref);
// Calculate Julian date (days since January 1, 4713 BC) from Gregorian calendar date
double Astronomy::julianDate(int year, int month, int day, int hours, int minutes, int seconds)
{
int julian_day;
double julian_date;
// From: https://en.wikipedia.org/wiki/Julian_day
julian_day = (1461 * (year + 4800 + (month - 14)/12))/4 +(367 * (month - 2 - 12 * ((month - 14)/12)))/12 - (3 * ((year + 4900 + (month - 14)/12)/100))/4 + day - 32075;
julian_date = julian_day + (hours/24.0 - 0.5) + minutes/(24.0*60.0) + seconds/(24.0*60.0*60.0);
return julian_date;
}
// Calculate Julian date
double Astronomy::julianDate(QDateTime dt)
{
QDateTime utc = dt.toUTC();
QDate date = utc.date();
QTime time = utc.time();
return julianDate(date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second());
}
// Get Julian date of J2000 Epoch
double Astronomy::jd_j2000(void)
{
static double j2000 = 0.0;
if (j2000 == 0.0) {
j2000 = julianDate(2000, 1, 1, 12, 0, 0);
}
return j2000;
}
// Get Julian date of B1950 Epoch
double Astronomy::jd_b1950(void)
{
static double b1950 = 0.0;
if (b1950 == 0.0) {
b1950 = julianDate(1949, 12, 31, 22, 9, 0);
}
return b1950;
}
// Get Julian date of current time Epoch
double Astronomy::jd_now(void)
{
time_t system_time;
struct tm *utc_time;
// Get current time in seconds since Unix Epoch (1970)
time(&system_time);
// Convert to UTC (GMT)
utc_time = gmtime(&system_time);
// Convert to Julian date
return julianDate(utc_time->tm_year + 1900, utc_time->tm_mon + 1, utc_time->tm_mday,
utc_time->tm_hour, utc_time->tm_min, utc_time->tm_sec);
}
// Precess a RA/DEC between two given Epochs
RADec Astronomy::precess(RADec rd_in, double jd_from, double jd_to)
{
RADec rd_out;
double x, y, z;
double xp, yp, zp;
double ra_rad, dec_rad;
double rot[3][3]; // [row][col]
double ra_deg;
double days_per_century = 36524.219878;
double t0 = (jd_from - jd_b1950())/days_per_century; // Tropical centuries since B1950.0
double t = (jd_to - jd_from)/days_per_century; // Tropical centuries from starting epoch to ending epoch
// From: https://www.cloudynights.com/topic/561254-ra-dec-epoch-conversion/
rot[0][0] = 1.0 - ((29696.0 + 26.0*t0)*t*t - 13.0*t*t*t)*.00000001;
rot[1][0] = ((2234941.0 + 1355.0*t0)*t - 676.0*t*t + 221.0*t*t*t)*.00000001;
rot[2][0] = ((971690.0 - 414.0*t0)*t + 207.0*t*t + 96.0*t*t*t)*.00000001;
rot[0][1] = -rot[1][0];
rot[1][1] = 1.0 - ((24975.0 + 30.0*t0)*t*t - 15.0*t*t*t)*.00000001;
rot[2][1] = -((10858.0 + 2.0*t0)*t*t)*.00000001;
rot[0][2] = -rot[2][0];
rot[1][2] = rot[2][1];
rot[2][2] = 1.0 - ((4721.0 - 4.0*t0)*t*t)*.00000001;
// Hours to degrees
ra_deg = rd_in.ra*(360.0/24.0);
// Convert to rectangular coordinates
ra_rad = Units::degreesToRadians(ra_deg);
dec_rad = Units::degreesToRadians(rd_in.dec);
x = cos(ra_rad) * cos(dec_rad);
y = sin(ra_rad) * cos(dec_rad);
z = sin(dec_rad);
// Rotate
xp = rot[0][0]*x + rot[0][1]*y + rot[0][2]*z;
yp = rot[1][0]*x + rot[1][1]*y + rot[1][2]*z;
zp = rot[2][0]*x + rot[2][1]*y + rot[2][2]*z;
// Convert back to spherical coordinates
rd_out.ra = Units::radiansToDegrees(atan(yp/xp));
if (xp < 0.0) {
rd_out.ra += 180.0;
} else if ((yp < 0) && (xp > 0)) {
rd_out.ra += 360.0;
}
rd_out.dec = Units::radiansToDegrees(asin(zp));
// Degrees to hours
rd_out.ra /= (360.0/24.0);
return rd_out;
}
// Calculate local mean sidereal time (LMST) in degrees
double Astronomy::localSiderealTime(QDateTime dateTime, double longitude)
{
double jd = julianDate(dateTime);
double d = (jd - jd_j2000()); // Days since J2000 epoch (including fraction)
double f = fmod(jd, 1.0); // Fractional part is decimal days
double ut = (f+0.5)*24.0; // Universal time in decimal hours
// https://astronomy.stackexchange.com/questions/24859/local-sidereal-time
// 100.46 is offset for GMST at 0h UT on 1 Jan 2000
// 0.985647 is number of degrees per day over 360 the Earth rotates in one solar day
// Approx to 0.3 arcseconds
return fmod(100.46 + 0.985647 * d + longitude + (360/24) * ut, 360.0);
}
// Convert from J2000 right ascension (decimal hours) and declination (decimal degrees) to altitude and azimuth, for location (decimal degrees) and time
AzAlt Astronomy::raDecToAzAlt(RADec rd, double latitude, double longitude, QDateTime dt, bool j2000)
{
AzAlt aa;
double ra_deg; // Right ascension in degrees
double lst_deg; // Local sidereal time in degrees
double ha; // Hour angle
double a, az;
double dec_rad, lat_rad, ha_rad, alt_rad; // Corresponding variables as radians
double jd = julianDate(dt);
// Precess RA/DEC from J2000 Epoch to desired (typically current) Epoch
if (j2000)
rd = precess(rd, jd_j2000(), jd);
// Calculate local mean sidereal time (LMST) in degrees
// https://astronomy.stackexchange.com/questions/24859/local-sidereal-time
// 100.46 is offset for GMST at 0h UT on 1 Jan 2000
// 0.985647 is number of degrees per day over 360 the Earth rotates in one solar day
// Approx to 0.3 arcseconds
lst_deg = Astronomy::localSiderealTime(dt, longitude);
// Convert right ascension from hours to degrees
ra_deg = rd.ra * (360.0/24.0);
// Calculate hour angle
ha = fmod(lst_deg - ra_deg, 360.0);
// Convert degrees to radians
dec_rad = Units::degreesToRadians(rd.dec);
lat_rad = Units::degreesToRadians(latitude);
ha_rad = Units::degreesToRadians(ha);
// Calculate altitude and azimuth - no correction for atmospheric refraction
// From: http://www.convertalot.com/celestial_horizon_co-ordinates_calculator.html
alt_rad = asin(sin(dec_rad)*sin(lat_rad) + cos(dec_rad)*cos(lat_rad)*cos(ha_rad));
a = Units::radiansToDegrees(acos((sin(dec_rad)-sin(alt_rad)*sin(lat_rad)) / (cos(alt_rad)*cos(lat_rad))));
if (sin(ha_rad) < 0.0) {
az = a;
} else {
az = 360.0 - a;
}
aa.alt = Units::radiansToDegrees(alt_rad);
aa.az = az;
return aa;
}
// Convert from altitude and azimuth, for location (decimal degrees) and time, to Jnow right ascension (decimal hours) and declination (decimal degrees)
// See: http://jonvoisey.net/blog/2018/07/data-converting-alt-az-to-ra-dec-example/
RADec Astronomy::azAltToRaDec(AzAlt aa, double latitude, double longitude, QDateTime dt)
{
RADec rd;
double lst_deg; // Local sidereal time
double ha_rad, ha_deg; // Hour angle
double alt_rad, az_rad, lat_rad, dec_rad; // Corresponding variables as radians
// Calculate local mean sidereal time (LMST) in degrees (see raDecToAzAlt)
lst_deg = Astronomy::localSiderealTime(dt, longitude);
// Convert degrees to radians
alt_rad = Units::degreesToRadians(aa.alt);
az_rad = Units::degreesToRadians(aa.az);
lat_rad = Units::degreesToRadians(latitude);
// Calculate declination
dec_rad = asin(sin(lat_rad)*sin(alt_rad)+cos(lat_rad)*cos(alt_rad)*cos(az_rad));
// Calculate hour angle
double quotient = (sin(alt_rad)-sin(lat_rad)*sin(dec_rad))/(cos(lat_rad)*cos(dec_rad));
// At extreme altitudes, we seem to get small numerical errors that causes values to be out of range, so clip to [-1,1]
if (quotient < -1.0) {
ha_rad = acos(-1.0);
} else if (quotient > 1.0) {
ha_rad = acos(1.0);
} else {
ha_rad = acos(quotient);
}
// Convert radians to degrees
rd.dec = Units::radiansToDegrees(dec_rad);
ha_deg = Units::radiansToDegrees(ha_rad);
// Calculate right ascension in decimal hours
rd.ra = modulo((lst_deg - ha_deg) / (360.0/24.0), 24.0);
return rd;
}
// Needs to work for negative a
double Astronomy::modulo(double a, double b)
{
return a - b * floor(a/b);
}
// Calculate azimuth and altitude angles to the sun from the given latitude and longitude at the given time
// Refer to:
// https://en.wikipedia.org/wiki/Position_of_the_Sun
// https://www.aa.quae.nl/en/reken/zonpositie.html
// Said to be accurate to .01 degree (36") for dates between 1950 and 2050
// although we use slightly more accurate constants and an extra term in the equation
// of centre from the second link
// For an alternative, see: http://www.psa.es/sdg/sunpos.htm
// Most accurate algorithm is supposedly: https://midcdmz.nrel.gov/spa/
void Astronomy::sunPosition(AzAlt& aa, RADec& rd, double latitude, double longitude, QDateTime dt)
{
double jd = julianDate(dt);
double n = (jd - jd_j2000()); // Days since J2000 epoch (including fraction)
double l = 280.461 + 0.9856474 * n; // Mean longitude of the Sun, corrected for the abberation of light
double g = 357.5291 + 0.98560028 * n; // Mean anomaly of the Sun - how far around orbit from perihlion, in degrees
l = modulo(l, 360.0);
g = modulo(g, 360.0);
double gr = Units::degreesToRadians(g);
double la = l + 1.9148 * sin(gr) + 0.0200 * sin(2.0*gr) + 0.0003 * sin(3.0*gr); // Ecliptic longitude (Ecliptic latitude b set to 0)
// Convert la, b=0, which give the position of the Sun in the ecliptic coordinate sytem, to
// equatorial coordinates
double e = 23.4393 - 3.563E-7 * n; // Obliquity of the ecliptic - tilt of Earth's axis of rotation
double er = Units::degreesToRadians(e);
double lr = Units::degreesToRadians(la);
double a = atan2(cos(er) * sin(lr), cos(lr)); // Right ascension, radians
double d = asin(sin(er) * sin(lr)); // Declination, radians
rd.ra = modulo(Units::radiansToDegrees(a), 360.0) / (360.0/24.0); // Convert to hours
rd.dec = Units::radiansToDegrees(d); // Convert to degrees
aa = raDecToAzAlt(rd, latitude, longitude, dt, false);
}
double Astronomy::moonDays(QDateTime dt)
{
QDateTime utc = dt.toUTC();
QDate date = utc.date();
QTime time = utc.time();
int y = date.year();
int m = date.month();
int D = date.day();
int d = 367 * y - 7 * (y + (m+9)/12) / 4 - 3 * ((y + (m-9)/7) / 100 + 1) / 4 + 275*m/9 + D - 730515;
return d + time.hour()/24.0 + time.minute()/(24.0*60.0) + time.second()/(24.0*60.0*60.0);
}
// Calculate azimuth and altitude angles to the moon from the given latitude and longitude at the given time
// Refer to: https://stjarnhimlen.se/comp/ppcomp.html
// Accurate to 4 arcminute
void Astronomy::moonPosition(AzAlt& aa, RADec& rd, double latitude, double longitude, QDateTime dt)
{
double d = moonDays(dt);
double ecl = Units::degreesToRadians(23.4393 - 3.563E-7 * d); // Obliquity of the ecliptic - tilt of Earth's axis of rotation
// Orbital elements for the Sun
double ws = Units::degreesToRadians(282.9404 + 4.70935E-5 * d);
double Ms = Units::degreesToRadians(356.0470 + 0.9856002585 * d);
// Orbital elements for the Moon
double Nm = Units::degreesToRadians(125.1228 - 0.0529538083 * d); // longitude of the ascending node
double im = Units::degreesToRadians(5.1454); // inclination to the ecliptic (plane of the Earth's orbit)
double wm = Units::degreesToRadians(318.0634 + 0.1643573223 * d); // argument of perihelion
double am = 60.2666; // (Earth radii) semi-major axis, or mean distance from Sun
double em = 0.054900; // ecm // eccentricity (0=circle, 0-1=ellipse, 1=parabola)
double Mm = Units::degreesToRadians(115.3654 + 13.0649929509 * d); // mean anomaly (0 at perihelion; increases uniformly with time), degrees
double Em = Mm + em * sin(Mm) * (1.0 + em * cos(Mm)); // Eccentric anomaly in radians
double xv = am * (cos(Em) - em);
double yv = am * (sqrt(1.0 - em*em) * sin(Em));
double vm = atan2(yv, xv); // True anomaly
double rm = sqrt(xv*xv + yv*yv); // Distance
// Compute position in space (for the Moon, this is geocentric)
double xh = rm * (cos(Nm) * cos(vm+wm) - sin(Nm) * sin(vm+wm) * cos(im));
double yh = rm * (sin(Nm) * cos(vm+wm) + cos(Nm) * sin(vm+wm) * cos(im));
double zh = rm * (sin(vm+wm) * sin(im));
// Convert to ecliptic longitude and latitude
double lonecl = atan2(yh, xh);
double latecl = atan2(zh, sqrt(xh*xh+yh*yh));
// Perturbations of the Moon
double Ls = Ms + ws; // Mean Longitude of the Sun (Ns=0)
double Lm = Mm + wm + Nm; // Mean longitude of the Moon
double D = Lm - Ls; // Mean elongation of the Moon
double F = Lm - Nm; // Argument of latitude for the Moon
double dlon;
dlon = -1.274 * sin(Mm - 2*D); // (the Evection)
dlon += +0.658 * sin(2*D); // (the Variation)
dlon += -0.186 * sin(Ms); // (the Yearly Equation)
dlon += -0.059 * sin(2*Mm - 2*D);
dlon += -0.057 * sin(Mm - 2*D + Ms);
dlon += +0.053 * sin(Mm + 2*D);
dlon += +0.046 * sin(2*D - Ms);
dlon += +0.041 * sin(Mm - Ms);
dlon += -0.035 * sin(D); // (the Parallactic Equation)
dlon += -0.031 * sin(Mm + Ms);
dlon += -0.015 * sin(2*F - 2*D);
dlon += +0.011 * sin(Mm - 4*D);
double dlat;
dlat = -0.173 * sin(F - 2*D);
dlat += -0.055 * sin(Mm - F - 2*D);
dlat += -0.046 * sin(Mm + F - 2*D);
dlat += +0.033 * sin(F + 2*D);
dlat += +0.017 * sin(2*Mm + F);
lonecl += Units::degreesToRadians(dlon);
latecl += Units::degreesToRadians(dlat);
rm += -0.58 * cos(Mm - 2*D);
rm += -0.46 * cos(2*D);
// Convert perturbed
xh = rm * cos(lonecl) * cos(latecl);
yh = rm * sin(lonecl) * cos(latecl);
zh = rm * sin(latecl);
// Convert to geocentric coordinates (already the case for the Moon)
double xg = xh;
double yg = yh;
double zg = zh;
// Convert to equatorial cordinates
double xe = xg;
double ye = yg * cos(ecl) - zg * sin(ecl);
double ze = yg * sin(ecl) + zg * cos(ecl);
// Compute right ascension and declination
double ra = atan2(ye, xe);
double dec = atan2(ze, sqrt(xe*xe+ye*ye));
rd.ra = modulo(Units::radiansToDegrees(ra), 360.0) / (360.0/24.0); // Convert to hours
rd.dec = Units::radiansToDegrees(dec); // Convert to degrees
// Convert from geocentric to topocentric
double mpar = asin(1/rm);
double lat = Units::degreesToRadians(latitude);
double gclat = Units::degreesToRadians(latitude - 0.1924 * sin(2.0 * lat));
double rho = 0.99833 + 0.00167 * cos(2*lat);
QTime time = dt.toUTC().time();
double UT = time.hour() + time.minute()/60.0 + time.second()/(60.0*60.0);
double GMST0 = Units::radiansToDegrees(Ls)/15.0 + 12; // In hours
double GMST = GMST0 + UT;
double LST = GMST + longitude/15.0;
double HA = Units::degreesToRadians(LST*15.0 - Units::radiansToDegrees(ra)); // Hour angle in radians
double g = atan(tan(gclat) / cos(HA));
double topRA = ra - mpar * rho * cos(gclat) * sin(HA) / cos(dec);
double topDec;
if (g != 0.0)
topDec = dec - mpar * rho * sin(gclat) * sin(g - dec) / sin(g);
else
topDec = dec - mpar * rho * sin(-dec) * cos(HA);
rd.ra = modulo(Units::radiansToDegrees(topRA), 360.0) / (360.0/24.0); // Convert to hours
rd.dec = Units::radiansToDegrees(topDec); // Convert to degrees
aa = raDecToAzAlt(rd, latitude, longitude, dt, false);
}
// Calculated adjustment to altitude angle from true to apparent due to atmospheric refraction using
// Saemundsson's formula (which is used by Stellarium and is primarily just for
// optical wavelengths)
// See: https://en.wikipedia.org/wiki/Atmospheric_refraction#Calculating_refraction
// Alt is in degrees. 90 = Zenith gives a factor of 0.
// Pressure in millibars
// Temperature in Celsuis
// We divide by 60.0 to get a value in degrees (as original formula is in arcminutes)
double Astronomy::refractionSaemundsson(double alt, double pressure, double temperature)
{
double pt = (pressure/1010.0) * (283.0/(273.0+temperature));
return pt * (1.02 / tan(Units::degreesToRadians(alt+10.3/(alt+5.11))) + 0.0019279) / 60.0;
}
// Calculated adjustment to altitude angle from true to apparent due to atmospheric refraction using
// code from Starlink Positional Astronomy Library. This is more accurate for
// radio than Saemundsson's formula, but also more complex.
// See: https://github.com/Starlink/pal
// Alt is in degrees. 90 = Zenith gives a factor of 0.
// Pressure in millibars
// Temperature in Celsuis
// Humdity in %
// Frequency in Hertz
// Latitude in decimal degrees
// HeightAboveSeaLevel in metres
// Temperature lapse rate in K/km
double Astronomy::refractionPAL(double alt, double pressure, double temperature, double humidity, double frequency,
double latitude, double heightAboveSeaLevel, double temperatureLapseRate)
{
double tdk = Units::celsiusToKelvin(temperature); // Ambient temperature at the observer (K)
double wl = (299792458.0/frequency)*1000000.0; // Wavelength in micrometres
double rh = humidity/100.0; // Relative humidity in range 0-1
double phi = Units::degreesToRadians(latitude); // Latitude of the observer (radian, astronomical)
double tlr = temperatureLapseRate/1000.0; // Temperature lapse rate in the troposphere (K/metre)
double refa, refb;
double z = 90.0-alt;
double zu = Units::degreesToRadians(z);
double zr;
palRefco(heightAboveSeaLevel, tdk, pressure, rh,
wl, phi, tlr, 1E-10,
&refa, &refb);
palRefz(zu, refa, refb, &zr);
return z-Units::radiansToDegrees(zr);
}
double Astronomy::raToDecimal(const QString& value)
{
QRegExp decimal("^([0-9]+(\\.[0-9]+)?)");
QRegExp hms("^([0-9]+)[ h]([0-9]+)[ m]([0-9]+(\\.[0-9]+)?)s?");
if (decimal.exactMatch(value))
return decimal.capturedTexts()[0].toDouble();
else if (hms.exactMatch(value))
{
return Units::hoursMinutesSecondsToDecimal(
hms.capturedTexts()[1].toDouble(),
hms.capturedTexts()[2].toDouble(),
hms.capturedTexts()[3].toDouble());
}
return 0.0;
}
double Astronomy::decToDecimal(const QString& value)
{
QRegExp decimal("^(-?[0-9]+(\\.[0-9]+)?)");
QRegExp dms(QString("^(-?[0-9]+)[ %1d]([0-9]+)[ 'm]([0-9]+(\\.[0-9]+)?)[\"s]?").arg(QChar(0xb0)));
if (decimal.exactMatch(value))
return decimal.capturedTexts()[0].toDouble();
else if (dms.exactMatch(value))
{
return Units::degreesMinutesSecondsToDecimal(
dms.capturedTexts()[1].toDouble(),
dms.capturedTexts()[2].toDouble(),
dms.capturedTexts()[3].toDouble());
}
return 0.0;
}
double Astronomy::lstAndRAToLongitude(double lst, double raHours)
{
double longitude = lst - (raHours * 15.0); // Convert hours to degrees
if (longitude < -180.0)
longitude += 360.0;
else if (longitude > 180.0)
longitude -= 360.0;
return -longitude; // East positive
}
// Return right ascension and declination of North Galactic Pole in J2000 Epoch
// http://www.lsc-group.phys.uwm.edu/lal/lsd/node1777.html
void Astronomy::northGalacticPoleJ2000(double& ra, double& dec)
{
ra = 192.8594813/15.0;
dec = 27.1282511;
}
// Convert from equatorial to Galactic coordinates, J2000 Epoch
void Astronomy::equatorialToGalactic(double ra, double dec, double& l, double& b)
{
const double raRad = Units::degreesToRadians(ra * 15.0);
const double decRad = Units::degreesToRadians(dec);
// Calculate RA and dec for North Galactic pole, J2000
double ngpRa, ngpDec;
northGalacticPoleJ2000(ngpRa, ngpDec);
const double ngpRaRad = Units::degreesToRadians(ngpRa * 15.0);
const double ngpDecRad = Units::degreesToRadians(ngpDec);
// Calculate galactic longitude in radians
double bRad = asin(sin(ngpDecRad)*sin(decRad)+cos(ngpDecRad)*cos(decRad)*cos(raRad - ngpRaRad));
// Calculate galactic latitiude in radians
double lRad = atan2(sin(decRad)-sin(bRad)*sin(ngpDecRad), cos(decRad)*cos(ngpDecRad)*sin(raRad - ngpRaRad));
// Ascending node of the galactic plane in degrees
double lAscend = 33.0;
// Convert to degrees in range -90,90 and 0,360
b = Units::radiansToDegrees(bRad);
l = Units::radiansToDegrees(lRad) + lAscend;
if (l < 0.0) {
l += 360.0;
}
if (l > 360.0) {
l -= 360.0;
}
}
// The following functions are from Starlink Positional Astronomy Library
// https://github.com/Starlink/pal
/* Pi */
static const double PAL__DPI = 3.1415926535897932384626433832795028841971693993751;
/* 2Pi */
static const double PAL__D2PI = 6.2831853071795864769252867665590057683943387987502;
/* pi/180: degrees to radians */
static const double PAL__DD2R = 0.017453292519943295769236907684886127134428718885417;
/* Radians to degrees */
static const double PAL__DR2D = 57.295779513082320876798154814105170332405472466564;
/* DMAX(A,B) - return maximum value - evaluates arguments multiple times */
#define DMAX(A,B) ((A) > (B) ? (A) : (B) )
/* DMIN(A,B) - return minimum value - evaluates arguments multiple times */
#define DMIN(A,B) ((A) < (B) ? (A) : (B) )
// Normalize angle into range +/- pi
double palDrange( double angle ) {
double result = fmod( angle, PAL__D2PI );
if( result > PAL__DPI ) {
result -= PAL__D2PI;
} else if( result < -PAL__DPI ) {
result += PAL__D2PI;
}
return result;
}
// Calculate stratosphere parameters
static void pal1Atms ( double rt, double tt, double dnt, double gamal,
double r, double * dn, double * rdndr ) {
double b;
double w;
b = gamal / tt;
w = (dnt - 1.0) * exp( -b * (r-rt) );
*dn = 1.0 + w;
*rdndr = -r * b * w;
}
// Calculate troposphere parameters
static void pal1Atmt ( double r0, double t0, double alpha, double gamm2,
double delm2, double c1, double c2, double c3, double c4,
double c5, double c6, double r,
double *t, double *dn, double *rdndr ) {
double tt0;
double tt0gm2;
double tt0dm2;
*t = DMAX( DMIN( t0 - alpha*(r-r0), 320.0), 100.0 );
tt0 = *t / t0;
tt0gm2 = pow( tt0, gamm2 );
tt0dm2 = pow( tt0, delm2 );
*dn = 1.0 + ( c1 * tt0gm2 - ( c2 - c5 / *t ) * tt0dm2 ) * tt0;
*rdndr = r * ( -c3 * tt0gm2 + ( c4 - c6 / tt0 ) * tt0dm2 );
}
// Adjust unrefracted zenith distance
static void palRefz ( double zu, double refa, double refb, double *zr ) {
/* Constants */
/* Largest usable ZD (deg) */
const double D93 = 93.0;
/* ZD at which one model hands over to the other (radians) */
const double Z83 = 83.0 * PAL__DD2R;
/* coefficients for high ZD model (used beyond ZD 83 deg) */
const double C1 = +0.55445;
const double C2 = -0.01133;
const double C3 = +0.00202;
const double C4 = +0.28385;
const double C5 = +0.02390;
/* High-ZD-model prefiction (deg) for that point */
const double REF83 = (C1+C2*7.0+C3*49.0)/(1.0+C4*7.0+C5*49.0);
double zu1,zl,s,c,t,tsq,tcu,ref,e,e2;
/* perform calculations for zu or 83 deg, whichever is smaller */
zu1 = DMIN(zu,Z83);
/* functions of ZD */
zl = zu1;
s = sin(zl);
c = cos(zl);
t = s/c;
tsq = t*t;
tcu = t*tsq;
/* refracted zd (mathematically to better than 1 mas at 70 deg) */
zl = zl-(refa*t+refb*tcu)/(1.0+(refa+3.0*refb*tsq)/(c*c));
/* further iteration */
s = sin(zl);
c = cos(zl);
t = s/c;
tsq = t*t;
tcu = t*tsq;
ref = zu1-zl+
(zl-zu1+refa*t+refb*tcu)/(1.0+(refa+3.0*refb*tsq)/(c*c));
/* special handling for large zu */
if (zu > zu1) {
e = 90.0-DMIN(D93,zu*PAL__DR2D);
e2 = e*e;
ref = (ref/REF83)*(C1+C2*e+C3*e2)/(1.0+C4*e+C5*e2);
}
/* return refracted zd */
*zr = zu-ref;
}
// Determine constants in atmospheric refraction model
static void palRefco ( double hm, double tdk, double pmb, double rh,
double wl, double phi, double tlr, double eps,
double *refa, double *refb ) {
double r1, r2;
/* Sample zenith distances: arctan(1) and arctan(4) */
const double ATN1 = 0.7853981633974483;
const double ATN4 = 1.325817663668033;
/* Determine refraction for the two sample zenith distances */
palRefro(ATN1,hm,tdk,pmb,rh,wl,phi,tlr,eps,&r1);
palRefro(ATN4,hm,tdk,pmb,rh,wl,phi,tlr,eps,&r2);
/* Solve for refraction constants */
*refa = (64.0*r1-r2)/60.0;
*refb = (r2-4.0*r1)/60.0;
}
// Atmospheric refraction for radio and optical/IR wavelengths
static void palRefro( double zobs, double hm, double tdk, double pmb,
double rh, double wl, double phi, double tlr,
double eps, double * ref ) {
/*
* Fixed parameters
*/
/* 93 degrees in radians */
const double D93 = 1.623156204;
/* Universal gas constant */
const double GCR = 8314.32;
/* Molecular weight of dry air */
const double DMD = 28.9644;
/* Molecular weight of water vapour */
const double DMW = 18.0152;
/* Mean Earth radius (metre) */
const double S = 6378120.;
/* Exponent of temperature dependence of water vapour pressure */
const double DELTA = 18.36;
/* Height of tropopause (metre) */
const double HT = 11000.;
/* Upper limit for refractive effects (metre) */
const double HS = 80000.;
/* Numerical integration: maximum number of strips. */
const int ISMAX=16384l;
/* Local variables */
int is, k, n, i, j;
int optic, loop; /* booleans */
double zobs1,zobs2,hmok,tdkok,pmbok,rhok,wlok,alpha,
tol,wlsq,gb,a,gamal,gamma,gamm2,delm2,
tdc,psat,pwo,w,
c1,c2,c3,c4,c5,c6,r0,tempo,dn0,rdndr0,sk0,f0,
rt,tt,dnt,rdndrt,sine,zt,ft,dnts,rdndrp,zts,fts,
rs,dns,rdndrs,zs,fs,refold,z0,zrange,fb,ff,fo,fe,
h,r,sz,rg,dr,tg,dn,rdndr,t,f,refp,reft;
/* The refraction integrand */
#define refi(DN,RDNDR) RDNDR/(DN+RDNDR)
/* Transform ZOBS into the normal range. */
zobs1 = palDrange(zobs);
zobs2 = DMIN(fabs(zobs1),D93);
/* keep other arguments within safe bounds. */
hmok = DMIN(DMAX(hm,-1e3),HS);
tdkok = DMIN(DMAX(tdk,100.0),500.0);
pmbok = DMIN(DMAX(pmb,0.0),10000.0);
rhok = DMIN(DMAX(rh,0.0),1.0);
wlok = DMAX(wl,0.1);
alpha = DMIN(DMAX(fabs(tlr),0.001),0.01);
/* tolerance for iteration. */
tol = DMIN(DMAX(fabs(eps),1e-12),0.1)/2.0;
/* decide whether optical/ir or radio case - switch at 100 microns. */
optic = wlok < 100.0;
/* set up model atmosphere parameters defined at the observer. */
wlsq = wlok*wlok;
gb = 9.784*(1.0-0.0026*cos(phi+phi)-0.00000028*hmok);
if (optic) {
a = (287.6155+(1.62887+0.01360/wlsq)/wlsq) * 273.15e-6/1013.25;
} else {
a = 77.6890e-6;
}
gamal = (gb*DMD)/GCR;
gamma = gamal/alpha;
gamm2 = gamma-2.0;
delm2 = DELTA-2.0;
tdc = tdkok-273.15;
psat = pow(10.0,(0.7859+0.03477*tdc)/(1.0+0.00412*tdc)) *
(1.0+pmbok*(4.5e-6+6.0e-10*tdc*tdc));
if (pmbok > 0.0) {
pwo = rhok*psat/(1.0-(1.0-rhok)*psat/pmbok);
} else {
pwo = 0.0;
}
w = pwo*(1.0-DMW/DMD)*gamma/(DELTA-gamma);
c1 = a*(pmbok+w)/tdkok;
if (optic) {
c2 = (a*w+11.2684e-6*pwo)/tdkok;
} else {
c2 = (a*w+6.3938e-6*pwo)/tdkok;
}
c3 = (gamma-1.0)*alpha*c1/tdkok;
c4 = (DELTA-1.0)*alpha*c2/tdkok;
if (optic) {
c5 = 0.0;
c6 = 0.0;
} else {
c5 = 375463e-6*pwo/tdkok;
c6 = c5*delm2*alpha/(tdkok*tdkok);
}
/* conditions at the observer. */
r0 = S+hmok;
pal1Atmt(r0,tdkok,alpha,gamm2,delm2,c1,c2,c3,c4,c5,c6,
r0,&tempo,&dn0,&rdndr0);
sk0 = dn0*r0*sin(zobs2);
f0 = refi(dn0,rdndr0);
/* conditions in the troposphere at the tropopause. */
rt = S+DMAX(HT,hmok);
pal1Atmt(r0,tdkok,alpha,gamm2,delm2,c1,c2,c3,c4,c5,c6,
rt,&tt,&dnt,&rdndrt);
sine = sk0/(rt*dnt);
zt = atan2(sine,sqrt(DMAX(1.0-sine*sine,0.0)));
ft = refi(dnt,rdndrt);
/* conditions in the stratosphere at the tropopause. */
pal1Atms(rt,tt,dnt,gamal,rt,&dnts,&rdndrp);
sine = sk0/(rt*dnts);
zts = atan2(sine,sqrt(DMAX(1.0-sine*sine,0.0)));
fts = refi(dnts,rdndrp);
/* conditions at the stratosphere limit. */
rs = S+HS;
pal1Atms(rt,tt,dnt,gamal,rs,&dns,&rdndrs);
sine = sk0/(rs*dns);
zs = atan2(sine,sqrt(DMAX(1.0-sine*sine,0.0)));
fs = refi(dns,rdndrs);
/* variable initialization to avoid compiler warning. */
reft = 0.0;
/* integrate the refraction integral in two parts; first in the
* troposphere (k=1), then in the stratosphere (k=2). */
for (k=1; k<=2; k++) {
/* initialize previous refraction to ensure at least two iterations. */
refold = 1.0;
/* start off with 8 strips. */
is = 8;
/* start z, z range, and start and end values. */
if (k==1) {
z0 = zobs2;
zrange = zt-z0;
fb = f0;
ff = ft;
} else {
z0 = zts;
zrange = zs-z0;
fb = fts;
ff = fs;
}
/* sums of odd and even values. */
fo = 0.0;
fe = 0.0;
/* first time through the loop we have to do every point. */
n = 1;
/* start of iteration loop (terminates at specified precision). */
loop = 1;
while (loop) {
/* strip width. */
h = zrange/((double)is);
/* initialize distance from earth centre for quadrature pass. */
if (k == 1) {
r = r0;
} else {
r = rt;
}
/* one pass (no need to compute evens after first time). */
for (i=1; i 1e-20) {
w = sk0/sz;
rg = r;
dr = 1.0e6;
j = 0;
while ( fabs(dr) > 1.0 && j < 4 ) {
j++;
if (k==1) {
pal1Atmt(r0,tdkok,alpha,gamm2,delm2,
c1,c2,c3,c4,c5,c6,rg,&tg,&dn,&rdndr);
} else {
pal1Atms(rt,tt,dnt,gamal,rg,&dn,&rdndr);
}
dr = (rg*dn-w)/(dn+rdndr);
rg = rg-dr;
}
r = rg;
}
/* find the refractive index and integrand at r. */
if (k==1) {
pal1Atmt(r0,tdkok,alpha,gamm2,delm2,
c1,c2,c3,c4,c5,c6,r,&t,&dn,&rdndr);
} else {
pal1Atms(rt,tt,dnt,gamal,r,&dn,&rdndr);
}
f = refi(dn,rdndr);
/* accumulate odd and (first time only) even values. */
if (n==1 && i%2 == 0) {
fe += f;
} else {
fo += f;
}
}
/* evaluate the integrand using simpson's rule. */
refp = h*(fb+4.0*fo+2.0*fe+ff)/3.0;
/* has the required precision been achieved (or can't be)? */
if (fabs(refp-refold) > tol && is < ISMAX) {
/* no: prepare for next iteration.*/
/* save current value for convergence test. */
refold = refp;
/* double the number of strips. */
is += is;
/* sum of all current values = sum of next pass's even values. */
fe += fo;
/* prepare for new odd values. */
fo = 0.0;
/* skip even values next time. */
n = 2;
} else {
/* yes: save troposphere component and terminate the loop. */
if (k==1) reft = refp;
loop = 0;
}
}
}
/* result. */
*ref = reft+refp;
if (zobs1 < 0.0) *ref = -(*ref);
}