pg_orrery/src/moon_funcs.c
Ryan Malloy 15fa553c0e Add optional JPL DE440/441 ephemeris support (v0.3.0)
Clean-room DE binary reader (~400 lines C) with Chebyshev/Clenshaw
evaluation — no GPL dependency on jpl_eph. Per-backend lazy
initialization preserves PARALLEL SAFE. Existing VSOP87/ELP82B
functions stay IMMUTABLE; new _de() variants are STABLE with
automatic fallback to compiled-in ephemerides on any DE failure.

Implementation:
- de_reader.c: header parse, record seek, Clenshaw recurrence
- eph_provider.c: GUC (pg_orbit.ephemeris_path), lazy init,
  ICRS-to-ecliptic frame rotation, on_proc_exit cleanup
- de_funcs.c: 11 new SQL functions (_de variants + diagnostics)
- Constant chain of custody rules 6-8 (frame rotation,
  same-provider, AU consistency)

Extract observe_from_geocentric() to astro_math.h for shared use
by planet_funcs.c, moon_funcs.c, and de_funcs.c.

57 → 68 functions, 11 → 12 regression test suites, all passing.
2026-02-16 19:54:48 -07:00

221 lines
7.1 KiB
C

/*
* moon_funcs.c -- Planetary moon observation
*
* SQL functions for observing moons of Jupiter, Saturn, Uranus, and Mars.
* Each moon's position is computed in the VSOP87 ecliptic J2000 frame
* relative to its parent planet, then combined with the parent's
* heliocentric position to get geocentric az/el.
*
* Pipeline for each moon:
* 1. Parent planet heliocentric position (VSOP87)
* 2. Moon position relative to parent (L12/TASS17/GUST86/MARSSAT)
* 3. Moon heliocentric = parent + moon_relative
* 4. Moon geocentric = moon_heliocentric - Earth_heliocentric
* 5. Ecliptic -> equatorial -> precess -> az/el
*/
#include "postgres.h"
#include "fmgr.h"
#include "funcapi.h"
#include "utils/timestamp.h"
#include "types.h"
#include "astro_math.h"
#include "vsop87.h"
#include "l12.h"
#include "tass17.h"
#include "gust86.h"
#include "marssat.h"
#include <math.h>
PG_FUNCTION_INFO_V1(galilean_observe);
PG_FUNCTION_INFO_V1(saturn_moon_observe);
PG_FUNCTION_INFO_V1(uranus_moon_observe);
PG_FUNCTION_INFO_V1(mars_moon_observe);
/*
* observe_from_geocentric() is now in astro_math.h as a static inline,
* shared by planet_funcs.c, moon_funcs.c, and de_funcs.c.
*/
/* ================================================================
* Internal: common pattern for all planetary moons
*
* Given: moon position relative to parent (VSOP87 ecliptic J2000, AU)
* parent's VSOP87 body index (0-based)
* observer, JD
*
* Computes geocentric position and returns topocentric az/el.
* ================================================================
*/
static void
observe_planetary_moon(const double moon_rel[3], int vsop_parent,
double jd, const pg_observer *obs,
pg_topocentric *result)
{
double parent_xyz[6];
double earth_xyz[6];
double geo_ecl[3];
/* Parent planet heliocentric */
GetVsop87Coor(jd, vsop_parent, parent_xyz);
/* Earth heliocentric */
GetVsop87Coor(jd, 2, earth_xyz); /* VSOP87 body 2 = Earth */
/* Moon geocentric = (parent + moon_relative) - Earth */
geo_ecl[0] = (parent_xyz[0] + moon_rel[0]) - earth_xyz[0];
geo_ecl[1] = (parent_xyz[1] + moon_rel[1]) - earth_xyz[1];
geo_ecl[2] = (parent_xyz[2] + moon_rel[2]) - earth_xyz[2];
observe_from_geocentric(geo_ecl, jd, obs, result);
}
/* ================================================================
* galilean_observe(body_id int, observer, timestamptz) -> topocentric
*
* Observe a Galilean moon of Jupiter.
* Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto
*
* Uses L1.2 theory (Lainey, Duriez & Vienne) for moon positions
* and VSOP87 for Jupiter and Earth.
* ================================================================
*/
Datum
galilean_observe(PG_FUNCTION_ARGS)
{
int32 body_id = PG_GETARG_INT32(0);
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
int64 ts = PG_GETARG_INT64(2);
double jd;
double moon_xyz[3];
pg_topocentric *result;
if (body_id < L12_IO || body_id > L12_CALLISTO)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("galilean_observe: body_id %d must be 0-3 (Io, Europa, Ganymede, Callisto)",
body_id)));
jd = timestamptz_to_jd(ts);
/* Moon position relative to Jupiter, VSOP87 ecliptic J2000, AU */
GetL12Coor(jd, body_id, moon_xyz, NULL);
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
observe_planetary_moon(moon_xyz, 4, jd, obs, result); /* VSOP87 body 4 = Jupiter */
PG_RETURN_POINTER(result);
}
/* ================================================================
* saturn_moon_observe(body_id int, observer, timestamptz) -> topocentric
*
* Observe a moon of Saturn.
* Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione,
* 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion
*
* Uses TASS 1.7 theory (Vienne & Duriez).
* ================================================================
*/
Datum
saturn_moon_observe(PG_FUNCTION_ARGS)
{
int32 body_id = PG_GETARG_INT32(0);
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
int64 ts = PG_GETARG_INT64(2);
double jd;
double moon_xyz[3];
pg_topocentric *result;
if (body_id < TASS17_MIMAS || body_id > TASS17_HYPERION)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("saturn_moon_observe: body_id %d must be 0-7 (Mimas-Hyperion)",
body_id)));
jd = timestamptz_to_jd(ts);
GetTass17Coor(jd, body_id, moon_xyz, NULL);
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
observe_planetary_moon(moon_xyz, 5, jd, obs, result); /* VSOP87 body 5 = Saturn */
PG_RETURN_POINTER(result);
}
/* ================================================================
* uranus_moon_observe(body_id int, observer, timestamptz) -> topocentric
*
* Observe a moon of Uranus.
* Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon
*
* Uses GUST86 theory (Laskar & Jacobson).
* ================================================================
*/
Datum
uranus_moon_observe(PG_FUNCTION_ARGS)
{
int32 body_id = PG_GETARG_INT32(0);
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
int64 ts = PG_GETARG_INT64(2);
double jd;
double moon_xyz[3];
pg_topocentric *result;
if (body_id < GUST86_MIRANDA || body_id > GUST86_OBERON)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("uranus_moon_observe: body_id %d must be 0-4 (Miranda-Oberon)",
body_id)));
jd = timestamptz_to_jd(ts);
GetGust86Coor(jd, body_id, moon_xyz, NULL);
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
observe_planetary_moon(moon_xyz, 6, jd, obs, result); /* VSOP87 body 6 = Uranus */
PG_RETURN_POINTER(result);
}
/* ================================================================
* mars_moon_observe(body_id int, observer, timestamptz) -> topocentric
*
* Observe a moon of Mars.
* Body IDs: 0=Phobos, 1=Deimos
*
* Uses MarsSat theory (Lainey, 2007).
* ================================================================
*/
Datum
mars_moon_observe(PG_FUNCTION_ARGS)
{
int32 body_id = PG_GETARG_INT32(0);
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
int64 ts = PG_GETARG_INT64(2);
double jd;
double moon_xyz[3];
pg_topocentric *result;
if (body_id < MARS_SAT_PHOBOS || body_id > MARS_SAT_DEIMOS)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("mars_moon_observe: body_id %d must be 0-1 (Phobos, Deimos)",
body_id)));
jd = timestamptz_to_jd(ts);
GetMarsSatCoor(jd, body_id, moon_xyz, NULL);
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
observe_planetary_moon(moon_xyz, 3, jd, obs, result); /* VSOP87 body 3 = Mars */
PG_RETURN_POINTER(result);
}