Shell script drives the Dockerfile builder stage across PG versions, capturing pass/fail + timing per version. Makefile targets: test-matrix, test-pg%, test-matrix-clean. Also runs standalone DE reader test in the builder stage to catch compiler-version regressions. Fix pork chop grid test: add ORDER BY to CROSS JOIN (optimizer chooses different join nesting across PG versions, reordering rows).
18 KiB
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, 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_orrery
Documentation: https://pg-orrery.warehack.ing
Build System
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
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.3.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: complete extension (68 functions)
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)
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(), 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_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
- SGP4 propagation: WGS-72 constants ONLY (mu, ae, J2, J3, J4, ke)
- Coordinate output (geodetic, topocentric): WGS-84 (a=6378.137km, f=1/298.257223563)
- TEME frame: Only 4 of 106 IAU-80 nutation terms (matching SGP4's internal model)
- Solar system pipeline: IAU 1976 precession, J2000 obliquity, GMST from Vallado Eq. 3-47
- Never mix: WGS-72 propagation + WGS-84 output. No other combination.
- DE frame rotation: DE positions (ICRS equatorial) pass through
equatorial_to_ecliptic()at the provider boundary before entering the observation pipeline - Same-provider rule: Both target and Earth must come from the same provider in any geocentric computation (never mix DE target with VSOP87 Earth)
- 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)
#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)
#define WGS84_A 6378.137 /* km */
#define WGS84_F (1.0 / 298.257223563)
Astronomical Constants
#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'sjpl_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 forPARALLEL SAFE. - VSOP87 fallback: Every
_de()function falls back to its VSOP87/ELP82B equivalent when DE is unavailable. - STABLE volatility: DE functions are
STABLE(notIMMUTABLE) because output depends on an external file. Existing VSOP87 functions remainIMMUTABLE.
GUC Configuration
-- 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 |
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/__stdcalldecorators - Removed
extern "C"wrapper (now compiled as C) - Removed unused SGP/SGP8/SDP8 model prototypes
- Added forward declarations (
-Wmissing-prototypes) - Changed bare
inlinetostatic inlinefor 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 |
PG Version Matrix
Test all 13 regression suites + DE reader unit test across PostgreSQL 14-18 using Docker:
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, arrival-before-departure, invalid body_id, RA out of [0,24), negative perihelion distance.
Documentation Site
Live: https://pg-orrery.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
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):
ssh -A warehack-ing@pg-orrery.warehack.ing
cd ~/pg_orrery
git pull origin phase/solar-system-expansion # or the current branch
cd docs
make prod # builds image + starts container
First-time setup on VPS:
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/solar-system-expansion
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 containersmake restart— stop + start productionmake clean— stop + remove volumesmake 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, neverelog(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 vendoreddeep.c+ 1 cache insdp4.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/solar-system-expansion - Tag releases:
v0.1.0,v0.2.0 - Commit messages: imperative mood, no AI attribution