# pg_orbit — Solar System Computation for PostgreSQL ## What This Is A PostgreSQL extension that moves orbital mechanics inside the database — the way PostGIS did for geography. Native C extension using PGXS, 68 SQL functions, 7 custom types, covering satellites (SGP4/SDP4), planets (VSOP87 + optional JPL DE441), Moon (ELP2000-82B), 19 planetary moons (L1.2/TASS17/GUST86/MarsSat), stars, comets, Jupiter radio bursts, and interplanetary Lambert transfers. **Current version:** 0.3.0 on branch `phase/solar-system-expansion` **Repository:** https://git.supported.systems/warehack.ing/pg_orbit **Documentation:** https://pg-orbit.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 13 regression test suites ``` Requires: PostgreSQL 17 development headers, GCC, Make. ### Docker ```bash make docker-build # Build standalone image (pg17 + pg_orbit) make docker-test # Smoke test the image make docker-push # Push to git.supported.systems registry ``` Image: `git.supported.systems/warehack.ing/pg_orbit:pg17` ## Project Layout ``` pg_orbit.control # Extension metadata (version 0.3.0) Makefile # PGXS build + Docker targets sql/ pg_orbit--0.1.0.sql # v0.1.0: satellite types/functions/operators pg_orbit--0.2.0.sql # v0.2.0: solar system (57 functions) pg_orbit--0.3.0.sql # v0.3.0: complete extension (68 functions) pg_orbit--0.1.0--0.2.0.sql # Migration: v0.1.0 → v0.2.0 (adds solar system) pg_orbit--0.2.0--0.3.0.sql # Migration: v0.2.0 → v0.3.0 (adds DE ephemeris) src/ pg_orbit.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(), pass_visible() gist_tle.c # GiST operator class (&&, <->) # --- 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() star_funcs.c # star_observe(), star_observe_safe() kepler_funcs.c # kepler_propagate(), comet_observe() 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 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/ # 13 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) | ## Function Domains (68 total) | Domain | Theory | Key Functions | Count | |--------|--------|---------------|-------| | Satellite | SGP4/SDP4 (Brouwer 1959) | `observe()`, `predict_passes()`, `ground_track()` | 22 | | Planets | VSOP87 (Bretagnon 1988) | `planet_observe()`, `planet_heliocentric()` | 3 | | Sun/Moon | VSOP87 + ELP2000-82B | `sun_observe()`, `moon_observe()` | 2 | | Planetary moons | L1.2, TASS17, GUST86, MarsSat | `galilean_observe()`, `saturn_moon_observe()` | 4 | | Stars | J2000 + IAU 1976 precession | `star_observe()`, `star_observe_safe()` | 2 | | Comets/asteroids | Two-body Keplerian | `kepler_propagate()`, `comet_observe()` | 2 | | 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()`, `moon_observe_de()` | 11 | | GiST index | Altitude-band approximation | `&&` (overlap), `<->` (distance) | 8 | | Diagnostics | -- | `pg_orbit_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 */ ``` ## 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_orbit.ephemeris_path = '/var/lib/postgres/de441.bin'; SELECT pg_reload_conf(); -- Check which provider is active SELECT * FROM pg_orbit_ephemeris_info(); ``` | GUC | Type | Default | Context | |-----|------|---------|---------| | `pg_orbit.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 | | `pg_orbit_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 13 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 | | 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 | | vallado_518 | 518 Vallado test vectors (AIAA 2006-6753-Rev1), per-satellite breakdown | ## 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, arrival-before-departure, invalid body_id, RA out of [0,24), negative perihelion distance. ## Documentation Site **Live:** https://pg-orbit.warehack.ing Starlight docs at `docs/` — 36 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 68 functions incl. DE variants), 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-orbit.warehack.ing cd ~/pg_orbit git pull origin phase/solar-system-expansion # or the current branch cd docs make prod # builds image + starts container ``` **First-time setup on VPS:** ```bash ssh -A warehack-ing@pg-orbit.warehack.ing git clone git@git.supported.systems:warehack.ing/pg_orbit.git cd pg_orbit && git checkout phase/solar-system-expansion cat > docs/.env << 'EOF' COMPOSE_PROJECT_NAME=pg-orbit-docs NODE_ENV=production VITE_HMR_HOST=pg-orbit.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-orbit-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_orbit) - 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/solar-system-expansion` - Tag releases: `v0.1.0`, `v0.2.0` - Commit messages: imperative mood, no AI attribution