Skip to content

SGP4 Integration

pg_orrery vendors Bill Gray’s sat_code library (MIT license, Project Pluto) for SGP4/SDP4 propagation. The relevant source files are vendored into src/sgp4/ with .cpp extensions renamed to .c --- the code contains zero C++ features and compiles as pure C99. This page covers why sat_code was chosen, how it integrates with PostgreSQL’s build and execution model, and the error handling contract between the two codebases.

Three SGP4 implementations were evaluated. The choice came down to one question: which library can run inside a PostgreSQL backend without modification?

Pure C. Despite upstream’s .cpp file extensions, the code contains zero C++ features. pg_orrery vendors the files as .c and compiles them with gcc. The public API in norad.h is a flat C function interface: SGP4_init(), SGP4(), SDP4_init(), SDP4(), parse_elements(), select_ephemeris().

No global mutable state. The propagator state lives in a caller-allocated double params[N_SAT_PARAMS] array. This maps directly to PostgreSQL’s palloc-based memory model.

Full SDP4. Includes deep-space propagation with lunar/solar perturbations for GEO, Molniya, and GPS orbits.

MIT license. Compatible with the PostgreSQL License.

Actively maintained. Used in Bill Gray’s Find_Orb production astrometry software.

sat_code’s upstream files use .cpp extensions but contain no C++ features --- no classes, templates, namespaces, exceptions, or STL. The vendored copies in src/sgp4/ are renamed to .c and compile with gcc alongside the rest of pg_orrery. There is no C/C++ boundary, no g++, and no -lstdc++.

src/*.c --[gcc]--> .o --|
src/sgp4/*.c --[gcc]--> .o --|--> pg_orrery.so
-lm
# Vendored SGP4/SDP4 sources (pure C, from Bill Gray's sat_code, MIT license)
SGP4_DIR = src/sgp4
SGP4_SRCS = $(SGP4_DIR)/sgp4.c $(SGP4_DIR)/sdp4.c \
$(SGP4_DIR)/deep.c $(SGP4_DIR)/common.c \
$(SGP4_DIR)/basics.c $(SGP4_DIR)/get_el.c \
$(SGP4_DIR)/tle_out.c
SGP4_OBJS = $(SGP4_SRCS:.c=.o)
# Include vendored SGP4 headers for our C sources
PG_CPPFLAGS = -I$(SGP4_DIR)
# Pure C — no C++ runtime needed
SHLIB_LINK += -lm

PGXS handles the -fPIC flag and pattern rules for .c to .o compilation, so the vendored SGP4 files need no special build rules.

pg_orrery’s C files include norad.h directly:

#include "norad.h" /* vendored SGP4 public API */
#include "types.h" /* pg_orrery types and WGS-72/84 constants */

The PG_CPPFLAGS = -I$(SGP4_DIR) flag makes norad.h available without a path prefix.

pg_orrery uses a small subset of sat_code’s public functions.

int select_ephemeris(const tle_t *tle);

Returns 0 for near-earth (SGP4) or 1 for deep-space (SDP4), based on the orbital period threshold of 225 minutes. Returns -1 if the mean motion or eccentricity is out of range --- an early indicator of an invalid TLE.

void SGP4_init(double *params, const tle_t *tle);
void SDP4_init(double *params, const tle_t *tle);

Compute the propagator initialization coefficients and store them in the caller-allocated params array. This is the expensive step (~5x the cost of a single propagation), so pg_orrery performs it once per TLE and reuses the params array for SRF functions that propagate the same TLE to multiple times.

int SGP4(double tsince, const tle_t *tle, const double *params,
double *pos, double *vel);
int SDP4(double tsince, const tle_t *tle, const double *params,
double *pos, double *vel);

Propagate to tsince minutes from epoch. Write position (km) and velocity (km/min) into caller-provided arrays. Return 0 on success or a negative error code.

int parse_elements(const char *line1, const char *line2, tle_t *tle);

Parse two-line element text into a tle_t struct. Returns 0 on success. pg_orrery calls this in tle_in() to validate input at storage time.

void write_elements_in_tle_format(char *obuff, const tle_t *tle);

Reconstruct text from parsed elements. Used in tle_out() for display.

pg_orrery stores TLEs in its own pg_tle struct (112 bytes, designed for PostgreSQL tuple storage). sat_code uses tle_t (a larger struct with additional fields for its own purposes). The conversion between them is a field-by-field copy with no unit conversion --- both use radians, radians/minute, and Julian dates.

