Add v0.16.0: twilight dawn/dusk, lunar phase, planet apparent magnitude
Twilight: 6 functions (civil/nautical/astronomical × dawn/dusk) reusing the existing find_next_crossing() bisection search with Sun depression angle thresholds (-6°, -12°, -18°). Returns NULL for polar regions where the threshold is never reached. Lunar phase: 4 functions computing Sun-Earth-Moon geometry from VSOP87 + ELP2000-82B. Phase angle [0,360) via elongation + cross product z-component for waxing/waning discrimination. 8 named phases in 45° bins. Moon age approximated from phase angle and mean synodic month. Planet magnitude: Mallama & Hilton (2018) polynomial model with VSOP87 heliocentric distances and phase angle via law of cosines. All 7 planets (Mercury-Neptune, excluding Earth). Saturn ring tilt not modeled. 151 → 162 SQL objects. 26 → 27 test suites, all passing.
This commit is contained in:
parent
e670ed7ed1
commit
46c8a30575
28
CLAUDE.md
28
CLAUDE.md
@ -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, 151 SQL objects (135 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, and IAU constellation identification with full name lookup (Roman 1987).
|
||||
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).
|
||||
|
||||
**Current version:** 0.15.0
|
||||
**Current version:** 0.16.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 26 regression test suites
|
||||
make installcheck PG_CONFIG=/usr/bin/pg_config # Run 27 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.15.0)
|
||||
pg_orrery.control # Extension metadata (version 0.16.0)
|
||||
Makefile # PGXS build + Docker targets
|
||||
sql/
|
||||
pg_orrery--0.1.0.sql # v0.1.0: satellite types/functions/operators
|
||||
@ -45,6 +45,7 @@ sql/
|
||||
pg_orrery--0.13.0.sql # v0.13.0: nutation, make_equatorial, rise/set (141 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.16.0.sql # v0.16.0: twilight, lunar phase, planet magnitude (162 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
|
||||
@ -59,6 +60,7 @@ sql/
|
||||
pg_orrery--0.12.0--0.13.0.sql # Migration: v0.12.0 → v0.13.0 (nutation, make_equatorial, rise/set)
|
||||
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)
|
||||
src/
|
||||
pg_orrery.c # PG_MODULE_MAGIC + _PG_init() (GUC registration)
|
||||
types.h # All struct definitions + constants + DE body ID mapping
|
||||
@ -85,9 +87,11 @@ src/
|
||||
orbital_elements_type.c # orbital_elements type, MPC parser, small_body_observe/equatorial/apparent()
|
||||
equatorial_funcs.c # equatorial type I/O, accessors, satellite/planet/sun/moon RA/Dec
|
||||
refraction_funcs.c # atmospheric_refraction(), _ext(), topo_elevation_apparent()
|
||||
rise_set_funcs.c # planet/sun/moon rise/set (geometric + refracted)
|
||||
rise_set_funcs.c # planet/sun/moon rise/set (geometric + refracted) + twilight dawn/dusk
|
||||
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)
|
||||
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)
|
||||
@ -112,7 +116,7 @@ src/
|
||||
PROVENANCE.md # Vendoring decision, modifications, verification
|
||||
LICENSE # MIT license (Bill Gray / Project Pluto)
|
||||
test/
|
||||
sql/ # 26 regression test suites
|
||||
sql/ # 27 regression test suites
|
||||
expected/ # Expected output
|
||||
data/vallado_518.json # 518 Vallado test vectors (AIAA 2006-6753-Rev1)
|
||||
docs/
|
||||
@ -139,7 +143,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 (151 SQL objects)
|
||||
## Function Domains (162 SQL objects)
|
||||
|
||||
| Domain | Theory | Key Functions | Count |
|
||||
|--------|--------|---------------|-------|
|
||||
@ -157,6 +161,9 @@ All types are fixed-size, `STORAGE = plain`, `ALIGNMENT = double`. No TOAST over
|
||||
| GiST index (TLE) | Altitude-band approximation | `&&` (overlap), `<->` (distance) | 8 |
|
||||
| GiST index (equatorial) | Spherical bounding box | `<->` (KNN ordering) | 8 |
|
||||
| Rise/set | Bisection (60s scan) | `planet_next_rise()`, `sun_next_rise_refracted()`, `*_rise_set_status()` | 15 |
|
||||
| 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 |
|
||||
| Constellation | Roman (1987) CDS VI/42 | `constellation()`, `constellation_full_name()` | 3 |
|
||||
| Diagnostics | -- | `pg_orrery_ephemeris_info()` | 1 |
|
||||
|
||||
@ -291,7 +298,7 @@ All numerical logic is byte-identical to upstream. Verified against 518 Vallado
|
||||
|
||||
## Testing
|
||||
|
||||
26 regression test suites via `make installcheck`:
|
||||
27 regression test suites via `make installcheck`:
|
||||
|
||||
| Suite | What it tests |
|
||||
|-------|--------------|
|
||||
@ -321,10 +328,11 @@ All numerical logic is byte-identical to upstream. Verified against 518 Vallado
|
||||
| rise_set | Planet/Sun/Moon rise/set (geometric + refracted), circumpolar, polar night |
|
||||
| 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 |
|
||||
|
||||
### PG Version Matrix
|
||||
|
||||
Test all 26 regression suites + DE reader unit test across PostgreSQL 14-18 using Docker:
|
||||
Test all 27 regression suites + DE reader unit test across PostgreSQL 14-18 using Docker:
|
||||
|
||||
```bash
|
||||
make test-matrix # Full matrix (PG 14-18)
|
||||
@ -350,7 +358,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 151 SQL objects incl. DE variants, equatorial GiST, refraction, rise/set, constellation), 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 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).
|
||||
|
||||
### Local Development
|
||||
```bash
|
||||
|
||||
9
Makefile
9
Makefile
@ -13,7 +13,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.12.0.sql sql/pg_orrery--0.11.0--0.12.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.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
|
||||
|
||||
# Our extension C sources
|
||||
OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \
|
||||
@ -32,7 +33,8 @@ OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \
|
||||
src/refraction_funcs.o \
|
||||
src/gist_equatorial.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
|
||||
|
||||
# Vendored SGP4/SDP4 sources (pure C, from Bill Gray's sat_code, MIT license)
|
||||
SGP4_DIR = src/sgp4
|
||||
@ -52,7 +54,8 @@ REGRESS = tle_parse sgp4_propagate coord_transforms pass_prediction gist_index c
|
||||
gist_equatorial v012_features \
|
||||
v013_features rise_set \
|
||||
constellation \
|
||||
v015_features
|
||||
v015_features \
|
||||
v016_features
|
||||
REGRESS_OPTS = --inputdir=test
|
||||
|
||||
# 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'
|
||||
default_version = '0.15.0'
|
||||
default_version = '0.16.0'
|
||||
module_pathname = '$libdir/pg_orrery'
|
||||
relocatable = true
|
||||
|
||||
79
sql/pg_orrery--0.15.0--0.16.0.sql
Normal file
79
sql/pg_orrery--0.15.0--0.16.0.sql
Normal file
@ -0,0 +1,79 @@
|
||||
-- pg_orrery 0.15.0 -> 0.16.0: twilight, lunar phase, planet magnitude
|
||||
|
||||
-- ============================================================
|
||||
-- Twilight functions (6)
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION sun_civil_dawn(observer, timestamptz) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME', 'sun_civil_dawn'
|
||||
LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_civil_dawn(observer, timestamptz) IS
|
||||
'Next civil dawn (Sun crosses -6 deg rising). Outdoor activities without artificial light.';
|
||||
|
||||
CREATE FUNCTION sun_civil_dusk(observer, timestamptz) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME', 'sun_civil_dusk'
|
||||
LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_civil_dusk(observer, timestamptz) IS
|
||||
'Next civil dusk (Sun crosses -6 deg setting). Artificial light needed.';
|
||||
|
||||
CREATE FUNCTION sun_nautical_dawn(observer, timestamptz) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME', 'sun_nautical_dawn'
|
||||
LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_nautical_dawn(observer, timestamptz) IS
|
||||
'Next nautical dawn (Sun crosses -12 deg rising). Horizon visible at sea.';
|
||||
|
||||
CREATE FUNCTION sun_nautical_dusk(observer, timestamptz) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME', 'sun_nautical_dusk'
|
||||
LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_nautical_dusk(observer, timestamptz) IS
|
||||
'Next nautical dusk (Sun crosses -12 deg setting). Horizon no longer visible at sea.';
|
||||
|
||||
CREATE FUNCTION sun_astronomical_dawn(observer, timestamptz) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME', 'sun_astronomical_dawn'
|
||||
LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_astronomical_dawn(observer, timestamptz) IS
|
||||
'Next astronomical dawn (Sun crosses -18 deg rising). Sky was fully dark.';
|
||||
|
||||
CREATE FUNCTION sun_astronomical_dusk(observer, timestamptz) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME', 'sun_astronomical_dusk'
|
||||
LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_astronomical_dusk(observer, timestamptz) IS
|
||||
'Next astronomical dusk (Sun crosses -18 deg setting). Sky becomes fully dark.';
|
||||
|
||||
-- ============================================================
|
||||
-- Lunar phase functions (4)
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION moon_phase_angle(timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME', 'moon_phase_angle'
|
||||
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION moon_phase_angle(timestamptz) IS
|
||||
'Sun-Earth-Moon phase angle in degrees [0,360). 0=new, 90=first quarter, 180=full, 270=last quarter.';
|
||||
|
||||
CREATE FUNCTION moon_illumination(timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME', 'moon_illumination'
|
||||
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION moon_illumination(timestamptz) IS
|
||||
'Illuminated fraction of the Moon disk [0.0, 1.0].';
|
||||
|
||||
CREATE FUNCTION moon_phase_name(timestamptz) RETURNS text
|
||||
AS 'MODULE_PATHNAME', 'moon_phase_name'
|
||||
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION moon_phase_name(timestamptz) IS
|
||||
'Moon phase name: new_moon, waxing_crescent, first_quarter, waxing_gibbous, full_moon, waning_gibbous, last_quarter, waning_crescent.';
|
||||
|
||||
CREATE FUNCTION moon_age(timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME', 'moon_age'
|
||||
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION moon_age(timestamptz) IS
|
||||
'Days since last new moon [0, ~29.53), approximated from phase angle.';
|
||||
|
||||
-- ============================================================
|
||||
-- Planet magnitude (1)
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION planet_magnitude(int4, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME', 'planet_magnitude'
|
||||
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_magnitude(int4, timestamptz) IS
|
||||
'Apparent visual magnitude of a planet (Mallama & Hilton 2018). Body IDs 1-8. Saturn ring tilt not modeled.';
|
||||
1674
sql/pg_orrery--0.16.0.sql
Normal file
1674
sql/pg_orrery--0.16.0.sql
Normal file
File diff suppressed because it is too large
Load Diff
236
src/lunar_phase_funcs.c
Normal file
236
src/lunar_phase_funcs.c
Normal file
@ -0,0 +1,236 @@
|
||||
/*
|
||||
* lunar_phase_funcs.c -- Lunar phase calculations
|
||||
*
|
||||
* Phase angle, illumination, phase name, and lunar age from
|
||||
* VSOP87 Sun + ELP2000-82B Moon positions.
|
||||
*
|
||||
* The Sun-Earth-Moon geometry determines phase:
|
||||
* 1. Earth heliocentric (VSOP87) -> negate -> Sun geocentric
|
||||
* 2. Moon geocentric ecliptic (ELP2000-82B)
|
||||
* 3. Elongation = angle between geocentric Sun and Moon
|
||||
* 4. Cross product z-component distinguishes waxing from waning
|
||||
*/
|
||||
|
||||
#include "postgres.h"
|
||||
#include "fmgr.h"
|
||||
#include "utils/timestamp.h"
|
||||
#include "utils/builtins.h"
|
||||
#include "types.h"
|
||||
#include "astro_math.h"
|
||||
#include "vsop87.h"
|
||||
#include "elp82b.h"
|
||||
#include <math.h>
|
||||
|
||||
PG_FUNCTION_INFO_V1(moon_phase_angle);
|
||||
PG_FUNCTION_INFO_V1(moon_illumination);
|
||||
PG_FUNCTION_INFO_V1(moon_phase_name);
|
||||
PG_FUNCTION_INFO_V1(moon_age);
|
||||
|
||||
/* Mean synodic month in days (Meeus, Astronomical Algorithms) */
|
||||
#define SYNODIC_MONTH 29.530588853
|
||||
|
||||
|
||||
/*
|
||||
* compute_phase_angle -- Sun-Earth-Moon phase angle in degrees [0, 360)
|
||||
*
|
||||
* 0 = new moon (Sun and Moon same direction from Earth)
|
||||
* 90 = first quarter (waxing)
|
||||
* 180 = full moon
|
||||
* 270 = last quarter (waning)
|
||||
*
|
||||
* Waxing/waning determined by the z-component of (sun_geo x moon_geo)
|
||||
* in the ecliptic plane: positive = Moon east of Sun = waxing.
|
||||
*/
|
||||
static double
|
||||
compute_phase_angle(double jd)
|
||||
{
|
||||
double earth_xyz[6];
|
||||
double moon_ecl[3];
|
||||
double sun_x, sun_y, sun_z;
|
||||
double moon_x, moon_y, moon_z;
|
||||
double dot, cross_z;
|
||||
double sun_r, moon_r;
|
||||
double elongation, angle;
|
||||
|
||||
/* Earth heliocentric -> Sun geocentric (negate) */
|
||||
GetVsop87Coor(jd, 2, earth_xyz); /* VSOP87 body 2 = Earth */
|
||||
sun_x = -earth_xyz[0];
|
||||
sun_y = -earth_xyz[1];
|
||||
sun_z = -earth_xyz[2];
|
||||
|
||||
/* Moon geocentric ecliptic (AU) */
|
||||
GetElp82bCoor(jd, moon_ecl);
|
||||
moon_x = moon_ecl[0];
|
||||
moon_y = moon_ecl[1];
|
||||
moon_z = moon_ecl[2];
|
||||
|
||||
/* Magnitudes for unit-vector dot product */
|
||||
sun_r = sqrt(sun_x * sun_x + sun_y * sun_y + sun_z * sun_z);
|
||||
moon_r = sqrt(moon_x * moon_x + moon_y * moon_y + moon_z * moon_z);
|
||||
|
||||
/* Elongation = angle between geocentric Sun and Moon directions */
|
||||
dot = (sun_x * moon_x + sun_y * moon_y + sun_z * moon_z)
|
||||
/ (sun_r * moon_r);
|
||||
if (dot > 1.0) dot = 1.0;
|
||||
if (dot < -1.0) dot = -1.0;
|
||||
elongation = acos(dot);
|
||||
|
||||
/* Cross product z-component: sign determines waxing vs waning */
|
||||
cross_z = sun_x * moon_y - sun_y * moon_x;
|
||||
|
||||
/*
|
||||
* Elongation is already the right quantity:
|
||||
* elongation 0 = same direction = new moon
|
||||
* elongation pi = opposite = full moon
|
||||
*
|
||||
* acos gives [0, 180]. Use cross_z to extend to [0, 360):
|
||||
* cross_z > 0: Moon east of Sun (waxing), stays [0, 180)
|
||||
* cross_z < 0: Moon west of Sun (waning), maps to [180, 360)
|
||||
*/
|
||||
angle = elongation * RAD_TO_DEG;
|
||||
|
||||
if (cross_z < 0.0)
|
||||
angle = 360.0 - angle;
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* moon_phase_angle(timestamptz) -> float8
|
||||
*
|
||||
* Returns the Sun-Earth-Moon phase angle in degrees [0, 360).
|
||||
* 0 = new moon
|
||||
* 90 = first quarter
|
||||
* 180 = full moon
|
||||
* 270 = last quarter
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
moon_phase_angle(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int64 ts = PG_GETARG_INT64(0);
|
||||
double jd;
|
||||
|
||||
jd = timestamptz_to_jd(ts);
|
||||
|
||||
PG_RETURN_FLOAT8(compute_phase_angle(jd));
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* moon_illumination(timestamptz) -> float8
|
||||
*
|
||||
* Returns the illuminated fraction of the Moon's disk [0.0, 1.0].
|
||||
* Uses the elongation between geocentric Sun and Moon directions:
|
||||
* k = (1 - cos(elongation)) / 2
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
moon_illumination(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int64 ts = PG_GETARG_INT64(0);
|
||||
double jd;
|
||||
double earth_xyz[6];
|
||||
double moon_ecl[3];
|
||||
double sun_x, sun_y, sun_z;
|
||||
double dot;
|
||||
double sun_r, moon_r;
|
||||
double elongation, illum;
|
||||
|
||||
jd = timestamptz_to_jd(ts);
|
||||
|
||||
/* Earth heliocentric -> Sun geocentric (negate) */
|
||||
GetVsop87Coor(jd, 2, earth_xyz);
|
||||
sun_x = -earth_xyz[0];
|
||||
sun_y = -earth_xyz[1];
|
||||
sun_z = -earth_xyz[2];
|
||||
|
||||
/* Moon geocentric ecliptic */
|
||||
GetElp82bCoor(jd, moon_ecl);
|
||||
|
||||
/* Elongation between geocentric Sun and Moon */
|
||||
sun_r = sqrt(sun_x * sun_x + sun_y * sun_y + sun_z * sun_z);
|
||||
moon_r = sqrt(moon_ecl[0] * moon_ecl[0] +
|
||||
moon_ecl[1] * moon_ecl[1] +
|
||||
moon_ecl[2] * moon_ecl[2]);
|
||||
|
||||
dot = (sun_x * moon_ecl[0] + sun_y * moon_ecl[1] + sun_z * moon_ecl[2])
|
||||
/ (sun_r * moon_r);
|
||||
if (dot > 1.0) dot = 1.0;
|
||||
if (dot < -1.0) dot = -1.0;
|
||||
elongation = acos(dot);
|
||||
|
||||
illum = (1.0 - cos(elongation)) / 2.0;
|
||||
|
||||
PG_RETURN_FLOAT8(illum);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* moon_phase_name(timestamptz) -> text
|
||||
*
|
||||
* Returns one of 8 phase names based on phase angle:
|
||||
* [0, 22.5) or [337.5, 360) -> 'new_moon'
|
||||
* [22.5, 67.5) -> 'waxing_crescent'
|
||||
* [67.5, 112.5) -> 'first_quarter'
|
||||
* [112.5, 157.5) -> 'waxing_gibbous'
|
||||
* [157.5, 202.5) -> 'full_moon'
|
||||
* [202.5, 247.5) -> 'waning_gibbous'
|
||||
* [247.5, 292.5) -> 'last_quarter'
|
||||
* [292.5, 337.5) -> 'waning_crescent'
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
moon_phase_name(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int64 ts = PG_GETARG_INT64(0);
|
||||
double jd;
|
||||
double angle;
|
||||
const char *name;
|
||||
|
||||
jd = timestamptz_to_jd(ts);
|
||||
angle = compute_phase_angle(jd);
|
||||
|
||||
if (angle < 22.5 || angle >= 337.5)
|
||||
name = "new_moon";
|
||||
else if (angle < 67.5)
|
||||
name = "waxing_crescent";
|
||||
else if (angle < 112.5)
|
||||
name = "first_quarter";
|
||||
else if (angle < 157.5)
|
||||
name = "waxing_gibbous";
|
||||
else if (angle < 202.5)
|
||||
name = "full_moon";
|
||||
else if (angle < 247.5)
|
||||
name = "waning_gibbous";
|
||||
else if (angle < 292.5)
|
||||
name = "last_quarter";
|
||||
else
|
||||
name = "waning_crescent";
|
||||
|
||||
PG_RETURN_TEXT_P(cstring_to_text(name));
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* moon_age(timestamptz) -> float8
|
||||
*
|
||||
* Returns the Moon's age in days since the last new moon [0, ~29.53).
|
||||
* Approximation from phase angle and mean synodic month:
|
||||
* age = phase_angle / 360 * 29.530588853
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
moon_age(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int64 ts = PG_GETARG_INT64(0);
|
||||
double jd;
|
||||
double angle, age;
|
||||
|
||||
jd = timestamptz_to_jd(ts);
|
||||
angle = compute_phase_angle(jd);
|
||||
age = angle / 360.0 * SYNODIC_MONTH;
|
||||
|
||||
PG_RETURN_FLOAT8(age);
|
||||
}
|
||||
144
src/magnitude_funcs.c
Normal file
144
src/magnitude_funcs.c
Normal file
@ -0,0 +1,144 @@
|
||||
/*
|
||||
* magnitude_funcs.c -- Planet apparent visual magnitude
|
||||
*
|
||||
* Uses the Mallama & Hilton (2018) magnitude model with
|
||||
* VSOP87 positions for distances and phase angles.
|
||||
*
|
||||
* Reference: Mallama & Hilton, "Computing Apparent Planetary
|
||||
* Magnitudes for The Astronomical Almanac", A&C vol. 25, 2018.
|
||||
*/
|
||||
|
||||
#include "postgres.h"
|
||||
#include "fmgr.h"
|
||||
#include "utils/timestamp.h"
|
||||
#include "types.h"
|
||||
#include "astro_math.h"
|
||||
#include "vsop87.h"
|
||||
#include <math.h>
|
||||
|
||||
PG_FUNCTION_INFO_V1(planet_magnitude);
|
||||
|
||||
|
||||
/*
|
||||
* Planet magnitude parameters -- Mallama & Hilton (2018), simplified.
|
||||
*
|
||||
* V(1,0) = absolute magnitude at r=1 AU, delta=1 AU, i=0 deg
|
||||
* Phase corrections are polynomial fits to i (phase angle in degrees).
|
||||
*
|
||||
* We use the linear+quadratic terms which are sufficient for
|
||||
* phase angles encountered from Earth (typically <180 deg).
|
||||
*
|
||||
* Saturn caveat: visual magnitude depends heavily on ring tilt
|
||||
* (can vary by ~1.5 mag). The simplified model here uses a fixed
|
||||
* V(1,0) without ring correction.
|
||||
*/
|
||||
typedef struct {
|
||||
double v10; /* V(1,0) */
|
||||
double c1; /* coefficient for i */
|
||||
double c2; /* coefficient for i^2 */
|
||||
double c3; /* coefficient for i^3 (0 if unused) */
|
||||
} mag_params;
|
||||
|
||||
static const mag_params planet_mag[] = {
|
||||
[0] = { 0, 0, 0, 0 }, /* Sun: unused placeholder */
|
||||
[1] = { -0.613, 6.328e-2, -1.6336e-3, 0 }, /* Mercury */
|
||||
[2] = { -4.384, 1.044e-3, 3.687e-4, 0 }, /* Venus */
|
||||
[3] = { 0, 0, 0, 0 }, /* Earth: unused */
|
||||
[4] = { -1.601, 2.267e-2, -1.302e-4, 0 }, /* Mars */
|
||||
[5] = { -9.395, 3.7e-4, 0, 0 }, /* Jupiter */
|
||||
[6] = { -8.95, 0, 0, 0 }, /* Saturn (ring tilt NOT modeled) */
|
||||
[7] = { -7.110, 0, 0, 0 }, /* Uranus */
|
||||
[8] = { -7.00, 0, 0, 0 }, /* Neptune */
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Compute apparent visual magnitude of a planet from Earth.
|
||||
*
|
||||
* Phase angle is the Sun-Planet-Earth angle, computed via the law
|
||||
* of cosines from three heliocentric/geocentric distances.
|
||||
*/
|
||||
static double
|
||||
compute_planet_magnitude(int body_id, double jd)
|
||||
{
|
||||
double earth_xyz[6], planet_xyz[6];
|
||||
double geo[3];
|
||||
double r, delta, R;
|
||||
double cos_i, i_deg;
|
||||
const mag_params *p;
|
||||
double V;
|
||||
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] +
|
||||
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]);
|
||||
|
||||
/* Sun-Earth distance */
|
||||
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);
|
||||
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;
|
||||
|
||||
/* Mallama & Hilton (2018) magnitude formula */
|
||||
p = &planet_mag[body_id];
|
||||
V = p->v10
|
||||
+ 5.0 * log10(r * delta)
|
||||
+ p->c1 * i_deg
|
||||
+ p->c2 * i_deg * i_deg
|
||||
+ p->c3 * i_deg * i_deg * i_deg;
|
||||
|
||||
return V;
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* planet_magnitude(body_id int4, timestamptz) -> float8
|
||||
*
|
||||
* Returns the apparent visual magnitude of a planet as seen from
|
||||
* Earth. Uses Mallama & Hilton (2018) magnitude model.
|
||||
*
|
||||
* Body IDs: 1=Mercury, ..., 8=Neptune (not Sun 0, Earth 3, or Moon 10)
|
||||
*
|
||||
* NOTE: Saturn magnitude does not account for ring tilt, which
|
||||
* can vary the apparent magnitude by ~1.5 mag. The returned value
|
||||
* is approximate for Saturn.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
planet_magnitude(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int32 body_id = PG_GETARG_INT32(0);
|
||||
int64 ts = PG_GETARG_INT64(1);
|
||||
double jd, mag;
|
||||
|
||||
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")));
|
||||
|
||||
jd = timestamptz_to_jd(ts);
|
||||
mag = compute_planet_magnitude(body_id, jd);
|
||||
|
||||
PG_RETURN_FLOAT8(mag);
|
||||
}
|
||||
@ -38,6 +38,12 @@ PG_FUNCTION_INFO_V1(moon_next_set_refracted);
|
||||
PG_FUNCTION_INFO_V1(sun_rise_set_status);
|
||||
PG_FUNCTION_INFO_V1(moon_rise_set_status);
|
||||
PG_FUNCTION_INFO_V1(planet_rise_set_status);
|
||||
PG_FUNCTION_INFO_V1(sun_civil_dawn);
|
||||
PG_FUNCTION_INFO_V1(sun_civil_dusk);
|
||||
PG_FUNCTION_INFO_V1(sun_nautical_dawn);
|
||||
PG_FUNCTION_INFO_V1(sun_nautical_dusk);
|
||||
PG_FUNCTION_INFO_V1(sun_astronomical_dawn);
|
||||
PG_FUNCTION_INFO_V1(sun_astronomical_dusk);
|
||||
|
||||
#define COARSE_STEP_JD (60.0 / 86400.0) /* 60 seconds */
|
||||
#define BISECT_TOL_JD (0.1 / 86400.0) /* 0.1 second */
|
||||
@ -65,6 +71,11 @@ PG_FUNCTION_INFO_V1(planet_rise_set_status);
|
||||
*/
|
||||
#define REFRACTION_ONLY_HORIZON_RAD (-0.00993) /* -0.569 deg */
|
||||
|
||||
/* Twilight depression angles (geometric Sun center below horizon) */
|
||||
#define CIVIL_TWILIGHT_RAD (-0.10472) /* -6.0 deg */
|
||||
#define NAUTICAL_TWILIGHT_RAD (-0.20944) /* -12.0 deg */
|
||||
#define ASTRONOMICAL_TWILIGHT_RAD (-0.30416) /* -18.0 deg */
|
||||
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* elevation_at_jd_body -- compute topocentric elevation for a body
|
||||
@ -684,3 +695,177 @@ planet_rise_set_status(PG_FUNCTION_ARGS)
|
||||
|
||||
PG_RETURN_TEXT_P(cstring_to_text(status));
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* sun_civil_dawn(observer, timestamptz) -> timestamptz
|
||||
*
|
||||
* Returns the next time civil twilight begins (Sun crosses -6 deg
|
||||
* heading upward). Civil twilight = enough light for outdoor
|
||||
* activities without artificial lighting.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
sun_civil_dawn(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
||||
int64 ts = PG_GETARG_INT64(1);
|
||||
double start_jd, stop_jd, result_jd;
|
||||
|
||||
start_jd = timestamptz_to_jd(ts);
|
||||
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
||||
|
||||
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
||||
start_jd, stop_jd,
|
||||
CIVIL_TWILIGHT_RAD, true);
|
||||
|
||||
if (result_jd < 0.0)
|
||||
PG_RETURN_NULL();
|
||||
|
||||
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* sun_civil_dusk(observer, timestamptz) -> timestamptz
|
||||
*
|
||||
* Returns the next time civil twilight ends (Sun crosses -6 deg
|
||||
* heading downward). After civil dusk, outdoor activities require
|
||||
* artificial lighting.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
sun_civil_dusk(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
||||
int64 ts = PG_GETARG_INT64(1);
|
||||
double start_jd, stop_jd, result_jd;
|
||||
|
||||
start_jd = timestamptz_to_jd(ts);
|
||||
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
||||
|
||||
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
||||
start_jd, stop_jd,
|
||||
CIVIL_TWILIGHT_RAD, false);
|
||||
|
||||
if (result_jd < 0.0)
|
||||
PG_RETURN_NULL();
|
||||
|
||||
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* sun_nautical_dawn(observer, timestamptz) -> timestamptz
|
||||
*
|
||||
* Returns the next time nautical twilight begins (Sun crosses -12 deg
|
||||
* heading upward). At nautical dawn the horizon becomes visible at
|
||||
* sea and bright stars are still visible for navigation.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
sun_nautical_dawn(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
||||
int64 ts = PG_GETARG_INT64(1);
|
||||
double start_jd, stop_jd, result_jd;
|
||||
|
||||
start_jd = timestamptz_to_jd(ts);
|
||||
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
||||
|
||||
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
||||
start_jd, stop_jd,
|
||||
NAUTICAL_TWILIGHT_RAD, true);
|
||||
|
||||
if (result_jd < 0.0)
|
||||
PG_RETURN_NULL();
|
||||
|
||||
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* sun_nautical_dusk(observer, timestamptz) -> timestamptz
|
||||
*
|
||||
* Returns the next time nautical twilight ends (Sun crosses -12 deg
|
||||
* heading downward). After nautical dusk the horizon is no longer
|
||||
* visible at sea; bright stars remain visible.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
sun_nautical_dusk(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
||||
int64 ts = PG_GETARG_INT64(1);
|
||||
double start_jd, stop_jd, result_jd;
|
||||
|
||||
start_jd = timestamptz_to_jd(ts);
|
||||
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
||||
|
||||
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
||||
start_jd, stop_jd,
|
||||
NAUTICAL_TWILIGHT_RAD, false);
|
||||
|
||||
if (result_jd < 0.0)
|
||||
PG_RETURN_NULL();
|
||||
|
||||
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* sun_astronomical_dawn(observer, timestamptz) -> timestamptz
|
||||
*
|
||||
* Returns the next time astronomical twilight begins (Sun crosses
|
||||
* -18 deg heading upward). Before astronomical dawn the sky is
|
||||
* fully dark — faintest objects are observable.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
sun_astronomical_dawn(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
||||
int64 ts = PG_GETARG_INT64(1);
|
||||
double start_jd, stop_jd, result_jd;
|
||||
|
||||
start_jd = timestamptz_to_jd(ts);
|
||||
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
||||
|
||||
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
||||
start_jd, stop_jd,
|
||||
ASTRONOMICAL_TWILIGHT_RAD, true);
|
||||
|
||||
if (result_jd < 0.0)
|
||||
PG_RETURN_NULL();
|
||||
|
||||
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* sun_astronomical_dusk(observer, timestamptz) -> timestamptz
|
||||
*
|
||||
* Returns the next time astronomical twilight ends (Sun crosses
|
||||
* -18 deg heading downward). After astronomical dusk the sky is
|
||||
* fully dark — faintest objects become observable.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
sun_astronomical_dusk(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
||||
int64 ts = PG_GETARG_INT64(1);
|
||||
double start_jd, stop_jd, result_jd;
|
||||
|
||||
start_jd = timestamptz_to_jd(ts);
|
||||
stop_jd = start_jd + DEFAULT_WINDOW_DAYS;
|
||||
|
||||
result_jd = find_next_crossing(BTYPE_SUN, 0, obs,
|
||||
start_jd, stop_jd,
|
||||
ASTRONOMICAL_TWILIGHT_RAD, false);
|
||||
|
||||
if (result_jd < 0.0)
|
||||
PG_RETURN_NULL();
|
||||
|
||||
PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd));
|
||||
}
|
||||
|
||||
243
test/expected/v016_features.out
Normal file
243
test/expected/v016_features.out
Normal file
@ -0,0 +1,243 @@
|
||||
-- v016_features.sql -- Tests for v0.16.0: twilight, lunar phase, planet magnitude
|
||||
--
|
||||
-- Verifies twilight dawn/dusk, lunar phase calculations,
|
||||
-- and planet apparent magnitude.
|
||||
CREATE EXTENSION IF NOT EXISTS pg_orrery;
|
||||
NOTICE: extension "pg_orrery" already exists, skipping
|
||||
-- ============================================================
|
||||
-- Twilight: ordering (astronomical < nautical < civil < sunrise)
|
||||
-- Eagle, Idaho on the 2024 summer solstice
|
||||
-- ============================================================
|
||||
-- Dawn ordering: astronomical dawn < nautical dawn < civil dawn < sunrise
|
||||
-- Use midnight MDT (07:00 UTC) so all "next" events land on the same morning
|
||||
SELECT sun_astronomical_dawn('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
< sun_nautical_dawn('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
AS astro_before_nautical;
|
||||
astro_before_nautical
|
||||
-----------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT sun_nautical_dawn('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
< sun_civil_dawn('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
AS nautical_before_civil;
|
||||
nautical_before_civil
|
||||
-----------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT sun_civil_dawn('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
< sun_next_rise('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
AS civil_before_sunrise;
|
||||
civil_before_sunrise
|
||||
----------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- Dusk ordering: sunset < civil dusk < nautical dusk < astronomical dusk
|
||||
-- Noon MDT (18:00 UTC) ensures all dusk events are still ahead
|
||||
SELECT sun_next_set('(43.7,-116.4,800)'::observer, '2024-06-21 18:00:00+00'::timestamptz)
|
||||
< sun_civil_dusk('(43.7,-116.4,800)'::observer, '2024-06-21 18:00:00+00'::timestamptz)
|
||||
AS sunset_before_civil;
|
||||
sunset_before_civil
|
||||
---------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT sun_civil_dusk('(43.7,-116.4,800)'::observer, '2024-06-21 18:00:00+00'::timestamptz)
|
||||
< sun_nautical_dusk('(43.7,-116.4,800)'::observer, '2024-06-21 18:00:00+00'::timestamptz)
|
||||
AS civil_before_nautical;
|
||||
civil_before_nautical
|
||||
-----------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT sun_nautical_dusk('(43.7,-116.4,800)'::observer, '2024-06-21 18:00:00+00'::timestamptz)
|
||||
< sun_astronomical_dusk('(43.7,-116.4,800)'::observer, '2024-06-21 18:00:00+00'::timestamptz)
|
||||
AS nautical_before_astro;
|
||||
nautical_before_astro
|
||||
-----------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- ============================================================
|
||||
-- Twilight: civil dawn ~30 min before sunrise at mid-latitude
|
||||
-- ============================================================
|
||||
SELECT extract(epoch FROM
|
||||
sun_next_rise('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
- sun_civil_dawn('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
) BETWEEN 1200 AND 3600
|
||||
AS civil_dawn_reasonable_offset;
|
||||
civil_dawn_reasonable_offset
|
||||
------------------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- ============================================================
|
||||
-- Twilight: high latitude summer -- no astronomical darkness
|
||||
-- At 60N in June, astronomical dusk should be NULL (never gets dark enough)
|
||||
-- ============================================================
|
||||
SELECT sun_astronomical_dusk('(60.0,25.0,0)'::observer, '2024-06-21 00:00:00+00'::timestamptz) IS NULL
|
||||
AS no_astro_dark_60n_summer;
|
||||
no_astro_dark_60n_summer
|
||||
--------------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- ============================================================
|
||||
-- Lunar phase: known full moon (2024-01-25 ~17:54 UTC)
|
||||
-- Phase angle should be near 180 deg, illumination near 1.0
|
||||
-- ============================================================
|
||||
SELECT round(moon_phase_angle('2024-01-25 18:00:00+00'::timestamptz)::numeric, 0)
|
||||
BETWEEN 170 AND 190
|
||||
AS full_moon_angle_near_180;
|
||||
full_moon_angle_near_180
|
||||
--------------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT round(moon_illumination('2024-01-25 18:00:00+00'::timestamptz)::numeric, 2)
|
||||
>= 0.95
|
||||
AS full_moon_high_illumination;
|
||||
full_moon_high_illumination
|
||||
-----------------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT moon_phase_name('2024-01-25 18:00:00+00'::timestamptz) = 'full_moon'
|
||||
AS full_moon_named;
|
||||
full_moon_named
|
||||
-----------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- ============================================================
|
||||
-- Lunar phase: known new moon (2024-01-11 ~11:57 UTC)
|
||||
-- Phase angle should be near 0 or 360, illumination near 0
|
||||
-- ============================================================
|
||||
SELECT moon_illumination('2024-01-11 12:00:00+00'::timestamptz)
|
||||
< 0.05
|
||||
AS new_moon_low_illumination;
|
||||
new_moon_low_illumination
|
||||
---------------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT moon_phase_name('2024-01-11 12:00:00+00'::timestamptz) = 'new_moon'
|
||||
AS new_moon_named;
|
||||
new_moon_named
|
||||
----------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- ============================================================
|
||||
-- Lunar phase: first quarter (2024-01-18 ~03:53 UTC)
|
||||
-- Phase angle near 90, illumination near 0.5
|
||||
-- ============================================================
|
||||
SELECT round(moon_phase_angle('2024-01-18 04:00:00+00'::timestamptz)::numeric, 0)
|
||||
BETWEEN 80 AND 100
|
||||
AS first_quarter_angle_near_90;
|
||||
first_quarter_angle_near_90
|
||||
-----------------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT moon_illumination('2024-01-18 04:00:00+00'::timestamptz)
|
||||
BETWEEN 0.4 AND 0.6
|
||||
AS first_quarter_half_illuminated;
|
||||
first_quarter_half_illuminated
|
||||
--------------------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT moon_phase_name('2024-01-18 04:00:00+00'::timestamptz) = 'first_quarter'
|
||||
AS first_quarter_named;
|
||||
first_quarter_named
|
||||
---------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- ============================================================
|
||||
-- Moon age: new moon has age near 0, full moon near 14.7
|
||||
-- ============================================================
|
||||
SELECT moon_age('2024-01-11 12:00:00+00'::timestamptz) < 2.0
|
||||
AS new_moon_young;
|
||||
new_moon_young
|
||||
----------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT moon_age('2024-01-25 18:00:00+00'::timestamptz)
|
||||
BETWEEN 12.0 AND 17.0
|
||||
AS full_moon_age_midcycle;
|
||||
full_moon_age_midcycle
|
||||
------------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- ============================================================
|
||||
-- Illumination range: always [0, 1]
|
||||
-- ============================================================
|
||||
SELECT moon_illumination('2024-06-01 00:00:00+00'::timestamptz) BETWEEN 0.0 AND 1.0
|
||||
AND moon_illumination('2024-09-01 00:00:00+00'::timestamptz) BETWEEN 0.0 AND 1.0
|
||||
AND moon_illumination('2024-12-01 00:00:00+00'::timestamptz) BETWEEN 0.0 AND 1.0
|
||||
AS illumination_always_valid;
|
||||
illumination_always_valid
|
||||
---------------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- ============================================================
|
||||
-- Planet magnitude: Jupiter should be bright (negative mag)
|
||||
-- ============================================================
|
||||
SELECT planet_magnitude(5, '2024-01-15 00:00:00+00'::timestamptz) < 0.0
|
||||
AS jupiter_is_bright;
|
||||
jupiter_is_bright
|
||||
-------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- ============================================================
|
||||
-- Planet magnitude: Venus is the brightest planet
|
||||
-- ============================================================
|
||||
SELECT planet_magnitude(2, '2024-06-01 12:00:00+00'::timestamptz)
|
||||
< planet_magnitude(4, '2024-06-01 12:00:00+00'::timestamptz)
|
||||
AS venus_brighter_than_mars;
|
||||
venus_brighter_than_mars
|
||||
--------------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- ============================================================
|
||||
-- Planet magnitude: Neptune is faint (~+7-8)
|
||||
-- ============================================================
|
||||
SELECT planet_magnitude(8, '2024-01-15 00:00:00+00'::timestamptz) > 7.0
|
||||
AS neptune_is_faint;
|
||||
neptune_is_faint
|
||||
------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- ============================================================
|
||||
-- Planet magnitude: all planets return finite values
|
||||
-- ============================================================
|
||||
SELECT bool_and(
|
||||
planet_magnitude(body_id, '2024-01-15 00:00:00+00'::timestamptz) IS NOT NULL
|
||||
AND planet_magnitude(body_id, '2024-01-15 00:00:00+00'::timestamptz) > -30
|
||||
AND planet_magnitude(body_id, '2024-01-15 00:00:00+00'::timestamptz) < 30
|
||||
) AS all_magnitudes_finite
|
||||
FROM (VALUES (1),(2),(4),(5),(6),(7),(8)) AS t(body_id);
|
||||
all_magnitudes_finite
|
||||
-----------------------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- ============================================================
|
||||
-- Planet magnitude: error cases
|
||||
-- ============================================================
|
||||
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
|
||||
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)
|
||||
161
test/sql/v016_features.sql
Normal file
161
test/sql/v016_features.sql
Normal file
@ -0,0 +1,161 @@
|
||||
-- v016_features.sql -- Tests for v0.16.0: twilight, lunar phase, planet magnitude
|
||||
--
|
||||
-- Verifies twilight dawn/dusk, lunar phase calculations,
|
||||
-- and planet apparent magnitude.
|
||||
CREATE EXTENSION IF NOT EXISTS pg_orrery;
|
||||
|
||||
-- ============================================================
|
||||
-- Twilight: ordering (astronomical < nautical < civil < sunrise)
|
||||
-- Eagle, Idaho on the 2024 summer solstice
|
||||
-- ============================================================
|
||||
|
||||
-- Dawn ordering: astronomical dawn < nautical dawn < civil dawn < sunrise
|
||||
-- Use midnight MDT (07:00 UTC) so all "next" events land on the same morning
|
||||
SELECT sun_astronomical_dawn('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
< sun_nautical_dawn('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
AS astro_before_nautical;
|
||||
|
||||
SELECT sun_nautical_dawn('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
< sun_civil_dawn('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
AS nautical_before_civil;
|
||||
|
||||
SELECT sun_civil_dawn('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
< sun_next_rise('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
AS civil_before_sunrise;
|
||||
|
||||
-- Dusk ordering: sunset < civil dusk < nautical dusk < astronomical dusk
|
||||
-- Noon MDT (18:00 UTC) ensures all dusk events are still ahead
|
||||
SELECT sun_next_set('(43.7,-116.4,800)'::observer, '2024-06-21 18:00:00+00'::timestamptz)
|
||||
< sun_civil_dusk('(43.7,-116.4,800)'::observer, '2024-06-21 18:00:00+00'::timestamptz)
|
||||
AS sunset_before_civil;
|
||||
|
||||
SELECT sun_civil_dusk('(43.7,-116.4,800)'::observer, '2024-06-21 18:00:00+00'::timestamptz)
|
||||
< sun_nautical_dusk('(43.7,-116.4,800)'::observer, '2024-06-21 18:00:00+00'::timestamptz)
|
||||
AS civil_before_nautical;
|
||||
|
||||
SELECT sun_nautical_dusk('(43.7,-116.4,800)'::observer, '2024-06-21 18:00:00+00'::timestamptz)
|
||||
< sun_astronomical_dusk('(43.7,-116.4,800)'::observer, '2024-06-21 18:00:00+00'::timestamptz)
|
||||
AS nautical_before_astro;
|
||||
|
||||
-- ============================================================
|
||||
-- Twilight: civil dawn ~30 min before sunrise at mid-latitude
|
||||
-- ============================================================
|
||||
|
||||
SELECT extract(epoch FROM
|
||||
sun_next_rise('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
- sun_civil_dawn('(43.7,-116.4,800)'::observer, '2024-06-21 07:00:00+00'::timestamptz)
|
||||
) BETWEEN 1200 AND 3600
|
||||
AS civil_dawn_reasonable_offset;
|
||||
|
||||
-- ============================================================
|
||||
-- Twilight: high latitude summer -- no astronomical darkness
|
||||
-- At 60N in June, astronomical dusk should be NULL (never gets dark enough)
|
||||
-- ============================================================
|
||||
|
||||
SELECT sun_astronomical_dusk('(60.0,25.0,0)'::observer, '2024-06-21 00:00:00+00'::timestamptz) IS NULL
|
||||
AS no_astro_dark_60n_summer;
|
||||
|
||||
-- ============================================================
|
||||
-- Lunar phase: known full moon (2024-01-25 ~17:54 UTC)
|
||||
-- Phase angle should be near 180 deg, illumination near 1.0
|
||||
-- ============================================================
|
||||
|
||||
SELECT round(moon_phase_angle('2024-01-25 18:00:00+00'::timestamptz)::numeric, 0)
|
||||
BETWEEN 170 AND 190
|
||||
AS full_moon_angle_near_180;
|
||||
|
||||
SELECT round(moon_illumination('2024-01-25 18:00:00+00'::timestamptz)::numeric, 2)
|
||||
>= 0.95
|
||||
AS full_moon_high_illumination;
|
||||
|
||||
SELECT moon_phase_name('2024-01-25 18:00:00+00'::timestamptz) = 'full_moon'
|
||||
AS full_moon_named;
|
||||
|
||||
-- ============================================================
|
||||
-- Lunar phase: known new moon (2024-01-11 ~11:57 UTC)
|
||||
-- Phase angle should be near 0 or 360, illumination near 0
|
||||
-- ============================================================
|
||||
|
||||
SELECT moon_illumination('2024-01-11 12:00:00+00'::timestamptz)
|
||||
< 0.05
|
||||
AS new_moon_low_illumination;
|
||||
|
||||
SELECT moon_phase_name('2024-01-11 12:00:00+00'::timestamptz) = 'new_moon'
|
||||
AS new_moon_named;
|
||||
|
||||
-- ============================================================
|
||||
-- Lunar phase: first quarter (2024-01-18 ~03:53 UTC)
|
||||
-- Phase angle near 90, illumination near 0.5
|
||||
-- ============================================================
|
||||
|
||||
SELECT round(moon_phase_angle('2024-01-18 04:00:00+00'::timestamptz)::numeric, 0)
|
||||
BETWEEN 80 AND 100
|
||||
AS first_quarter_angle_near_90;
|
||||
|
||||
SELECT moon_illumination('2024-01-18 04:00:00+00'::timestamptz)
|
||||
BETWEEN 0.4 AND 0.6
|
||||
AS first_quarter_half_illuminated;
|
||||
|
||||
SELECT moon_phase_name('2024-01-18 04:00:00+00'::timestamptz) = 'first_quarter'
|
||||
AS first_quarter_named;
|
||||
|
||||
-- ============================================================
|
||||
-- Moon age: new moon has age near 0, full moon near 14.7
|
||||
-- ============================================================
|
||||
|
||||
SELECT moon_age('2024-01-11 12:00:00+00'::timestamptz) < 2.0
|
||||
AS new_moon_young;
|
||||
|
||||
SELECT moon_age('2024-01-25 18:00:00+00'::timestamptz)
|
||||
BETWEEN 12.0 AND 17.0
|
||||
AS full_moon_age_midcycle;
|
||||
|
||||
-- ============================================================
|
||||
-- Illumination range: always [0, 1]
|
||||
-- ============================================================
|
||||
|
||||
SELECT moon_illumination('2024-06-01 00:00:00+00'::timestamptz) BETWEEN 0.0 AND 1.0
|
||||
AND moon_illumination('2024-09-01 00:00:00+00'::timestamptz) BETWEEN 0.0 AND 1.0
|
||||
AND moon_illumination('2024-12-01 00:00:00+00'::timestamptz) BETWEEN 0.0 AND 1.0
|
||||
AS illumination_always_valid;
|
||||
|
||||
-- ============================================================
|
||||
-- Planet magnitude: Jupiter should be bright (negative mag)
|
||||
-- ============================================================
|
||||
|
||||
SELECT planet_magnitude(5, '2024-01-15 00:00:00+00'::timestamptz) < 0.0
|
||||
AS jupiter_is_bright;
|
||||
|
||||
-- ============================================================
|
||||
-- Planet magnitude: Venus is the brightest planet
|
||||
-- ============================================================
|
||||
|
||||
SELECT planet_magnitude(2, '2024-06-01 12:00:00+00'::timestamptz)
|
||||
< planet_magnitude(4, '2024-06-01 12:00:00+00'::timestamptz)
|
||||
AS venus_brighter_than_mars;
|
||||
|
||||
-- ============================================================
|
||||
-- Planet magnitude: Neptune is faint (~+7-8)
|
||||
-- ============================================================
|
||||
|
||||
SELECT planet_magnitude(8, '2024-01-15 00:00:00+00'::timestamptz) > 7.0
|
||||
AS neptune_is_faint;
|
||||
|
||||
-- ============================================================
|
||||
-- Planet magnitude: all planets return finite values
|
||||
-- ============================================================
|
||||
|
||||
SELECT bool_and(
|
||||
planet_magnitude(body_id, '2024-01-15 00:00:00+00'::timestamptz) IS NOT NULL
|
||||
AND planet_magnitude(body_id, '2024-01-15 00:00:00+00'::timestamptz) > -30
|
||||
AND planet_magnitude(body_id, '2024-01-15 00:00:00+00'::timestamptz) < 30
|
||||
) AS all_magnitudes_finite
|
||||
FROM (VALUES (1),(2),(4),(5),(6),(7),(8)) AS t(body_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Planet magnitude: error cases
|
||||
-- ============================================================
|
||||
|
||||
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(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(9, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'body_id=9: %', SQLERRM; END $$;
|
||||
Loading…
x
Reference in New Issue
Block a user