Skip to content

SGP4 Integration

pg_orbit wraps Bill Gray’s sat_code library (MIT license, Project Pluto) for SGP4/SDP4 propagation. 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 linkage. All public functions are declared extern "C" in norad.h. The library compiles as C++ but exposes 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 is compiled as C++ but pg_orbit is a C extension. The integration works because sat_code’s public API is extern "C":

src/*.c --[gcc]--> .o --|
lib/sat_code/*.cpp --[g++]--> .o --|--> pg_orbit.so
-lstdc++ -lm

The Makefile compiles sat_code’s .cpp files with g++ and links them alongside pg_orbit’s .c files with -lstdc++ for the C++ runtime. This is the same pattern PostGIS uses for GEOS integration.

# sat_code C++ sources
SAT_CODE_DIR = lib/sat_code
SAT_CODE_SRCS = $(SAT_CODE_DIR)/sgp4.cpp $(SAT_CODE_DIR)/sdp4.cpp \
$(SAT_CODE_DIR)/deep.cpp $(SAT_CODE_DIR)/common.cpp \
$(SAT_CODE_DIR)/basics.cpp $(SAT_CODE_DIR)/get_el.cpp \
$(SAT_CODE_DIR)/tle_out.cpp
SAT_CODE_OBJS = $(SAT_CODE_SRCS:.cpp=.o)
# Include sat_code headers for our C sources
PG_CPPFLAGS = -I$(SAT_CODE_DIR)
# C++ runtime for sat_code
SHLIB_LINK += -lstdc++ -lm
# Compile C++ with position-independent code for shared library
$(SAT_CODE_DIR)/%.o: $(SAT_CODE_DIR)/%.cpp
$(CXX) $(CXXFLAGS) -fPIC -I$(SAT_CODE_DIR) -c -o $@ $<

The -fPIC flag is required because the compiled objects become part of a shared library (.so). Without it, the linker would reject the C++ objects.

pg_orbit’s C files include norad.h directly:

#include "norad.h" /* sat_code public API */
#include "types.h" /* pg_orbit types and WGS-72/84 constants */

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

pg_orbit 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_orbit 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_orbit 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_orbit 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_orbit classifies them by physical meaning and responds accordingly.

Codesat_code constantPhysical meaningpg_orbit 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 included as a git submodule at lib/sat_code/. This provides:

  • Pinned version. The submodule pointer records the exact commit. Upstream changes do not affect pg_orbit until the submodule is explicitly updated.
  • Clear provenance. git submodule status shows the upstream repository (github.com/Bill-Gray/sat_code) and commit hash.
  • Easy updates. git submodule update --remote pulls the latest upstream, which can then be tested against the Vallado 518 vectors before committing the update.
FilePurpose
sgp4.cppSGP4 near-earth propagator
sdp4.cppSDP4 deep-space propagator
deep.cppLunar/solar perturbation routines for SDP4
common.cppShared initialization code for SGP4/SDP4
basics.cppUtility functions (angle normalization, etc.)
get_el.cppTLE parsing (parse_elements())
tle_out.cppTLE text reconstruction
norad.hPublic API declarations, tle_t struct, constants
norad_in.hInternal constants (WGS-72 values)

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