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:
parent
557f94364d
commit
22b272fd0c
28
CLAUDE.md
28
CLAUDE.md
@ -1,9 +1,9 @@
|
|||||||
# pg_orrery — A Database Orrery for PostgreSQL
|
# pg_orrery — A Database Orrery for PostgreSQL
|
||||||
|
|
||||||
## What This Is
|
## 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
|
**Repository:** https://git.supported.systems/warehack.ing/pg_orrery
|
||||||
**Documentation:** https://pg-orrery.warehack.ing
|
**Documentation:** https://pg-orrery.warehack.ing
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ A database orrery — celestial mechanics types and functions for PostgreSQL. Na
|
|||||||
```bash
|
```bash
|
||||||
make PG_CONFIG=/usr/bin/pg_config # Compile with PGXS
|
make PG_CONFIG=/usr/bin/pg_config # Compile with PGXS
|
||||||
sudo make install PG_CONFIG=/usr/bin/pg_config # Install extension
|
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.
|
Requires: PostgreSQL 17 development headers, GCC, Make.
|
||||||
@ -27,7 +27,7 @@ Image: `git.supported.systems/warehack.ing/pg_orrery:pg17`
|
|||||||
|
|
||||||
## Project Layout
|
## 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
|
Makefile # PGXS build + Docker targets
|
||||||
sql/
|
sql/
|
||||||
pg_orrery--0.1.0.sql # v0.1.0: satellite types/functions/operators
|
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.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.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.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.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.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
|
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.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.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.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/
|
src/
|
||||||
pg_orrery.c # PG_MODULE_MAGIC + _PG_init() (GUC registration)
|
pg_orrery.c # PG_MODULE_MAGIC + _PG_init() (GUC registration)
|
||||||
types.h # All struct definitions + constants + DE body ID mapping
|
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_data.h / .c # Roman (1987) IAU boundary table (CDS VI/42, 357 segments)
|
||||||
constellation_funcs.c # constellation() from equatorial or RA/Dec
|
constellation_funcs.c # constellation() from equatorial or RA/Dec
|
||||||
lunar_phase_funcs.c # moon_phase_angle(), moon_illumination(), moon_phase_name(), moon_age()
|
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)
|
l12.c / l12.h # L1.2 Galilean moon theory (Lieske 1998)
|
||||||
tass17.c / tass17.h # TASS 1.7 Saturn moon theory (Vienne & Duriez 1995)
|
tass17.c / tass17.h # TASS 1.7 Saturn moon theory (Vienne & Duriez 1995)
|
||||||
gust86.c / gust86.h # GUST86 Uranus moon theory (Laskar & Jacobson 1987)
|
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) |
|
| `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 |
|
| `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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| Constellation | Roman (1987) CDS VI/42 | `constellation()`, `constellation_full_name()` | 3 |
|
||||||
| Diagnostics | -- | `pg_orrery_ephemeris_info()` | 1 |
|
| Diagnostics | -- | `pg_orrery_ephemeris_info()` | 1 |
|
||||||
|
|
||||||
@ -298,7 +307,7 @@ All numerical logic is byte-identical to upstream. Verified against 518 Vallado
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
27 regression test suites via `make installcheck`:
|
28 regression test suites via `make installcheck`:
|
||||||
|
|
||||||
| Suite | What it tests |
|
| 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 |
|
| 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) |
|
| 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 |
|
| 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
|
### 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
|
```bash
|
||||||
make test-matrix # Full matrix (PG 14-18)
|
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.
|
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
|
### Local Development
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
9
Makefile
9
Makefile
@ -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.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.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.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
|
# Our extension C sources
|
||||||
OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \
|
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/gist_equatorial.o \
|
||||||
src/rise_set_funcs.o \
|
src/rise_set_funcs.o \
|
||||||
src/constellation_data.o src/constellation_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)
|
# Vendored SGP4/SDP4 sources (pure C, from Bill Gray's sat_code, MIT license)
|
||||||
SGP4_DIR = src/sgp4
|
SGP4_DIR = src/sgp4
|
||||||
@ -55,7 +57,8 @@ REGRESS = tle_parse sgp4_propagate coord_transforms pass_prediction gist_index c
|
|||||||
v013_features rise_set \
|
v013_features rise_set \
|
||||||
constellation \
|
constellation \
|
||||||
v015_features \
|
v015_features \
|
||||||
v016_features
|
v016_features \
|
||||||
|
v017_features
|
||||||
REGRESS_OPTS = --inputdir=test
|
REGRESS_OPTS = --inputdir=test
|
||||||
|
|
||||||
# Pure C — no C++ runtime needed. LAPACK for OD solver (dgelss_).
|
# Pure C — no C++ runtime needed. LAPACK for OD solver (dgelss_).
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
comment = 'A database orrery — celestial mechanics types and functions for PostgreSQL'
|
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'
|
module_pathname = '$libdir/pg_orrery'
|
||||||
relocatable = true
|
relocatable = true
|
||||||
|
|||||||
139
sql/pg_orrery--0.16.0--0.17.0.sql
Normal file
139
sql/pg_orrery--0.16.0--0.17.0.sql
Normal 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
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
362
src/eclipse_funcs.c
Normal 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
22
src/libration.h
Normal 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
368
src/libration_funcs.c
Normal 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);
|
||||||
|
}
|
||||||
@ -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
|
* Uses the Mallama & Hilton (2018) magnitude model with
|
||||||
* VSOP87 positions for distances and phase angles.
|
* 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
|
* Reference: Mallama & Hilton, "Computing Apparent Planetary
|
||||||
* Magnitudes for The Astronomical Almanac", A&C vol. 25, 2018.
|
* Magnitudes for The Astronomical Almanac", A&C vol. 25, 2018.
|
||||||
*/
|
*/
|
||||||
@ -17,6 +20,8 @@
|
|||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
PG_FUNCTION_INFO_V1(planet_magnitude);
|
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
|
* Computes the three distances (r, delta, R) and the phase angle
|
||||||
* of cosines from three heliocentric/geocentric distances.
|
* (Sun-Planet-Earth angle) from VSOP87 positions. Used by
|
||||||
|
* magnitude, elongation, and phase functions.
|
||||||
*/
|
*/
|
||||||
static double
|
typedef struct
|
||||||
compute_planet_magnitude(int body_id, double jd)
|
{
|
||||||
|
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 earth_xyz[6], planet_xyz[6];
|
||||||
double geo[3];
|
double gv[3];
|
||||||
double r, delta, R;
|
double cos_i;
|
||||||
double cos_i, i_deg;
|
|
||||||
double V;
|
|
||||||
int vsop_body = body_id - 1; /* pg_orrery 1-based -> VSOP87 0-based */
|
int vsop_body = body_id - 1; /* pg_orrery 1-based -> VSOP87 0-based */
|
||||||
|
|
||||||
GetVsop87Coor(jd, 2, earth_xyz); /* Earth (VSOP87 body 2) */
|
GetVsop87Coor(jd, 2, earth_xyz); /* Earth (VSOP87 body 2) */
|
||||||
GetVsop87Coor(jd, vsop_body, planet_xyz); /* target planet */
|
GetVsop87Coor(jd, vsop_body, planet_xyz); /* target planet */
|
||||||
|
|
||||||
/* Heliocentric distance to 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[1] * planet_xyz[1] +
|
||||||
planet_xyz[2] * planet_xyz[2]);
|
planet_xyz[2] * planet_xyz[2]);
|
||||||
|
|
||||||
/* Geocentric vector and distance */
|
/* Geocentric vector and distance */
|
||||||
geo[0] = planet_xyz[0] - earth_xyz[0];
|
gv[0] = planet_xyz[0] - earth_xyz[0];
|
||||||
geo[1] = planet_xyz[1] - earth_xyz[1];
|
gv[1] = planet_xyz[1] - earth_xyz[1];
|
||||||
geo[2] = planet_xyz[2] - earth_xyz[2];
|
gv[2] = planet_xyz[2] - earth_xyz[2];
|
||||||
delta = sqrt(geo[0] * geo[0] + geo[1] * geo[1] + geo[2] * geo[2]);
|
geo->delta = sqrt(gv[0] * gv[0] + gv[1] * gv[1] + gv[2] * gv[2]);
|
||||||
|
|
||||||
/* Sun-Earth distance */
|
/* 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[1] * earth_xyz[1] +
|
||||||
earth_xyz[2] * earth_xyz[2]);
|
earth_xyz[2] * earth_xyz[2]);
|
||||||
|
|
||||||
/* Phase angle via law of cosines: triangle Sun-Planet-Earth */
|
/* Phase angle via law of cosines: vertex at planet */
|
||||||
cos_i = (r * r + delta * delta - R * R) / (2.0 * r * delta);
|
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;
|
||||||
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)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -182,23 +209,90 @@ compute_planet_magnitude(int body_id, double jd)
|
|||||||
Datum
|
Datum
|
||||||
planet_magnitude(PG_FUNCTION_ARGS)
|
planet_magnitude(PG_FUNCTION_ARGS)
|
||||||
{
|
{
|
||||||
int32 body_id = PG_GETARG_INT32(0);
|
int32 body_id = PG_GETARG_INT32(0);
|
||||||
int64 ts = PG_GETARG_INT64(1);
|
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)
|
validate_planet_body_id(body_id, "planet_magnitude");
|
||||||
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")));
|
|
||||||
|
|
||||||
jd = timestamptz_to_jd(ts);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 $$;
|
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)
|
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 $$;
|
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 $$;
|
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)
|
NOTICE: body_id=9: planet_magnitude: body_id 9 must be 1-8 (Mercury-Neptune)
|
||||||
|
|||||||
285
test/expected/v017_features.out
Normal file
285
test/expected/v017_features.out
Normal 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
204
test/sql/v017_features.sql
Normal 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;
|
||||||
Loading…
x
Reference in New Issue
Block a user