v0.15.0: constellation full name lookup, rise/set status diagnostics

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.
This commit is contained in:
Ryan Malloy 2026-02-25 19:38:52 -07:00
parent e720e0fd25
commit 501872d45d
11 changed files with 2223 additions and 14 deletions

View File

@ -1,9 +1,9 @@
# pg_orrery — A Database Orrery for PostgreSQL # pg_orrery — A Database Orrery for PostgreSQL
## What This Is ## What This Is
A database orrery — celestial mechanics types and functions for PostgreSQL. Native C extension using PGXS, 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 **Repository:** https://git.supported.systems/warehack.ing/pg_orrery
**Documentation:** https://pg-orrery.warehack.ing **Documentation:** https://pg-orrery.warehack.ing
@ -11,7 +11,7 @@ A database orrery — celestial mechanics types and functions for PostgreSQL. Na
```bash ```bash
make PG_CONFIG=/usr/bin/pg_config # Compile with PGXS make PG_CONFIG=/usr/bin/pg_config # Compile with PGXS
sudo make install PG_CONFIG=/usr/bin/pg_config # Install extension sudo make install PG_CONFIG=/usr/bin/pg_config # Install extension
make installcheck PG_CONFIG=/usr/bin/pg_config # Run 25 regression test suites make installcheck PG_CONFIG=/usr/bin/pg_config # Run 26 regression test suites
``` ```
Requires: PostgreSQL 17 development headers, GCC, Make. Requires: PostgreSQL 17 development headers, GCC, Make.
@ -27,7 +27,7 @@ Image: `git.supported.systems/warehack.ing/pg_orrery:pg17`
## Project Layout ## Project Layout
``` ```
pg_orrery.control # Extension metadata (version 0.14.0) pg_orrery.control # Extension metadata (version 0.15.0)
Makefile # PGXS build + Docker targets Makefile # PGXS build + Docker targets
sql/ sql/
pg_orrery--0.1.0.sql # v0.1.0: satellite types/functions/operators pg_orrery--0.1.0.sql # v0.1.0: satellite types/functions/operators
@ -44,6 +44,7 @@ sql/
pg_orrery--0.12.0.sql # v0.12.0: equatorial GiST, DE moon equatorial (132 objects) 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.13.0.sql # v0.13.0: nutation, make_equatorial, rise/set (141 objects)
pg_orrery--0.14.0.sql # v0.14.0: refracted rise/set, constellation ID (147 objects) pg_orrery--0.14.0.sql # v0.14.0: refracted rise/set, constellation ID (147 objects)
pg_orrery--0.15.0.sql # v0.15.0: constellation full name, rise/set status (151 objects)
pg_orrery--0.1.0--0.2.0.sql # Migration: v0.1.0 → v0.2.0 (adds solar system) pg_orrery--0.1.0--0.2.0.sql # Migration: v0.1.0 → v0.2.0 (adds solar system)
pg_orrery--0.2.0--0.3.0.sql # Migration: v0.2.0 → v0.3.0 (adds DE ephemeris) pg_orrery--0.2.0--0.3.0.sql # Migration: v0.2.0 → v0.3.0 (adds DE ephemeris)
pg_orrery--0.3.0--0.4.0.sql # Migration: v0.3.0 → v0.4.0 pg_orrery--0.3.0--0.4.0.sql # Migration: v0.3.0 → v0.4.0
@ -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.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.12.0--0.13.0.sql # Migration: v0.12.0 → v0.13.0 (nutation, make_equatorial, rise/set)
pg_orrery--0.13.0--0.14.0.sql # Migration: v0.13.0 → v0.14.0 (refracted rise/set, constellation ID) pg_orrery--0.13.0--0.14.0.sql # Migration: v0.13.0 → v0.14.0 (refracted rise/set, constellation ID)
pg_orrery--0.14.0--0.15.0.sql # Migration: v0.14.0 → v0.15.0 (constellation full name, rise/set status)
src/ src/
pg_orrery.c # PG_MODULE_MAGIC + _PG_init() (GUC registration) pg_orrery.c # PG_MODULE_MAGIC + _PG_init() (GUC registration)
types.h # All struct definitions + constants + DE body ID mapping types.h # All struct definitions + constants + DE body ID mapping
@ -110,7 +112,7 @@ src/
PROVENANCE.md # Vendoring decision, modifications, verification PROVENANCE.md # Vendoring decision, modifications, verification
LICENSE # MIT license (Bill Gray / Project Pluto) LICENSE # MIT license (Bill Gray / Project Pluto)
test/ test/
sql/ # 25 regression test suites sql/ # 26 regression test suites
expected/ # Expected output expected/ # Expected output
data/vallado_518.json # 518 Vallado test vectors (AIAA 2006-6753-Rev1) data/vallado_518.json # 518 Vallado test vectors (AIAA 2006-6753-Rev1)
docs/ docs/
@ -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) | | `orbital_elements` | 72 | Classical Keplerian elements for comets/asteroids (epoch, q, e, inc, omega, Omega, tp, H, G) |
| `equatorial` | 24 | Apparent RA (hours), Dec (degrees), distance (km) — of date | | `equatorial` | 24 | Apparent RA (hours), Dec (degrees), distance (km) — of date |
## Function Domains (147 SQL objects) ## Function Domains (151 SQL objects)
| Domain | Theory | Key Functions | Count | | 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 | | 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 (TLE) | Altitude-band approximation | `&&` (overlap), `<->` (distance) | 8 |
| GiST index (equatorial) | Spherical bounding box | `<->` (KNN ordering) | 8 | | GiST index (equatorial) | Spherical bounding box | `<->` (KNN ordering) | 8 |
| Rise/set | Bisection (60s scan) | `planet_next_rise()`, `sun_next_rise_refracted()`, `moon_next_set_refracted()` | 12 | | Rise/set | Bisection (60s scan) | `planet_next_rise()`, `sun_next_rise_refracted()`, `*_rise_set_status()` | 15 |
| Constellation | Roman (1987) CDS VI/42 | `constellation()` (equatorial + RA/Dec overloads) | 2 | | Constellation | Roman (1987) CDS VI/42 | `constellation()`, `constellation_full_name()` | 3 |
| Diagnostics | -- | `pg_orrery_ephemeris_info()` | 1 | | 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). 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 ## Testing
25 regression test suites via `make installcheck`: 26 regression test suites via `make installcheck`:
| Suite | What it tests | | 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 | | v013_features | Nutation correction, make_equatorial constructor |
| rise_set | Planet/Sun/Moon rise/set (geometric + refracted), circumpolar, polar night | | rise_set | Planet/Sun/Moon rise/set (geometric + refracted), circumpolar, polar night |
| constellation | Roman (1987) boundary lookup, known stars, solar system objects, edge cases | | constellation | Roman (1987) boundary lookup, known stars, solar system objects, edge cases |
| v015_features | constellation_full_name lookup, rise_set_status diagnostics (circumpolar/never_rises) |
### PG Version Matrix ### 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 ```bash
make test-matrix # Full matrix (PG 14-18) 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. 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 ### Local Development
```bash ```bash

