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.
Why sat_code
Section titled “Why sat_code”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.
The canonical implementation from the STR#3 revision paper. Two problems:
- Written in C++ with heavy use of global state. The propagator coefficients live in file-scope variables, making it impossible to declare functions
PARALLEL SAFE. - License unclear for embedding in a PostgreSQL extension distributed as a shared library.
Various GitHub forks, typically C++ class hierarchies assuming an object-per-satellite lifecycle. This conflicts with PostgreSQL’s per-call execution model --- you cannot persist C++ objects across function invocations without managing their lifecycle in a memory context callback, adding complexity for no benefit.
The C/C++ boundary
Section titled “The C/C++ boundary”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++ -lmThe 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.
Build rules
Section titled “Build rules”# sat_code C++ sourcesSAT_CODE_DIR = lib/sat_codeSAT_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.cppSAT_CODE_OBJS = $(SAT_CODE_SRCS:.cpp=.o)
# Include sat_code headers for our C sourcesPG_CPPFLAGS = -I$(SAT_CODE_DIR)
# C++ runtime for sat_codeSHLIB_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.
Header inclusion
Section titled “Header inclusion”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.
The sat_code API surface
Section titled “The sat_code API surface”pg_orbit uses a small subset of sat_code’s public functions.
Initialization
Section titled “Initialization”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.
Propagation
Section titled “Propagation”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.
TLE parsing
Section titled “TLE parsing”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.
TLE struct conversion
Section titled “TLE struct conversion”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 voidpg_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:
- Each translation unit is self-contained --- no hidden coupling through shared internal functions.
- The functions are small (under 20 lines). Binary size increase is negligible.
- The compiler can inline them within each translation unit.
- If the helpers ever need to diverge (e.g.,
pass_funcs.cworking in km/min whilecoord_funcs.cworks in km/s), they can do so independently.
Error codes
Section titled “Error codes”sat_code returns integer error codes from SGP4() and SDP4(). pg_orbit classifies them by physical meaning and responds accordingly.
| Code | sat_code constant | Physical meaning | pg_orbit response |
|---|---|---|---|
| 0 | --- | Normal propagation | Return result |
| -1 | SXPX_ERR_NEARLY_PARABOLIC | Eccentricity | ereport(ERROR) |
| -2 | SXPX_ERR_NEGATIVE_MAJOR_AXIS | Orbit has decayed | ereport(ERROR) |
| -3 | SXPX_WARN_ORBIT_WITHIN_EARTH | Entire orbit below surface | ereport(NOTICE), return result |
| -4 | SXPX_WARN_PERIGEE_WITHIN_EARTH | Perigee below surface | ereport(NOTICE), return result |
| -5 | SXPX_ERR_NEGATIVE_XN | Negative mean motion | ereport(ERROR) |
| -6 | SXPX_ERR_CONVERGENCE_FAIL | Kepler equation diverged | ereport(ERROR) |
The warning/error distinction
Section titled “The warning/error distinction”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.
Context-dependent handling
Section titled “Context-dependent handling”The error response changes based on the calling context:
| Context | Fatal error (-1, -2, -5, -6) | Warning (-3, -4) |
|---|---|---|
Direct propagation (sgp4_propagate) | ereport(ERROR) --- abort query | ereport(NOTICE) --- return result |
Safe propagation (sgp4_propagate_safe) | Return NULL | ereport(NOTICE) --- return result |
Pass prediction (elevation_at_jd) | Return elevation --- continue scan | Ignore --- return elevation |
SRF series (sgp4_propagate_series) | ereport(ERROR) --- abort series | ereport(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 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.
The git submodule
Section titled “The git submodule”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 statusshows the upstream repository (github.com/Bill-Gray/sat_code) and commit hash. - Easy updates.
git submodule update --remotepulls the latest upstream, which can then be tested against the Vallado 518 vectors before committing the update.
Files used from sat_code
Section titled “Files used from sat_code”| File | Purpose |
|---|---|
sgp4.cpp | SGP4 near-earth propagator |
sdp4.cpp | SDP4 deep-space propagator |
deep.cpp | Lunar/solar perturbation routines for SDP4 |
common.cpp | Shared initialization code for SGP4/SDP4 |
basics.cpp | Utility functions (angle normalization, etc.) |
get_el.cpp | TLE parsing (parse_elements()) |
tle_out.cpp | TLE text reconstruction |
norad.h | Public API declarations, tle_t struct, constants |
norad_in.h | Internal 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.