# pg_orbit Orbital mechanics types and functions for PostgreSQL. pg_orbit adds native SQL types for TLEs, orbital positions, ground stations, and satellite passes. It wraps Bill Gray's [sat_code](https://github.com/Bill-Gray/sat_code) (MIT) for SGP4/SDP4 propagation, provides coordinate transforms between inertial and ground-referenced frames, predicts passes over observer locations, and supports GiST-indexed conjunction screening on altitude bands. Think PostGIS, but for objects in orbit. ## Installation Requirements: - PostgreSQL 14+ development headers (`pg_config` in PATH) - GCC and Make - C++ compiler (for sat_code) ```bash git clone --recurse-submodules https://github.com/... cd pg_orbit make sudo make install ``` Then in your database: ```sql CREATE EXTENSION pg_orbit; ``` If you cloned without `--recurse-submodules`, initialize the sat_code dependency: ```bash git submodule update --init ``` ## Quick Start ```sql -- Create a table with a TLE column CREATE TABLE satellites ( norad_id int PRIMARY KEY, name text NOT NULL, tle tle NOT NULL ); -- Insert a TLE (standard two-line format, concatenated with newline) INSERT INTO satellites VALUES (25544, 'ISS', '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9002 2 25544 51.6400 208.5000 0006000 30.0000 330.0000 15.50000000400000'); -- Propagate to a point in time SELECT sgp4_propagate(tle, now()) FROM satellites WHERE norad_id = 25544; -- Subsatellite point (nadir) SELECT subsatellite_point(tle, now()) FROM satellites WHERE norad_id = 25544; -- All passes over Boulder, CO in the next 24 hours (min 10 deg elevation) SELECT sat.name, p.* FROM satellites sat, LATERAL predict_passes(sat.tle, '40.0N 105.3W 1655m'::observer, now(), now() + '24h', 10.0) p ORDER BY pass_aos_time(p); -- Conjunction screening with GiST index CREATE INDEX ON satellites USING gist (tle); SELECT a.name, b.name, tle_distance(a.tle, b.tle, now()) FROM satellites a, satellites b WHERE a.tle && b.tle AND a.norad_id < b.norad_id AND tle_distance(a.tle, b.tle, now()) < 10.0; ``` ## Types | Type | Size | Description | |------|------|-------------| | `tle` | 112 bytes | Parsed mean orbital elements (epoch, Keplerian elements, drag terms, identifiers). Stored as a fixed-size struct, not raw text. | | `eci_position` | 48 bytes | Position (km) and velocity (km/s) in the True Equator Mean Equinox (TEME) frame. | | `geodetic` | 24 bytes | Latitude/longitude (degrees) and altitude (km) on the WGS-84 ellipsoid. | | `topocentric` | 32 bytes | Azimuth, elevation (degrees), slant range (km), and range rate (km/s) relative to an observer. | | `observer` | 24 bytes | Ground station location. Accepts human-readable input: `'40.0N 105.3W 1655m'` or decimal degrees. | | `pass_event` | 48 bytes | Satellite pass with AOS/MAX/LOS times, max elevation, and AOS/LOS azimuths. | ### Input Formats **tle** -- Standard two-line format (lines joined by newline): ```sql SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9002 2 25544 51.6400 208.5000 0006000 30.0000 330.0000 15.50000000400000'::tle; ``` **observer** -- Flexible ground station input: ```sql -- Compass notation with altitude SELECT '40.0N 105.3W 1655m'::observer; -- Decimal degrees (positive East, altitude in meters) SELECT '40.0 -105.3 1655'::observer; ``` ## Functions ### TLE Accessors | Function | Returns | Description | |----------|---------|-------------| | `tle_norad_id(tle)` | `int4` | NORAD catalog number | | `tle_epoch(tle)` | `float8` | Epoch as Julian date (UTC) | | `tle_inclination(tle)` | `float8` | Inclination in degrees | | `tle_eccentricity(tle)` | `float8` | Eccentricity (dimensionless) | | `tle_raan(tle)` | `float8` | Right ascension of ascending node (degrees) | | `tle_arg_perigee(tle)` | `float8` | Argument of perigee (degrees) | | `tle_mean_anomaly(tle)` | `float8` | Mean anomaly (degrees) | | `tle_mean_motion(tle)` | `float8` | Mean motion (rev/day) | | `tle_bstar(tle)` | `float8` | B* drag coefficient (1/earth-radii) | | `tle_period(tle)` | `float8` | Orbital period (minutes) | | `tle_perigee(tle)` | `float8` | Perigee altitude (km above WGS-72 ellipsoid) | | `tle_apogee(tle)` | `float8` | Apogee altitude (km above WGS-72 ellipsoid) | | `tle_age(tle, timestamptz)` | `float8` | TLE age in days (positive = past epoch) | | `tle_intl_desig(tle)` | `text` | International designator (COSPAR ID) | ### Propagation | Function | Returns | Description | |----------|---------|-------------| | `sgp4_propagate(tle, timestamptz)` | `eci_position` | Propagate to a point in time. Uses SGP4 for near-earth, SDP4 for deep-space. | | `sgp4_propagate_series(tle, start, stop, step)` | `SETOF (t, x, y, z, vx, vy, vz)` | Time series of TEME positions at regular intervals. | | `tle_distance(tle, tle, timestamptz)` | `float8` | Euclidean distance (km) between two objects at a reference time. | ### Coordinate Transforms | Function | Returns | Description | |----------|---------|-------------| | `eci_to_geodetic(eci_position, timestamptz)` | `geodetic` | TEME to WGS-84 geodetic (lat/lon/alt). Requires time for Earth rotation. | | `eci_to_topocentric(eci_position, observer, timestamptz)` | `topocentric` | TEME to observer-relative az/el/range. | | `subsatellite_point(tle, timestamptz)` | `geodetic` | Nadir point on WGS-84 ellipsoid. Propagates internally. | | `ground_track(tle, start, stop, step)` | `SETOF (t, lat, lon, alt)` | Ground track as time series of subsatellite points. | ### ECI Accessors | Function | Returns | Description | |----------|---------|-------------| | `eci_x(eci_position)` | `float8` | X position (km, TEME) | | `eci_y(eci_position)` | `float8` | Y position (km, TEME) | | `eci_z(eci_position)` | `float8` | Z position (km, TEME) | | `eci_vx(eci_position)` | `float8` | X velocity (km/s) | | `eci_vy(eci_position)` | `float8` | Y velocity (km/s) | | `eci_vz(eci_position)` | `float8` | Z velocity (km/s) | | `eci_speed(eci_position)` | `float8` | Velocity magnitude (km/s) | | `eci_altitude(eci_position)` | `float8` | Geocentric altitude (km) | ### Topocentric Accessors | Function | Returns | Description | |----------|---------|-------------| | `topo_azimuth(topocentric)` | `float8` | Azimuth in degrees (0=N, 90=E, 180=S, 270=W) | | `topo_elevation(topocentric)` | `float8` | Elevation in degrees (0=horizon, 90=zenith) | | `topo_range(topocentric)` | `float8` | Slant range (km) | | `topo_range_rate(topocentric)` | `float8` | Range rate (km/s, positive = receding) | ### Pass Prediction | Function | Returns | Description | |----------|---------|-------------| | `next_pass(tle, observer, timestamptz)` | `pass_event` | Next pass over observer. Searches up to 7 days. | | `predict_passes(tle, observer, start, stop [, min_el])` | `SETOF pass_event` | All passes in a time window. Optional minimum elevation (degrees). | | `pass_visible(tle, observer, start, stop)` | `boolean` | True if any pass occurs in the time window. | ### Pass Event Accessors | Function | Returns | Description | |----------|---------|-------------| | `pass_aos_time(pass_event)` | `timestamptz` | Acquisition of signal time | | `pass_max_el_time(pass_event)` | `timestamptz` | Maximum elevation time | | `pass_los_time(pass_event)` | `timestamptz` | Loss of signal time | | `pass_max_elevation(pass_event)` | `float8` | Maximum elevation (degrees) | | `pass_aos_azimuth(pass_event)` | `float8` | AOS azimuth (degrees, 0=N) | | `pass_los_azimuth(pass_event)` | `float8` | LOS azimuth (degrees, 0=N) | | `pass_duration(pass_event)` | `interval` | Pass duration (LOS - AOS) | ### Operators | Operator | Operands | Description | |----------|----------|-------------| | `&&` | `tle, tle` | Altitude band overlap. Necessary (not sufficient) condition for conjunction. | | `<->` | `tle, tle` | Minimum altitude-band separation in km. Returns 0 if bands overlap. | Both operators are supported by the GiST `tle_ops` operator class for indexed scans. ## GiST Indexing The `tle_ops` operator class indexes TLEs by their altitude band (perigee to apogee). This provides fast filtering for conjunction screening: only pairs whose altitude bands overlap can possibly be close to each other. ```sql -- Create the index CREATE INDEX idx_tle_alt ON satellites USING gist (tle); -- The && operator triggers index scans EXPLAIN SELECT a.name, b.name FROM satellites a, satellites b WHERE a.tle && b.tle AND a.norad_id < b.norad_id; -- KNN ordering by altitude-band distance SELECT name, tle <-> (SELECT tle FROM satellites WHERE norad_id = 25544) AS sep FROM satellites ORDER BY tle <-> (SELECT tle FROM satellites WHERE norad_id = 25544) LIMIT 20; ``` The index reduces conjunction candidate pairs from O(n^2) to the set of objects with intersecting altitude bands, which is then refined by computing actual `tle_distance()` at a specific time. ## Geodetic Constants TLEs are mean elements fitted using WGS-72 constants. Using WGS-84 constants for propagation introduces kilometer-scale position errors because the elements absorb geodetic model biases during the fitting process. pg_orbit enforces this: - **Propagation (SGP4/SDP4):** WGS-72 constants only (mu, ae, J2, J3, J4, ke) - **Coordinate output (geodetic, topocentric):** WGS-84 (a=6378.137 km, f=1/298.257223563) These are not interchangeable. Mixing them is a silent accuracy loss. ## Building from Source ```bash # Build (requires pg_config in PATH) make # Install to PostgreSQL extension directory sudo make install # Run regression tests against a live database make installcheck ``` Override `pg_config` location if needed: ```bash make PG_CONFIG=/usr/lib/postgresql/16/bin/pg_config sudo make PG_CONFIG=/usr/lib/postgresql/16/bin/pg_config install ``` ### Project Layout ``` pg_orbit.control Extension metadata (version 0.1.0) Makefile PGXS build sql/ pg_orbit--0.1.0.sql Type, function, operator, and GiST definitions src/ pg_orbit.c PG_MODULE_MAGIC entry point tle_type.c TLE input/output/binary/accessors eci_type.c ECI position type observer_type.c Observer type with flexible parsing sgp4_funcs.c SGP4 propagation and distance coord_funcs.c Coordinate transforms (TEME/geodetic/topocentric) pass_funcs.c Pass prediction (next_pass, predict_passes) gist_tle.c GiST operator class for altitude-band indexing types.h Shared struct definitions and constants lib/ sat_code/ Bill Gray's SGP4/SDP4 (MIT, git submodule) test/ sql/ Regression test SQL expected/ Expected output data/ vallado_518.csv 518 verification test vectors (Vallado et al.) ``` ## Testing pg_orbit uses the standard PostgreSQL regression test framework. ```bash make installcheck ``` Test categories: | Suite | Coverage | |-------|----------| | `tle_parse` | TLE input/output round-trip, malformed input rejection | | `sgp4_propagate` | Vallado 518 test vectors, deep-space and high-eccentricity edge cases | | `coord_transforms` | TEME to geodetic, TEME to topocentric accuracy | | `pass_prediction` | Known ISS passes, polar and retrograde orbits | | `gist_index` | Index scan vs. sequential scan result equivalence | The Vallado 518 test vectors are the standard SGP4 verification dataset. Each row contains a NORAD ID, minutes since epoch, and expected position/velocity. All 518 must pass to machine epsilon. ## License [PostgreSQL License](LICENSE). Copyright (c) 2025, Ryan Malloy. The bundled sat_code library is separately licensed under the MIT license.