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.
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. 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.
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.
Compilation
Section titled “Compilation”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 -lmBuild rules
Section titled “Build rules”# Vendored SGP4/SDP4 sources (pure C, from Bill Gray's sat_code, MIT license)SGP4_DIR = src/sgp4SGP4_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.cSGP4_OBJS = $(SGP4_SRCS:.c=.o)
# Include vendored SGP4 headers for our C sourcesPG_CPPFLAGS = -I$(SGP4_DIR)
# Pure C — no C++ runtime neededSHLIB_LINK += -lmPGXS handles the -fPIC flag and pattern rules for .c to .o compilation, so the vendored SGP4 files need no special build rules.
Header inclusion
Section titled “Header inclusion”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.
The SGP4 API surface
Section titled “The SGP4 API surface”pg_orrery 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_orrery 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_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.
TLE struct conversion
Section titled “TLE struct conversion”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 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_orrery classifies them by physical meaning and responds accordingly.
| Code | sat_code constant | Physical meaning | pg_orrery 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.
Build integration
Section titled “Build integration”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.mddocuments the upstream repository (github.com/Bill-Gray/sat_code), commit hash, the.cppto.crename 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 --initstep, no risk of missing submodule state. - Pure C build. Renaming
.cppto.celiminates theg++and-lstdc++dependencies. The entire extension compiles with a single C compiler.
Vendored files
Section titled “Vendored files”| File | Purpose |
|---|---|
sgp4.c | SGP4 near-earth propagator |
sdp4.c | SDP4 deep-space propagator |
deep.c | Lunar/solar perturbation routines for SDP4 |
common.c | Shared initialization code for SGP4/SDP4 |
basics.c | Utility functions (angle normalization, etc.) |
get_el.c | TLE parsing (parse_elements()) |
tle_out.c | TLE text reconstruction |
norad.h | Public API declarations, tle_t struct, constants |
norad_in.h | Internal constants (WGS-72 values) |
PROVENANCE.md | Upstream commit, modifications, verification notes |
LICENSE | MIT 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.