6 custom types (tle, eci_position, geodetic, topocentric, observer, pass_event), 67 SQL functions, 2 operators (&&, <->), and a GiST operator class for altitude-band indexing. Wraps Bill Gray's sat_code for SGP4/SDP4 propagation with WGS-72 constants for propagation and WGS-84 for coordinate output. All 5 regression tests pass on PG 18.
5.7 KiB
pg_orbit — PostgreSQL Extension for Orbital Mechanics
What This Is
A PostgreSQL extension that makes TLE/orbital data first-class types — the way PostGIS does for geographic data. Native C extension using PGXS, wrapping Bill Gray's sat_code SGP4/SDP4 implementation.
Build System
make # Compile with PGXS
make install # Install to PostgreSQL extensions dir
make installcheck # Run regression tests
Requires: PostgreSQL 14+ development headers (pg_config in PATH), GCC, Make.
Project Layout
pg_orbit.control # Extension metadata
Makefile # PGXS build
sql/
pg_orbit--0.1.0.sql # All type/function/operator definitions
src/
pg_orbit.c # PG_MODULE_MAGIC entry point
tle_type.c # TLE custom type (input/output/binary/accessors)
eci_type.c # ECI position type + geodetic/topocentric types
observer_type.c # Observer location type with flexible parsing
sgp4_funcs.c # sgp4_propagate(), sgp4_propagate_series()
coord_funcs.c # eci_to_geodetic(), eci_to_topocentric(), subsatellite_point()
pass_funcs.c # next_pass(), predict_passes(), pass_visible()
gist_tle.c # GiST operator class for altitude-band indexing
types.h # Shared struct definitions
lib/
sat_code/ # Bill Gray's SGP4 (MIT license, git submodule)
test/
sql/ # Regression test SQL
expected/ # Expected output
data/
vallado_518.csv # 518 verification test vectors
docs/
DESIGN.md # Architecture decisions, theory-to-code mappings
Type System
Core Types (all varlena or fixed-size, stored in tuples)
| Type | Storage | Description |
|---|---|---|
tle |
~160 bytes fixed | Parsed mean elements (not raw text) |
eci_position |
48 bytes | x,y,z + vx,vy,vz (km, km/s) in TEME |
geodetic |
24 bytes | lat, lon (radians), alt (km) above WGS-84 |
topocentric |
32 bytes | azimuth, elevation, range, range_rate |
observer |
24 bytes | lat, lon (radians), alt_m (meters) |
pass_event |
56 bytes | AOS/MAX/LOS times + max_el + AOS/LOS az |
TLE Internal Struct
Stores all parsed mean elements from the two-line format:
- epoch (Julian date, float64)
- inclination, eccentricity, RAAN, arg_perigee, mean_anomaly (radians, float64)
- mean_motion (rev/day, float64), mean_motion_dot, mean_motion_ddot
- bstar (drag coefficient, float64)
- norad_id (int32), elset_num (int32), rev_num (int32)
- classification (char), intl_designator (8 chars)
- ephemeris_type (int8)
Constant Chain of Custody
This is the most critical design constraint.
TLEs are mean elements fitted using WGS-72 constants. The elements absorb geodetic model biases — using WGS-84 constants for propagation silently corrupts position accuracy by kilometers.
Rules
- SGP4 propagation: WGS-72 constants ONLY (mu, ae, J2, J3, J4, ke)
- Coordinate output (geodetic, topocentric): Convert to WGS-84 (a=6378.137km, f=1/298.257223563)
- TEME frame: Use only 4 of 106 IAU-80 nutation terms (matching SGP4's internal model)
- Never mix: WGS-72 propagation + WGS-84 output. No other combination.
WGS-72 Constants (from Hoots & Roehrich STR#3)
#define WGS72_MU 398600.8 /* km^3/s^2 */
#define WGS72_AE 6378.135 /* km */
#define WGS72_J2 0.001082616
#define WGS72_J3 -0.00000253881
#define WGS72_J4 -0.00000165597
#define WGS72_KE 0.0743669161 /* (min)^(-1), = sqrt(mu) * 60 / ae^(3/2) */
#define WGS72_XPDOTP 1440.0 / (2.0 * M_PI) /* min/rev */
WGS-84 Constants (for output only)
#define WGS84_A 6378.137 /* km */
#define WGS84_F (1.0 / 298.257223563)
#define WGS84_E2 (WGS84_F * (2.0 - WGS84_F))
sat_code Submodule
Bill Gray's SGP4 implementation: https://github.com/Bill-Gray/sat_code
Key files we use:
sgp4.c/sgp4.h— SGP4/SDP4 propagatornorad.h— TLE struct definitions and constants
The submodule lives at lib/sat_code/. To initialize:
git submodule update --init
Integration Pattern
#include "lib/sat_code/norad.h"
// Parse TLE lines into sat_code's tle_t struct
// Call SGP4_init() once per TLE
// Call SGP4() with minutes-since-epoch for each propagation
Testing
Vallado 518 Test Vectors
The definitive SGP4 verification dataset. Each row: NORAD ID, minutes since epoch, expected x,y,z,vx,vy,vz. All 518 must pass to machine epsilon before any other work proceeds.
Regression Tests
Standard PostgreSQL make installcheck framework:
test/sql/*.sql— test queriestest/expected/*.out— expected output- Tests run against a temporary database
Test Categories
- tle_parse — TLE input/output round-trip, malformed input rejection
- sgp4_propagate — Vallado vectors, edge cases (deep space, high eccentricity)
- coord_transforms — TEME->geodetic, TEME->topocentric accuracy
- pass_prediction — Known ISS passes, edge cases (polar, retrograde)
- gist_index — Index scan vs sequential scan equivalence
Coding Style
- Standard PostgreSQL extension C style
ereport(ERROR, ...)for user-facing errors, neverelog(ERROR, ...)- All memory allocation through
palloc/pfree(PostgreSQL memory contexts) - Comments explain "why", not "what"
- No global mutable state — all computation from function arguments
- Functions that call
SGP4()must handle the error return code
Git Conventions
- One commit per logical change
- Branch per phase:
phase/1-tle-sgp4,phase/2-coordinates, etc. - Tag releases:
v0.1.0,v0.2.0, etc. - Commit messages: imperative mood, no AI attribution