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
|
# 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, 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
|
**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 26 regression test suites
|
make installcheck PG_CONFIG=/usr/bin/pg_config # Run 27 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.15.0)
|
pg_orrery.control # Extension metadata (version 0.16.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
|
||||||
@ -45,6 +45,7 @@ sql/
|
|||||||
pg_orrery--0.13.0.sql # v0.13.0: nutation, make_equatorial, rise/set (141 objects)
|
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.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.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
|
||||||
@ -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.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.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)
|
||||||
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
|
||||||
@ -85,9 +87,11 @@ src/
|
|||||||
orbital_elements_type.c # orbital_elements type, MPC parser, small_body_observe/equatorial/apparent()
|
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
|
equatorial_funcs.c # equatorial type I/O, accessors, satellite/planet/sun/moon RA/Dec
|
||||||
refraction_funcs.c # atmospheric_refraction(), _ext(), topo_elevation_apparent()
|
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_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()
|
||||||
|
magnitude_funcs.c # planet_magnitude() (Mallama & Hilton 2018)
|
||||||
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)
|
||||||
@ -112,7 +116,7 @@ src/
|
|||||||
PROVENANCE.md # Vendoring decision, modifications, verification
|
PROVENANCE.md # Vendoring decision, modifications, verification
|
||||||
LICENSE # MIT license (Bill Gray / Project Pluto)
|
LICENSE # MIT license (Bill Gray / Project Pluto)
|
||||||
test/
|
test/
|
||||||
sql/ # 26 regression test suites
|
sql/ # 27 regression test suites
|
||||||
expected/ # Expected output
|
expected/ # Expected output
|
||||||
data/vallado_518.json # 518 Vallado test vectors (AIAA 2006-6753-Rev1)
|
data/vallado_518.json # 518 Vallado test vectors (AIAA 2006-6753-Rev1)
|
||||||
docs/
|
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) |
|
| `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 (151 SQL objects)
|
## Function Domains (162 SQL objects)
|
||||||
|
|
||||||
| Domain | Theory | Key Functions | Count |
|
| 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 (TLE) | Altitude-band approximation | `&&` (overlap), `<->` (distance) | 8 |
|
||||||
| GiST index (equatorial) | Spherical bounding box | `<->` (KNN ordering) | 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 |
|
| 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 |
|
| Constellation | Roman (1987) CDS VI/42 | `constellation()`, `constellation_full_name()` | 3 |
|
||||||
| Diagnostics | -- | `pg_orrery_ephemeris_info()` | 1 |
|
| Diagnostics | -- | `pg_orrery_ephemeris_info()` | 1 |
|
||||||
|
|
||||||
@ -291,7 +298,7 @@ All numerical logic is byte-identical to upstream. Verified against 518 Vallado
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
26 regression test suites via `make installcheck`:
|
27 regression test suites via `make installcheck`:
|
||||||
|
|
||||||
| Suite | What it tests |
|
| 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 |
|
| rise_set | Planet/Sun/Moon rise/set (geometric + refracted), circumpolar, polar night |
|
||||||
| 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 |
|
||||||
|
|
||||||
### PG Version Matrix
|
### 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
|
```bash
|
||||||
make test-matrix # Full matrix (PG 14-18)
|
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.
|
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
|
### Local Development
|
||||||
```bash
|
```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.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.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
|
||||||
|
|
||||||
# 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 \
|
||||||
@ -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/refraction_funcs.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
|
||||||
|
|
||||||
# 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
|
||||||
@ -52,7 +54,8 @@ REGRESS = tle_parse sgp4_propagate coord_transforms pass_prediction gist_index c
|
|||||||
gist_equatorial v012_features \
|
gist_equatorial v012_features \
|
||||||
v013_features rise_set \
|
v013_features rise_set \
|
||||||
constellation \
|
constellation \
|
||||||
v015_features
|
v015_features \
|
||||||
|
v016_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.15.0'
|
default_version = '0.16.0'
|
||||||
module_pathname = '$libdir/pg_orrery'
|
module_pathname = '$libdir/pg_orrery'
|
||||||
relocatable = true
|
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(sun_rise_set_status);
|
||||||
PG_FUNCTION_INFO_V1(moon_rise_set_status);
|
PG_FUNCTION_INFO_V1(moon_rise_set_status);
|
||||||
PG_FUNCTION_INFO_V1(planet_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 COARSE_STEP_JD (60.0 / 86400.0) /* 60 seconds */
|
||||||
#define BISECT_TOL_JD (0.1 / 86400.0) /* 0.1 second */
|
#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 */
|
#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
|
* 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));
|
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