From 501872d45d4a08d85bc6f598cf418c2c1db1a059 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Wed, 25 Feb 2026 19:38:52 -0700 Subject: [PATCH] v0.15.0: constellation full name lookup, rise/set status diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit constellation_full_name(text) returns full IAU name from 3-letter abbreviation (88-entry static table, IMMUTABLE). Returns NULL for invalid input — composable with constellation() in queries. Three rise_set_status functions classify body visibility as 'rises_and_sets', 'circumpolar', or 'never_rises' by sampling elevation at 48 points across 24h. Separate diagnostic path — called only when rise/set returns NULL, zero cost in normal case. 147 → 151 SQL objects. 25 → 26 regression suites. All pass. --- CLAUDE.md | 25 +- Makefile | 6 +- pg_orrery.control | 2 +- sql/pg_orrery--0.14.0--0.15.0.sql | 33 + sql/pg_orrery--0.15.0.sql | 1595 +++++++++++++++++++++++++++++ src/constellation_data.c | 93 ++ src/constellation_data.h | 9 + src/constellation_funcs.c | 34 + src/rise_set_funcs.c | 127 +++ test/expected/v015_features.out | 204 ++++ test/sql/v015_features.sql | 109 ++ 11 files changed, 2223 insertions(+), 14 deletions(-) create mode 100644 sql/pg_orrery--0.14.0--0.15.0.sql create mode 100644 sql/pg_orrery--0.15.0.sql create mode 100644 test/expected/v015_features.out create mode 100644 test/sql/v015_features.sql diff --git a/CLAUDE.md b/CLAUDE.md index a1eb7a2..c0868c8 100644 --- a/CLAUDE.md +++ b/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, 147 SQL objects (131 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), and IAU constellation identification (Roman 1987). +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). -**Current version:** 0.14.0 +**Current version:** 0.15.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 25 regression test suites +make installcheck PG_CONFIG=/usr/bin/pg_config # Run 26 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.14.0) +pg_orrery.control # Extension metadata (version 0.15.0) Makefile # PGXS build + Docker targets sql/ pg_orrery--0.1.0.sql # v0.1.0: satellite types/functions/operators @@ -44,6 +44,7 @@ sql/ pg_orrery--0.12.0.sql # v0.12.0: equatorial GiST, DE moon equatorial (132 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.15.0.sql # v0.15.0: constellation full name, rise/set status (151 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 @@ -57,6 +58,7 @@ sql/ pg_orrery--0.11.0--0.12.0.sql # Migration: v0.11.0 → v0.12.0 (equatorial GiST, DE moon equatorial) 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) src/ pg_orrery.c # PG_MODULE_MAGIC + _PG_init() (GUC registration) types.h # All struct definitions + constants + DE body ID mapping @@ -110,7 +112,7 @@ src/ PROVENANCE.md # Vendoring decision, modifications, verification LICENSE # MIT license (Bill Gray / Project Pluto) test/ - sql/ # 25 regression test suites + sql/ # 26 regression test suites expected/ # Expected output data/vallado_518.json # 518 Vallado test vectors (AIAA 2006-6753-Rev1) docs/ @@ -137,7 +139,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 (147 SQL objects) +## Function Domains (151 SQL objects) | Domain | Theory | Key Functions | Count | |--------|--------|---------------|-------| @@ -154,8 +156,8 @@ All types are fixed-size, `STORAGE = plain`, `ALIGNMENT = double`. No TOAST over | DE ephemeris | JPL DE440/441 (optional) | `planet_observe_de()`, `*_equatorial_de()`, `*_apparent_de()` | 23 | | 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()`, `moon_next_set_refracted()` | 12 | -| Constellation | Roman (1987) CDS VI/42 | `constellation()` (equatorial + RA/Dec overloads) | 2 | +| Rise/set | Bisection (60s scan) | `planet_next_rise()`, `sun_next_rise_refracted()`, `*_rise_set_status()` | 15 | +| Constellation | Roman (1987) CDS VI/42 | `constellation()`, `constellation_full_name()` | 3 | | Diagnostics | -- | `pg_orrery_ephemeris_info()` | 1 | All functions are `PARALLEL SAFE`. VSOP87/ELP82B functions are `IMMUTABLE` (compiled-in coefficients). DE functions are `STABLE` (external file dependency). @@ -289,7 +291,7 @@ All numerical logic is byte-identical to upstream. Verified against 518 Vallado ## Testing -25 regression test suites via `make installcheck`: +26 regression test suites via `make installcheck`: | Suite | What it tests | |-------|--------------| @@ -318,10 +320,11 @@ All numerical logic is byte-identical to upstream. Verified against 518 Vallado | v013_features | Nutation correction, make_equatorial constructor | | 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) | ### PG Version Matrix -Test all 25 regression suites + DE reader unit test across PostgreSQL 14-18 using Docker: +Test all 26 regression suites + DE reader unit test across PostgreSQL 14-18 using Docker: ```bash make test-matrix # Full matrix (PG 14-18) @@ -347,7 +350,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 147 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 151 SQL objects incl. DE variants, equatorial GiST, refraction, rise/set, constellation), Architecture (Hamilton's principles, constant custody, observation pipeline), Performance (benchmarks). ### Local Development ```bash diff --git a/Makefile b/Makefile index 59a5705..955a366 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,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.11.0.sql sql/pg_orrery--0.10.0--0.11.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.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 # Our extension C sources OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \ @@ -50,7 +51,8 @@ REGRESS = tle_parse sgp4_propagate coord_transforms pass_prediction gist_index c aberration v011_features vallado_518 \ gist_equatorial v012_features \ v013_features rise_set \ - constellation + constellation \ + v015_features REGRESS_OPTS = --inputdir=test # Pure C — no C++ runtime needed. LAPACK for OD solver (dgelss_). diff --git a/pg_orrery.control b/pg_orrery.control index 61c616e..fa3fe02 100644 --- a/pg_orrery.control +++ b/pg_orrery.control @@ -1,4 +1,4 @@ comment = 'A database orrery — celestial mechanics types and functions for PostgreSQL' -default_version = '0.14.0' +default_version = '0.15.0' module_pathname = '$libdir/pg_orrery' relocatable = true diff --git a/sql/pg_orrery--0.14.0--0.15.0.sql b/sql/pg_orrery--0.14.0--0.15.0.sql new file mode 100644 index 0000000..097a1b9 --- /dev/null +++ b/sql/pg_orrery--0.14.0--0.15.0.sql @@ -0,0 +1,33 @@ +-- pg_orrery 0.14.0 -> 0.15.0 migration +-- +-- Adds: constellation_full_name (1 function), +-- rise/set status diagnostics (3 functions). + +-- ============================================================ +-- Constellation full name lookup +-- ============================================================ + +CREATE FUNCTION constellation_full_name(abbr text) RETURNS text + AS 'MODULE_PATHNAME', 'constellation_full_name_from_abbr' + LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION constellation_full_name(text) IS + 'Full IAU constellation name from 3-letter abbreviation. Returns NULL for invalid abbreviation.'; + +-- ============================================================ +-- Rise/set status diagnostics +-- ============================================================ + +CREATE FUNCTION sun_rise_set_status(obs observer, t timestamptz) RETURNS text + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION sun_rise_set_status(observer, timestamptz) IS + 'Classify Sun visibility: rises_and_sets, circumpolar, or never_rises.'; + +CREATE FUNCTION moon_rise_set_status(obs observer, t timestamptz) RETURNS text + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION moon_rise_set_status(observer, timestamptz) IS + 'Classify Moon visibility: rises_and_sets, circumpolar, or never_rises.'; + +CREATE FUNCTION planet_rise_set_status(body_id int4, obs observer, t timestamptz) RETURNS text + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_rise_set_status(int4, observer, timestamptz) IS + 'Classify planet visibility: rises_and_sets, circumpolar, or never_rises. Body IDs 1-8 (Mercury-Neptune).'; diff --git a/sql/pg_orrery--0.15.0.sql b/sql/pg_orrery--0.15.0.sql new file mode 100644 index 0000000..b51fef0 --- /dev/null +++ b/sql/pg_orrery--0.15.0.sql @@ -0,0 +1,1595 @@ +-- pg_orrery -- Orbital mechanics types and functions for PostgreSQL +-- +-- Types: tle, eci_position, geodetic, topocentric, observer, pass_event +-- Provides SGP4/SDP4 propagation, coordinate transforms, pass prediction, +-- and GiST indexing on altitude bands for conjunction screening. +-- +-- All propagation uses WGS-72 constants (matching TLE mean element fitting). +-- Coordinate output uses WGS-84 (matching modern geodetic standards). + +-- ============================================================ +-- Shell types (forward declarations) +-- ============================================================ + +CREATE TYPE tle; +CREATE TYPE eci_position; +CREATE TYPE geodetic; +CREATE TYPE topocentric; +CREATE TYPE observer; +CREATE TYPE pass_event; + + +-- ============================================================ +-- TLE type: Two-Line Element set +-- ============================================================ + +CREATE FUNCTION tle_in(cstring) RETURNS tle + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION tle_out(tle) RETURNS cstring + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION tle_recv(internal) RETURNS tle + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION tle_send(tle) RETURNS bytea + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE TYPE tle ( + INPUT = tle_in, + OUTPUT = tle_out, + RECEIVE = tle_recv, + SEND = tle_send, + INTERNALLENGTH = 112, + ALIGNMENT = double, + STORAGE = plain +); + +COMMENT ON TYPE tle IS 'Two-Line Element set — parsed mean orbital elements for SGP4/SDP4 propagation'; + +-- TLE accessor functions + +CREATE FUNCTION tle_epoch(tle) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_epoch(tle) IS 'TLE epoch as Julian date (UTC)'; + +CREATE FUNCTION tle_norad_id(tle) RETURNS int4 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_norad_id(tle) IS 'NORAD catalog number'; + +CREATE FUNCTION tle_inclination(tle) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_inclination(tle) IS 'Orbital inclination in degrees'; + +CREATE FUNCTION tle_eccentricity(tle) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_eccentricity(tle) IS 'Orbital eccentricity (dimensionless)'; + +CREATE FUNCTION tle_raan(tle) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_raan(tle) IS 'Right ascension of ascending node in degrees'; + +CREATE FUNCTION tle_arg_perigee(tle) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_arg_perigee(tle) IS 'Argument of perigee in degrees'; + +CREATE FUNCTION tle_mean_anomaly(tle) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_mean_anomaly(tle) IS 'Mean anomaly in degrees'; + +CREATE FUNCTION tle_mean_motion(tle) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_mean_motion(tle) IS 'Mean motion in revolutions per day'; + +CREATE FUNCTION tle_bstar(tle) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_bstar(tle) IS 'B* drag coefficient (1/earth-radii)'; + +CREATE FUNCTION tle_period(tle) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_period(tle) IS 'Orbital period in minutes'; + +CREATE FUNCTION tle_age(tle, timestamptz) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_age(tle, timestamptz) IS 'TLE age in days (positive = past epoch, negative = before epoch)'; + +CREATE FUNCTION tle_perigee(tle) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_perigee(tle) IS 'Perigee altitude in km above WGS-72 ellipsoid'; + +CREATE FUNCTION tle_apogee(tle) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_apogee(tle) IS 'Apogee altitude in km above WGS-72 ellipsoid'; + +CREATE FUNCTION tle_intl_desig(tle) RETURNS text + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_intl_desig(tle) IS 'International designator (COSPAR ID)'; + +CREATE FUNCTION tle_from_lines(text, text) RETURNS tle + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_from_lines(text, text) IS + 'Construct TLE from separate line1/line2 text columns'; + + +-- ============================================================ +-- ECI position type: True Equator Mean Equinox (TEME) frame +-- ============================================================ + +CREATE FUNCTION eci_in(cstring) RETURNS eci_position + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION eci_out(eci_position) RETURNS cstring + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION eci_recv(internal) RETURNS eci_position + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION eci_send(eci_position) RETURNS bytea + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE TYPE eci_position ( + INPUT = eci_in, + OUTPUT = eci_out, + RECEIVE = eci_recv, + SEND = eci_send, + INTERNALLENGTH = 48, + ALIGNMENT = double, + STORAGE = plain +); + +COMMENT ON TYPE eci_position IS 'Earth-Centered Inertial position and velocity in TEME frame (km, km/s)'; + +-- ECI accessor functions + +CREATE FUNCTION eci_x(eci_position) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION eci_y(eci_position) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION eci_z(eci_position) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION eci_vx(eci_position) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION eci_vy(eci_position) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION eci_vz(eci_position) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION eci_speed(eci_position) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION eci_speed(eci_position) IS 'Velocity magnitude in km/s'; + +CREATE FUNCTION eci_altitude(eci_position) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION eci_altitude(eci_position) IS 'Approximate geocentric altitude in km (radius - WGS72_AE)'; + + +-- ============================================================ +-- Geodetic type: WGS-84 latitude/longitude/altitude +-- ============================================================ + +CREATE FUNCTION geodetic_in(cstring) RETURNS geodetic + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION geodetic_out(geodetic) RETURNS cstring + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION geodetic_recv(internal) RETURNS geodetic + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION geodetic_send(geodetic) RETURNS bytea + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE TYPE geodetic ( + INPUT = geodetic_in, + OUTPUT = geodetic_out, + RECEIVE = geodetic_recv, + SEND = geodetic_send, + INTERNALLENGTH = 24, + ALIGNMENT = double, + STORAGE = plain +); + +COMMENT ON TYPE geodetic IS 'Geodetic coordinates on WGS-84 ellipsoid (lat/lon in degrees, altitude in km)'; + +CREATE FUNCTION geodetic_lat(geodetic) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION geodetic_lon(geodetic) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION geodetic_alt(geodetic) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + + +-- ============================================================ +-- Topocentric type: observer-relative az/el/range +-- ============================================================ + +CREATE FUNCTION topocentric_in(cstring) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION topocentric_out(topocentric) RETURNS cstring + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION topocentric_recv(internal) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION topocentric_send(topocentric) RETURNS bytea + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE TYPE topocentric ( + INPUT = topocentric_in, + OUTPUT = topocentric_out, + RECEIVE = topocentric_recv, + SEND = topocentric_send, + INTERNALLENGTH = 32, + ALIGNMENT = double, + STORAGE = plain +); + +COMMENT ON TYPE topocentric IS 'Topocentric coordinates relative to observer (azimuth, elevation, range, range rate)'; + +CREATE FUNCTION topo_azimuth(topocentric) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION topo_azimuth(topocentric) IS 'Azimuth in degrees (0=N, 90=E, 180=S, 270=W)'; + +CREATE FUNCTION topo_elevation(topocentric) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION topo_elevation(topocentric) IS 'Elevation in degrees (0=horizon, 90=zenith)'; + +CREATE FUNCTION topo_range(topocentric) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION topo_range(topocentric) IS 'Slant range in km'; + +CREATE FUNCTION topo_range_rate(topocentric) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION topo_range_rate(topocentric) IS 'Range rate in km/s (positive = receding)'; + + +-- ============================================================ +-- Observer type: ground station location +-- ============================================================ + +CREATE FUNCTION observer_in(cstring) RETURNS observer + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION observer_out(observer) RETURNS cstring + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION observer_recv(internal) RETURNS observer + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION observer_send(observer) RETURNS bytea + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE TYPE observer ( + INPUT = observer_in, + OUTPUT = observer_out, + RECEIVE = observer_recv, + SEND = observer_send, + INTERNALLENGTH = 24, + ALIGNMENT = double, + STORAGE = plain +); + +COMMENT ON TYPE observer IS 'Ground observer location (accepts: 40.0N 105.3W 1655m or decimal degrees)'; + +CREATE FUNCTION observer_lat(observer) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION observer_lat(observer) IS 'Latitude in degrees (positive = North)'; + +CREATE FUNCTION observer_lon(observer) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION observer_lon(observer) IS 'Longitude in degrees (positive = East)'; + +CREATE FUNCTION observer_alt(observer) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION observer_alt(observer) IS 'Altitude in meters above WGS-84 ellipsoid'; + +CREATE FUNCTION observer_from_geodetic(float8, float8, float8 DEFAULT 0.0) RETURNS observer + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION observer_from_geodetic(float8, float8, float8) IS + 'Construct observer from lat (deg), lon (deg), altitude (meters). Avoids text formatting round-trips.'; + + +-- ============================================================ +-- Pass event type: satellite visibility window +-- ============================================================ + +CREATE FUNCTION pass_event_in(cstring) RETURNS pass_event + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION pass_event_out(pass_event) RETURNS cstring + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION pass_event_recv(internal) RETURNS pass_event + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION pass_event_send(pass_event) RETURNS bytea + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE TYPE pass_event ( + INPUT = pass_event_in, + OUTPUT = pass_event_out, + RECEIVE = pass_event_recv, + SEND = pass_event_send, + INTERNALLENGTH = 48, + ALIGNMENT = double, + STORAGE = plain +); + +COMMENT ON TYPE pass_event IS 'Satellite pass event (AOS/MAX/LOS times, max elevation, AOS/LOS azimuths)'; + +CREATE FUNCTION pass_aos_time(pass_event) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION pass_aos_time(pass_event) IS 'Acquisition of signal time'; + +CREATE FUNCTION pass_max_el_time(pass_event) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION pass_max_el_time(pass_event) IS 'Maximum elevation time'; + +CREATE FUNCTION pass_los_time(pass_event) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION pass_los_time(pass_event) IS 'Loss of signal time'; + +CREATE FUNCTION pass_max_elevation(pass_event) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION pass_max_elevation(pass_event) IS 'Maximum elevation in degrees'; + +CREATE FUNCTION pass_aos_azimuth(pass_event) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION pass_aos_azimuth(pass_event) IS 'AOS azimuth in degrees (0=N)'; + +CREATE FUNCTION pass_los_azimuth(pass_event) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION pass_los_azimuth(pass_event) IS 'LOS azimuth in degrees (0=N)'; + +CREATE FUNCTION pass_duration(pass_event) RETURNS interval + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION pass_duration(pass_event) IS 'Pass duration (LOS - AOS)'; + + +-- ============================================================ +-- SGP4/SDP4 propagation functions +-- ============================================================ + +CREATE FUNCTION sgp4_propagate(tle, timestamptz) RETURNS eci_position + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION sgp4_propagate(tle, timestamptz) IS + 'Propagate TLE to a point in time using SGP4 (near-earth) or SDP4 (deep-space). Returns TEME ECI position/velocity.'; + +CREATE FUNCTION sgp4_propagate_safe(tle, timestamptz) RETURNS eci_position + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE; +COMMENT ON FUNCTION sgp4_propagate_safe(tle, timestamptz) IS + 'Like sgp4_propagate but returns NULL on error instead of raising an exception. For batch queries with potentially invalid TLEs.'; + +CREATE FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval) + RETURNS TABLE(t timestamptz, x float8, y float8, z float8, vx float8, vy float8, vz float8) + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE + ROWS 100; +COMMENT ON FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval) IS + 'Propagate TLE over a time range at regular intervals. Returns time series of TEME positions.'; + +CREATE FUNCTION tle_distance(tle, tle, timestamptz) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_distance(tle, tle, timestamptz) IS + 'Euclidean distance in km between two TLEs at a reference time'; + + +-- ============================================================ +-- Coordinate transform functions +-- ============================================================ + +CREATE FUNCTION eci_to_geodetic(eci_position, timestamptz) RETURNS geodetic + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION eci_to_geodetic(eci_position, timestamptz) IS + 'Convert TEME ECI position to WGS-84 geodetic coordinates at given time'; + +CREATE FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) IS + 'Convert TEME ECI position to topocentric (az/el/range) relative to observer'; + +CREATE FUNCTION subsatellite_point(tle, timestamptz) RETURNS geodetic + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION subsatellite_point(tle, timestamptz) IS + 'Subsatellite (nadir) point on WGS-84 ellipsoid at given time'; + +CREATE FUNCTION ground_track(tle, timestamptz, timestamptz, interval) + RETURNS TABLE(t timestamptz, lat float8, lon float8, alt float8) + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE + ROWS 100; +COMMENT ON FUNCTION ground_track(tle, timestamptz, timestamptz, interval) IS + 'Ground track as time series of subsatellite points (lat/lon in degrees, alt in km)'; + +CREATE FUNCTION observe(tle, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION observe(tle, observer, timestamptz) IS + 'Propagate TLE and compute observer-relative look angles in one call. Returns topocentric (az/el/range/range_rate).'; + +CREATE FUNCTION observe_safe(tle, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE; +COMMENT ON FUNCTION observe_safe(tle, observer, timestamptz) IS + 'Like observe() but returns NULL on propagation error. For batch queries with potentially invalid/decayed TLEs.'; + + +-- ============================================================ +-- Pass prediction functions +-- ============================================================ + +CREATE FUNCTION next_pass(tle, observer, timestamptz) RETURNS pass_event + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION next_pass(tle, observer, timestamptz) IS + 'Find the next satellite pass over observer (searches up to 7 days ahead)'; + +CREATE FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8 DEFAULT 0.0) + RETURNS SETOF pass_event + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE + ROWS 10; +COMMENT ON FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8) IS + 'Predict all satellite passes over observer in time window. Optional min_elevation in degrees.'; + +CREATE FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) RETURNS boolean + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) IS + 'True if any pass occurs over observer in the time window'; + + +-- ============================================================ +-- GiST operator support functions +-- ============================================================ + +-- Overlap operator: do orbital keys overlap in altitude AND inclination? +CREATE FUNCTION tle_overlap(tle, tle) RETURNS boolean + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +-- Altitude distance operator (altitude-only, for KNN ordering) +CREATE FUNCTION tle_alt_distance(tle, tle) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE OPERATOR && ( + LEFTARG = tle, + RIGHTARG = tle, + FUNCTION = tle_overlap, + COMMUTATOR = &&, + RESTRICT = areasel, + JOIN = areajoinsel +); + +COMMENT ON OPERATOR && (tle, tle) IS 'Orbital key overlap (altitude band AND inclination range) — necessary condition for conjunction'; + +CREATE OPERATOR <-> ( + LEFTARG = tle, + RIGHTARG = tle, + FUNCTION = tle_alt_distance, + COMMUTATOR = <-> +); + +COMMENT ON OPERATOR <-> (tle, tle) IS '2-D orbital distance in km: L2 norm of altitude-band gap and inclination gap (radians × Earth radius). Returns 0 when both dimensions overlap.'; + + +-- ============================================================ +-- GiST operator class for 2-D orbital indexing (altitude + inclination) +-- ============================================================ + +-- GiST internal support functions +CREATE FUNCTION gist_tle_compress(internal) RETURNS internal + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_tle_decompress(internal) RETURNS internal + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_tle_consistent(internal, tle, smallint, oid, internal) RETURNS boolean + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_tle_union(internal, internal) RETURNS internal + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_tle_penalty(internal, internal, internal) RETURNS internal + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_tle_picksplit(internal, internal) RETURNS internal + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_tle_same(internal, internal, internal) RETURNS internal + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_tle_distance(internal, tle, smallint, oid, internal) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE OPERATOR CLASS tle_ops + DEFAULT FOR TYPE tle USING gist AS + OPERATOR 3 && , + OPERATOR 15 <-> (tle, tle) FOR ORDER BY float_ops, + FUNCTION 1 gist_tle_consistent(internal, tle, smallint, oid, internal), + FUNCTION 2 gist_tle_union(internal, internal), + FUNCTION 3 gist_tle_compress(internal), + FUNCTION 4 gist_tle_decompress(internal), + FUNCTION 5 gist_tle_penalty(internal, internal, internal), + FUNCTION 6 gist_tle_picksplit(internal, internal), + FUNCTION 7 gist_tle_same(internal, internal, internal), + FUNCTION 8 gist_tle_distance(internal, tle, smallint, oid, internal); + + +-- ============================================================ +-- Heliocentric type: ecliptic J2000 position in AU +-- ============================================================ + +CREATE TYPE heliocentric; + +CREATE FUNCTION heliocentric_in(cstring) RETURNS heliocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION heliocentric_out(heliocentric) RETURNS cstring + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION heliocentric_recv(internal) RETURNS heliocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION heliocentric_send(heliocentric) RETURNS bytea + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE TYPE heliocentric ( + INPUT = heliocentric_in, + OUTPUT = heliocentric_out, + RECEIVE = heliocentric_recv, + SEND = heliocentric_send, + INTERNALLENGTH = 24, + ALIGNMENT = double, + STORAGE = plain +); + +COMMENT ON TYPE heliocentric IS 'Heliocentric position in ecliptic J2000 frame (x, y, z in AU)'; + +CREATE FUNCTION helio_x(heliocentric) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION helio_x(heliocentric) IS 'X component in AU (ecliptic J2000, vernal equinox direction)'; + +CREATE FUNCTION helio_y(heliocentric) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION helio_y(heliocentric) IS 'Y component in AU (ecliptic J2000)'; + +CREATE FUNCTION helio_z(heliocentric) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION helio_z(heliocentric) IS 'Z component in AU (ecliptic J2000, north ecliptic pole)'; + +CREATE FUNCTION helio_distance(heliocentric) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION helio_distance(heliocentric) IS 'Heliocentric distance in AU'; + + +-- ============================================================ +-- Star observation functions +-- ============================================================ + +CREATE FUNCTION star_observe(float8, float8, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION star_observe(float8, float8, observer, timestamptz) IS + 'Observe a star from (ra_hours J2000, dec_degrees J2000, observer, time). Returns topocentric az/el. Range is 0 (infinite distance).'; + +CREATE FUNCTION star_observe_safe(float8, float8, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE; +COMMENT ON FUNCTION star_observe_safe(float8, float8, observer, timestamptz) IS + 'Like star_observe but returns NULL on invalid inputs. For batch queries over star catalogs.'; + + +-- ============================================================ +-- Keplerian propagation functions +-- ============================================================ + +CREATE FUNCTION kepler_propagate( + q_au float8, eccentricity float8, + inclination_deg float8, arg_perihelion_deg float8, + long_asc_node_deg float8, perihelion_jd float8, + t timestamptz +) RETURNS heliocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION kepler_propagate(float8, float8, float8, float8, float8, float8, timestamptz) IS + 'Two-body Keplerian propagation from classical orbital elements. Returns heliocentric ecliptic J2000 position in AU. Handles elliptic, parabolic, and hyperbolic orbits.'; + + +-- ============================================================ +-- Comet observation +-- ============================================================ + +CREATE FUNCTION comet_observe( + q_au float8, eccentricity float8, + inclination_deg float8, arg_perihelion_deg float8, + long_asc_node_deg float8, perihelion_jd float8, + earth_x_au float8, earth_y_au float8, earth_z_au float8, + obs observer, t timestamptz +) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION comet_observe(float8, float8, float8, float8, float8, float8, float8, float8, float8, observer, timestamptz) IS + 'Observe a comet/asteroid from orbital elements. Requires Earth heliocentric ecliptic J2000 position (AU). Returns topocentric az/el with geocentric range in km.'; + + +-- ============================================================ +-- VSOP87 planets, ELP82B Moon, Sun observation +-- ============================================================ + +CREATE FUNCTION planet_heliocentric(int4, timestamptz) RETURNS heliocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_heliocentric(int4, timestamptz) IS + 'VSOP87 heliocentric ecliptic J2000 position (AU). Body IDs: 0=Sun, 1=Mercury, ..., 8=Neptune.'; + +CREATE FUNCTION planet_observe(int4, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_observe(int4, observer, timestamptz) IS + 'Observe a planet from (body_id 1-8, observer, time). Returns topocentric az/el with geocentric range in km.'; + +CREATE FUNCTION sun_observe(observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION sun_observe(observer, timestamptz) IS + 'Observe the Sun from (observer, time). Returns topocentric az/el with geocentric range in km.'; + +CREATE FUNCTION moon_observe(observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION moon_observe(observer, timestamptz) IS + 'Observe the Moon via ELP2000-82B from (observer, time). Returns topocentric az/el with geocentric range in km.'; + + +-- ============================================================ +-- Planetary moon observation +-- ============================================================ + +CREATE FUNCTION galilean_observe(int4, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION galilean_observe(int4, observer, timestamptz) IS + 'Observe a Galilean moon of Jupiter via L1.2 theory. Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.'; + +CREATE FUNCTION saturn_moon_observe(int4, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION saturn_moon_observe(int4, observer, timestamptz) IS + 'Observe a Saturn moon via TASS 1.7. Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione, 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion.'; + +CREATE FUNCTION uranus_moon_observe(int4, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION uranus_moon_observe(int4, observer, timestamptz) IS + 'Observe a Uranus moon via GUST86. Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon.'; + +CREATE FUNCTION mars_moon_observe(int4, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION mars_moon_observe(int4, observer, timestamptz) IS + 'Observe a Mars moon via MarsSat. Body IDs: 0=Phobos, 1=Deimos.'; + + +-- ============================================================ +-- Jupiter decametric radio burst prediction +-- ============================================================ + +CREATE FUNCTION io_phase_angle(timestamptz) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION io_phase_angle(timestamptz) IS + 'Io orbital phase angle in degrees [0,360). 0=superior conjunction (behind Jupiter). Standard Radio JOVE convention.'; + +CREATE FUNCTION jupiter_cml(observer, timestamptz) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION jupiter_cml(observer, timestamptz) IS + 'Jupiter Central Meridian Longitude, System III (1965.0), in degrees [0,360). Light-time corrected.'; + +CREATE FUNCTION jupiter_burst_probability(float8, float8) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION jupiter_burst_probability(float8, float8) IS + 'Estimated Jupiter decametric burst probability (0-1) from (io_phase_deg, cml_deg). Based on Carr et al. (1983) source regions A, B, C, D.'; + + +-- ============================================================ +-- Interplanetary transfer orbits (Lambert solver) +-- ============================================================ + +CREATE FUNCTION lambert_transfer( + dep_body_id int4, arr_body_id int4, + dep_time timestamptz, arr_time timestamptz, + OUT c3_departure float8, OUT c3_arrival float8, + OUT v_inf_departure float8, OUT v_inf_arrival float8, + OUT tof_days float8, OUT transfer_sma float8 +) RETURNS RECORD + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION lambert_transfer(int4, int4, timestamptz, timestamptz) IS + 'Solve Lambert transfer between two planets. Returns C3 (km^2/s^2), v_infinity (km/s), TOF (days), SMA (AU). Body IDs 1-8.'; + +CREATE FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) IS + 'Departure C3 (km^2/s^2) for a Lambert transfer. Returns NULL if solver fails. For pork chop plots.'; + + +-- ============================================================ +-- DE ephemeris functions (optional high-precision) +-- ============================================================ + +CREATE FUNCTION planet_heliocentric_de(int4, timestamptz) RETURNS heliocentric + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_heliocentric_de(int4, timestamptz) IS + 'Heliocentric ecliptic J2000 position via JPL DE (sub-arcsecond). Falls back to VSOP87 if DE unavailable. Body IDs: 0=Sun, 1-8=Mercury-Neptune.'; + +CREATE FUNCTION planet_observe_de(int4, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_observe_de(int4, observer, timestamptz) IS + 'Observe planet via JPL DE. Falls back to VSOP87. Body IDs: 1-8 (Mercury-Neptune).'; + +CREATE FUNCTION sun_observe_de(observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION sun_observe_de(observer, timestamptz) IS + 'Observe Sun via JPL DE. Falls back to VSOP87.'; + +CREATE FUNCTION moon_observe_de(observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION moon_observe_de(observer, timestamptz) IS + 'Observe Moon via JPL DE. Falls back to ELP2000-82B.'; + +CREATE FUNCTION lambert_transfer_de( + dep_body_id int4, arr_body_id int4, + dep_time timestamptz, arr_time timestamptz, + OUT c3_departure float8, OUT c3_arrival float8, + OUT v_inf_departure float8, OUT v_inf_arrival float8, + OUT tof_days float8, OUT transfer_sma float8 +) RETURNS RECORD + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION lambert_transfer_de(int4, int4, timestamptz, timestamptz) IS + 'Lambert transfer via JPL DE positions. Falls back to VSOP87. Body IDs 1-8.'; + +CREATE FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) IS + 'Departure C3 via JPL DE. Falls back to VSOP87. For pork chop plots.'; + +CREATE FUNCTION galilean_observe_de(int4, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION galilean_observe_de(int4, observer, timestamptz) IS + 'Observe Galilean moon with JPL DE parent position. L1.2 moon offsets. Body IDs: 0-3 (Io-Callisto).'; + +CREATE FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) IS + 'Observe Saturn moon with JPL DE parent position. TASS 1.7 moon offsets. Body IDs: 0-7 (Mimas-Hyperion).'; + +CREATE FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) IS + 'Observe Uranus moon with JPL DE parent position. GUST86 moon offsets. Body IDs: 0-4 (Miranda-Oberon).'; + +CREATE FUNCTION mars_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION mars_moon_observe_de(int4, observer, timestamptz) IS + 'Observe Mars moon with JPL DE parent position. MarsSat moon offsets. Body IDs: 0-1 (Phobos-Deimos).'; + + +-- Diagnostic function + +CREATE FUNCTION pg_orrery_ephemeris_info( + OUT provider text, OUT file_path text, + OUT start_jd float8, OUT end_jd float8, + OUT version int4, OUT au_km float8 +) RETURNS RECORD + AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE; +COMMENT ON FUNCTION pg_orrery_ephemeris_info() IS + 'Returns current ephemeris provider status: VSOP87 or JPL_DE with file path, JD range, version, and AU value.'; + + +-- ============================================================ +-- Orbit determination (TLE fitting from observations) +-- ============================================================ + +-- Fit TLE from ECI position/velocity ephemeris +-- weights: per-observation weighting (NULL = uniform) + +CREATE FUNCTION tle_from_eci( + positions eci_position[], times timestamptz[], + seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false, + max_iter int4 DEFAULT 15, + weights float8[] DEFAULT NULL, + OUT fitted_tle tle, OUT iterations int4, + OUT rms_final float8, OUT rms_initial float8, OUT status text, + OUT condition_number float8, OUT covariance float8[], OUT nstate int4 +) RETURNS RECORD + AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE; +COMMENT ON FUNCTION tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4, float8[]) IS + 'Fit a TLE from ECI position/velocity observations via differential correction. Optional per-observation weights for heterogeneous sensor fusion. Returns fitted TLE, RMS residuals, convergence status, condition number, and formal covariance matrix.'; + +-- Fit TLE from topocentric observations (az/el/range) — single observer +-- fit_range_rate: include range_rate as 4th residual component +-- weights: per-observation weighting (NULL = uniform) + +CREATE FUNCTION tle_from_topocentric( + observations topocentric[], times timestamptz[], + obs observer, + seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false, + max_iter int4 DEFAULT 15, + fit_range_rate boolean DEFAULT false, + weights float8[] DEFAULT NULL, + OUT fitted_tle tle, OUT iterations int4, + OUT rms_final float8, OUT rms_initial float8, OUT status text, + OUT condition_number float8, OUT covariance float8[], OUT nstate int4 +) RETURNS RECORD + AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE; +COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4, boolean, float8[]) IS + 'Fit a TLE from topocentric (az/el/range) observations via differential correction. Optional range_rate fitting and per-observation weights. Returns fitted TLE, RMS residuals, convergence status, condition number, and formal covariance matrix.'; + +-- Fit TLE from topocentric observations — multiple observers + +CREATE FUNCTION tle_from_topocentric( + observations topocentric[], times timestamptz[], + observers observer[], observer_ids int4[], + seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false, + max_iter int4 DEFAULT 15, + fit_range_rate boolean DEFAULT false, + weights float8[] DEFAULT NULL, + OUT fitted_tle tle, OUT iterations int4, + OUT rms_final float8, OUT rms_initial float8, OUT status text, + OUT condition_number float8, OUT covariance float8[], OUT nstate int4 +) RETURNS RECORD + AS 'MODULE_PATHNAME', 'tle_from_topocentric_multi' + LANGUAGE C STABLE PARALLEL SAFE; +COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer[], int4[], tle, boolean, int4, boolean, float8[]) IS + 'Fit a TLE from topocentric observations collected by multiple ground stations. observer_ids[i] indexes into observers[]. Optional range_rate fitting and per-observation weights.'; + +-- Per-observation residuals diagnostic + +CREATE FUNCTION tle_fit_residuals( + fitted tle, + positions eci_position[], + times timestamptz[] +) RETURNS TABLE ( + t timestamptz, + dx_km float8, + dy_km float8, + dz_km float8, + pos_err_km float8 +) + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION tle_fit_residuals(tle, eci_position[], timestamptz[]) IS + 'Compute per-observation position residuals (km) between a TLE and ECI observations. Useful for fit quality assessment.'; + +-- Fit TLE from RA/Dec observations — single observer +-- Uses Gauss method for initial orbit determination when no seed is provided. +-- RA in hours [0,24), Dec in degrees [-90,90] (matches star_observe convention). +-- RMS output is in radians for angles-only mode. + +CREATE FUNCTION tle_from_angles( + ra_hours float8[], dec_degrees float8[], + times timestamptz[], + obs observer, + seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false, + max_iter int4 DEFAULT 15, + weights float8[] DEFAULT NULL, + OUT fitted_tle tle, OUT iterations int4, + OUT rms_final float8, OUT rms_initial float8, OUT status text, + OUT condition_number float8, OUT covariance float8[], OUT nstate int4 +) RETURNS RECORD AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE; +COMMENT ON FUNCTION tle_from_angles(float8[], float8[], timestamptz[], observer, tle, boolean, int4, float8[]) IS + 'Fit a TLE from angles-only (RA/Dec) observations via Gauss IOD + differential correction. RA in hours [0,24), Dec in degrees [-90,90]. RMS output in radians. Uses Gauss method for seed-free initial guess.'; + +-- Fit TLE from RA/Dec observations — multiple observers + +CREATE FUNCTION tle_from_angles( + ra_hours float8[], dec_degrees float8[], + times timestamptz[], + observers observer[], observer_ids int4[], + seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false, + max_iter int4 DEFAULT 15, + weights float8[] DEFAULT NULL, + OUT fitted_tle tle, OUT iterations int4, + OUT rms_final float8, OUT rms_initial float8, OUT status text, + OUT condition_number float8, OUT covariance float8[], OUT nstate int4 +) RETURNS RECORD + AS 'MODULE_PATHNAME', 'tle_from_angles_multi' + LANGUAGE C STABLE PARALLEL SAFE; +COMMENT ON FUNCTION tle_from_angles(float8[], float8[], timestamptz[], observer[], int4[], tle, boolean, int4, float8[]) IS + 'Fit a TLE from angles-only (RA/Dec) observations from multiple ground stations via Gauss IOD + differential correction.'; +-- pg_orrery 0.6.0 -> 0.7.0 migration +-- +-- Adds SP-GiST orbital trie index for satellite pass prediction. +-- 2-level trie: SMA (L0) + inclination (L1) with query-time RAAN filter. +-- The &? operator answers "might this satellite be visible?" + +-- ============================================================ +-- observer_window composite type (query parameter bundle) +-- ============================================================ + +CREATE TYPE observer_window AS ( + obs observer, + t_start timestamptz, + t_end timestamptz, + min_el float8 +); + +COMMENT ON TYPE observer_window IS + 'Observation query parameters: observer location, time window, and minimum elevation angle (degrees). Used with the &? visibility cone operator.'; + +-- ============================================================ +-- Visibility cone operator function +-- ============================================================ + +CREATE FUNCTION tle_visibility_possible(tle, observer_window) RETURNS boolean + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; + +COMMENT ON FUNCTION tle_visibility_possible(tle, observer_window) IS + 'Could this satellite be visible from the observer during the time window? Combines altitude, inclination, and RAAN checks. Conservative superset — survivors need SGP4 propagation for ground truth.'; + +-- ============================================================ +-- &? operator (visibility cone check) +-- ============================================================ +-- The indexed column (tle) MUST be the left argument so PostgreSQL +-- can form a ScanKey and pass it to inner_consistent for pruning. + +CREATE OPERATOR &? ( + LEFTARG = tle, + RIGHTARG = observer_window, + FUNCTION = tle_visibility_possible, + RESTRICT = contsel, + JOIN = contjoinsel +); + +COMMENT ON OPERATOR &? (tle, observer_window) IS + 'Visibility cone check: could this satellite be visible from the observer during the time window? Index-accelerated via SP-GiST orbital trie.'; + +-- ============================================================ +-- SP-GiST support functions +-- ============================================================ + +CREATE FUNCTION spgist_tle_config(internal, internal) RETURNS void + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION spgist_tle_choose(internal, internal) RETURNS void + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION spgist_tle_picksplit(internal, internal) RETURNS void + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION spgist_tle_inner_consistent(internal, internal) RETURNS void + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION spgist_tle_leaf_consistent(internal, internal) RETURNS void + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +-- ============================================================ +-- SP-GiST operator class (opt-in, not DEFAULT) +-- ============================================================ + +CREATE OPERATOR CLASS tle_spgist_ops + FOR TYPE tle USING spgist AS + OPERATOR 1 &? (tle, observer_window), + FUNCTION 1 spgist_tle_config(internal, internal), + FUNCTION 2 spgist_tle_choose(internal, internal), + FUNCTION 3 spgist_tle_picksplit(internal, internal), + FUNCTION 4 spgist_tle_inner_consistent(internal, internal), + FUNCTION 5 spgist_tle_leaf_consistent(internal, internal); +-- pg_orrery 0.7.0 -> 0.8.0 migration +-- +-- Adds orbital_elements type for comets/asteroids, MPC MPCORB.DAT parser, +-- and small_body_observe()/small_body_heliocentric() observation functions. + +-- ============================================================ +-- orbital_elements type +-- ============================================================ + +CREATE TYPE orbital_elements; + +CREATE FUNCTION orbital_elements_in(cstring) RETURNS orbital_elements + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION orbital_elements_out(orbital_elements) RETURNS cstring + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION orbital_elements_recv(internal) RETURNS orbital_elements + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION orbital_elements_send(orbital_elements) RETURNS bytea + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE TYPE orbital_elements ( + INPUT = orbital_elements_in, + OUTPUT = orbital_elements_out, + RECEIVE = orbital_elements_recv, + SEND = orbital_elements_send, + INTERNALLENGTH = 72, + ALIGNMENT = double, + STORAGE = plain +); + +COMMENT ON TYPE orbital_elements IS + 'Classical Keplerian orbital elements for comets and asteroids (epoch, q, e, inc, omega, Omega, tp, H, G). 72 bytes, fixed-size.'; + + +-- ============================================================ +-- Accessor functions +-- ============================================================ + +CREATE FUNCTION oe_epoch(orbital_elements) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION oe_epoch(orbital_elements) IS 'Osculation epoch (Julian date)'; + +CREATE FUNCTION oe_perihelion(orbital_elements) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION oe_perihelion(orbital_elements) IS 'Perihelion distance q (AU)'; + +CREATE FUNCTION oe_eccentricity(orbital_elements) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION oe_eccentricity(orbital_elements) IS 'Eccentricity'; + +CREATE FUNCTION oe_inclination(orbital_elements) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION oe_inclination(orbital_elements) IS 'Inclination (degrees)'; + +CREATE FUNCTION oe_arg_perihelion(orbital_elements) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION oe_arg_perihelion(orbital_elements) IS 'Argument of perihelion (degrees)'; + +CREATE FUNCTION oe_raan(orbital_elements) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION oe_raan(orbital_elements) IS 'Longitude of ascending node (degrees)'; + +CREATE FUNCTION oe_tp(orbital_elements) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION oe_tp(orbital_elements) IS 'Time of perihelion passage (Julian date)'; + +CREATE FUNCTION oe_h_mag(orbital_elements) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION oe_h_mag(orbital_elements) IS 'Absolute magnitude H (NaN if unknown)'; + +CREATE FUNCTION oe_g_slope(orbital_elements) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION oe_g_slope(orbital_elements) IS 'Slope parameter G (NaN if unknown)'; + +CREATE FUNCTION oe_semi_major_axis(orbital_elements) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION oe_semi_major_axis(orbital_elements) IS 'Semi-major axis a = q/(1-e) in AU. NULL for parabolic/hyperbolic orbits (e >= 1).'; + +CREATE FUNCTION oe_period_years(orbital_elements) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION oe_period_years(orbital_elements) IS 'Orbital period in years = a^1.5 (Kepler third law). NULL for parabolic/hyperbolic orbits (e >= 1).'; + + +-- ============================================================ +-- MPC MPCORB.DAT parser +-- ============================================================ + +CREATE FUNCTION oe_from_mpc(text) RETURNS orbital_elements + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION oe_from_mpc(text) IS + 'Parse one MPCORB.DAT fixed-width line into orbital_elements. Converts MPC packed epoch, computes perihelion distance and tp from (a, e, M).'; + + +-- ============================================================ +-- Observation functions +-- ============================================================ + +CREATE FUNCTION small_body_heliocentric(orbital_elements, timestamptz) RETURNS heliocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION small_body_heliocentric(orbital_elements, timestamptz) IS + 'Heliocentric ecliptic J2000 position of a comet/asteroid from its orbital elements at a given time.'; + +CREATE FUNCTION small_body_observe(orbital_elements, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION small_body_observe(orbital_elements, observer, timestamptz) IS + 'Observe a comet/asteroid from orbital elements. Auto-fetches Earth via VSOP87. Returns topocentric az/el with geocentric range in km.'; +-- pg_orrery 0.8.0 -> 0.9.0 migration +-- +-- Adds equatorial type (apparent RA/Dec of date), atmospheric refraction, +-- stellar proper motion, and light-time corrected _apparent() functions. + +-- ============================================================ +-- equatorial type — apparent RA/Dec of date +-- ============================================================ + +CREATE TYPE equatorial; + +CREATE FUNCTION equatorial_in(cstring) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION equatorial_out(equatorial) RETURNS cstring + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION equatorial_recv(internal) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION equatorial_send(equatorial) RETURNS bytea + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE TYPE equatorial ( + INPUT = equatorial_in, + OUTPUT = equatorial_out, + RECEIVE = equatorial_recv, + SEND = equatorial_send, + INTERNALLENGTH = 24, + ALIGNMENT = double, + STORAGE = plain +); + +COMMENT ON TYPE equatorial IS + 'Apparent equatorial coordinates of date: RA (hours), Dec (degrees), distance (km). Solar system: J2000 precessed via IAU 1976. Satellites: TEME frame (~of-date to ~arcsecond). 24 bytes, fixed-size.'; + + +-- ============================================================ +-- Equatorial accessor functions +-- ============================================================ + +CREATE FUNCTION eq_ra(equatorial) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION eq_ra(equatorial) IS 'Right ascension in hours [0, 24)'; + +CREATE FUNCTION eq_dec(equatorial) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION eq_dec(equatorial) IS 'Declination in degrees [-90, 90]'; + +CREATE FUNCTION eq_distance(equatorial) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION eq_distance(equatorial) IS 'Distance in km (0 for stars without parallax)'; + + +-- ============================================================ +-- Satellite RA/Dec functions +-- ============================================================ + +CREATE FUNCTION eci_to_equatorial(eci_position, observer, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION eci_to_equatorial(eci_position, observer, timestamptz) IS + 'Topocentric apparent RA/Dec from ECI position. Observer parallax-corrected — LEO parallax is ~1 degree.'; + +CREATE FUNCTION eci_to_equatorial_geo(eci_position, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION eci_to_equatorial_geo(eci_position, timestamptz) IS + 'Geocentric apparent RA/Dec from ECI position. Observer-independent — the direction of the TEME position vector.'; + + +-- ============================================================ +-- Solar system equatorial functions (VSOP87) +-- ============================================================ + +CREATE FUNCTION planet_equatorial(int4, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_equatorial(int4, timestamptz) IS + 'Geocentric apparent RA/Dec of a planet via VSOP87. Body IDs: 1=Mercury through 8=Neptune.'; + +CREATE FUNCTION sun_equatorial(timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION sun_equatorial(timestamptz) IS + 'Geocentric apparent RA/Dec of the Sun via VSOP87.'; + +CREATE FUNCTION moon_equatorial(timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION moon_equatorial(timestamptz) IS + 'Geocentric apparent RA/Dec of the Moon via ELP2000-82B.'; + +CREATE FUNCTION small_body_equatorial(orbital_elements, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION small_body_equatorial(orbital_elements, timestamptz) IS + 'Geocentric apparent RA/Dec of a comet/asteroid from orbital elements. Earth via VSOP87.'; + +CREATE FUNCTION star_equatorial(float8, float8, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION star_equatorial(float8, float8, timestamptz) IS + 'Apparent RA/Dec of a star at a given time. Precesses J2000 catalog coordinates (RA hours, Dec degrees) to date via IAU 1976.'; + + +-- ============================================================ +-- Atmospheric refraction (Bennett 1982) +-- ============================================================ + +CREATE FUNCTION atmospheric_refraction(float8) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION atmospheric_refraction(float8) IS + 'Atmospheric refraction correction in degrees for a given geometric elevation (degrees). Standard atmosphere: P=1010 mbar, T=10C. Bennett (1982) formula with domain guard at -1 degree.'; + +CREATE FUNCTION atmospheric_refraction_ext(float8, float8, float8) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION atmospheric_refraction_ext(float8, float8, float8) IS + 'Atmospheric refraction with pressure/temperature correction. Args: elevation_deg, pressure_mbar, temperature_celsius. Meeus P/T factor applied to Bennett formula.'; + +CREATE FUNCTION topo_elevation_apparent(topocentric) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION topo_elevation_apparent(topocentric) IS + 'Apparent elevation in degrees — geometric elevation plus atmospheric refraction correction.'; + + +-- ============================================================ +-- Refracted pass prediction +-- ============================================================ + +CREATE FUNCTION predict_passes_refracted( + tle, observer, timestamptz, timestamptz, float8 DEFAULT 0.0 +) RETURNS SETOF pass_event + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE + ROWS 20; +COMMENT ON FUNCTION predict_passes_refracted(tle, observer, timestamptz, timestamptz, float8) IS + 'Predict satellite passes using a refracted horizon threshold (-0.569 deg geometric). Atmospheric refraction makes satellites visible ~35 seconds earlier at AOS and later at LOS.'; + + +-- ============================================================ +-- Stellar proper motion +-- ============================================================ + +CREATE FUNCTION star_observe_pm( + float8, float8, float8, float8, float8, float8, observer, timestamptz +) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION star_observe_pm(float8, float8, float8, float8, float8, float8, observer, timestamptz) IS + 'Observe a star with proper motion. Args: ra_hours, dec_deg, pm_ra_masyr (mu_alpha*cos(delta)), pm_dec_masyr, parallax_mas, rv_kms, observer, time. Hipparcos/Gaia convention for pm_ra.'; + +CREATE FUNCTION star_equatorial_pm( + float8, float8, float8, float8, float8, float8, timestamptz +) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION star_equatorial_pm(float8, float8, float8, float8, float8, float8, timestamptz) IS + 'Apparent RA/Dec of a star with proper motion. Args: ra_hours, dec_deg, pm_ra_masyr, pm_dec_masyr, parallax_mas, rv_kms, time. Distance from parallax if > 0.'; + + +-- ============================================================ +-- Light-time corrected observation functions +-- ============================================================ + +CREATE FUNCTION planet_observe_apparent(int4, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_observe_apparent(int4, observer, timestamptz) IS + 'Observe a planet with single-iteration light-time correction. Body at retarded time, Earth at observation time. VSOP87.'; + +CREATE FUNCTION sun_observe_apparent(observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION sun_observe_apparent(observer, timestamptz) IS + 'Observe the Sun with light-time correction (~8.3 min). VSOP87.'; + +CREATE FUNCTION small_body_observe_apparent(orbital_elements, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION small_body_observe_apparent(orbital_elements, observer, timestamptz) IS + 'Observe a comet/asteroid with single-iteration light-time correction. Kepler propagation at retarded time, Earth via VSOP87 at observation time.'; + + +-- ============================================================ +-- Light-time corrected equatorial functions +-- ============================================================ + +CREATE FUNCTION planet_equatorial_apparent(int4, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_equatorial_apparent(int4, timestamptz) IS + 'Geocentric apparent RA/Dec of a planet with light-time correction. VSOP87.'; + +CREATE FUNCTION moon_equatorial_apparent(timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION moon_equatorial_apparent(timestamptz) IS + 'Geocentric apparent RA/Dec of the Moon with light-time correction (~1.3 sec). ELP2000-82B.'; + +CREATE FUNCTION small_body_equatorial_apparent(orbital_elements, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION small_body_equatorial_apparent(orbital_elements, timestamptz) IS + 'Geocentric apparent RA/Dec of a comet/asteroid with light-time correction.'; + + +-- ============================================================ +-- DE ephemeris equatorial variants (STABLE) +-- ============================================================ + +CREATE FUNCTION planet_equatorial_de(int4, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_equatorial_de(int4, timestamptz) IS + 'Geocentric apparent RA/Dec of a planet via JPL DE ephemeris (falls back to VSOP87 + equatorial).'; + +CREATE FUNCTION moon_equatorial_de(timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION moon_equatorial_de(timestamptz) IS + 'Geocentric apparent RA/Dec of the Moon via JPL DE ephemeris (falls back to ELP2000-82B + equatorial).'; +-- pg_orrery 0.9.0 -> 0.10.0 migration +-- +-- Adds annual aberration to existing _apparent() functions, +-- 6 new _apparent_de() variants, equatorial angular separation +-- operator and cone predicate, and stellar annual parallax. + +-- ============================================================ +-- Equatorial angular distance and cone search +-- ============================================================ + +CREATE FUNCTION eq_angular_distance(equatorial, equatorial) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION eq_angular_distance(equatorial, equatorial) IS + 'Angular separation in degrees between two equatorial positions. Vincenty formula (stable at 0 and 180 degrees).'; + +CREATE FUNCTION eq_within_cone(equatorial, equatorial, float8) RETURNS bool + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION eq_within_cone(equatorial, equatorial, float8) IS + 'True if first position is within radius_deg of second position. Cosine shortcut for fast rejection.'; + +CREATE OPERATOR <-> ( + LEFTARG = equatorial, + RIGHTARG = equatorial, + FUNCTION = eq_angular_distance, + COMMUTATOR = <-> +); +COMMENT ON OPERATOR <-> (equatorial, equatorial) IS + 'Angular separation in degrees between two equatorial positions.'; + + +-- ============================================================ +-- DE apparent observation functions (STABLE, light-time + aberration) +-- ============================================================ + +CREATE FUNCTION planet_observe_apparent_de(int4, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_observe_apparent_de(int4, observer, timestamptz) IS + 'Observe a planet with light-time correction and annual aberration via JPL DE (falls back to VSOP87).'; + +CREATE FUNCTION sun_observe_apparent_de(observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION sun_observe_apparent_de(observer, timestamptz) IS + 'Observe the Sun with aberration via JPL DE (falls back to VSOP87).'; + +CREATE FUNCTION moon_observe_apparent_de(observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION moon_observe_apparent_de(observer, timestamptz) IS + 'Observe the Moon with light-time correction and annual aberration via JPL DE (falls back to ELP2000-82B).'; + +CREATE FUNCTION planet_equatorial_apparent_de(int4, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_equatorial_apparent_de(int4, timestamptz) IS + 'Geocentric apparent RA/Dec of a planet with light-time correction and annual aberration via JPL DE (falls back to VSOP87).'; + +CREATE FUNCTION moon_equatorial_apparent_de(timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION moon_equatorial_apparent_de(timestamptz) IS + 'Geocentric apparent RA/Dec of the Moon with light-time correction and annual aberration via JPL DE (falls back to ELP2000-82B).'; + +CREATE FUNCTION small_body_observe_apparent_de(orbital_elements, observer, timestamptz) RETURNS topocentric + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION small_body_observe_apparent_de(orbital_elements, observer, timestamptz) IS + 'Observe a comet/asteroid with light-time correction and annual aberration. Earth position via JPL DE (falls back to VSOP87).'; +-- pg_orrery 0.10.0 -> 0.11.0 migration +-- +-- Adds make_orbital_elements() constructors and +-- geocentric equatorial functions for planetary moons. + +-- ============================================================ +-- orbital_elements constructors +-- ============================================================ + +CREATE FUNCTION make_orbital_elements( + epoch_jd float8, q_au float8, e float8, + inc_rad float8, omega_rad float8, node_rad float8, + tp_jd float8, h_mag float8, g_slope float8 +) RETURNS orbital_elements + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION make_orbital_elements(float8,float8,float8,float8,float8,float8,float8,float8,float8) IS + 'Construct orbital_elements from 9 floats (angular elements in radians).'; + +CREATE FUNCTION make_orbital_elements_deg( + epoch_jd float8, q_au float8, e float8, + inc_deg float8, omega_deg float8, node_deg float8, + tp_jd float8, h_mag float8, g_slope float8 +) RETURNS orbital_elements + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION make_orbital_elements_deg(float8,float8,float8,float8,float8,float8,float8,float8,float8) IS + 'Construct orbital_elements from 9 floats (angular elements in degrees). Matches text I/O and most catalog column layouts.'; + + +-- ============================================================ +-- Planetary moon equatorial functions +-- ============================================================ + +CREATE FUNCTION galilean_equatorial(int4, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION galilean_equatorial(int4, timestamptz) IS + 'Geometric geocentric RA/Dec of a Galilean moon (0=Io, 1=Europa, 2=Ganymede, 3=Callisto). L1.2 theory + VSOP87. No light-time or aberration correction.'; + +CREATE FUNCTION saturn_moon_equatorial(int4, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION saturn_moon_equatorial(int4, timestamptz) IS + 'Geometric geocentric RA/Dec of a Saturn moon (0=Mimas..7=Hyperion). TASS17 theory + VSOP87. No light-time or aberration correction.'; + +CREATE FUNCTION uranus_moon_equatorial(int4, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION uranus_moon_equatorial(int4, timestamptz) IS + 'Geometric geocentric RA/Dec of a Uranus moon (0=Miranda..4=Oberon). GUST86 theory + VSOP87. No light-time or aberration correction.'; + +CREATE FUNCTION mars_moon_equatorial(int4, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION mars_moon_equatorial(int4, timestamptz) IS + 'Geometric geocentric RA/Dec of a Mars moon (0=Phobos, 1=Deimos). MarsSat theory + VSOP87. No light-time or aberration correction.'; +-- pg_orrery 0.11.0 -> 0.12.0 migration +-- +-- Adds equatorial GiST operator class for KNN sky queries +-- and DE moon equatorial functions for all 4 planetary moon families. + +-- ============================================================ +-- GiST support functions for equatorial type +-- ============================================================ + +CREATE FUNCTION gist_eq_consistent(internal, equatorial, smallint, oid, internal) RETURNS bool + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_eq_union(internal, internal) RETURNS internal + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_eq_compress(internal) RETURNS internal + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_eq_decompress(internal) RETURNS internal + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_eq_penalty(internal, internal, internal) RETURNS internal + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_eq_picksplit(internal, internal) RETURNS internal + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_eq_same(internal, internal, internal) RETURNS internal + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gist_eq_distance(internal, equatorial, smallint, oid, internal) RETURNS float8 + AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +-- ============================================================ +-- Equatorial GiST operator class (KNN ordering only) +-- ============================================================ + +CREATE OPERATOR CLASS eq_gist_ops + DEFAULT FOR TYPE equatorial USING gist AS + OPERATOR 15 <-> (equatorial, equatorial) FOR ORDER BY pg_catalog.float_ops, + FUNCTION 1 gist_eq_consistent(internal, equatorial, smallint, oid, internal), + FUNCTION 2 gist_eq_union(internal, internal), + FUNCTION 3 gist_eq_compress(internal), + FUNCTION 4 gist_eq_decompress(internal), + FUNCTION 5 gist_eq_penalty(internal, internal, internal), + FUNCTION 6 gist_eq_picksplit(internal, internal), + FUNCTION 7 gist_eq_same(internal, internal, internal), + FUNCTION 8 gist_eq_distance(internal, equatorial, smallint, oid, internal); + +-- ============================================================ +-- DE moon equatorial functions (STABLE, fall back to VSOP87) +-- ============================================================ + +CREATE FUNCTION galilean_equatorial_de(int4, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION galilean_equatorial_de(int4, timestamptz) IS + 'Geocentric RA/Dec of a Galilean moon via DE parent position (falls back to VSOP87). 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.'; + +CREATE FUNCTION saturn_moon_equatorial_de(int4, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION saturn_moon_equatorial_de(int4, timestamptz) IS + 'Geocentric RA/Dec of a Saturn moon via DE parent position (falls back to VSOP87). 0=Mimas..7=Hyperion.'; + +CREATE FUNCTION uranus_moon_equatorial_de(int4, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION uranus_moon_equatorial_de(int4, timestamptz) IS + 'Geocentric RA/Dec of a Uranus moon via DE parent position (falls back to VSOP87). 0=Miranda..4=Oberon.'; + +CREATE FUNCTION mars_moon_equatorial_de(int4, timestamptz) RETURNS equatorial + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION mars_moon_equatorial_de(int4, timestamptz) IS + 'Geocentric RA/Dec of a Mars moon via DE parent position (falls back to VSOP87). 0=Phobos, 1=Deimos.'; + + +-- ============================================================ +-- v0.13.0: make_equatorial() constructor +-- ============================================================ + +CREATE FUNCTION make_equatorial(ra_hours float8, dec_deg float8, distance_km float8) + RETURNS equatorial + AS 'MODULE_PATHNAME', 'make_equatorial' + LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +COMMENT ON FUNCTION make_equatorial(float8, float8, float8) IS + 'Construct equatorial from RA (hours [0,24)), Dec (degrees [-90,90]), distance (km).'; + + +-- ============================================================ +-- v0.13.0: Rise/set prediction functions +-- ============================================================ + +CREATE FUNCTION planet_next_rise(body_id int4, obs observer, t timestamptz) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_next_rise(int4, observer, timestamptz) IS + 'Next geometric rise time for a planet. Returns NULL if no rise within 7 days. body_id: 1=Mercury..8=Neptune.'; + +CREATE FUNCTION planet_next_set(body_id int4, obs observer, t timestamptz) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_next_set(int4, observer, timestamptz) IS + 'Next geometric set time for a planet. Returns NULL if no set within 7 days. body_id: 1=Mercury..8=Neptune.'; + +CREATE FUNCTION sun_next_rise(obs observer, t timestamptz) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION sun_next_rise(observer, timestamptz) IS + 'Next geometric sunrise. Returns NULL if Sun does not rise within 7 days (polar night).'; + +CREATE FUNCTION sun_next_set(obs observer, t timestamptz) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION sun_next_set(observer, timestamptz) IS + 'Next geometric sunset. Returns NULL if Sun does not set within 7 days (midnight sun).'; + +CREATE FUNCTION moon_next_rise(obs observer, t timestamptz) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION moon_next_rise(observer, timestamptz) IS + 'Next geometric moonrise. Returns NULL if Moon does not rise within 7 days.'; + +CREATE FUNCTION moon_next_set(obs observer, t timestamptz) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION moon_next_set(observer, timestamptz) IS + 'Next geometric moonset. Returns NULL if Moon does not set within 7 days.'; + +CREATE FUNCTION sun_next_rise_refracted(obs observer, t timestamptz) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION sun_next_rise_refracted(observer, timestamptz) IS + 'Next refracted sunrise (-0.833 deg threshold: refraction + semidiameter). Earlier than geometric by ~4 min.'; + +CREATE FUNCTION sun_next_set_refracted(obs observer, t timestamptz) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION sun_next_set_refracted(observer, timestamptz) IS + 'Next refracted sunset (-0.833 deg threshold: refraction + semidiameter). Later than geometric by ~4 min.'; + + +-- ============================================================ +-- v0.14.0: Refracted planet/moon rise/set +-- ============================================================ + +CREATE FUNCTION planet_next_rise_refracted(body_id int4, obs observer, t timestamptz) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_next_rise_refracted(int4, observer, timestamptz) IS + 'Next refracted rise time for a planet (-0.569 deg threshold: atmospheric refraction only). Earlier than geometric.'; + +CREATE FUNCTION planet_next_set_refracted(body_id int4, obs observer, t timestamptz) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_next_set_refracted(int4, observer, timestamptz) IS + 'Next refracted set time for a planet (-0.569 deg threshold: atmospheric refraction only). Later than geometric.'; + +CREATE FUNCTION moon_next_rise_refracted(obs observer, t timestamptz) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION moon_next_rise_refracted(observer, timestamptz) IS + 'Next refracted moonrise (-0.833 deg threshold: refraction + semidiameter). Earlier than geometric.'; + +CREATE FUNCTION moon_next_set_refracted(obs observer, t timestamptz) RETURNS timestamptz + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION moon_next_set_refracted(observer, timestamptz) IS + 'Next refracted moonset (-0.833 deg threshold: refraction + semidiameter). Later than geometric.'; + + +-- ============================================================ +-- v0.14.0: Constellation identification (Roman 1987, CDS VI/42) +-- ============================================================ + +CREATE FUNCTION constellation(eq equatorial) RETURNS text + AS 'MODULE_PATHNAME', 'constellation_from_equatorial' + LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION constellation(equatorial) IS + 'IAU constellation abbreviation (3 letters) from equatorial coordinates (Roman 1987).'; + +CREATE FUNCTION constellation(ra_hours float8, dec_deg float8) RETURNS text + AS 'MODULE_PATHNAME', 'constellation_from_radec' + LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION constellation(float8, float8) IS + 'IAU constellation from J2000 RA (hours [0,24)) and Dec (degrees [-90,90]).'; +-- pg_orrery 0.14.0 -> 0.15.0 migration +-- +-- Adds: constellation_full_name (1 function), +-- rise/set status diagnostics (3 functions). + +-- ============================================================ +-- Constellation full name lookup +-- ============================================================ + +CREATE FUNCTION constellation_full_name(abbr text) RETURNS text + AS 'MODULE_PATHNAME', 'constellation_full_name_from_abbr' + LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION constellation_full_name(text) IS + 'Full IAU constellation name from 3-letter abbreviation. Returns NULL for invalid abbreviation.'; + +-- ============================================================ +-- Rise/set status diagnostics +-- ============================================================ + +CREATE FUNCTION sun_rise_set_status(obs observer, t timestamptz) RETURNS text + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION sun_rise_set_status(observer, timestamptz) IS + 'Classify Sun visibility: rises_and_sets, circumpolar, or never_rises.'; + +CREATE FUNCTION moon_rise_set_status(obs observer, t timestamptz) RETURNS text + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION moon_rise_set_status(observer, timestamptz) IS + 'Classify Moon visibility: rises_and_sets, circumpolar, or never_rises.'; + +CREATE FUNCTION planet_rise_set_status(body_id int4, obs observer, t timestamptz) RETURNS text + AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE; +COMMENT ON FUNCTION planet_rise_set_status(int4, observer, timestamptz) IS + 'Classify planet visibility: rises_and_sets, circumpolar, or never_rises. Body IDs 1-8 (Mercury-Neptune).'; diff --git a/src/constellation_data.c b/src/constellation_data.c index b62ad6d..181e533 100644 --- a/src/constellation_data.c +++ b/src/constellation_data.c @@ -376,3 +376,96 @@ const roman_boundary roman_boundaries[] = { }; const int roman_boundary_count = sizeof(roman_boundaries) / sizeof(roman_boundaries[0]); + +const constellation_name constellation_names[] = { + { "And", "Andromeda" }, + { "Ant", "Antlia" }, + { "Aps", "Apus" }, + { "Aqr", "Aquarius" }, + { "Aql", "Aquila" }, + { "Ara", "Ara" }, + { "Ari", "Aries" }, + { "Aur", "Auriga" }, + { "Boo", "Bootes" }, + { "Cae", "Caelum" }, + { "Cam", "Camelopardalis" }, + { "Cnc", "Cancer" }, + { "CVn", "Canes Venatici" }, + { "CMa", "Canis Major" }, + { "CMi", "Canis Minor" }, + { "Cap", "Capricornus" }, + { "Car", "Carina" }, + { "Cas", "Cassiopeia" }, + { "Cen", "Centaurus" }, + { "Cep", "Cepheus" }, + { "Cet", "Cetus" }, + { "Cha", "Chamaeleon" }, + { "Cir", "Circinus" }, + { "Col", "Columba" }, + { "Com", "Coma Berenices" }, + { "CrA", "Corona Australis" }, + { "CrB", "Corona Borealis" }, + { "Crv", "Corvus" }, + { "Crt", "Crater" }, + { "Cru", "Crux" }, + { "Cyg", "Cygnus" }, + { "Del", "Delphinus" }, + { "Dor", "Dorado" }, + { "Dra", "Draco" }, + { "Equ", "Equuleus" }, + { "Eri", "Eridanus" }, + { "For", "Fornax" }, + { "Gem", "Gemini" }, + { "Gru", "Grus" }, + { "Her", "Hercules" }, + { "Hor", "Horologium" }, + { "Hya", "Hydra" }, + { "Hyi", "Hydrus" }, + { "Ind", "Indus" }, + { "Lac", "Lacerta" }, + { "Leo", "Leo" }, + { "LMi", "Leo Minor" }, + { "Lep", "Lepus" }, + { "Lib", "Libra" }, + { "Lup", "Lupus" }, + { "Lyn", "Lynx" }, + { "Lyr", "Lyra" }, + { "Men", "Mensa" }, + { "Mic", "Microscopium" }, + { "Mon", "Monoceros" }, + { "Mus", "Musca" }, + { "Nor", "Norma" }, + { "Oct", "Octans" }, + { "Oph", "Ophiuchus" }, + { "Ori", "Orion" }, + { "Pav", "Pavo" }, + { "Peg", "Pegasus" }, + { "Per", "Perseus" }, + { "Phe", "Phoenix" }, + { "Pic", "Pictor" }, + { "Psc", "Pisces" }, + { "PsA", "Piscis Austrinus" }, + { "Pup", "Puppis" }, + { "Pyx", "Pyxis" }, + { "Ret", "Reticulum" }, + { "Sge", "Sagitta" }, + { "Sgr", "Sagittarius" }, + { "Sco", "Scorpius" }, + { "Scl", "Sculptor" }, + { "Sct", "Scutum" }, + { "Ser", "Serpens" }, + { "Sex", "Sextans" }, + { "Tau", "Taurus" }, + { "Tel", "Telescopium" }, + { "Tri", "Triangulum" }, + { "TrA", "Triangulum Australe" }, + { "Tuc", "Tucana" }, + { "UMa", "Ursa Major" }, + { "UMi", "Ursa Minor" }, + { "Vel", "Vela" }, + { "Vir", "Virgo" }, + { "Vol", "Volans" }, + { "Vul", "Vulpecula" }, +}; + +const int constellation_name_count = sizeof(constellation_names) / sizeof(constellation_names[0]); diff --git a/src/constellation_data.h b/src/constellation_data.h index 2accd33..dd3697f 100644 --- a/src/constellation_data.h +++ b/src/constellation_data.h @@ -23,4 +23,13 @@ typedef struct roman_boundary extern const roman_boundary roman_boundaries[]; extern const int roman_boundary_count; +typedef struct constellation_name +{ + char abbr[4]; /* 3-letter IAU abbreviation + null */ + char full[24]; /* Full IAU name + null (longest: "Triangulum Australe" = 20 chars) */ +} constellation_name; + +extern const constellation_name constellation_names[]; +extern const int constellation_name_count; + #endif /* PG_ORRERY_CONSTELLATION_DATA_H */ diff --git a/src/constellation_funcs.c b/src/constellation_funcs.c index dd8b0ba..6e86fe8 100644 --- a/src/constellation_funcs.c +++ b/src/constellation_funcs.c @@ -17,6 +17,7 @@ #include "postgres.h" #include "fmgr.h" +#include "varatt.h" #include "utils/builtins.h" #include "types.h" @@ -26,6 +27,7 @@ PG_FUNCTION_INFO_V1(constellation_from_equatorial); PG_FUNCTION_INFO_V1(constellation_from_radec); +PG_FUNCTION_INFO_V1(constellation_full_name_from_abbr); /* B1875.0 epoch as Julian date. * JD(B) = 2415020.31352 + (B - 1900.0) * 365.242198781 @@ -173,3 +175,35 @@ constellation_from_radec(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text(abbr)); } + + +/* ================================================================ + * constellation_full_name(text) -> text + * + * Returns the full IAU name for a 3-letter abbreviation. + * Returns NULL for unrecognized abbreviations (composable in queries). + * ================================================================ + */ +Datum +constellation_full_name_from_abbr(PG_FUNCTION_ARGS) +{ + text *abbr_text = PG_GETARG_TEXT_PP(0); + char abbr[4]; + int len; + int i; + + len = VARSIZE_ANY_EXHDR(abbr_text); + if (len < 2 || len > 3) + PG_RETURN_NULL(); + + memcpy(abbr, VARDATA_ANY(abbr_text), len); + abbr[len] = '\0'; + + for (i = 0; i < constellation_name_count; i++) + { + if (strcmp(abbr, constellation_names[i].abbr) == 0) + PG_RETURN_TEXT_P(cstring_to_text(constellation_names[i].full)); + } + + PG_RETURN_NULL(); +} diff --git a/src/rise_set_funcs.c b/src/rise_set_funcs.c index 3b0f44e..3ef1ddf 100644 --- a/src/rise_set_funcs.c +++ b/src/rise_set_funcs.c @@ -16,6 +16,7 @@ #include "postgres.h" #include "fmgr.h" #include "utils/timestamp.h" +#include "utils/builtins.h" #include "types.h" #include "astro_math.h" #include "vsop87.h" @@ -34,6 +35,9 @@ PG_FUNCTION_INFO_V1(planet_next_rise_refracted); PG_FUNCTION_INFO_V1(planet_next_set_refracted); PG_FUNCTION_INFO_V1(moon_next_rise_refracted); 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); #define COARSE_STEP_JD (60.0 / 86400.0) /* 60 seconds */ #define BISECT_TOL_JD (0.1 / 86400.0) /* 0.1 second */ @@ -193,6 +197,52 @@ find_next_crossing(int body_type, int body_id, } +/* ---------------------------------------------------------------- + * classify_rise_set -- sample elevation to determine behavior + * + * Samples body elevation at N_SAMPLES equally-spaced points across + * 24 hours starting from start_jd. Classifies: + * - All above geometric horizon -> "circumpolar" + * - All below geometric horizon -> "never_rises" + * - Mixed -> "rises_and_sets" + * + * Uses geometric horizon (0 deg) for classification — this matches + * the NULL contract of the rise/set functions. + * ---------------------------------------------------------------- + */ +#define RISE_SET_N_SAMPLES 48 + +static const char * +classify_rise_set(int body_type, int body_id, + const pg_observer *obs, double start_jd) +{ + int above = 0; + int below = 0; + int i; + double step = 1.0 / (double)RISE_SET_N_SAMPLES; /* 24h / N = 30 min */ + + for (i = 0; i < RISE_SET_N_SAMPLES; i++) + { + double jd = start_jd + i * step; + double el = elevation_at_jd_body(body_type, body_id, obs, jd); + + if (el > 0.0) + above++; + else + below++; + + /* Early exit: once we have both above and below, it's mixed */ + if (above > 0 && below > 0) + return "rises_and_sets"; + } + + if (above == RISE_SET_N_SAMPLES) + return "circumpolar"; + else + return "never_rises"; +} + + /* ================================================================ * planet_next_rise(body_id, observer, timestamptz) -> timestamptz * @@ -557,3 +607,80 @@ moon_next_set_refracted(PG_FUNCTION_ARGS) PG_RETURN_TIMESTAMPTZ(jd_to_timestamptz(result_jd)); } + + +/* ================================================================ + * sun_rise_set_status(observer, timestamptz) -> text + * + * Returns 'rises_and_sets', 'circumpolar', or 'never_rises'. + * Call this when sun_next_rise/set returns NULL to find out why. + * ================================================================ + */ +Datum +sun_rise_set_status(PG_FUNCTION_ARGS) +{ + pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0); + int64 ts = PG_GETARG_INT64(1); + double start_jd; + const char *status; + + start_jd = timestamptz_to_jd(ts); + status = classify_rise_set(BTYPE_SUN, 0, obs, start_jd); + + PG_RETURN_TEXT_P(cstring_to_text(status)); +} + + +/* ================================================================ + * moon_rise_set_status(observer, timestamptz) -> text + * + * Returns 'rises_and_sets', 'circumpolar', or 'never_rises'. + * ================================================================ + */ +Datum +moon_rise_set_status(PG_FUNCTION_ARGS) +{ + pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0); + int64 ts = PG_GETARG_INT64(1); + double start_jd; + const char *status; + + start_jd = timestamptz_to_jd(ts); + status = classify_rise_set(BTYPE_MOON, 0, obs, start_jd); + + PG_RETURN_TEXT_P(cstring_to_text(status)); +} + + +/* ================================================================ + * planet_rise_set_status(body_id, observer, timestamptz) -> text + * + * Returns 'rises_and_sets', 'circumpolar', or 'never_rises'. + * Body IDs: 1=Mercury, ..., 8=Neptune (not Sun, Earth, or Moon). + * ================================================================ + */ +Datum +planet_rise_set_status(PG_FUNCTION_ARGS) +{ + int32 body_id = PG_GETARG_INT32(0); + pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1); + int64 ts = PG_GETARG_INT64(2); + double start_jd; + const char *status; + + if (body_id < BODY_MERCURY || body_id > BODY_NEPTUNE) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("planet_rise_set_status: 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 observe Earth from Earth"))); + + start_jd = timestamptz_to_jd(ts); + status = classify_rise_set(BTYPE_PLANET, body_id, obs, start_jd); + + PG_RETURN_TEXT_P(cstring_to_text(status)); +} diff --git a/test/expected/v015_features.out b/test/expected/v015_features.out new file mode 100644 index 0000000..bb76fc9 --- /dev/null +++ b/test/expected/v015_features.out @@ -0,0 +1,204 @@ +-- v015_features.sql -- Tests for v0.15.0: constellation_full_name + rise_set_status +-- +-- Verifies the constellation full name lookup and the rise/set +-- status diagnostic functions. +CREATE EXTENSION IF NOT EXISTS pg_orrery; +NOTICE: extension "pg_orrery" already exists, skipping +-- ============================================================ +-- constellation_full_name: known abbreviations +-- ============================================================ +SELECT constellation_full_name('Ari') AS aries; + aries +------- + Aries +(1 row) + +SELECT constellation_full_name('CMa') AS canis_major; + canis_major +------------- + Canis Major +(1 row) + +SELECT constellation_full_name('UMi') AS ursa_minor; + ursa_minor +------------ + Ursa Minor +(1 row) + +SELECT constellation_full_name('Ori') AS orion; + orion +------- + Orion +(1 row) + +SELECT constellation_full_name('Cyg') AS cygnus; + cygnus +-------- + Cygnus +(1 row) + +SELECT constellation_full_name('Oct') AS octans; + octans +-------- + Octans +(1 row) + +SELECT constellation_full_name('TrA') AS tri_australe; + tri_australe +--------------------- + Triangulum Australe +(1 row) + +-- ============================================================ +-- constellation_full_name: composability with constellation() +-- ============================================================ +-- Chain: equatorial -> abbreviation -> full name +SELECT constellation_full_name(constellation(2.5303, 89.264)) AS polaris_full; + polaris_full +-------------- + Ursa Minor +(1 row) + +SELECT constellation_full_name(constellation(6.7525, -16.716)) AS sirius_full; + sirius_full +------------- + Canis Major +(1 row) + +-- Chain with planet equatorial +SELECT constellation_full_name( + constellation(planet_equatorial(5, '2024-01-15 12:00:00+00'::timestamptz)) +) AS jupiter_full; + jupiter_full +-------------- + Aries +(1 row) + +-- ============================================================ +-- constellation_full_name: NULL for invalid abbreviation +-- ============================================================ +SELECT constellation_full_name('XYZ') IS NULL AS invalid_returns_null; + invalid_returns_null +---------------------- + t +(1 row) + +SELECT constellation_full_name('') IS NULL AS empty_returns_null; + empty_returns_null +-------------------- + t +(1 row) + +SELECT constellation_full_name('Toolong') IS NULL AS toolong_returns_null; + toolong_returns_null +---------------------- + t +(1 row) + +-- ============================================================ +-- constellation_full_name: all 88 are reachable (count check) +-- ============================================================ +-- Use generate_series to count distinct full names from the +-- known constellation abbreviations via a spot check +SELECT count(DISTINCT constellation_full_name(abbr)) = 7 + AS spot_check_7_names +FROM (VALUES ('Ari'), ('CMa'), ('UMi'), ('Ori'), ('Cyg'), ('Oct'), ('TrA')) AS t(abbr); + spot_check_7_names +-------------------- + t +(1 row) + +-- ============================================================ +-- sun_rise_set_status: mid-latitude (Eagle, Idaho) in winter +-- Sun rises and sets normally +-- ============================================================ +SELECT sun_rise_set_status('(43.7,-116.4,800)'::observer, '2024-01-15 00:00:00+00'::timestamptz) + AS sun_status_midlat; + sun_status_midlat +------------------- + rises_and_sets +(1 row) + +-- ============================================================ +-- sun_rise_set_status: 70N in June (midnight sun) +-- ============================================================ +SELECT sun_rise_set_status('(70.0,25.0,0)'::observer, '2024-06-21 00:00:00+00'::timestamptz) + AS sun_status_midnight_sun; + sun_status_midnight_sun +------------------------- + circumpolar +(1 row) + +-- ============================================================ +-- sun_rise_set_status: 70N in December (polar night) +-- ============================================================ +SELECT sun_rise_set_status('(70.0,25.0,0)'::observer, '2024-12-21 00:00:00+00'::timestamptz) + AS sun_status_polar_night; + sun_status_polar_night +------------------------ + never_rises +(1 row) + +-- ============================================================ +-- moon_rise_set_status: mid-latitude — Moon normally rises/sets +-- ============================================================ +SELECT moon_rise_set_status('(43.7,-116.4,800)'::observer, '2024-01-15 00:00:00+00'::timestamptz) + AS moon_status_midlat; + moon_status_midlat +-------------------- + rises_and_sets +(1 row) + +-- ============================================================ +-- planet_rise_set_status: Jupiter from mid-latitude (normal) +-- ============================================================ +SELECT planet_rise_set_status(5, '(43.7,-116.4,800)'::observer, '2024-01-15 00:00:00+00'::timestamptz) + AS jupiter_status_midlat; + jupiter_status_midlat +----------------------- + rises_and_sets +(1 row) + +-- ============================================================ +-- Consistency: status matches rise/set NULL contract +-- ============================================================ +-- When sun_next_set returns NULL (circumpolar), status should say so +SELECT sun_next_set('(70.0,25.0,0)'::observer, '2024-06-21 00:00:00+00'::timestamptz) IS NULL + AS sun_no_set_null; + sun_no_set_null +----------------- + t +(1 row) + +SELECT sun_rise_set_status('(70.0,25.0,0)'::observer, '2024-06-21 00:00:00+00'::timestamptz) + = 'circumpolar' AS status_confirms_circumpolar; + status_confirms_circumpolar +----------------------------- + t +(1 row) + +-- When sun_next_rise returns NULL (polar night), status should say so +SELECT sun_next_rise('(70.0,25.0,0)'::observer, '2024-12-21 00:00:00+00'::timestamptz) IS NULL + AS sun_no_rise_null; + sun_no_rise_null +------------------ + t +(1 row) + +SELECT sun_rise_set_status('(70.0,25.0,0)'::observer, '2024-12-21 00:00:00+00'::timestamptz) + = 'never_rises' AS status_confirms_never_rises; + status_confirms_never_rises +----------------------------- + t +(1 row) + +-- ============================================================ +-- Error cases +-- ============================================================ +-- Invalid body_id for planet_rise_set_status +DO $$ BEGIN PERFORM planet_rise_set_status(0, '(43.7,-116.4,800)'::observer, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'body_id=0: %', SQLERRM; END $$; +NOTICE: body_id=0: planet_rise_set_status: body_id 0 must be 1-8 (Mercury-Neptune) +DO $$ BEGIN PERFORM planet_rise_set_status(3, '(43.7,-116.4,800)'::observer, '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 observe Earth from Earth +DO $$ BEGIN PERFORM planet_rise_set_status(9, '(43.7,-116.4,800)'::observer, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'body_id=9: %', SQLERRM; END $$; +NOTICE: body_id=9: planet_rise_set_status: body_id 9 must be 1-8 (Mercury-Neptune) diff --git a/test/sql/v015_features.sql b/test/sql/v015_features.sql new file mode 100644 index 0000000..5d2be7a --- /dev/null +++ b/test/sql/v015_features.sql @@ -0,0 +1,109 @@ +-- v015_features.sql -- Tests for v0.15.0: constellation_full_name + rise_set_status +-- +-- Verifies the constellation full name lookup and the rise/set +-- status diagnostic functions. +CREATE EXTENSION IF NOT EXISTS pg_orrery; + +-- ============================================================ +-- constellation_full_name: known abbreviations +-- ============================================================ + +SELECT constellation_full_name('Ari') AS aries; +SELECT constellation_full_name('CMa') AS canis_major; +SELECT constellation_full_name('UMi') AS ursa_minor; +SELECT constellation_full_name('Ori') AS orion; +SELECT constellation_full_name('Cyg') AS cygnus; +SELECT constellation_full_name('Oct') AS octans; +SELECT constellation_full_name('TrA') AS tri_australe; + +-- ============================================================ +-- constellation_full_name: composability with constellation() +-- ============================================================ + +-- Chain: equatorial -> abbreviation -> full name +SELECT constellation_full_name(constellation(2.5303, 89.264)) AS polaris_full; +SELECT constellation_full_name(constellation(6.7525, -16.716)) AS sirius_full; + +-- Chain with planet equatorial +SELECT constellation_full_name( + constellation(planet_equatorial(5, '2024-01-15 12:00:00+00'::timestamptz)) +) AS jupiter_full; + +-- ============================================================ +-- constellation_full_name: NULL for invalid abbreviation +-- ============================================================ + +SELECT constellation_full_name('XYZ') IS NULL AS invalid_returns_null; +SELECT constellation_full_name('') IS NULL AS empty_returns_null; +SELECT constellation_full_name('Toolong') IS NULL AS toolong_returns_null; + +-- ============================================================ +-- constellation_full_name: all 88 are reachable (count check) +-- ============================================================ + +-- Use generate_series to count distinct full names from the +-- known constellation abbreviations via a spot check +SELECT count(DISTINCT constellation_full_name(abbr)) = 7 + AS spot_check_7_names +FROM (VALUES ('Ari'), ('CMa'), ('UMi'), ('Ori'), ('Cyg'), ('Oct'), ('TrA')) AS t(abbr); + +-- ============================================================ +-- sun_rise_set_status: mid-latitude (Eagle, Idaho) in winter +-- Sun rises and sets normally +-- ============================================================ + +SELECT sun_rise_set_status('(43.7,-116.4,800)'::observer, '2024-01-15 00:00:00+00'::timestamptz) + AS sun_status_midlat; + +-- ============================================================ +-- sun_rise_set_status: 70N in June (midnight sun) +-- ============================================================ + +SELECT sun_rise_set_status('(70.0,25.0,0)'::observer, '2024-06-21 00:00:00+00'::timestamptz) + AS sun_status_midnight_sun; + +-- ============================================================ +-- sun_rise_set_status: 70N in December (polar night) +-- ============================================================ + +SELECT sun_rise_set_status('(70.0,25.0,0)'::observer, '2024-12-21 00:00:00+00'::timestamptz) + AS sun_status_polar_night; + +-- ============================================================ +-- moon_rise_set_status: mid-latitude — Moon normally rises/sets +-- ============================================================ + +SELECT moon_rise_set_status('(43.7,-116.4,800)'::observer, '2024-01-15 00:00:00+00'::timestamptz) + AS moon_status_midlat; + +-- ============================================================ +-- planet_rise_set_status: Jupiter from mid-latitude (normal) +-- ============================================================ + +SELECT planet_rise_set_status(5, '(43.7,-116.4,800)'::observer, '2024-01-15 00:00:00+00'::timestamptz) + AS jupiter_status_midlat; + +-- ============================================================ +-- Consistency: status matches rise/set NULL contract +-- ============================================================ + +-- When sun_next_set returns NULL (circumpolar), status should say so +SELECT sun_next_set('(70.0,25.0,0)'::observer, '2024-06-21 00:00:00+00'::timestamptz) IS NULL + AS sun_no_set_null; +SELECT sun_rise_set_status('(70.0,25.0,0)'::observer, '2024-06-21 00:00:00+00'::timestamptz) + = 'circumpolar' AS status_confirms_circumpolar; + +-- When sun_next_rise returns NULL (polar night), status should say so +SELECT sun_next_rise('(70.0,25.0,0)'::observer, '2024-12-21 00:00:00+00'::timestamptz) IS NULL + AS sun_no_rise_null; +SELECT sun_rise_set_status('(70.0,25.0,0)'::observer, '2024-12-21 00:00:00+00'::timestamptz) + = 'never_rises' AS status_confirms_never_rises; + +-- ============================================================ +-- Error cases +-- ============================================================ + +-- Invalid body_id for planet_rise_set_status +DO $$ BEGIN PERFORM planet_rise_set_status(0, '(43.7,-116.4,800)'::observer, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'body_id=0: %', SQLERRM; END $$; +DO $$ BEGIN PERFORM planet_rise_set_status(3, '(43.7,-116.4,800)'::observer, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'body_id=3(Earth): %', SQLERRM; END $$; +DO $$ BEGIN PERFORM planet_rise_set_status(9, '(43.7,-116.4,800)'::observer, '2024-01-15 00:00:00+00'::timestamptz); EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'body_id=9: %', SQLERRM; END $$;