pg_orrery/CLAUDE.md
Ryan Malloy 15a830dc40 Initial implementation of pg_orbit PostgreSQL extension
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.
2026-02-15 17:07:07 -07:00

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

  1. SGP4 propagation: WGS-72 constants ONLY (mu, ae, J2, J3, J4, ke)
  2. Coordinate output (geodetic, topocentric): Convert to WGS-84 (a=6378.137km, f=1/298.257223563)
  3. TEME frame: Use only 4 of 106 IAU-80 nutation terms (matching SGP4's internal model)
  4. 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 propagator
  • norad.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 queries
  • test/expected/*.out — expected output
  • Tests run against a temporary database

Test Categories

  1. tle_parse — TLE input/output round-trip, malformed input rejection
  2. sgp4_propagate — Vallado vectors, edge cases (deep space, high eccentricity)
  3. coord_transforms — TEME->geodetic, TEME->topocentric accuracy
  4. pass_prediction — Known ISS passes, edge cases (polar, retrograde)
  5. gist_index — Index scan vs sequential scan equivalence

Coding Style

  • Standard PostgreSQL extension C style
  • ereport(ERROR, ...) for user-facing errors, never elog(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