/* * astro_math.h -- Shared astronomical math for pg_orrery * * Static inline functions used by star_funcs.c, kepler_funcs.c, * and future planet/moon observation code. * * Using static inline preserves the project convention of no * cross-translation-unit symbol coupling. */ #ifndef PG_ORRERY_ASTRO_MATH_H #define PG_ORRERY_ASTRO_MATH_H #include #include "types.h" #define DEG_TO_RAD (M_PI / 180.0) #define RAD_TO_DEG (180.0 / M_PI) #define ARCSEC_TO_RAD (M_PI / (180.0 * 3600.0)) /* Pre-computed obliquity trig (IAU 1976, OBLIQUITY_J2000 = 0.40909280422232897 rad). * Avoids recomputing cos/sin on every frame rotation call. */ #define COS_OBLIQUITY_J2000 0.91748206206918181 #define SIN_OBLIQUITY_J2000 0.39777715593191371 /* * Greenwich Mean Sidereal Time from Julian date. * Vallado, "Fundamentals of Astrodynamics", Eq. 3-47. * Returns angle in radians. */ static inline double gmst_from_jd(double jd) { double t_ut1 = (jd - 2451545.0) / 36525.0; double gmst = 67310.54841 + (876600.0 * 3600.0 + 8640184.812866) * t_ut1 + 0.093104 * t_ut1 * t_ut1 - 6.2e-6 * t_ut1 * t_ut1 * t_ut1; gmst = fmod(gmst * M_PI / 43200.0, 2.0 * M_PI); if (gmst < 0.0) gmst += 2.0 * M_PI; return gmst; } /* * IAU 1976 precession: rotate J2000 equatorial to mean equatorial of date. * * Source: Lieske (1979), A&A 73, 282. * Three angles zeta_A, z_A, theta_A define the precession rotation. * Valid within ~1 arcsecond for several centuries around J2000. */ static inline void precess_j2000_to_date(double jd, double ra_j2000, double dec_j2000, double *ra_date, double *dec_date) { double T, T2, T3; double zeta_A, z_A, theta_A; double cos_dec, sin_dec, cos_ra_zeta, sin_ra_zeta; double cos_theta, sin_theta; double A, B, C; T = (jd - J2000_JD) / 36525.0; T2 = T * T; T3 = T2 * T; /* Precession angles in arcseconds (Lieske 1979) */ zeta_A = (2306.2181 + 1.39656 * T - 0.000139 * T2) * T + (0.30188 - 0.000344 * T) * T2 + 0.017998 * T3; z_A = (2306.2181 + 1.39656 * T - 0.000139 * T2) * T + (1.09468 + 0.000066 * T) * T2 + 0.018203 * T3; theta_A = (2004.3109 - 0.85330 * T - 0.000217 * T2) * T - (0.42665 + 0.000217 * T) * T2 - 0.041833 * T3; /* Arcseconds to radians */ zeta_A *= ARCSEC_TO_RAD; z_A *= ARCSEC_TO_RAD; theta_A *= ARCSEC_TO_RAD; /* Direct formula: R3(-z_A) R2(theta_A) R3(-zeta_A) applied to (ra, dec) */ cos_dec = cos(dec_j2000); sin_dec = sin(dec_j2000); cos_ra_zeta = cos(ra_j2000 + zeta_A); sin_ra_zeta = sin(ra_j2000 + zeta_A); cos_theta = cos(theta_A); sin_theta = sin(theta_A); A = cos_dec * sin_ra_zeta; B = cos_theta * cos_dec * cos_ra_zeta - sin_theta * sin_dec; C = sin_theta * cos_dec * cos_ra_zeta + cos_theta * sin_dec; *dec_date = asin(C); *ra_date = atan2(A, B) + z_A; if (*ra_date < 0.0) *ra_date += 2.0 * M_PI; if (*ra_date >= 2.0 * M_PI) *ra_date -= 2.0 * M_PI; } /* * Equatorial (hour angle, declination) to horizontal (azimuth, elevation). * All angles in radians. * Azimuth convention: 0=N, pi/2=E, pi=S, 3*pi/2=W. */ static inline void equatorial_to_horizontal(double ha, double dec, double lat, double *az, double *el) { double sin_lat = sin(lat); double cos_lat = cos(lat); double sin_dec = sin(dec); double cos_dec = cos(dec); double cos_ha = cos(ha); double y, x; *el = asin(sin_lat * sin_dec + cos_lat * cos_dec * cos_ha); y = -cos_dec * sin(ha); x = cos_lat * sin_dec - sin_lat * cos_dec * cos_ha; *az = atan2(y, x); if (*az < 0.0) *az += 2.0 * M_PI; } /* * Ecliptic J2000 to equatorial J2000. * Simple rotation around X-axis by -obliquity. * * NOT safe for aliased (in-place) calls: ecl and equ must not overlap. * Writing equ[1] before reading ecl[1] for equ[2] produces wrong results * when ecl == equ. The vendored sgp4/sdp4.c has separate in-place versions * that use a temp variable; do not confuse the two. */ static inline void ecliptic_to_equatorial(const double *ecl, double *equ) { equ[0] = ecl[0]; equ[1] = ecl[1] * COS_OBLIQUITY_J2000 - ecl[2] * SIN_OBLIQUITY_J2000; equ[2] = ecl[1] * SIN_OBLIQUITY_J2000 + ecl[2] * COS_OBLIQUITY_J2000; } /* * Equatorial J2000 to ecliptic J2000. * Rotation around X-axis by +obliquity. * * NOT safe for aliased (in-place) calls: equ and ecl must not overlap. * Same ordering hazard as ecliptic_to_equatorial() above. */ static inline void equatorial_to_ecliptic(const double *equ, double *ecl) { ecl[0] = equ[0]; ecl[1] = equ[1] * COS_OBLIQUITY_J2000 + equ[2] * SIN_OBLIQUITY_J2000; ecl[2] = -equ[1] * SIN_OBLIQUITY_J2000 + equ[2] * COS_OBLIQUITY_J2000; } /* * Cartesian to spherical: (x, y, z) -> (ra, dec, dist). * ra in [0, 2*pi), dec in [-pi/2, pi/2], dist in same units as input. */ static inline void cartesian_to_spherical(const double *xyz, double *ra, double *dec, double *dist) { *dist = sqrt(xyz[0] * xyz[0] + xyz[1] * xyz[1] + xyz[2] * xyz[2]); *dec = asin(xyz[2] / *dist); *ra = atan2(xyz[1], xyz[0]); if (*ra < 0.0) *ra += 2.0 * M_PI; } /* * Geocentric observation pipeline (shared by all observation functions). * * Takes geocentric ecliptic J2000 position in AU, observer location, * and Julian date. Converts through equatorial, precesses to date, * and computes topocentric az/el. * * This is the canonical path: * ecliptic J2000 -> equatorial J2000 -> precess to date -> * sidereal time -> hour angle -> az/el */ static inline void observe_from_geocentric(const double geo_ecl_au[3], double jd, const pg_observer *obs, pg_topocentric *result) { double geo_equ[3]; double ra_j2000, dec_j2000, geo_dist; double ra_date, dec_date; double gmst_val, lst, ha; double az, el; /* Ecliptic J2000 -> equatorial J2000 */ ecliptic_to_equatorial(geo_ecl_au, geo_equ); /* Cartesian -> spherical */ cartesian_to_spherical(geo_equ, &ra_j2000, &dec_j2000, &geo_dist); /* Precess J2000 -> date */ precess_j2000_to_date(jd, ra_j2000, dec_j2000, &ra_date, &dec_date); /* Hour angle and az/el */ gmst_val = gmst_from_jd(jd); lst = gmst_val + obs->lon; ha = lst - ra_date; equatorial_to_horizontal(ha, dec_date, obs->lat, &az, &el); result->azimuth = az; result->elevation = el; result->range_km = geo_dist * AU_KM; result->range_rate = 0.0; /* no velocity computation yet */ } #endif /* PG_ORRERY_ASTRO_MATH_H */