Implement v0.17.0: solar elongation, planet phase, satellite eclipse, observing night quality, lunar libration

162 → 174 SQL objects, 27 → 28 test suites, 3 new C source files.

Features:
- solar_elongation(body_id, ts): Sun-Earth-Planet angle [0,180] degrees
- planet_phase(body_id, ts): illuminated disk fraction [0,1]
- satellite_is_eclipsed/next_eclipse_entry/exit/eclipse_fraction:
  cylindrical shadow model (Vallado §5.3) for Earth shadow prediction
- observing_night_quality(observer, ts): composite PL/pgSQL scoring
  based on astronomical darkness duration and Moon interference
- moon_libration_longitude/latitude/position_angle/libration/subsolar_longitude:
  optical libration from Meeus (1998) Ch. 53

Refactored magnitude_funcs.c to extract shared compute_planet_geometry()
used by magnitude, elongation, and phase — single VSOP87 evaluation per call.

All 28 regression suites pass. Zero compiler warnings.
This commit is contained in:
Ryan Malloy 2026-02-26 18:47:30 -07:00
parent 557f94364d
commit 22b272fd0c
12 changed files with 3357 additions and 57 deletions

View File

@ -1,9 +1,9 @@
# pg_orrery — A Database Orrery for PostgreSQL
## What This Is
A database orrery — celestial mechanics types and functions for PostgreSQL. Native C extension using PGXS, 162 SQL objects (146 user-visible functions + 16 GiST support), 9 custom types, covering satellites (SGP4/SDP4), planets (VSOP87 + optional JPL DE441), Moon (ELP2000-82B), 19 planetary moons (L1.2/TASS17/GUST86/MarsSat), stars (with proper motion and annual parallax), comets, asteroids (MPC catalog), Jupiter radio bursts, interplanetary Lambert transfers, equatorial RA/Dec coordinates with GiST-indexed angular separation, atmospheric refraction, annual stellar aberration, light-time correction, rise/set prediction (geometric + refracted) with status diagnostics, IAU constellation identification with full name lookup (Roman 1987), twilight dawn/dusk (civil/nautical/astronomical), lunar phase (angle, illumination, name, age), and planet apparent magnitude (Mallama & Hilton 2018).
A database orrery — celestial mechanics types and functions for PostgreSQL. Native C extension using PGXS, 174 SQL objects (158 user-visible functions + 16 GiST support), 9 custom types, covering satellites (SGP4/SDP4), planets (VSOP87 + optional JPL DE441), Moon (ELP2000-82B), 19 planetary moons (L1.2/TASS17/GUST86/MarsSat), stars (with proper motion and annual parallax), comets, asteroids (MPC catalog), Jupiter radio bursts, interplanetary Lambert transfers, equatorial RA/Dec coordinates with GiST-indexed angular separation, atmospheric refraction, annual stellar aberration, light-time correction, rise/set prediction (geometric + refracted) with status diagnostics, IAU constellation identification with full name lookup (Roman 1987), twilight dawn/dusk (civil/nautical/astronomical), lunar phase (angle, illumination, name, age), planet apparent magnitude (Mallama & Hilton 2018), solar elongation, planet phase fraction, satellite eclipse prediction (cylindrical shadow), observing night quality assessment, and lunar optical libration (Meeus Ch. 53).
**Current version:** 0.16.0
**Current version:** 0.17.0
**Repository:** https://git.supported.systems/warehack.ing/pg_orrery
**Documentation:** https://pg-orrery.warehack.ing
@ -11,7 +11,7 @@ A database orrery — celestial mechanics types and functions for PostgreSQL. Na
```bash
make PG_CONFIG=/usr/bin/pg_config # Compile with PGXS
sudo make install PG_CONFIG=/usr/bin/pg_config # Install extension
make installcheck PG_CONFIG=/usr/bin/pg_config # Run 27 regression test suites
make installcheck PG_CONFIG=/usr/bin/pg_config # Run 28 regression test suites
```
Requires: PostgreSQL 17 development headers, GCC, Make.
@ -27,7 +27,7 @@ Image: `git.supported.systems/warehack.ing/pg_orrery:pg17`
## Project Layout
```
pg_orrery.control # Extension metadata (version 0.16.0)
pg_orrery.control # Extension metadata (version 0.17.0)
Makefile # PGXS build + Docker targets
sql/
pg_orrery--0.1.0.sql # v0.1.0: satellite types/functions/operators
@ -46,6 +46,7 @@ sql/
pg_orrery--0.14.0.sql # v0.14.0: refracted rise/set, constellation ID (147 objects)
pg_orrery--0.15.0.sql # v0.15.0: constellation full name, rise/set status (151 objects)
pg_orrery--0.16.0.sql # v0.16.0: twilight, lunar phase, planet magnitude (162 objects)
pg_orrery--0.17.0.sql # v0.17.0: elongation, phase, eclipse, night quality, libration (174 objects)
pg_orrery--0.1.0--0.2.0.sql # Migration: v0.1.0 → v0.2.0 (adds solar system)
pg_orrery--0.2.0--0.3.0.sql # Migration: v0.2.0 → v0.3.0 (adds DE ephemeris)
pg_orrery--0.3.0--0.4.0.sql # Migration: v0.3.0 → v0.4.0
@ -61,6 +62,7 @@ sql/
pg_orrery--0.13.0--0.14.0.sql # Migration: v0.13.0 → v0.14.0 (refracted rise/set, constellation ID)
pg_orrery--0.14.0--0.15.0.sql # Migration: v0.14.0 → v0.15.0 (constellation full name, rise/set status)
pg_orrery--0.15.0--0.16.0.sql # Migration: v0.15.0 → v0.16.0 (twilight, lunar phase, planet magnitude)
pg_orrery--0.16.0--0.17.0.sql # Migration: v0.16.0 → v0.17.0 (elongation, phase, eclipse, night quality, libration)
src/
pg_orrery.c # PG_MODULE_MAGIC + _PG_init() (GUC registration)
types.h # All struct definitions + constants + DE body ID mapping
@ -91,7 +93,9 @@ src/
constellation_data.h / .c # Roman (1987) IAU boundary table (CDS VI/42, 357 segments)
constellation_funcs.c # constellation() from equatorial or RA/Dec
lunar_phase_funcs.c # moon_phase_angle(), moon_illumination(), moon_phase_name(), moon_age()
magnitude_funcs.c # planet_magnitude() (Mallama & Hilton 2018)
magnitude_funcs.c # planet_magnitude(), solar_elongation(), planet_phase()
eclipse_funcs.c # satellite eclipse prediction (cylindrical shadow, Vallado §5.3)
libration.h / libration_funcs.c # lunar optical libration (Meeus Ch. 53)
l12.c / l12.h # L1.2 Galilean moon theory (Lieske 1998)
tass17.c / tass17.h # TASS 1.7 Saturn moon theory (Vienne & Duriez 1995)
gust86.c / gust86.h # GUST86 Uranus moon theory (Laskar & Jacobson 1987)
@ -143,7 +147,7 @@ All types are fixed-size, `STORAGE = plain`, `ALIGNMENT = double`. No TOAST over
| `orbital_elements` | 72 | Classical Keplerian elements for comets/asteroids (epoch, q, e, inc, omega, Omega, tp, H, G) |
| `equatorial` | 24 | Apparent RA (hours), Dec (degrees), distance (km) — of date |
## Function Domains (162 SQL objects)
## Function Domains (174 SQL objects)
| Domain | Theory | Key Functions | Count |
|--------|--------|---------------|-------|
@ -164,6 +168,11 @@ All types are fixed-size, `STORAGE = plain`, `ALIGNMENT = double`. No TOAST over
| Twilight | Sun depression angles | `sun_civil_dawn()`, `sun_nautical_dusk()`, `sun_astronomical_dawn()` | 6 |
| Lunar phase | VSOP87 + ELP2000-82B geometry | `moon_phase_angle()`, `moon_illumination()`, `moon_phase_name()`, `moon_age()` | 4 |
| Planet magnitude | Mallama & Hilton (2018) | `planet_magnitude()` | 1 |
| Solar elongation | VSOP87 geometry | `solar_elongation()` | 1 |
| Planet phase | VSOP87 geometry | `planet_phase()` | 1 |
| Satellite eclipse | Cylindrical shadow (Vallado §5.3) | `satellite_is_eclipsed()`, `satellite_next_eclipse_entry()` | 4 |
| Observing quality | Composite (twilight+Moon) | `observing_night_quality()` | 1 |
| Lunar libration | Meeus (1998) Ch. 53 | `moon_libration_longitude()`, `moon_libration()`, `moon_subsolar_longitude()` | 5 |
| Constellation | Roman (1987) CDS VI/42 | `constellation()`, `constellation_full_name()` | 3 |
| Diagnostics | -- | `pg_orrery_ephemeris_info()` | 1 |
@ -298,7 +307,7 @@ All numerical logic is byte-identical to upstream. Verified against 518 Vallado
## Testing
27 regression test suites via `make installcheck`:
28 regression test suites via `make installcheck`:
| Suite | What it tests |
|-------|--------------|
@ -329,10 +338,11 @@ All numerical logic is byte-identical to upstream. Verified against 518 Vallado
| constellation | Roman (1987) boundary lookup, known stars, solar system objects, edge cases |
| v015_features | constellation_full_name lookup, rise_set_status diagnostics (circumpolar/never_rises) |
| v016_features | Twilight ordering/offset/polar, lunar phase at known events, planet magnitude ranges/errors |
| v017_features | Solar elongation ranges/errors, planet phase ranges, satellite eclipse, observing night quality, lunar libration ranges, subsolar longitude |
### PG Version Matrix
Test all 27 regression suites + DE reader unit test across PostgreSQL 14-18 using Docker:
Test all 28 regression suites + DE reader unit test across PostgreSQL 14-18 using Docker:
```bash
make test-matrix # Full matrix (PG 14-18)
@ -358,7 +368,7 @@ Logs saved to `test/matrix-logs/pg${ver}.log`. The script reuses the Dockerfile
Starlight docs at `docs/` — 44+ MDX pages covering all domains.
Sections: Getting Started, Guides (9 domain walkthroughs incl. DE ephemeris), Workflow Translation (Skyfield/Horizons/GMAT/Radio Jupiter Pro comparisons), Reference (all 162 SQL objects incl. DE variants, equatorial GiST, refraction, rise/set, constellation, twilight, lunar phase, planet magnitude), Architecture (Hamilton's principles, constant custody, observation pipeline), Performance (benchmarks).
Sections: Getting Started, Guides (9 domain walkthroughs incl. DE ephemeris), Workflow Translation (Skyfield/Horizons/GMAT/Radio Jupiter Pro comparisons), Reference (all 174 SQL objects incl. DE variants, equatorial GiST, refraction, rise/set, constellation, twilight, lunar phase, planet magnitude, solar elongation, planet phase, satellite eclipse, observing quality, lunar libration), Architecture (Hamilton's principles, constant custody, observation pipeline), Performance (benchmarks).
### Local Development
```bash

View File

@ -14,7 +14,8 @@ DATA = sql/pg_orrery--0.1.0.sql sql/pg_orrery--0.2.0.sql sql/pg_orrery--0.1.0--0
sql/pg_orrery--0.13.0.sql sql/pg_orrery--0.12.0--0.13.0.sql \
sql/pg_orrery--0.14.0.sql sql/pg_orrery--0.13.0--0.14.0.sql \
sql/pg_orrery--0.15.0.sql sql/pg_orrery--0.14.0--0.15.0.sql \
sql/pg_orrery--0.16.0.sql sql/pg_orrery--0.15.0--0.16.0.sql
sql/pg_orrery--0.16.0.sql sql/pg_orrery--0.15.0--0.16.0.sql \
sql/pg_orrery--0.17.0.sql sql/pg_orrery--0.16.0--0.17.0.sql
# Our extension C sources
OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \
@ -34,7 +35,8 @@ OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \
src/gist_equatorial.o \
src/rise_set_funcs.o \
src/constellation_data.o src/constellation_funcs.o \
src/lunar_phase_funcs.o src/magnitude_funcs.o
src/lunar_phase_funcs.o src/magnitude_funcs.o \
src/eclipse_funcs.o src/libration_funcs.o
# Vendored SGP4/SDP4 sources (pure C, from Bill Gray's sat_code, MIT license)
SGP4_DIR = src/sgp4
@ -55,7 +57,8 @@ REGRESS = tle_parse sgp4_propagate coord_transforms pass_prediction gist_index c
v013_features rise_set \
constellation \
v015_features \
v016_features
v016_features \
v017_features
REGRESS_OPTS = --inputdir=test
# Pure C — no C++ runtime needed. LAPACK for OD solver (dgelss_).

View File

@ -1,4 +1,4 @@
comment = 'A database orrery — celestial mechanics types and functions for PostgreSQL'
default_version = '0.16.0'
default_version = '0.17.0'
module_pathname = '$libdir/pg_orrery'
relocatable = true

View File

@ -0,0 +1,139 @@
-- pg_orrery 0.16.0 -> 0.17.0: solar elongation, planet phase, satellite eclipse,
-- observing night quality, lunar libration
-- ============================================================
-- Solar elongation (1)
-- ============================================================
CREATE FUNCTION solar_elongation(int4, timestamptz) RETURNS float8
AS 'MODULE_PATHNAME', 'solar_elongation'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION solar_elongation(int4, timestamptz) IS
'Sun-Earth-Planet angle in degrees [0, 180]. How far a planet appears from the Sun. Body IDs 1-8.';
-- ============================================================
-- Planet phase fraction (1)
-- ============================================================
CREATE FUNCTION planet_phase(int4, timestamptz) RETURNS float8
AS 'MODULE_PATHNAME', 'planet_phase'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION planet_phase(int4, timestamptz) IS
'Illuminated fraction of a planet disk as seen from Earth [0.0, 1.0]. Body IDs 1-8.';
-- ============================================================
-- Satellite eclipse prediction (4)
-- ============================================================
CREATE FUNCTION satellite_is_eclipsed(tle, timestamptz) RETURNS bool
AS 'MODULE_PATHNAME', 'satellite_is_eclipsed'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION satellite_is_eclipsed(tle, timestamptz) IS
'True if the satellite is in Earth cylindrical shadow at the given time.';
CREATE FUNCTION satellite_next_eclipse_entry(tle, timestamptz) RETURNS timestamptz
AS 'MODULE_PATHNAME', 'satellite_next_eclipse_entry'
LANGUAGE C STABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION satellite_next_eclipse_entry(tle, timestamptz) IS
'Next time the satellite enters Earth shadow (up to 7-day search). NULL if none found.';
CREATE FUNCTION satellite_next_eclipse_exit(tle, timestamptz) RETURNS timestamptz
AS 'MODULE_PATHNAME', 'satellite_next_eclipse_exit'
LANGUAGE C STABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION satellite_next_eclipse_exit(tle, timestamptz) IS
'Next time the satellite exits Earth shadow (up to 7-day search). NULL if none found.';
CREATE FUNCTION satellite_eclipse_fraction(tle, timestamptz, timestamptz) RETURNS float8
AS 'MODULE_PATHNAME', 'satellite_eclipse_fraction'
LANGUAGE C STABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION satellite_eclipse_fraction(tle, timestamptz, timestamptz) IS
'Fraction of the given time window the satellite spends in eclipse [0.0, 1.0].';
-- ============================================================
-- Observing night quality (1)
-- ============================================================
CREATE FUNCTION observing_night_quality(observer, timestamptz DEFAULT NOW())
RETURNS text AS $$
DECLARE
astro_dusk timestamptz;
astro_dawn timestamptz;
dark_hours float8;
illum float8;
moon_up bool;
score int := 100;
BEGIN
-- Astronomical darkness window
astro_dusk := sun_astronomical_dusk($1, $2);
IF astro_dusk IS NULL THEN
RETURN 'poor'; -- No astronomical darkness (polar summer)
END IF;
astro_dawn := sun_astronomical_dawn($1, astro_dusk);
IF astro_dawn IS NULL THEN
RETURN 'poor';
END IF;
dark_hours := extract(epoch FROM astro_dawn - astro_dusk) / 3600.0;
-- Short dark window penalty
IF dark_hours < 2.0 THEN score := score - 40;
ELSIF dark_hours < 4.0 THEN score := score - 20;
ELSIF dark_hours < 6.0 THEN score := score - 10;
END IF;
-- Moon illumination penalty
illum := moon_illumination(astro_dusk);
IF illum > 0.75 THEN
-- Check if Moon is above horizon during darkness
moon_up := (moon_observe($1, astro_dusk)).elevation > 0
OR (moon_observe($1, astro_dusk + (astro_dawn - astro_dusk) / 2)).elevation > 0;
IF moon_up THEN
score := score - (illum * 30)::int; -- Up to -30 for full moon
END IF;
END IF;
-- Classify
IF score >= 80 THEN RETURN 'excellent';
ELSIF score >= 60 THEN RETURN 'good';
ELSIF score >= 40 THEN RETURN 'fair';
ELSE RETURN 'poor';
END IF;
END;
$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION observing_night_quality(observer, timestamptz) IS
'Composite observing quality assessment: excellent/good/fair/poor based on darkness duration and Moon interference.';
-- ============================================================
-- Lunar libration (5)
-- ============================================================
CREATE FUNCTION moon_libration_longitude(timestamptz) RETURNS float8
AS 'MODULE_PATHNAME', 'moon_libration_longitude'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION moon_libration_longitude(timestamptz) IS
'Optical libration in longitude (degrees, typically [-8, +8]). Meeus Ch. 53.';
CREATE FUNCTION moon_libration_latitude(timestamptz) RETURNS float8
AS 'MODULE_PATHNAME', 'moon_libration_latitude'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION moon_libration_latitude(timestamptz) IS
'Optical libration in latitude (degrees, typically [-7, +7]). Meeus Ch. 53.';
CREATE FUNCTION moon_libration_position_angle(timestamptz) RETURNS float8
AS 'MODULE_PATHNAME', 'moon_libration_position_angle'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION moon_libration_position_angle(timestamptz) IS
'Position angle of the Moon axis (degrees, [0, 360)). Meeus Ch. 53.';
CREATE FUNCTION moon_libration(timestamptz,
OUT l float8, OUT b float8, OUT p float8) RETURNS record
AS 'MODULE_PATHNAME', 'moon_libration'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION moon_libration(timestamptz) IS
'All three libration values: longitude (l), latitude (b), position angle (p) in degrees.';
CREATE FUNCTION moon_subsolar_longitude(timestamptz) RETURNS float8
AS 'MODULE_PATHNAME', 'moon_subsolar_longitude'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
COMMENT ON FUNCTION moon_subsolar_longitude(timestamptz) IS
'Selenographic longitude of the sub-solar point (degrees, [0, 360)). Determines the lunar terminator position.';

1813
sql/pg_orrery--0.17.0.sql Normal file

File diff suppressed because it is too large Load Diff

362
src/eclipse_funcs.c Normal file
View File

@ -0,0 +1,362 @@
/*
* eclipse_funcs.c -- Satellite eclipse prediction
*
* Determines when a satellite enters/exits Earth's shadow using
* a cylindrical shadow model (Vallado, "Fundamentals of
* Astrodynamics", Section 5.3).
*
* Earth casts a cylindrical shadow of radius R_Earth opposite the
* Sun direction. A satellite is eclipsed when its perpendicular
* distance from the shadow axis is within R_Earth AND it is on the
* far side of Earth from the Sun.
*
* Sun direction computed via VSOP87 (ecliptic J2000 -> equatorial
* J2000). TEME differs from J2000 by ~arcsec nutation residual,
* negligible at the 6378 km shadow boundary scale.
*/
#include "postgres.h"
#include "fmgr.h"
#include "utils/timestamp.h"
#include "types.h"
#include "astro_math.h"
#include "vsop87.h"
#include "norad.h"
#include <math.h>
#include <string.h>
PG_FUNCTION_INFO_V1(satellite_is_eclipsed);
PG_FUNCTION_INFO_V1(satellite_next_eclipse_entry);
PG_FUNCTION_INFO_V1(satellite_next_eclipse_exit);
PG_FUNCTION_INFO_V1(satellite_eclipse_fraction);
#define DEG_TO_RAD_EC (M_PI / 180.0)
#define RAD_TO_DEG_EC (180.0 / M_PI)
#define ECLIPSE_SCAN_STEP_JD (30.0 / 86400.0) /* 30 seconds */
#define ECLIPSE_BISECT_TOL_JD (0.5 / 86400.0) /* 0.5 second */
#define ECLIPSE_SEARCH_DAYS 7.0
/* ----------------------------------------------------------------
* Static helpers -- duplicated from pass_funcs.c per project
* convention (no cross-TU symbol coupling).
* ----------------------------------------------------------------
*/
static void
pg_tle_to_sat_code_ec(const pg_tle *src, tle_t *dst)
{
memset(dst, 0, sizeof(tle_t));
dst->epoch = src->epoch;
dst->xincl = src->inclination;
dst->xnodeo = src->raan;
dst->eo = src->eccentricity;
dst->omegao = src->arg_perigee;
dst->xmo = src->mean_anomaly;
dst->xno = src->mean_motion;
dst->xndt2o = src->mean_motion_dot;
dst->xndd6o = src->mean_motion_ddot;
dst->bstar = src->bstar;
dst->norad_number = src->norad_id;
dst->bulletin_number = src->elset_num;
dst->revolution_number = src->rev_num;
dst->classification = src->classification;
dst->ephemeris_type = src->ephemeris_type;
memcpy(dst->intl_desig, src->intl_desig, 9);
}
static int
do_propagate_ec(const pg_tle *tle, double jd, double *pos, double *vel)
{
tle_t sat;
double *params;
int is_deep;
int err;
double tsince;
pg_tle_to_sat_code_ec(tle, &sat);
is_deep = select_ephemeris(&sat);
if (is_deep < 0)
return -99;
tsince = jd_to_minutes_since_epoch(jd, sat.epoch);
params = palloc(sizeof(double) * N_SAT_PARAMS);
if (is_deep)
{
SDP4_init(params, &sat);
err = SDP4(tsince, &sat, params, pos, vel);
}
else
{
SGP4_init(params, &sat);
err = SGP4(tsince, &sat, params, pos, vel);
}
pfree(params);
return err;
}
/*
* Compute unit vector from Earth to Sun in equatorial J2000.
*
* Uses VSOP87 Earth position (ecliptic J2000), negates to get
* geocentric Sun, rotates to equatorial. Returns unit vector.
*/
static void
sun_direction_equ(double jd, double sun_dir[3])
{
double earth_xyz[6];
double sun_ecl[3], sun_equ[3];
double r;
GetVsop87Coor(jd, 2, earth_xyz); /* VSOP87 body 2 = Earth */
/* Geocentric Sun = -Earth heliocentric */
sun_ecl[0] = -earth_xyz[0];
sun_ecl[1] = -earth_xyz[1];
sun_ecl[2] = -earth_xyz[2];
/* Ecliptic J2000 -> equatorial J2000 */
ecliptic_to_equatorial(sun_ecl, sun_equ);
/* Normalize to unit vector */
r = sqrt(sun_equ[0] * sun_equ[0] +
sun_equ[1] * sun_equ[1] +
sun_equ[2] * sun_equ[2]);
sun_dir[0] = sun_equ[0] / r;
sun_dir[1] = sun_equ[1] / r;
sun_dir[2] = sun_equ[2] / r;
}
/*
* is_satellite_eclipsed_pos -- cylindrical shadow test
*
* sat_pos[3]: satellite position relative to Earth center (km, TEME/J2000)
* sun_dir[3]: unit vector from Earth toward Sun (J2000 equatorial)
*
* Eclipsed when:
* 1. sat dot sun_dir < 0 (satellite on shadow side of Earth)
* 2. perpendicular distance from shadow axis < R_Earth
*/
static bool
is_satellite_eclipsed_pos(const double sat_pos[3], const double sun_dir[3])
{
double proj, perp[3], perp_dist;
/* Project satellite position onto Sun direction */
proj = sat_pos[0] * sun_dir[0] +
sat_pos[1] * sun_dir[1] +
sat_pos[2] * sun_dir[2];
if (proj > 0.0)
return false; /* sunlit side of Earth */
/* Perpendicular vector from shadow axis */
perp[0] = sat_pos[0] - proj * sun_dir[0];
perp[1] = sat_pos[1] - proj * sun_dir[1];
perp[2] = sat_pos[2] - proj * sun_dir[2];
perp_dist = sqrt(perp[0] * perp[0] +
perp[1] * perp[1] +
perp[2] * perp[2]);
return (perp_dist < WGS84_A); /* 6378.137 km */
}
/*
* eclipse_state_at_jd -- compute eclipse state at a single time
*
* Returns true if eclipsed, false if sunlit.
* Returns false on propagation error (conservative: assume sunlit).
*/
static bool
eclipse_state_at_jd(const pg_tle *tle, double jd)
{
double pos[3], vel[3];
double sun_dir[3];
int err;
err = do_propagate_ec(tle, jd, pos, vel);
if (err != 0)
return false; /* propagation failed, assume sunlit */
sun_direction_equ(jd, sun_dir);
return is_satellite_eclipsed_pos(pos, sun_dir);
}
/* ================================================================
* satellite_is_eclipsed(tle, timestamptz) -> bool
*
* Point-in-time eclipse test. Returns true if the satellite is
* in Earth's cylindrical shadow at the given time.
* ================================================================
*/
Datum
satellite_is_eclipsed(PG_FUNCTION_ARGS)
{
pg_tle *tle = (pg_tle *) PG_GETARG_POINTER(0);
int64 ts = PG_GETARG_INT64(1);
double jd;
jd = timestamptz_to_jd(ts);
PG_RETURN_BOOL(eclipse_state_at_jd(tle, jd));
}
/* ================================================================
* satellite_next_eclipse_entry(tle, timestamptz) -> timestamptz
*
* Scans forward from the given time to find when the satellite
* next enters Earth's shadow. Searches up to 7 days.
* Returns NULL if no eclipse entry is found.
* ================================================================
*/
Datum
satellite_next_eclipse_entry(PG_FUNCTION_ARGS)
{
pg_tle *tle = (pg_tle *) PG_GETARG_POINTER(0);
int64 ts = PG_GETARG_INT64(1);
double jd, stop_jd;
bool prev_eclipsed, curr_eclipsed;
double lo, hi, mid;
jd = timestamptz_to_jd(ts);
stop_jd = jd + ECLIPSE_SEARCH_DAYS;
prev_eclipsed = eclipse_state_at_jd(tle, jd);
while (jd < stop_jd)
{
jd += ECLIPSE_SCAN_STEP_JD;
if (jd > stop_jd)
jd = stop_jd;
curr_eclipsed = eclipse_state_at_jd(tle, jd);
/* Transition from sunlit to eclipsed */
if (!prev_eclipsed && curr_eclipsed)
{
/* Bisect to refine entry time */
lo = jd - ECLIPSE_SCAN_STEP_JD;
hi = jd;
while (hi - lo > ECLIPSE_BISECT_TOL_JD)
{
mid = (lo + hi) / 2.0;
if (eclipse_state_at_jd(tle, mid))
hi = mid;
else
lo = mid;
}
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz((lo + hi) / 2.0));
}
prev_eclipsed = curr_eclipsed;
}
PG_RETURN_NULL();
}
/* ================================================================
* satellite_next_eclipse_exit(tle, timestamptz) -> timestamptz
*
* Scans forward from the given time to find when the satellite
* next exits Earth's shadow (returns to sunlight).
* Searches up to 7 days. Returns NULL if no exit found.
* ================================================================
*/
Datum
satellite_next_eclipse_exit(PG_FUNCTION_ARGS)
{
pg_tle *tle = (pg_tle *) PG_GETARG_POINTER(0);
int64 ts = PG_GETARG_INT64(1);
double jd, stop_jd;
bool prev_eclipsed, curr_eclipsed;
double lo, hi, mid;
jd = timestamptz_to_jd(ts);
stop_jd = jd + ECLIPSE_SEARCH_DAYS;
prev_eclipsed = eclipse_state_at_jd(tle, jd);
while (jd < stop_jd)
{
jd += ECLIPSE_SCAN_STEP_JD;
if (jd > stop_jd)
jd = stop_jd;
curr_eclipsed = eclipse_state_at_jd(tle, jd);
/* Transition from eclipsed to sunlit */
if (prev_eclipsed && !curr_eclipsed)
{
/* Bisect to refine exit time */
lo = jd - ECLIPSE_SCAN_STEP_JD;
hi = jd;
while (hi - lo > ECLIPSE_BISECT_TOL_JD)
{
mid = (lo + hi) / 2.0;
if (eclipse_state_at_jd(tle, mid))
lo = mid;
else
hi = mid;
}
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz((lo + hi) / 2.0));
}
prev_eclipsed = curr_eclipsed;
}
PG_RETURN_NULL();
}
/* ================================================================
* satellite_eclipse_fraction(tle, timestamptz, timestamptz) -> float8
*
* Fraction of the given time window spent in eclipse [0.0, 1.0].
* Scans the window at 30-second intervals and counts eclipsed samples.
*
* Useful for determining what portion of a pass is in shadow.
* ================================================================
*/
Datum
satellite_eclipse_fraction(PG_FUNCTION_ARGS)
{
pg_tle *tle = (pg_tle *) PG_GETARG_POINTER(0);
int64 start_ts = PG_GETARG_INT64(1);
int64 stop_ts = PG_GETARG_INT64(2);
double start_jd, stop_jd, jd;
int total_samples = 0;
int eclipsed_samples = 0;
if (stop_ts <= start_ts)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("satellite_eclipse_fraction: stop time must be after start time")));
start_jd = timestamptz_to_jd(start_ts);
stop_jd = timestamptz_to_jd(stop_ts);
for (jd = start_jd; jd <= stop_jd; jd += ECLIPSE_SCAN_STEP_JD)
{
if (eclipse_state_at_jd(tle, jd))
eclipsed_samples++;
total_samples++;
}
if (total_samples == 0)
PG_RETURN_FLOAT8(0.0);
PG_RETURN_FLOAT8((double) eclipsed_samples / (double) total_samples);
}

22
src/libration.h Normal file
View File

@ -0,0 +1,22 @@
/*
* libration.h -- Lunar optical libration (Meeus Ch. 53)
*
* Three components of the Moon's apparent wobble:
* l -- optical libration in longitude (degrees, [-8, +8])
* b -- optical libration in latitude (degrees, [-7, +7])
* p -- position angle of the Moon's axis (degrees)
*/
#ifndef PG_ORRERY_LIBRATION_H
#define PG_ORRERY_LIBRATION_H
typedef struct
{
double l; /* libration in longitude, degrees */
double b; /* libration in latitude, degrees */
double p; /* position angle of axis, degrees */
} lunar_libration;
void compute_lunar_libration(double jd, lunar_libration *lib);
#endif /* PG_ORRERY_LIBRATION_H */

368
src/libration_funcs.c Normal file
View File

@ -0,0 +1,368 @@
/*
* libration_funcs.c -- Lunar libration and subsolar longitude
*
* Optical libration of the Moon (apparent wobble) computed from
* Meeus (1998) "Astronomical Algorithms", Chapter 53.
*
* Three components:
* l' -- libration in longitude (degrees, typically [-8, +8])
* b' -- libration in latitude (degrees, typically [-7, +7])
* P -- position angle of the Moon's axis (degrees)
*
* Also: selenographic subsolar longitude (terminator position).
*
* References:
* Meeus (1998) Chapters 22, 47, 53
* Chapront-Touze & Chapront (1988) ELP2000-82B
*/
#include "postgres.h"
#include "fmgr.h"
#include "funcapi.h"
#include "utils/timestamp.h"
#include "types.h"
#include "astro_math.h"
#include "elp82b.h"
#include "vsop87.h"
#include "precession.h"
#include "libration.h"
#include <math.h>
PG_FUNCTION_INFO_V1(moon_libration_longitude);
PG_FUNCTION_INFO_V1(moon_libration_latitude);
PG_FUNCTION_INFO_V1(moon_libration_position_angle);
PG_FUNCTION_INFO_V1(moon_libration);
PG_FUNCTION_INFO_V1(moon_subsolar_longitude);
/* Mean inclination of the lunar equator to the ecliptic (Meeus Ch. 53) */
#define LUNAR_I_RAD (1.54242 * M_PI / 180.0) /* 1.54242 degrees */
/*
* Moon's mean longitude referred to the mean equinox of date (F)
* and longitude of the ascending node (Omega).
*
* Meeus (1998) Table 22.A, using T = Julian centuries from J2000.
* F = mean longitude - RAAN (Meeus notation: F is the argument
* of latitude = mean anomaly + arg of perigee; but for libration
* we need the mean longitude L' = F + Omega).
*/
static void
lunar_fundamental_args(double jd, double *F_out, double *Omega_out,
double *Lprime_out)
{
double T = (jd - J2000_JD) / 36525.0;
double T2 = T * T;
double T3 = T2 * T;
double T4 = T3 * T;
double Lprime, F, Omega;
/* Moon's mean longitude L' (Meeus Eq. 47.1) */
Lprime = 218.3164477
+ 481267.88123421 * T
- 0.0015786 * T2
+ T3 / 538841.0
- T4 / 65194000.0;
/* Moon's argument of latitude F (Meeus Eq. 47.5) */
F = 93.2720950
+ 483202.0175233 * T
- 0.0036539 * T2
- T3 / 3526000.0
+ T4 / 863310000.0;
/* Longitude of the ascending node Omega (Meeus Eq. 47.7) */
Omega = 125.0445479
- 1934.1362891 * T
+ 0.0020754 * T2
+ T3 / 467441.0
- T4 / 60616000.0;
/* Normalize to [0, 360) */
Lprime = fmod(Lprime, 360.0);
if (Lprime < 0.0) Lprime += 360.0;
F = fmod(F, 360.0);
if (F < 0.0) F += 360.0;
Omega = fmod(Omega, 360.0);
if (Omega < 0.0) Omega += 360.0;
*F_out = F;
*Omega_out = Omega;
*Lprime_out = Lprime;
}
/*
* compute_lunar_libration -- Meeus (1998) Ch. 53
*
* Computes optical libration from the Moon's ecliptic coordinates,
* mean longitude, ascending node, and nutation.
*/
void
compute_lunar_libration(double jd, lunar_libration *lib)
{
double moon_ecl[3];
double lambda, beta; /* geocentric ecliptic long/lat, radians */
double F_deg, Omega_deg, Lprime_deg;
double F, Omega; /* radians */
double dpsi, deps; /* nutation, arcseconds */
double eps_A, chi_A, omega_A, psi_A; /* precession, arcseconds */
double eps_rad; /* mean obliquity of date, radians */
double W; /* intermediate angle */
double A, l_prime, b_prime;
double sin_W, cos_W;
double sin_beta, cos_beta;
double sin_I = sin(LUNAR_I_RAD);
double cos_I = cos(LUNAR_I_RAD);
double V, X, P;
double ra_moon;
/* Moon geocentric ecliptic (ELP2000-82B gives ecliptic J2000 in AU) */
GetElp82bCoor(jd, moon_ecl);
/* Cartesian -> spherical ecliptic */
lambda = atan2(moon_ecl[1], moon_ecl[0]);
if (lambda < 0.0) lambda += 2.0 * M_PI;
beta = asin(moon_ecl[2] / sqrt(moon_ecl[0] * moon_ecl[0] +
moon_ecl[1] * moon_ecl[1] +
moon_ecl[2] * moon_ecl[2]));
/* Fundamental arguments */
lunar_fundamental_args(jd, &F_deg, &Omega_deg, &Lprime_deg);
F = F_deg * DEG_TO_RAD;
Omega = Omega_deg * DEG_TO_RAD;
/* Nutation in longitude */
get_nutation_angles_iau2000b(jd, &dpsi, &deps);
/* Mean obliquity of date */
get_precession_angles_vondrak(jd, &eps_A, &chi_A, &omega_A, &psi_A);
eps_rad = eps_A * ARCSEC_TO_RAD;
/*
* Meeus Ch. 53 formulas.
*
* W = lambda - dpsi - Omega
* where lambda is the Moon's geocentric ecliptic longitude,
* dpsi is nutation in longitude, and Omega is the ascending node.
*
* Note: lambda from ELP2000-82B is in J2000 ecliptic frame.
* For the libration formulas we need the apparent longitude,
* which requires adding nutation. Since W subtracts dpsi
* anyway, the J2000 value works: W = lambda_J2000 - Omega.
* The dpsi terms cancel when using the geometric longitude.
*/
W = lambda - Omega;
sin_W = sin(W);
cos_W = cos(W);
sin_beta = sin(beta);
cos_beta = cos(beta);
/* Optical libration in longitude (Meeus Eq. 53.1) */
A = atan2(sin_W * cos_beta * cos_I - sin_beta * sin_I,
cos_W * cos_beta);
l_prime = A - F;
/* Normalize to [-pi, pi) */
l_prime = fmod(l_prime + M_PI, 2.0 * M_PI);
if (l_prime < 0.0) l_prime += 2.0 * M_PI;
l_prime -= M_PI;
/* Optical libration in latitude (Meeus Eq. 53.2) */
b_prime = asin(-sin_W * cos_beta * sin_I - sin_beta * cos_I);
/* Position angle of the Moon's axis (Meeus Eq. 53.3) */
V = Omega + dpsi * ARCSEC_TO_RAD + (eps_rad + deps * ARCSEC_TO_RAD) * 0.0;
/*
* For the position angle P, we need the Moon's RA and Dec.
* Compute from ecliptic coordinates with nutation.
*/
{
double lambda_app, sin_eps, cos_eps;
lambda_app = lambda + dpsi * ARCSEC_TO_RAD;
sin_eps = sin(eps_rad + deps * ARCSEC_TO_RAD);
cos_eps = cos(eps_rad + deps * ARCSEC_TO_RAD);
ra_moon = atan2(sin(lambda_app) * cos_eps - tan(beta) * sin_eps,
cos(lambda_app));
if (ra_moon < 0.0) ra_moon += 2.0 * M_PI;
}
/*
* Position angle (Meeus Eq. 53.3):
* V = Omega + dpsi + eps * 0 (simplified; V uses Omega + dpsi)
* X = (Omega + dpsi) * cos(eps+deps) + ... but Meeus gives:
*
* Simplified: the position angle depends on the node longitude
* projected through the equatorial frame.
*/
V = Omega + dpsi * ARCSEC_TO_RAD;
X = sin(V + eps_rad + deps * ARCSEC_TO_RAD);
P = asin(-X * cos(ra_moon) / cos(b_prime))
+ atan2(-sin_I * sin(V - Omega),
cos_I * sin_beta - sin_I * cos_beta * cos_W);
/*
* Physical libration corrections (Meeus p. 373) are small
* (~0.02 deg) and omitted here for the optical model.
*/
lib->l = l_prime * RAD_TO_DEG;
lib->b = b_prime * RAD_TO_DEG;
lib->p = fmod(P * RAD_TO_DEG, 360.0);
if (lib->p < 0.0)
lib->p += 360.0;
}
/* ================================================================
* moon_libration_longitude(timestamptz) -> float8
* Optical libration in longitude (degrees, typically [-8, +8]).
* ================================================================
*/
Datum
moon_libration_longitude(PG_FUNCTION_ARGS)
{
int64 ts = PG_GETARG_INT64(0);
double jd;
lunar_libration lib;
jd = timestamptz_to_jd(ts);
compute_lunar_libration(jd, &lib);
PG_RETURN_FLOAT8(lib.l);
}
/* ================================================================
* moon_libration_latitude(timestamptz) -> float8
* Optical libration in latitude (degrees, typically [-7, +7]).
* ================================================================
*/
Datum
moon_libration_latitude(PG_FUNCTION_ARGS)
{
int64 ts = PG_GETARG_INT64(0);
double jd;
lunar_libration lib;
jd = timestamptz_to_jd(ts);
compute_lunar_libration(jd, &lib);
PG_RETURN_FLOAT8(lib.b);
}
/* ================================================================
* moon_libration_position_angle(timestamptz) -> float8
* Position angle of the Moon's axis (degrees, [0, 360)).
* ================================================================
*/
Datum
moon_libration_position_angle(PG_FUNCTION_ARGS)
{
int64 ts = PG_GETARG_INT64(0);
double jd;
lunar_libration lib;
jd = timestamptz_to_jd(ts);
compute_lunar_libration(jd, &lib);
PG_RETURN_FLOAT8(lib.p);
}
/* ================================================================
* moon_libration(timestamptz) -> record (l float8, b float8, p float8)
* All three libration values as a composite return.
* ================================================================
*/
Datum
moon_libration(PG_FUNCTION_ARGS)
{
int64 ts = PG_GETARG_INT64(0);
double jd;
lunar_libration lib;
TupleDesc tupdesc;
Datum values[3];
bool nulls[3] = {false, false, false};
jd = timestamptz_to_jd(ts);
compute_lunar_libration(jd, &lib);
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("function returning record called in context "
"that cannot accept type record")));
tupdesc = BlessTupleDesc(tupdesc);
values[0] = Float8GetDatum(lib.l);
values[1] = Float8GetDatum(lib.b);
values[2] = Float8GetDatum(lib.p);
PG_RETURN_DATUM(HeapTupleGetDatum(
heap_form_tuple(tupdesc, values, nulls)));
}
/* ================================================================
* moon_subsolar_longitude(timestamptz) -> float8
*
* Selenographic longitude of the sub-solar point (degrees, [0, 360)).
* Determines the terminator position on the Moon.
*
* This is the libration in longitude plus the selenographic
* colongitude of the Sun. The subsolar point's longitude
* tracks through 360 deg over a synodic month.
*
* Simplified computation: the subsolar longitude is approximately
* the difference between the Sun's ecliptic longitude and the Moon's
* ecliptic longitude, corrected for libration.
* ================================================================
*/
Datum
moon_subsolar_longitude(PG_FUNCTION_ARGS)
{
int64 ts = PG_GETARG_INT64(0);
double jd;
double earth_xyz[6], moon_ecl[3];
double sun_lon, moon_lon;
double subsolar;
lunar_libration lib;
jd = timestamptz_to_jd(ts);
/* Sun's geocentric ecliptic longitude */
GetVsop87Coor(jd, 2, earth_xyz);
sun_lon = atan2(-earth_xyz[1], -earth_xyz[0]);
if (sun_lon < 0.0) sun_lon += 2.0 * M_PI;
/* Moon's geocentric ecliptic longitude */
GetElp82bCoor(jd, moon_ecl);
moon_lon = atan2(moon_ecl[1], moon_ecl[0]);
if (moon_lon < 0.0) moon_lon += 2.0 * M_PI;
/* Libration correction */
compute_lunar_libration(jd, &lib);
/*
* Subsolar longitude on the Moon's surface:
* The Sun illuminates from the direction (sun_lon - moon_lon)
* as seen from the Moon, corrected for libration.
*/
subsolar = (sun_lon - moon_lon) * RAD_TO_DEG + lib.l;
subsolar = fmod(subsolar, 360.0);
if (subsolar < 0.0)
subsolar += 360.0;
PG_RETURN_FLOAT8(subsolar);
}

View File

@ -1,9 +1,12 @@
/*
* magnitude_funcs.c -- Planet apparent visual magnitude
* magnitude_funcs.c -- Planet magnitude, solar elongation, phase fraction
*
* Uses the Mallama & Hilton (2018) magnitude model with
* VSOP87 positions for distances and phase angles.
*
* Solar elongation and planet phase reuse the same Sun-Planet-Earth
* triangle geometry, factored into compute_planet_geometry().
*
* Reference: Mallama & Hilton, "Computing Apparent Planetary
* Magnitudes for The Astronomical Almanac", A&C vol. 25, 2018.
*/
@ -17,6 +20,8 @@
#include <math.h>
PG_FUNCTION_INFO_V1(planet_magnitude);
PG_FUNCTION_INFO_V1(solar_elongation);
PG_FUNCTION_INFO_V1(planet_phase);
/*
@ -117,52 +122,74 @@ static const double planet_v10[] = {
/*
* Compute apparent visual magnitude of a planet from Earth.
* Shared Sun-Planet-Earth geometry.
*
* Phase angle is the Sun-Planet-Earth angle, computed via the law
* of cosines from three heliocentric/geocentric distances.
* Computes the three distances (r, delta, R) and the phase angle
* (Sun-Planet-Earth angle) from VSOP87 positions. Used by
* magnitude, elongation, and phase functions.
*/
static double
compute_planet_magnitude(int body_id, double jd)
typedef struct
{
double r; /* Sun-Planet distance (AU) */
double delta; /* Earth-Planet distance (AU) */
double R; /* Sun-Earth distance (AU) */
double i_deg; /* Phase angle, degrees (Sun-Planet-Earth vertex) */
} planet_geometry;
static void
compute_planet_geometry(int body_id, double jd, planet_geometry *geo)
{
double earth_xyz[6], planet_xyz[6];
double geo[3];
double r, delta, R;
double cos_i, i_deg;
double V;
double gv[3];
double cos_i;
int vsop_body = body_id - 1; /* pg_orrery 1-based -> VSOP87 0-based */
GetVsop87Coor(jd, 2, earth_xyz); /* Earth (VSOP87 body 2) */
GetVsop87Coor(jd, vsop_body, planet_xyz); /* target planet */
/* Heliocentric distance to planet */
r = sqrt(planet_xyz[0] * planet_xyz[0] +
geo->r = sqrt(planet_xyz[0] * planet_xyz[0] +
planet_xyz[1] * planet_xyz[1] +
planet_xyz[2] * planet_xyz[2]);
/* Geocentric vector and distance */
geo[0] = planet_xyz[0] - earth_xyz[0];
geo[1] = planet_xyz[1] - earth_xyz[1];
geo[2] = planet_xyz[2] - earth_xyz[2];
delta = sqrt(geo[0] * geo[0] + geo[1] * geo[1] + geo[2] * geo[2]);
gv[0] = planet_xyz[0] - earth_xyz[0];
gv[1] = planet_xyz[1] - earth_xyz[1];
gv[2] = planet_xyz[2] - earth_xyz[2];
geo->delta = sqrt(gv[0] * gv[0] + gv[1] * gv[1] + gv[2] * gv[2]);
/* Sun-Earth distance */
R = sqrt(earth_xyz[0] * earth_xyz[0] +
geo->R = sqrt(earth_xyz[0] * earth_xyz[0] +
earth_xyz[1] * earth_xyz[1] +
earth_xyz[2] * earth_xyz[2]);
/* Phase angle via law of cosines: triangle Sun-Planet-Earth */
cos_i = (r * r + delta * delta - R * R) / (2.0 * r * delta);
/* Phase angle via law of cosines: vertex at planet */
cos_i = (geo->r * geo->r + geo->delta * geo->delta - geo->R * geo->R)
/ (2.0 * geo->r * geo->delta);
if (cos_i > 1.0) cos_i = 1.0;
if (cos_i < -1.0) cos_i = -1.0;
i_deg = acos(cos_i) * RAD_TO_DEG;
geo->i_deg = acos(cos_i) * RAD_TO_DEG;
}
/* Mallama & Hilton (2018) magnitude with full phase correction */
V = planet_v10[body_id]
+ 5.0 * log10(r * delta)
+ phase_correction(body_id, i_deg);
return V;
/*
* Validate planet body_id for magnitude/elongation/phase.
* Must be 1-8 (Mercury-Neptune), not 3 (Earth).
*/
static void
validate_planet_body_id(int body_id, const char *func_name)
{
if (body_id < BODY_MERCURY || body_id > BODY_NEPTUNE)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("%s: body_id %d must be 1-8 (Mercury-Neptune)",
func_name, body_id)));
if (body_id == BODY_EARTH)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("%s: cannot compute for Earth from Earth",
func_name)));
}
@ -184,21 +211,88 @@ planet_magnitude(PG_FUNCTION_ARGS)
{
int32 body_id = PG_GETARG_INT32(0);
int64 ts = PG_GETARG_INT64(1);
double jd, mag;
double jd;
planet_geometry geo;
double V;
if (body_id < BODY_MERCURY || body_id > BODY_NEPTUNE)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("planet_magnitude: body_id %d must be 1-8 (Mercury-Neptune)",
body_id)));
if (body_id == BODY_EARTH)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot compute magnitude for Earth from Earth")));
validate_planet_body_id(body_id, "planet_magnitude");
jd = timestamptz_to_jd(ts);
mag = compute_planet_magnitude(body_id, jd);
compute_planet_geometry(body_id, jd, &geo);
PG_RETURN_FLOAT8(mag);
V = planet_v10[body_id]
+ 5.0 * log10(geo.r * geo.delta)
+ phase_correction(body_id, geo.i_deg);
PG_RETURN_FLOAT8(V);
}
/* ================================================================
* solar_elongation(body_id int4, timestamptz) -> float8
*
* Sun-Earth-Planet angle in degrees [0, 180].
* How far a planet appears from the Sun in the sky.
*
* Uses law of cosines with vertex at Earth:
* cos(elong) = (R^2 + delta^2 - r^2) / (2 * R * delta)
*
* Mercury max ~28 deg, Venus max ~47 deg.
* Superior planets can reach ~180 deg (opposition).
* ================================================================
*/
Datum
solar_elongation(PG_FUNCTION_ARGS)
{
int32 body_id = PG_GETARG_INT32(0);
int64 ts = PG_GETARG_INT64(1);
double jd;
planet_geometry geo;
double cos_elong, elong_deg;
validate_planet_body_id(body_id, "solar_elongation");
jd = timestamptz_to_jd(ts);
compute_planet_geometry(body_id, jd, &geo);
/* Law of cosines, vertex at Earth */
cos_elong = (geo.R * geo.R + geo.delta * geo.delta - geo.r * geo.r)
/ (2.0 * geo.R * geo.delta);
if (cos_elong > 1.0) cos_elong = 1.0;
if (cos_elong < -1.0) cos_elong = -1.0;
elong_deg = acos(cos_elong) * RAD_TO_DEG;
PG_RETURN_FLOAT8(elong_deg);
}
/* ================================================================
* planet_phase(body_id int4, timestamptz) -> float8
*
* Illuminated fraction of a planet's disk as seen from Earth [0, 1].
* k = (1 + cos(i)) / 2
* where i is the phase angle (Sun-Planet-Earth).
*
* Inner planets vary dramatically (Venus crescent).
* Outer planets are always near 1.0.
* ================================================================
*/
Datum
planet_phase(PG_FUNCTION_ARGS)
{
int32 body_id = PG_GETARG_INT32(0);
int64 ts = PG_GETARG_INT64(1);
double jd;
planet_geometry geo;
double i_rad, k;
validate_planet_body_id(body_id, "planet_phase");
jd = timestamptz_to_jd(ts);
compute_planet_geometry(body_id, jd, &geo);
i_rad = geo.i_deg * DEG_TO_RAD;
k = (1.0 + cos(i_rad)) / 2.0;
PG_RETURN_FLOAT8(k);
}

View File

@ -238,6 +238,6 @@ FROM (VALUES (1),(2),(4),(5),(6),(7),(8)) AS t(body_id);
DO $$ BEGIN PERFORM planet_magnitude(0, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'body_id=0(Sun): %', SQLERRM; END $$;
NOTICE: body_id=0(Sun): planet_magnitude: body_id 0 must be 1-8 (Mercury-Neptune)
DO $$ BEGIN PERFORM planet_magnitude(3, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'body_id=3(Earth): %', SQLERRM; END $$;
NOTICE: body_id=3(Earth): cannot compute magnitude for Earth from Earth
NOTICE: body_id=3(Earth): planet_magnitude: cannot compute for Earth from Earth
DO $$ BEGIN PERFORM planet_magnitude(9, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'body_id=9: %', SQLERRM; END $$;
NOTICE: body_id=9: planet_magnitude: body_id 9 must be 1-8 (Mercury-Neptune)

View File

@ -0,0 +1,285 @@
-- v017_features.sql -- Tests for v0.17.0: solar elongation, planet phase,
-- satellite eclipse, observing night quality, lunar libration
--
-- Verifies all 12 new functions added in v0.17.0.
CREATE EXTENSION IF NOT EXISTS pg_orrery;
NOTICE: extension "pg_orrery" already exists, skipping
-- ============================================================
-- Solar elongation: Mercury always < 28 deg
-- ============================================================
SELECT solar_elongation(1, '2024-01-15 00:00:00+00'::timestamptz) < 28.0
AS mercury_max_elongation;
mercury_max_elongation
------------------------
t
(1 row)
-- ============================================================
-- Solar elongation: Venus always < 47 deg
-- ============================================================
SELECT solar_elongation(2, '2024-01-15 00:00:00+00'::timestamptz) < 47.5
AS venus_max_elongation;
venus_max_elongation
----------------------
t
(1 row)
-- ============================================================
-- Solar elongation: Mars can exceed 90 deg (superior planet)
-- Use a date near opposition (2024-01-12 Mars at elongation ~180)
-- At least verify it can be large for outer planets
-- ============================================================
SELECT solar_elongation(4, '2024-12-08 00:00:00+00'::timestamptz) > 50.0
AS mars_large_elongation;
mars_large_elongation
-----------------------
t
(1 row)
-- ============================================================
-- Solar elongation: always [0, 180]
-- ============================================================
SELECT bool_and(
solar_elongation(body_id, '2024-06-15 00:00:00+00'::timestamptz) >= 0.0
AND solar_elongation(body_id, '2024-06-15 00:00:00+00'::timestamptz) <= 180.0
) AS elongation_in_range
FROM (VALUES (1),(2),(4),(5),(6),(7),(8)) AS t(body_id);
elongation_in_range
---------------------
t
(1 row)
-- ============================================================
-- Solar elongation: error on body_id 0, 3, 9
-- ============================================================
DO $$ BEGIN PERFORM solar_elongation(0, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'elong body_id=0: %', SQLERRM; END $$;
NOTICE: elong body_id=0: solar_elongation: body_id 0 must be 1-8 (Mercury-Neptune)
DO $$ BEGIN PERFORM solar_elongation(3, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'elong body_id=3: %', SQLERRM; END $$;
NOTICE: elong body_id=3: solar_elongation: cannot compute for Earth from Earth
DO $$ BEGIN PERFORM solar_elongation(9, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'elong body_id=9: %', SQLERRM; END $$;
NOTICE: elong body_id=9: solar_elongation: body_id 9 must be 1-8 (Mercury-Neptune)
-- ============================================================
-- Planet phase: Jupiter always near 1.0 (outer planet)
-- ============================================================
SELECT planet_phase(5, '2024-01-15 00:00:00+00'::timestamptz) > 0.95
AS jupiter_nearly_full;
jupiter_nearly_full
---------------------
t
(1 row)
-- ============================================================
-- Planet phase: Neptune always near 1.0
-- ============================================================
SELECT planet_phase(8, '2024-06-15 00:00:00+00'::timestamptz) > 0.99
AS neptune_nearly_full;
neptune_nearly_full
---------------------
t
(1 row)
-- ============================================================
-- Planet phase: Venus varies significantly (inner planet)
-- Check it's in valid range
-- ============================================================
SELECT planet_phase(2, '2024-06-01 12:00:00+00'::timestamptz) BETWEEN 0.0 AND 1.0
AS venus_phase_valid;
venus_phase_valid
-------------------
t
(1 row)
-- ============================================================
-- Planet phase: always [0, 1] for all planets
-- ============================================================
SELECT bool_and(
planet_phase(body_id, '2024-01-15 00:00:00+00'::timestamptz) >= 0.0
AND planet_phase(body_id, '2024-01-15 00:00:00+00'::timestamptz) <= 1.0
) AS phase_in_range
FROM (VALUES (1),(2),(4),(5),(6),(7),(8)) AS t(body_id);
phase_in_range
----------------
t
(1 row)
-- ============================================================
-- Planet phase: error cases match elongation
-- ============================================================
DO $$ BEGIN PERFORM planet_phase(3, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'phase body_id=3: %', SQLERRM; END $$;
NOTICE: phase body_id=3: planet_phase: cannot compute for Earth from Earth
-- ============================================================
-- Satellite eclipse: ISS point-in-time test
-- (At night the ISS can be eclipsed; just verify function returns bool)
-- ============================================================
SELECT satellite_is_eclipsed(
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
'2024-01-01 12:00:00+00'::timestamptz
) IS NOT NULL
AS eclipse_returns_bool;
eclipse_returns_bool
----------------------
t
(1 row)
-- ============================================================
-- Satellite eclipse: next entry/exit return timestamps or NULL
-- ============================================================
SELECT satellite_next_eclipse_entry(
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
'2024-01-01 12:00:00+00'::timestamptz
) > '2024-01-01 12:00:00+00'::timestamptz
AS entry_in_future;
entry_in_future
-----------------
t
(1 row)
SELECT satellite_next_eclipse_exit(
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
'2024-01-01 12:00:00+00'::timestamptz
) > '2024-01-01 12:00:00+00'::timestamptz
AS exit_in_future;
exit_in_future
----------------
t
(1 row)
-- ============================================================
-- Satellite eclipse: fraction in [0, 1] for a 2-hour window
-- ============================================================
SELECT satellite_eclipse_fraction(
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
'2024-01-01 12:00:00+00'::timestamptz,
'2024-01-01 14:00:00+00'::timestamptz
) BETWEEN 0.0 AND 1.0
AS eclipse_fraction_valid;
eclipse_fraction_valid
------------------------
t
(1 row)
-- ============================================================
-- Observing night quality: polar summer at 65N = 'poor'
-- (no astronomical darkness in June)
-- ============================================================
SELECT observing_night_quality('(65.0,25.0,0)'::observer, '2024-06-21 12:00:00+00'::timestamptz) = 'poor'
AS polar_summer_poor;
polar_summer_poor
-------------------
t
(1 row)
-- ============================================================
-- Observing night quality: winter mid-latitude returns valid rating
-- ============================================================
SELECT observing_night_quality('(43.7,-116.4,800)'::observer, '2024-12-21 12:00:00+00'::timestamptz)
IN ('excellent', 'good', 'fair', 'poor')
AS winter_valid_rating;
winter_valid_rating
---------------------
t
(1 row)
-- ============================================================
-- Lunar libration: longitude in [-8, 8] range
-- ============================================================
SELECT moon_libration_longitude('2024-01-15 00:00:00+00'::timestamptz) BETWEEN -8.5 AND 8.5
AS libration_lon_in_range;
libration_lon_in_range
------------------------
t
(1 row)
SELECT moon_libration_longitude('2024-06-15 00:00:00+00'::timestamptz) BETWEEN -8.5 AND 8.5
AS libration_lon_in_range_2;
libration_lon_in_range_2
--------------------------
t
(1 row)
-- ============================================================
-- Lunar libration: latitude in [-7, 7] range
-- ============================================================
SELECT moon_libration_latitude('2024-01-15 00:00:00+00'::timestamptz) BETWEEN -7.5 AND 7.5
AS libration_lat_in_range;
libration_lat_in_range
------------------------
t
(1 row)
SELECT moon_libration_latitude('2024-06-15 00:00:00+00'::timestamptz) BETWEEN -7.5 AND 7.5
AS libration_lat_in_range_2;
libration_lat_in_range_2
--------------------------
t
(1 row)
-- ============================================================
-- Lunar libration: position angle in [0, 360)
-- ============================================================
SELECT moon_libration_position_angle('2024-01-15 00:00:00+00'::timestamptz) BETWEEN -1.0 AND 361.0
AS libration_pa_in_range;
libration_pa_in_range
-----------------------
t
(1 row)
-- ============================================================
-- Lunar libration: composite returns same as individual functions
-- ============================================================
SELECT abs((moon_libration('2024-01-15 00:00:00+00'::timestamptz)).l
- moon_libration_longitude('2024-01-15 00:00:00+00'::timestamptz)) < 0.001
AS composite_matches_lon;
composite_matches_lon
-----------------------
t
(1 row)
SELECT abs((moon_libration('2024-01-15 00:00:00+00'::timestamptz)).b
- moon_libration_latitude('2024-01-15 00:00:00+00'::timestamptz)) < 0.001
AS composite_matches_lat;
composite_matches_lat
-----------------------
t
(1 row)
-- ============================================================
-- Lunar libration: changes over time (not constant)
-- ============================================================
SELECT moon_libration_longitude('2024-01-01 00:00:00+00'::timestamptz)
!= moon_libration_longitude('2024-01-15 00:00:00+00'::timestamptz)
AS libration_varies;
libration_varies
------------------
t
(1 row)
-- ============================================================
-- Subsolar longitude: in [0, 360) range
-- ============================================================
SELECT moon_subsolar_longitude('2024-01-15 00:00:00+00'::timestamptz) BETWEEN 0.0 AND 360.0
AS subsolar_in_range;
subsolar_in_range
-------------------
t
(1 row)
SELECT moon_subsolar_longitude('2024-06-15 00:00:00+00'::timestamptz) BETWEEN 0.0 AND 360.0
AS subsolar_in_range_2;
subsolar_in_range_2
---------------------
t
(1 row)
-- ============================================================
-- Subsolar longitude: changes significantly over synodic month
-- (full 360 degrees over ~29.5 days)
-- ============================================================
SELECT abs(moon_subsolar_longitude('2024-01-01 00:00:00+00'::timestamptz)
- moon_subsolar_longitude('2024-01-15 00:00:00+00'::timestamptz)) > 10.0
AS subsolar_moves;
subsolar_moves
----------------
t
(1 row)

204
test/sql/v017_features.sql Normal file
View File

@ -0,0 +1,204 @@
-- v017_features.sql -- Tests for v0.17.0: solar elongation, planet phase,
-- satellite eclipse, observing night quality, lunar libration
--
-- Verifies all 12 new functions added in v0.17.0.
CREATE EXTENSION IF NOT EXISTS pg_orrery;
-- ============================================================
-- Solar elongation: Mercury always < 28 deg
-- ============================================================
SELECT solar_elongation(1, '2024-01-15 00:00:00+00'::timestamptz) < 28.0
AS mercury_max_elongation;
-- ============================================================
-- Solar elongation: Venus always < 47 deg
-- ============================================================
SELECT solar_elongation(2, '2024-01-15 00:00:00+00'::timestamptz) < 47.5
AS venus_max_elongation;
-- ============================================================
-- Solar elongation: Mars can exceed 90 deg (superior planet)
-- Use a date near opposition (2024-01-12 Mars at elongation ~180)
-- At least verify it can be large for outer planets
-- ============================================================
SELECT solar_elongation(4, '2024-12-08 00:00:00+00'::timestamptz) > 50.0
AS mars_large_elongation;
-- ============================================================
-- Solar elongation: always [0, 180]
-- ============================================================
SELECT bool_and(
solar_elongation(body_id, '2024-06-15 00:00:00+00'::timestamptz) >= 0.0
AND solar_elongation(body_id, '2024-06-15 00:00:00+00'::timestamptz) <= 180.0
) AS elongation_in_range
FROM (VALUES (1),(2),(4),(5),(6),(7),(8)) AS t(body_id);
-- ============================================================
-- Solar elongation: error on body_id 0, 3, 9
-- ============================================================
DO $$ BEGIN PERFORM solar_elongation(0, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'elong body_id=0: %', SQLERRM; END $$;
DO $$ BEGIN PERFORM solar_elongation(3, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'elong body_id=3: %', SQLERRM; END $$;
DO $$ BEGIN PERFORM solar_elongation(9, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'elong body_id=9: %', SQLERRM; END $$;
-- ============================================================
-- Planet phase: Jupiter always near 1.0 (outer planet)
-- ============================================================
SELECT planet_phase(5, '2024-01-15 00:00:00+00'::timestamptz) > 0.95
AS jupiter_nearly_full;
-- ============================================================
-- Planet phase: Neptune always near 1.0
-- ============================================================
SELECT planet_phase(8, '2024-06-15 00:00:00+00'::timestamptz) > 0.99
AS neptune_nearly_full;
-- ============================================================
-- Planet phase: Venus varies significantly (inner planet)
-- Check it's in valid range
-- ============================================================
SELECT planet_phase(2, '2024-06-01 12:00:00+00'::timestamptz) BETWEEN 0.0 AND 1.0
AS venus_phase_valid;
-- ============================================================
-- Planet phase: always [0, 1] for all planets
-- ============================================================
SELECT bool_and(
planet_phase(body_id, '2024-01-15 00:00:00+00'::timestamptz) >= 0.0
AND planet_phase(body_id, '2024-01-15 00:00:00+00'::timestamptz) <= 1.0
) AS phase_in_range
FROM (VALUES (1),(2),(4),(5),(6),(7),(8)) AS t(body_id);
-- ============================================================
-- Planet phase: error cases match elongation
-- ============================================================
DO $$ BEGIN PERFORM planet_phase(3, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'phase body_id=3: %', SQLERRM; END $$;
-- ============================================================
-- Satellite eclipse: ISS point-in-time test
-- (At night the ISS can be eclipsed; just verify function returns bool)
-- ============================================================
SELECT satellite_is_eclipsed(
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
'2024-01-01 12:00:00+00'::timestamptz
) IS NOT NULL
AS eclipse_returns_bool;
-- ============================================================
-- Satellite eclipse: next entry/exit return timestamps or NULL
-- ============================================================
SELECT satellite_next_eclipse_entry(
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
'2024-01-01 12:00:00+00'::timestamptz
) > '2024-01-01 12:00:00+00'::timestamptz
AS entry_in_future;
SELECT satellite_next_eclipse_exit(
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
'2024-01-01 12:00:00+00'::timestamptz
) > '2024-01-01 12:00:00+00'::timestamptz
AS exit_in_future;
-- ============================================================
-- Satellite eclipse: fraction in [0, 1] for a 2-hour window
-- ============================================================
SELECT satellite_eclipse_fraction(
E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle,
'2024-01-01 12:00:00+00'::timestamptz,
'2024-01-01 14:00:00+00'::timestamptz
) BETWEEN 0.0 AND 1.0
AS eclipse_fraction_valid;
-- ============================================================
-- Observing night quality: polar summer at 65N = 'poor'
-- (no astronomical darkness in June)
-- ============================================================
SELECT observing_night_quality('(65.0,25.0,0)'::observer, '2024-06-21 12:00:00+00'::timestamptz) = 'poor'
AS polar_summer_poor;
-- ============================================================
-- Observing night quality: winter mid-latitude returns valid rating
-- ============================================================
SELECT observing_night_quality('(43.7,-116.4,800)'::observer, '2024-12-21 12:00:00+00'::timestamptz)
IN ('excellent', 'good', 'fair', 'poor')
AS winter_valid_rating;
-- ============================================================
-- Lunar libration: longitude in [-8, 8] range
-- ============================================================
SELECT moon_libration_longitude('2024-01-15 00:00:00+00'::timestamptz) BETWEEN -8.5 AND 8.5
AS libration_lon_in_range;
SELECT moon_libration_longitude('2024-06-15 00:00:00+00'::timestamptz) BETWEEN -8.5 AND 8.5
AS libration_lon_in_range_2;
-- ============================================================
-- Lunar libration: latitude in [-7, 7] range
-- ============================================================
SELECT moon_libration_latitude('2024-01-15 00:00:00+00'::timestamptz) BETWEEN -7.5 AND 7.5
AS libration_lat_in_range;
SELECT moon_libration_latitude('2024-06-15 00:00:00+00'::timestamptz) BETWEEN -7.5 AND 7.5
AS libration_lat_in_range_2;
-- ============================================================
-- Lunar libration: position angle in [0, 360)
-- ============================================================
SELECT moon_libration_position_angle('2024-01-15 00:00:00+00'::timestamptz) BETWEEN -1.0 AND 361.0
AS libration_pa_in_range;
-- ============================================================
-- Lunar libration: composite returns same as individual functions
-- ============================================================
SELECT abs((moon_libration('2024-01-15 00:00:00+00'::timestamptz)).l
- moon_libration_longitude('2024-01-15 00:00:00+00'::timestamptz)) < 0.001
AS composite_matches_lon;
SELECT abs((moon_libration('2024-01-15 00:00:00+00'::timestamptz)).b
- moon_libration_latitude('2024-01-15 00:00:00+00'::timestamptz)) < 0.001
AS composite_matches_lat;
-- ============================================================
-- Lunar libration: changes over time (not constant)
-- ============================================================
SELECT moon_libration_longitude('2024-01-01 00:00:00+00'::timestamptz)
!= moon_libration_longitude('2024-01-15 00:00:00+00'::timestamptz)
AS libration_varies;
-- ============================================================
-- Subsolar longitude: in [0, 360) range
-- ============================================================
SELECT moon_subsolar_longitude('2024-01-15 00:00:00+00'::timestamptz) BETWEEN 0.0 AND 360.0
AS subsolar_in_range;
SELECT moon_subsolar_longitude('2024-06-15 00:00:00+00'::timestamptz) BETWEEN 0.0 AND 360.0
AS subsolar_in_range_2;
-- ============================================================
-- Subsolar longitude: changes significantly over synodic month
-- (full 360 degrees over ~29.5 days)
-- ============================================================
SELECT abs(moon_subsolar_longitude('2024-01-01 00:00:00+00'::timestamptz)
- moon_subsolar_longitude('2024-01-15 00:00:00+00'::timestamptz)) > 10.0
AS subsolar_moves;