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).
350 lines
8.1 KiB
C
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;
|
|
}
|