pg_orrery/src/astro_math.h
Ryan Malloy 3915d1784f Rename pg_orbit to pg_orrery
An existing product called PG Orbit (a mobile PostgreSQL client)
creates a naming conflict. pg_orrery — a database orrery built from
Keplerian parameters and SQL instead of brass gears.

Build system: control file, Makefile, Dockerfile, docker init script.
C source: GUC prefix, PG_FUNCTION_INFO_V1 symbol, header guards,
ereport prefixes, comments across ~30 files including vendored SGP4.
SQL: all 5 install/migration scripts, function name pg_orrery_ephemeris_info.
Tests: 9 SQL suites, 8 expected outputs, standalone DE reader test.
Documentation: CLAUDE.md, README.md, DESIGN.md, Starlight site infra,
36 MDX pages, OG renderer, logo SVG, docker-compose, agent threads.

All 13 regression suites pass. Docs site builds (37 pages).
2026-02-17 13:36:22 -07:00

221 lines
6.6 KiB
C

/*
* 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 <math.h>
#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 */