View File

@ -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.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.12.0.sql sql/pg_orrery--0.11.0--0.12.0.sql \
sql/pg_orrery--0.13.0.sql sql/pg_orrery--0.12.0--0.13.0.sql \ sql/pg_orrery--0.13.0.sql sql/pg_orrery--0.12.0--0.13.0.sql \
sql/pg_orrery--0.14.0.sql sql/pg_orrery--0.13.0--0.14.0.sql sql/pg_orrery--0.14.0.sql sql/pg_orrery--0.13.0--0.14.0.sql \
sql/pg_orrery--0.15.0.sql sql/pg_orrery--0.14.0--0.15.0.sql
# Our extension C sources # Our extension C sources
OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \ OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \
@ -50,7 +51,8 @@ REGRESS = tle_parse sgp4_propagate coord_transforms pass_prediction gist_index c
aberration v011_features vallado_518 \ aberration v011_features vallado_518 \
gist_equatorial v012_features \ gist_equatorial v012_features \
v013_features rise_set \ v013_features rise_set \
constellation constellation \
v015_features
REGRESS_OPTS = --inputdir=test REGRESS_OPTS = --inputdir=test
# Pure C — no C++ runtime needed. LAPACK for OD solver (dgelss_). # Pure C — no C++ runtime needed. LAPACK for OD solver (dgelss_).

View File

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

View File

@ -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).';

1595
sql/pg_orrery--0.15.0.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@ -376,3 +376,96 @@ const roman_boundary roman_boundaries[] = {
}; };
const int roman_boundary_count = sizeof(roman_boundaries) / sizeof(roman_boundaries[0]); 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]);

View File

@ -23,4 +23,13 @@ typedef struct roman_boundary
extern const roman_boundary roman_boundaries[]; extern const roman_boundary roman_boundaries[];
extern const int roman_boundary_count; 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 */ #endif /* PG_ORRERY_CONSTELLATION_DATA_H */

View File

@ -17,6 +17,7 @@
#include "postgres.h" #include "postgres.h"
#include "fmgr.h" #include "fmgr.h"
#include "varatt.h"
#include "utils/builtins.h" #include "utils/builtins.h"
#include "types.h" #include "types.h"
@ -26,6 +27,7 @@
PG_FUNCTION_INFO_V1(constellation_from_equatorial); PG_FUNCTION_INFO_V1(constellation_from_equatorial);
PG_FUNCTION_INFO_V1(constellation_from_radec); PG_FUNCTION_INFO_V1(constellation_from_radec);
PG_FUNCTION_INFO_V1(constellation_full_name_from_abbr);
/* B1875.0 epoch as Julian date. /* B1875.0 epoch as Julian date.
* JD(B) = 2415020.31352 + (B - 1900.0) * 365.242198781 * 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)); 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();
}

View File

@ -16,6 +16,7 @@
#include "postgres.h" #include "postgres.h"
#include "fmgr.h" #include "fmgr.h"
#include "utils/timestamp.h" #include "utils/timestamp.h"
#include "utils/builtins.h"
#include "types.h" #include "types.h"
#include "astro_math.h" #include "astro_math.h"
#include "vsop87.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(planet_next_set_refracted);
PG_FUNCTION_INFO_V1(moon_next_rise_refracted); PG_FUNCTION_INFO_V1(moon_next_rise_refracted);
PG_FUNCTION_INFO_V1(moon_next_set_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 COARSE_STEP_JD (60.0 / 86400.0) /* 60 seconds */
#define BISECT_TOL_JD (0.1 / 86400.0) /* 0.1 second */ #define BISECT_TOL_JD (0.1 / 86400.0) /* 0.1 second */
@ -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 * 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)); 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));
}

View File

@ -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)

109
test/sql/v015_features.sql Normal file
View File

@ -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 $$;