pg_orrery/CLAUDE.md
Ryan Malloy 4d64b78fb8 Add v0.19.0: sun almanac, conjunction detection, penumbral fraction, physical libration
Four new functions (184 → 188 SQL objects):
- sun_almanac_events(): merged rise/set + twilight SRF (4 threshold scans)
- planet_conjunctions(): angular separation minima via daily scan + ternary search
- satellite_penumbral_fraction(): continuous 0.0-1.0 shadow depth
- moon_physical_libration(): Meeus p. 373 Fourier corrections (tau, rho)

30 regression test suites, all passing.
2026-02-28 13:51:35 -07:00

439 lines
28 KiB
Markdown

# 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, 188 SQL objects (172 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 + event windows + sun almanac) with status diagnostics, IAU constellation identification with full name lookup (Roman 1987), twilight dawn/dusk (civil/nautical/astronomical), lunar phase (angle, illumination, name, age), planet apparent magnitude with Saturn ring correction (Mallama & Hilton 2018), solar elongation, planet phase fraction, conjunction detection, satellite eclipse prediction (conical shadow with penumbral fraction), observing night quality assessment, lunar libration (optical + physical, Meeus Ch. 53 + p. 373), and angular separation rate.
**Current version:** 0.19.0
**Repository:** https://git.supported.systems/warehack.ing/pg_orrery
**Documentation:** https://pg-orrery.warehack.ing
## Build System
```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 30 regression test suites
```
Requires: PostgreSQL 17 development headers, GCC, Make.
### Docker
```bash
make docker-build # Build standalone image (pg17 + pg_orrery)
make docker-test # Smoke test the image
make docker-push # Push to git.supported.systems registry
```
Image: `git.supported.systems/warehack.ing/pg_orrery:pg17`
## Project Layout
```
pg_orrery.control # Extension metadata (version 0.19.0)
Makefile # PGXS build + Docker targets
sql/
pg_orrery--0.1.0.sql # v0.1.0: satellite types/functions/operators
pg_orrery--0.2.0.sql # v0.2.0: solar system (57 functions)
pg_orrery--0.3.0.sql # v0.3.0: DE ephemeris (68 functions)
pg_orrery--0.4.0.sql # v0.4.0: orbit determination
pg_orrery--0.5.0.sql # v0.5.0: SP-GiST orbital trie
pg_orrery--0.6.0.sql # v0.6.0: conjunction screening
pg_orrery--0.7.0.sql # v0.7.0: GiST improvements
pg_orrery--0.8.0.sql # v0.8.0: orbital_elements type + MPC parser (82 functions)
pg_orrery--0.9.0.sql # v0.9.0: equatorial type, refraction, proper motion, light-time (106 functions)
pg_orrery--0.10.0.sql # v0.10.0: angular separation, cone search, apparent functions (114 functions)
pg_orrery--0.11.0.sql # v0.11.0: orbital_elements constructors, moon equatorial (120 functions)
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.16.0.sql # v0.16.0: twilight, lunar phase, planet magnitude (162 objects)
pg_orrery--0.17.0.sql # v0.17.0: elongation, phase, eclipse, night quality, libration (174 objects)
pg_orrery--0.18.0.sql # v0.18.0: ring tilt, penumbral eclipse, rise/set windows, angular rate (184 objects)
pg_orrery--0.19.0.sql # v0.19.0: sun almanac, conjunctions, penumbral fraction, physical libration (188 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
pg_orrery--0.4.0--0.5.0.sql # Migration: v0.4.0 → v0.5.0
pg_orrery--0.5.0--0.6.0.sql # Migration: v0.5.0 → v0.6.0
pg_orrery--0.6.0--0.7.0.sql # Migration: v0.6.0 → v0.7.0
pg_orrery--0.7.0--0.8.0.sql # Migration: v0.7.0 → v0.8.0 (orbital_elements type)
pg_orrery--0.8.0--0.9.0.sql # Migration: v0.8.0 → v0.9.0 (equatorial, refraction, proper motion, light-time)
pg_orrery--0.9.0--0.10.0.sql # Migration: v0.9.0 → v0.10.0 (angular separation, cone search)
pg_orrery--0.10.0--0.11.0.sql # Migration: v0.10.0 → v0.11.0 (constructors, 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.13.0--0.14.0.sql # Migration: v0.13.0 → v0.14.0 (refracted rise/set, constellation ID)
pg_orrery--0.14.0--0.15.0.sql # Migration: v0.14.0 → v0.15.0 (constellation full name, rise/set status)
pg_orrery--0.15.0--0.16.0.sql # Migration: v0.15.0 → v0.16.0 (twilight, lunar phase, planet magnitude)
pg_orrery--0.16.0--0.17.0.sql # Migration: v0.16.0 → v0.17.0 (elongation, phase, eclipse, night quality, libration)
pg_orrery--0.17.0--0.18.0.sql # Migration: v0.17.0 → v0.18.0 (ring tilt, penumbral eclipse, rise/set windows, angular rate)
pg_orrery--0.18.0--0.19.0.sql # Migration: v0.18.0 → v0.19.0 (sun almanac, conjunctions, penumbral fraction, physical libration)
src/
pg_orrery.c # PG_MODULE_MAGIC + _PG_init() (GUC registration)
types.h # All struct definitions + constants + DE body ID mapping
astro_math.h # Shared astronomical helpers + observe_from_geocentric()
# --- Satellite (v0.1.0) ---
tle_type.c # TLE custom type (I/O, binary, 15 accessors)
eci_type.c # ECI position type + geodetic/topocentric types
observer_type.c # Observer type with flexible string parsing
sgp4_funcs.c # sgp4_propagate(), _safe(), _series(), tle_distance()
coord_funcs.c # eci_to_geodetic(), eci_to_topocentric(), ground_track()
pass_funcs.c # next_pass(), predict_passes(), predict_passes_refracted(), pass_visible()
gist_tle.c # GiST operator class for TLE (&&, <->)
gist_equatorial.c # GiST operator class for equatorial (KNN <->)
# --- Solar System (v0.2.0) ---
vsop87.c / vsop87.h # VSOP87 planetary ephemeris (Bretagnon 1988)
elp82b.c / elp82b.h # ELP2000-82B lunar ephemeris (Chapront 1988)
precession.c / precession.h # IAU 1976 precession (Lieske 1979)
sidereal_time.c / .h # GMST calculation (Vallado Eq. 3-47)
elliptic_to_rectangular.c/.h # Orbital element conversions
planet_funcs.c # planet_observe(), planet_heliocentric(), sun/moon_observe(), _equatorial(), _apparent()
star_funcs.c # star_observe(), star_observe_safe(), star_equatorial(), star_observe_pm(), star_equatorial_pm()
kepler_funcs.c # kepler_propagate(), comet_observe()
kepler.h # Shared Kepler solver interface (kepler_position())
orbital_elements_type.c # orbital_elements type, MPC parser, small_body_observe/equatorial/apparent()
equatorial_funcs.c # equatorial type I/O, accessors, satellite/planet/sun/moon RA/Dec, angular rate, conjunction detection
refraction_funcs.c # atmospheric_refraction(), _ext(), topo_elevation_apparent()
rise_set_funcs.c # planet/sun/moon rise/set (geometric + refracted) + twilight dawn/dusk + event window SRFs + sun almanac
constellation_data.h / .c # Roman (1987) IAU boundary table (CDS VI/42, 357 segments)
constellation_funcs.c # constellation() from equatorial or RA/Dec
lunar_phase_funcs.c # moon_phase_angle(), moon_illumination(), moon_phase_name(), moon_age()
magnitude_funcs.c # planet_magnitude() (with Saturn ring correction), solar_elongation(), planet_phase(), saturn_ring_tilt()
eclipse_funcs.c # satellite eclipse prediction (conical shadow with penumbral fraction, Vallado §5.3)
libration.h / libration_funcs.c # lunar libration (optical Meeus Ch. 53 + physical p. 373)
l12.c / l12.h # L1.2 Galilean moon theory (Lieske 1998)
tass17.c / tass17.h # TASS 1.7 Saturn moon theory (Vienne & Duriez 1995)
gust86.c / gust86.h # GUST86 Uranus moon theory (Laskar & Jacobson 1987)
marssat.c / marssat.h # MarsSat Mars moon theory (Jacobson 2014)
moon_funcs.c # galilean/saturn/uranus/mars_moon_observe()
radio_funcs.c # io_phase_angle(), jupiter_cml(), burst_probability()
lambert.c / lambert.h # Lambert transfer solver (Izzo 2015)
transfer_funcs.c # lambert_transfer(), lambert_c3()
# --- JPL DE Ephemeris (v0.3.0) ---
de_reader.h / de_reader.c # Clean-room JPL DE binary reader (Chebyshev/Clenshaw)
eph_provider.h / eph_provider.c # Provider dispatch, GUC, lazy init, frame rotation
de_funcs.c # All _de() SQL function implementations (incl. moon equatorial DE)
sgp4/ # Vendored SGP4/SDP4 (Bill Gray's sat_code, MIT license)
sgp4.c # Near-earth propagator (period < 225 min)
sdp4.c # Deep-space propagator (period >= 225 min)
deep.c # Lunar/solar perturbations, resonance integration
common.c # Shared initialization (Brouwer mean elements, Kepler solver)
basics.c # select_ephemeris(), FMod2p()
get_el.c # TLE parsing (parse_elements(), checksum)
tle_out.c # TLE output (write_elements_in_tle_format())
norad.h / norad_in.h # Public + internal headers
PROVENANCE.md # Vendoring decision, modifications, verification
LICENSE # MIT license (Bill Gray / Project Pluto)
test/
sql/ # 30 regression test suites
expected/ # Expected output
data/vallado_518.json # 518 Vallado test vectors (AIAA 2006-6753-Rev1)
docs/
DESIGN.md # Architecture decisions, theory-to-code mappings
Dockerfile # Starlight docs site (Astro + Caddy)
package.json # Docs site dependencies
astro.config.mjs # Starlight configuration
src/content/docs/ # MDX documentation pages
```
## Type System
All types are fixed-size, `STORAGE = plain`, `ALIGNMENT = double`. No TOAST overhead.
| Type | Bytes | Description |
|------|-------|-------------|
| `tle` | 112 | Parsed mean orbital elements for SGP4/SDP4 |
| `eci_position` | 48 | x,y,z + vx,vy,vz (km, km/s) in TEME frame |
| `geodetic` | 24 | lat, lon (radians), alt (km) above WGS-84 |
| `topocentric` | 32 | azimuth, elevation, range, range_rate |
| `observer` | 24 | lat, lon (radians), alt_m (meters) |
| `pass_event` | 48 | AOS/MAX/LOS times + max_el + AOS/LOS azimuth |
| `heliocentric` | 24 | x, y, z in AU (ecliptic J2000 frame) |
| `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 (188 SQL objects)
| Domain | Theory | Key Functions | Count |
|--------|--------|---------------|-------|
| Satellite | SGP4/SDP4 (Brouwer 1959) | `observe()`, `predict_passes()`, `eci_to_equatorial()` | 25 |
| Planets | VSOP87 (Bretagnon 1988) | `planet_observe()`, `planet_equatorial()`, `planet_observe_apparent()` | 7 |
| Sun/Moon | VSOP87 + ELP2000-82B | `sun_observe()`, `moon_observe()`, `sun/moon_equatorial()` | 6 |
| Planetary moons | L1.2, TASS17, GUST86, MarsSat | `galilean_observe()`, `saturn_moon_observe()`, `*_equatorial()` | 12 |
| Stars | J2000 + IAU 1976 precession | `star_observe()`, `star_equatorial()`, `star_observe_pm()` | 5 |
| Comets/asteroids | Two-body Keplerian + MPC | `small_body_observe()`, `small_body_equatorial()`, `oe_from_mpc()` | 19 |
| Refraction | Bennett (1982) | `atmospheric_refraction()`, `predict_passes_refracted()` | 4 |
| Equatorial spatial | Vincenty formula | `eq_angular_distance()`, `eq_within_cone()`, `eq_angular_rate()`, `<->` | 4 |
| Conjunction detection | VSOP87/ELP2000-82B + ternary search | `planet_conjunctions()` | 1 |
| Jupiter radio | Carr et al. (1983) | `jupiter_burst_probability()` | 3 |
| Transfers | Lambert (Izzo 2015) | `lambert_transfer()`, `lambert_c3()` | 2 |
| 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()`, `*_rise_set_status()`, `*_rise_set_events()`, `sun_almanac_events()` | 19 |
| Twilight | Sun depression angles | `sun_civil_dawn()`, `sun_nautical_dusk()`, `sun_astronomical_dawn()` | 6 |
| Lunar phase | VSOP87 + ELP2000-82B geometry | `moon_phase_angle()`, `moon_illumination()`, `moon_phase_name()`, `moon_age()` | 4 |
| Planet magnitude | Mallama & Hilton (2018) | `planet_magnitude()`, `saturn_ring_tilt()` | 2 |
| Solar elongation | VSOP87 geometry | `solar_elongation()` | 1 |
| Planet phase | VSOP87 geometry | `planet_phase()` | 1 |
| Satellite eclipse | Conical shadow (Vallado §5.3) | `satellite_is_eclipsed()`, `satellite_next_eclipse_entry()`, `satellite_shadow_state()`, `satellite_penumbral_fraction()` | 9 |
| Observing quality | Composite (twilight+Moon) | `observing_night_quality()` | 1 |
| Lunar libration | Meeus (1998) Ch. 53 + p. 373 | `moon_libration_longitude()`, `moon_libration()`, `moon_subsolar_longitude()`, `moon_physical_libration()` | 6 |
| 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).
## Body IDs
### Planets (VSOP87 convention)
| ID | Body | ID | Body |
|----|------|----|------|
| 0 | Sun | 5 | Jupiter |
| 1 | Mercury | 6 | Saturn |
| 2 | Venus | 7 | Uranus |
| 3 | Earth | 8 | Neptune |
| 4 | Mars | 10 | Moon |
### Planetary Moons (per-family indexing)
- **Galilean** (0-3): Io, Europa, Ganymede, Callisto
- **Saturn** (0-7): Mimas, Enceladus, Tethys, Dione, Rhea, Titan, Iapetus, Hyperion
- **Uranus** (0-4): Miranda, Ariel, Umbriel, Titania, Oberon
- **Mars** (0-1): Phobos, Deimos
## Constant Chain of Custody
**The most critical design constraint.** TLEs absorb geodetic model biases — using wrong constants silently corrupts positions by kilometers.
### Rules
1. **SGP4 propagation**: WGS-72 constants ONLY (mu, ae, J2, J3, J4, ke)
2. **Coordinate output** (geodetic, topocentric): WGS-84 (a=6378.137km, f=1/298.257223563)
3. **TEME frame**: Only 4 of 106 IAU-80 nutation terms (matching SGP4's internal model)
4. **Solar system pipeline**: IAU 1976 precession, J2000 obliquity, GMST from Vallado Eq. 3-47
5. **Never mix**: WGS-72 propagation + WGS-84 output. No other combination.
6. **DE frame rotation**: DE positions (ICRS equatorial) pass through `equatorial_to_ecliptic()` at the provider boundary before entering the observation pipeline
7. **Same-provider rule**: Both target and Earth must come from the same provider in any geocentric computation (never mix DE target with VSOP87 Earth)
8. **DE AU consistency**: Verify DE header AU matches compiled-in `AU_KM` (149597870.7) at init time
### WGS-72 Constants (from Hoots & Roehrich STR#3, propagation only)
```c
#define WGS72_MU 398600.8 /* km^3/s^2 */
#define WGS72_AE 6378.135 /* km */
#define WGS72_J2 0.001082616
#define WGS72_KE 0.0743669161331734132 /* (min)^(-1) */
```
### WGS-84 Constants (coordinate output only)
```c
#define WGS84_A 6378.137 /* km */
#define WGS84_F (1.0 / 298.257223563)
```
### Astronomical Constants
```c
#define AU_KM 149597870.7 /* IAU 2012 */
#define GAUSS_K 0.01720209895 /* AU^(3/2)/day */
#define OBLIQUITY_J2000 0.40909280422232897 /* 23.4392911 deg in radians */
#define J2000_JD 2451545.0 /* 2000 Jan 1.5 TT */
#define C_LIGHT_AU_DAY 173.1446327 /* speed of light in AU/day */
```
## JPL DE Ephemeris (Optional)
v0.3.0 adds optional JPL DE440/441 ephemeris support (~0.1 milliarcsecond accuracy) alongside the existing VSOP87 pipeline (~1 arcsecond). DE functions are separate `_de()` variants — existing VSOP87 functions are completely unchanged.
### Architecture
- **Clean-room DE reader** (`de_reader.c`): ~250 lines of C. Parses the JPL binary format, evaluates Chebyshev polynomials via Clenshaw recurrence. No GPL dependency (avoids Bill Gray's `jpl_eph`).
- **Per-backend lazy init**: Each PostgreSQL backend opens its own file descriptor on first `_de()` call. Never opens in `_PG_init()` (postmaster context). Safe for `PARALLEL SAFE`.
- **VSOP87 fallback**: Every `_de()` function falls back to its VSOP87/ELP82B equivalent when DE is unavailable.
- **STABLE volatility**: DE functions are `STABLE` (not `IMMUTABLE`) because output depends on an external file. Existing VSOP87 functions remain `IMMUTABLE`.
### GUC Configuration
```sql
-- Set the path to a JPL DE binary file (requires superuser)
ALTER SYSTEM SET pg_orrery.ephemeris_path = '/var/lib/postgres/de441.bin';
SELECT pg_reload_conf();
-- Check which provider is active
SELECT * FROM pg_orrery_ephemeris_info();
```
| GUC | Type | Default | Context |
|-----|------|---------|---------|
| `pg_orrery.ephemeris_path` | string | `''` (empty = VSOP87 only) | `SIGHUP` (superuser only) |
### DE Function Variants
Every `_de()` function mirrors an existing VSOP87 function:
| DE Function | VSOP87 Equivalent | Volatility |
|-------------|-------------------|------------|
| `planet_heliocentric_de()` | `planet_heliocentric()` | STABLE |
| `planet_observe_de()` | `planet_observe()` | STABLE |
| `sun_observe_de()` | `sun_observe()` | STABLE |
| `moon_observe_de()` | `moon_observe()` | STABLE |
| `lambert_transfer_de()` | `lambert_transfer()` | STABLE |
| `lambert_c3_de()` | `lambert_c3()` | STABLE |
| `galilean_observe_de()` | `galilean_observe()` | STABLE |
| `saturn_moon_observe_de()` | `saturn_moon_observe()` | STABLE |
| `uranus_moon_observe_de()` | `uranus_moon_observe()` | STABLE |
| `mars_moon_observe_de()` | `mars_moon_observe()` | STABLE |
| `planet_equatorial_de()` | `planet_equatorial()` | STABLE |
| `moon_equatorial_de()` | `moon_equatorial()` | STABLE |
| `galilean_equatorial_de()` | `galilean_equatorial()` | STABLE |
| `saturn_moon_equatorial_de()` | `saturn_moon_equatorial()` | STABLE |
| `uranus_moon_equatorial_de()` | `uranus_moon_equatorial()` | STABLE |
| `mars_moon_equatorial_de()` | `mars_moon_equatorial()` | STABLE |
| `planet_observe_apparent_de()` | `planet_observe_apparent()` | STABLE |
| `sun_observe_apparent_de()` | `sun_observe_apparent()` | STABLE |
| `moon_observe_apparent_de()` | `moon_observe_apparent()` | STABLE |
| `planet_equatorial_apparent_de()` | `planet_equatorial_apparent()` | STABLE |
| `moon_equatorial_apparent_de()` | `moon_equatorial_apparent()` | STABLE |
| `small_body_observe_apparent_de()` | `small_body_observe_apparent()` | STABLE |
| `pg_orrery_ephemeris_info()` | — | STABLE |
## Vendored SGP4/SDP4
Bill Gray's sat_code (MIT license): https://github.com/Bill-Gray/sat_code
Vendored into `src/sgp4/` from upstream commit `ff7b98957dfa2979700a482bde9de9542807293e`. The `.cpp` files were renamed to `.c` — the code is valid C99 with zero C++ features (no classes, templates, namespaces, exceptions, or STL). This eliminates the `g++` and `-lstdc++` dependencies.
Modifications from upstream are minimal and documented in `src/sgp4/PROVENANCE.md`:
- Renamed `.cpp``.c` (no code changes — already valid C99)
- Stripped Win32 `DLL_FUNC`/`__stdcall` decorators
- Removed `extern "C"` wrapper (now compiled as C)
- Removed unused SGP/SGP8/SDP8 model prototypes
- Added forward declarations (`-Wmissing-prototypes`)
- Changed bare `inline` to `static inline` for C99 compliance
- Added equation citations referencing STR#3 and Vallado Rev-1
All numerical logic is byte-identical to upstream. Verified against 518 Vallado test vectors (AIAA 2006-6753-Rev1).
## Testing
30 regression test suites via `make installcheck`:
| Suite | What it tests |
|-------|--------------|
| tle_parse | TLE I/O round-trip, malformed input rejection, all 15 accessors |
| sgp4_propagate | SGP4/SDP4, propagation series, tle_distance |
| coord_transforms | TEME-to-geodetic, TEME-to-topocentric, ground_track |
| pass_prediction | predict_passes, next_pass, pass_visible, min elevation filter |
| gist_index | `&&` overlap, `<->` distance, GiST index scan, KNN ordering (TLE) |
| convenience | observe(), observe_safe(), tle_from_lines(), observer_from_geodetic() |
| star_observe | Star observation, IAU 1976 precession, heliocentric type I/O |
| kepler_comet | Keplerian propagation (elliptic/parabolic/hyperbolic), comet_observe |
| planet_observe | VSOP87 planets, sun_observe, moon_observe (ELP2000-82B) |
| moon_observe | Galilean/Saturn/Uranus/Mars moons, Io phase, Jupiter CML, burst probability |
| lambert_transfer | Lambert solver, lambert_c3, pork chop grid, error handling |
| de_ephemeris | DE function fallback to VSOP87, cross-provider consistency, error handling |
| od_fit | Orbit determination from ECI/topocentric/angles-only observations |
| spgist_tle | SP-GiST orbital trie index operations |
| orbital_elements | orbital_elements type I/O, MPC parser, small_body_observe/heliocentric |
| equatorial | equatorial type I/O, RA/Dec for planets/stars/satellites, proper motion, light-time |
| refraction | Bennett refraction, P/T correction, apparent elevation, refracted pass prediction |
| aberration | Annual aberration magnitude, DE apparent fallback, angular distance, cone search, stellar parallax |
| vallado_518 | 518 Vallado test vectors (AIAA 2006-6753-Rev1), per-satellite breakdown |
| v011_features | make_orbital_elements constructors, moon equatorial functions |
| gist_equatorial | Equatorial GiST KNN ordering, RA wrapping, cone search, EXPLAIN index scan |
| v012_features | DE moon equatorial fallback to VSOP87, invalid body_id rejection |
| 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) |
| v016_features | Twilight ordering/offset/polar, lunar phase at known events, planet magnitude ranges/errors |
| v017_features | Solar elongation ranges/errors, planet phase ranges, satellite eclipse, observing night quality, lunar libration ranges, subsolar longitude |
| v018_features | Saturn ring tilt range/variation, penumbral eclipse (shadow state, penumbra precedes umbra), rise/set event windows (Sun/Moon/planet, refracted vs geometric), angular separation rate (generic + planet convenience) |
| v019_features | Sun almanac events (count/order/types/polar/refraction/window guard), conjunction detection (Jupiter-Saturn 2020, Moon-Venus, same-body error, threshold filter), penumbral fraction (range/bounds/eclipse consistency), physical libration (small corrections, time variation, total libration range) |
### PG Version Matrix
Test all 30 regression suites + DE reader unit test across PostgreSQL 14-18 using Docker:
```bash
make test-matrix # Full matrix (PG 14-18)
make test-pg18 # Single version
PG_TEST_VERSIONS="16 17" make test-matrix # Subset
make test-matrix-clean # Remove logs + test images
```
Logs saved to `test/matrix-logs/pg${ver}.log`. The script reuses the Dockerfile `builder` stage as the test engine — no additional test infrastructure.
**Adding a new PG version:** Update `PG_TEST_VERSIONS` default in `Makefile` and `PG_VERSIONS` default in `test/pg-version-matrix.sh`.
## Error Handling Patterns
- `_safe()` variants (`sgp4_propagate_safe`, `observe_safe`, `star_observe_safe`) return NULL on error instead of raising exceptions. Use these for batch queries over potentially invalid data.
- SGP4 error codes: -1 (nearly parabolic), -2 (negative semi-major axis/decayed), -3/-4 (orbit within Earth, returns with NOTICE), -5 (negative mean motion), -6 (convergence failure)
- Pass prediction: propagation failures return -pi elevation (below horizon), shedding the failed timestep without aborting the scan.
- Input validation: same-body Lambert check, same-body conjunction check, arrival-before-departure, invalid body_id, RA out of [0,24), negative perihelion distance, almanac/conjunction window overflow.
## Documentation Site
**Live:** https://pg-orrery.warehack.ing
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 188 SQL objects incl. DE variants, equatorial GiST, refraction, rise/set, sun almanac, conjunction detection, constellation, twilight, lunar phase, planet magnitude, Saturn ring tilt, solar elongation, planet phase, satellite eclipse with penumbral fraction, observing quality, lunar libration with physical corrections, angular separation rate), Architecture (Hamilton's principles, constant custody, observation pipeline), Performance (benchmarks).
### Local Development
```bash
cd docs && npm run dev # Dev server on :4321
cd docs && npm run build # Static build to dist/
```
### Production Deployment
The docs site deploys to the `warehack.ing` VPS (`149.28.126.25`) which runs caddy-docker-proxy with wildcard DNS for `*.warehack.ing`.
**Deploy (or redeploy after changes):**
```bash
ssh -A warehack-ing@pg-orrery.warehack.ing
cd ~/pg_orrery
git pull origin phase/spgist-orbital-trie # or the current branch
cd docs
make prod # builds image + starts container
```
**First-time setup on VPS:**
```bash
ssh -A warehack-ing@pg-orrery.warehack.ing
git clone git@git.supported.systems:warehack.ing/pg_orrery.git
cd pg_orrery && git checkout phase/spgist-orbital-trie
cat > docs/.env << 'EOF'
COMPOSE_PROJECT_NAME=pg-orrery-docs
NODE_ENV=production
VITE_HMR_HOST=pg-orrery.warehack.ing
EOF
cd docs && make prod
```
**Makefile targets:**
- `make prod` — build + start production (Caddy serves static files)
- `make dev` — build + start dev mode (hot-reload, volume mounts)
- `make down` — stop containers
- `make restart` — stop + start production
- `make clean` — stop + remove volumes
- `make logs` — tail container logs
**Infrastructure:** Container `pg-orrery-docs` joins external `caddy` network. caddy-docker-proxy reads labels to auto-configure reverse proxy + TLS (Let's Encrypt via Vultr DNS challenge). TLS cert provisioning takes ~2 minutes on first deploy.
**Do NOT run the docs container locally** if also deployed on the VPS — competing ACME DNS challenges will corrupt each other's TXT records.
## Coding Style
- Standard PostgreSQL extension C style
- `ereport(ERROR, ...)` for user-facing errors, never `elog(ERROR, ...)`
- All memory via `palloc`/`pfree` (PostgreSQL memory contexts)
- Comments explain "why", not "what"
- No global mutable state — all computation from function arguments (exceptions: per-backend DE handle via `on_proc_exit`; 3 statics in vendored `deep.c` + 1 cache in `sdp4.c`, safe in PostgreSQL's fork model, never modified by pg_orrery)
- Every function handling SGP4 must check the error return code
- All functions marked `PARALLEL SAFE`
- DE functions: always fall back to VSOP87/ELP82B on any error
## Git Conventions
- One commit per logical change
- Branch per phase: `phase/spgist-orbital-trie`
- Tag releases: `v0.1.0`, `v0.2.0`, `v0.3.0`
- Commit messages: imperative mood, no AI attribution