static void
pg_tle_to_sat_code(const pg_tle *src, tle_t *dst)
{
memset(dst, 0, sizeof(tle_t));
dst->epoch = src->epoch;
dst->xincl = src->inclination;
dst->xnodeo = src->raan;
dst->eo = src->eccentricity;
dst->omegao = src->arg_perigee;
dst->xmo = src->mean_anomaly;
dst->xno = src->mean_motion;
dst->xndt2o = src->mean_motion_dot;
dst->xndd6o = src->mean_motion_ddot;
dst->bstar = src->bstar;
/* ... identification fields ... */
}

This conversion is duplicated in sgp4_funcs.c, coord_funcs.c, and pass_funcs.c. Each file contains its own static copy. The duplication is intentional:

  1. Each translation unit is self-contained --- no hidden coupling through shared internal functions.
  2. The functions are small (under 20 lines). Binary size increase is negligible.
  3. The compiler can inline them within each translation unit.
  4. If the helpers ever need to diverge (e.g., pass_funcs.c working in km/min while coord_funcs.c works in km/s), they can do so independently.

sat_code returns integer error codes from SGP4() and SDP4(). pg_orrery classifies them by physical meaning and responds accordingly.

Codesat_code constantPhysical meaningpg_orrery response
0---Normal propagationReturn result
-1SXPX_ERR_NEARLY_PARABOLICEccentricity 1\geq 1ereport(ERROR)
-2SXPX_ERR_NEGATIVE_MAJOR_AXISOrbit has decayedereport(ERROR)
-3SXPX_WARN_ORBIT_WITHIN_EARTHEntire orbit below surfaceereport(NOTICE), return result
-4SXPX_WARN_PERIGEE_WITHIN_EARTHPerigee below surfaceereport(NOTICE), return result
-5SXPX_ERR_NEGATIVE_XNNegative mean motionereport(ERROR)
-6SXPX_ERR_CONVERGENCE_FAILKepler equation divergedereport(ERROR)

Codes -3 and -4 are warnings, not errors. A satellite with perigee within Earth is plausible during reentry or shortly after launch --- the state vector is still mathematically valid. The NOTICE tells the user the situation is unusual; the result is still returned.

Codes -1, -2, -5, and -6 indicate the propagator model has broken down. The output position would be meaningless. These raise ereport(ERROR), which aborts the current query.

The error response changes based on the calling context:

ContextFatal error (-1, -2, -5, -6)Warning (-3, -4)
Direct propagation (sgp4_propagate)ereport(ERROR) --- abort queryereport(NOTICE) --- return result
Safe propagation (sgp4_propagate_safe)Return NULLereport(NOTICE) --- return result
Pass prediction (elevation_at_jd)Return π-\pi elevation --- continue scanIgnore --- return elevation
SRF series (sgp4_propagate_series)ereport(ERROR) --- abort seriesereport(NOTICE) --- return result

The pass prediction context is the most interesting. A TLE valid for part of a search window should not abort the entire pass search. Returning π-\pi radians (well below any physical horizon) causes the coarse scan to treat the time point as “satellite below horizon” and continue looking for passes at other times.

sat_code is vendored into src/sgp4/ --- the minimal set of source files needed for SGP4/SDP4 propagation, committed directly into the pg_orrery repository. A PROVENANCE.md file in that directory records the upstream repository, the exact commit hash, and every modification made during vendoring.

This approach provides:

  • Pinned version. The vendored commit is recorded in src/sgp4/PROVENANCE.md. Upstream changes do not affect pg_orrery until the files are explicitly re-vendored.
  • Clear provenance. PROVENANCE.md documents the upstream repository (github.com/Bill-Gray/sat_code), commit hash, the .cpp to .c rename rationale, and a line-by-line list of every modification.
  • No submodule complexity. Cloning the repository gets a complete, buildable tree. No git submodule update --init step, no risk of missing submodule state.
  • Pure C build. Renaming .cpp to .c eliminates the g++ and -lstdc++ dependencies. The entire extension compiles with a single C compiler.
FilePurpose
sgp4.cSGP4 near-earth propagator
sdp4.cSDP4 deep-space propagator
deep.cLunar/solar perturbation routines for SDP4
common.cShared initialization code for SGP4/SDP4
basics.cUtility functions (angle normalization, etc.)
get_el.cTLE parsing (parse_elements())
tle_out.cTLE text reconstruction
norad.hPublic API declarations, tle_t struct, constants
norad_in.hInternal constants (WGS-72 values)
PROVENANCE.mdUpstream commit, modifications, verification notes
LICENSEMIT license from upstream

Other sat_code files (obs_eph.cpp, sat_id.cpp, etc.) are not vendored. pg_orrery uses sat_code strictly as a propagation library, not as a satellite identification or observation planning tool.