pg_orrery/src/eph_provider.c
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

350 lines
8.1 KiB
C

/*
* eph_provider.c -- Ephemeris provider management for pg_orrery
*
* Manages the lifecycle of the optional JPL DE ephemeris reader
* within PostgreSQL's multi-process architecture.
*
* Critical design:
* - Each backend (including parallel workers) opens its own
* file descriptor via lazy initialization on first DE call.
* - The postmaster never opens the file (_PG_init only registers GUCs).
* - on_proc_exit callback ensures cleanup.
* - ICRS equatorial -> ecliptic J2000 rotation happens here,
* at the provider boundary, before data enters the observation
* pipeline.
*
* Constant chain of custody:
* - DE returns positions in ICRS equatorial (AU, km internally)
* - We rotate to ecliptic J2000 via equatorial_to_ecliptic()
* - Both target and Earth always come from the same provider
*/
#include "postgres.h"
#include "fmgr.h"
#include "utils/guc.h"
#include "storage/ipc.h"
#include "eph_provider.h"
#include "de_reader.h"
#include "astro_math.h"
#include <math.h>
#include <string.h>
/* GUC variable: path to DE ephemeris file */
static char *eph_path_guc = NULL;
/*
* Per-backend state (safe: each process gets its own copy after fork).
* These are NOT shared memory — they live in each backend's address space.
*/
static de_handle *de_handle_ptr = NULL;
static bool de_init_attempted = false;
static bool de_init_success = false;
void
eph_register_gucs(void)
{
DefineCustomStringVariable(
"pg_orrery.ephemeris_path",
"Path to JPL DE binary ephemeris file (DE430/DE440/DE441).",
"When set, enables high-precision _de() function variants. "
"Default empty = no DE support (VSOP87 only). "
"Relative paths are relative to $PGDATA.",
&eph_path_guc,
"", /* default: empty = disabled */
PGC_BACKEND, /* Set at backend startup only.
* Live reload (PGC_SIGHUP) would require an
* assign_hook that resets de_init_attempted,
* closes any open handle, and re-initializes. */
GUC_SUPERUSER_ONLY, /* only superuser can set */
NULL, /* check_hook */
NULL, /* assign_hook */
NULL /* show_hook */
);
}
/*
* Lazy initialization: open the DE file on first call.
* Never called from _PG_init() (postmaster context).
*/
static void
ensure_de_init(void)
{
int errcode;
if (de_init_attempted)
return;
de_init_attempted = true;
de_init_success = false;
/* No path configured? DE is simply unavailable. */
if (eph_path_guc == NULL || eph_path_guc[0] == '\0')
return;
de_handle_ptr = de_reader_open(eph_path_guc, &errcode);
if (de_handle_ptr == NULL)
{
ereport(NOTICE,
(errmsg("pg_orrery: cannot open DE ephemeris at \"%s\" (error %d), "
"DE functions will fall back to VSOP87",
eph_path_guc, errcode)));
return;
}
/* Verify AU consistency with pg_orrery's compiled-in value */
if (fabs(de_handle_ptr->au_km - AU_KM) > 0.01)
{
ereport(NOTICE,
(errmsg("pg_orrery: DE ephemeris AU = %.3f km, expected %.3f km; "
"DE functions will fall back to VSOP87",
de_handle_ptr->au_km, (double)AU_KM)));
de_reader_close(de_handle_ptr);
de_handle_ptr = NULL;
return;
}
ereport(DEBUG1,
(errmsg("pg_orrery: DE%d ephemeris loaded, JD %.1f to %.1f",
de_handle_ptr->de_version,
de_handle_ptr->start_jd,
de_handle_ptr->end_jd)));
de_init_success = true;
}
void
eph_cleanup(int code, Datum arg)
{
if (de_handle_ptr != NULL)
{
de_reader_close(de_handle_ptr);
de_handle_ptr = NULL;
}
de_init_attempted = false;
de_init_success = false;
}
bool
eph_de_available(void)
{
ensure_de_init();
return de_init_success;
}
EphProvider
eph_current_provider(void)
{
if (eph_de_available())
return EPH_JPL_DE;
return EPH_VSOP87;
}
const char *
eph_get_path(void)
{
return eph_path_guc;
}
/*
* Internal: get heliocentric position of a DE body and convert
* from ICRS equatorial to ecliptic J2000.
*
* Returns true on success.
*/
static bool
de_get_helio_ecliptic(int de_target, double jd, double ecl[3])
{
double icrs[3];
int err;
if (!de_handle_ptr)
return false;
/* Get position relative to Sun (DE_SUN = 10) */
err = de_reader_get_pos(de_handle_ptr, jd, de_target, 10, icrs);
if (err != DE_OK)
return false;
/* ICRS equatorial -> ecliptic J2000 */
equatorial_to_ecliptic(icrs, ecl);
return true;
}
/*
* Internal: get Earth's heliocentric position from DE.
*
* Earth = EMB(helio) - Moon(geocentric) / (1 + EMRAT).
* The EMB-to-Earth correction is also in de_reader.c:get_earth_pos();
* both must use the same formula.
*/
static bool
de_get_earth_helio_ecliptic(double jd, double ecl[3])
{
double icrs[3];
int err;
if (!de_handle_ptr)
return false;
/* EMB heliocentric, then correct to Earth below */
err = de_reader_get_pos(de_handle_ptr, jd, DE_EMB, DE_SUN, icrs);
if (err != DE_OK)
return false;
/* Correct EMB-helio to Earth-helio: subtract Moon/(1+EMRAT) */
{
double moon_icrs[3];
double factor;
err = de_reader_get_pos(de_handle_ptr, jd, DE_MOON, -1, moon_icrs);
if (err != DE_OK)
return false;
factor = 1.0 / (1.0 + de_handle_ptr->emrat);
icrs[0] -= moon_icrs[0] * factor;
icrs[1] -= moon_icrs[1] * factor;
icrs[2] -= moon_icrs[2] * factor;
}
equatorial_to_ecliptic(icrs, ecl);
return true;
}
bool
eph_de_planet(int body_id, double jd, double xyz[6])
{
int de_target;
double ecl[3];
if (!eph_de_available())
return false;
de_target = pgbody_to_de_target(body_id);
if (de_target < 0)
return false;
/* Earth is special: derived from EMB and Moon */
if (body_id == BODY_EARTH)
{
if (!de_get_earth_helio_ecliptic(jd, ecl))
return false;
}
else
{
if (!de_get_helio_ecliptic(de_target, jd, ecl))
return false;
}
xyz[0] = ecl[0];
xyz[1] = ecl[1];
xyz[2] = ecl[2];
xyz[3] = 0.0; /* velocity not yet implemented */
xyz[4] = 0.0;
xyz[5] = 0.0;
return true;
}
bool
eph_de_earth(double jd, double xyz[6])
{
double ecl[3];
if (!eph_de_available())
return false;
if (!de_get_earth_helio_ecliptic(jd, ecl))
return false;
xyz[0] = ecl[0];
xyz[1] = ecl[1];
xyz[2] = ecl[2];
xyz[3] = 0.0;
xyz[4] = 0.0;
xyz[5] = 0.0;
return true;
}
bool
eph_de_moon(double jd, double xyz[3])
{
double icrs[3];
int err;
if (!eph_de_available())
return false;
/* Moon geocentric: use center=-1 for raw geocentric Moon directly.
* The Moon is stored geocentric in the DE file, so center=-1 avoids
* the roundabout of computing Earth (EMB - Moon/(1+EMRAT)) just to
* subtract it back out (~4x fewer interpolations). */
err = de_reader_get_pos(de_handle_ptr, jd, DE_MOON, -1, icrs);
if (err != DE_OK)
return false;
/* ICRS -> ecliptic J2000 */
equatorial_to_ecliptic(icrs, xyz);
return true;
}
bool
eph_de_sun(double jd, double xyz[6])
{
/* Sun is at the origin of heliocentric coordinates */
(void)jd;
if (!eph_de_available())
return false;
xyz[0] = xyz[1] = xyz[2] = 0.0;
xyz[3] = xyz[4] = xyz[5] = 0.0;
return true;
}
double
eph_de_start_jd(void)
{
if (de_handle_ptr)
return de_handle_ptr->start_jd;
return 0.0;
}
double
eph_de_end_jd(void)
{
if (de_handle_ptr)
return de_handle_ptr->end_jd;
return 0.0;
}
int
eph_de_version(void)
{
if (de_handle_ptr)
return de_handle_ptr->de_version;
return 0;
}
double
eph_de_au_km(void)
{
if (de_handle_ptr)
return de_handle_ptr->au_km;
return 0.0;
}