/* * 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 #include /* 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; }