Skip to content

Design Principles

pg_orbit is engineering software that computes physical quantities. A wrong answer delivered confidently is worse than no answer at all. The design principles that govern the extension trace directly to Margaret Hamilton’s work on the Apollo guidance computer --- software that could not afford to be approximately correct.

These principles are not aspirational. They are enforced structurally in the code.

Hamilton’s most fundamental principle: design the system correctly from the start, rather than patching it after deployment. In pg_orbit, this manifests as the constant chain of custody --- the strict separation between WGS-72 constants (used for SGP4 propagation) and WGS-84 constants (used for coordinate output).

This separation was not bolted on after a bug was found. It was the first architectural decision, made before any code was written. The types.h header carries both constant sets with explicit comments about which functions may use which set.

/* WGS-72 constants (for SGP4 propagation ONLY) */
#define WGS72_MU 398600.8 /* km^3/s^2 */
#define WGS72_AE 6378.135 /* km */
/* WGS-84 constants (for coordinate output ONLY) */
#define WGS84_A 6378.137 /* km */
#define WGS84_F (1.0 / 298.257223563)

The 2-meter difference between WGS-72 and WGS-84 equatorial radii looks insignificant. It compounds through index operations, altitude computations, and conjunction screening. Getting this wrong would not produce a crash --- it would produce subtly wrong results that pass every test except comparison with an independent reference implementation.

See Constant Chain of Custody for the full treatment.

The Apollo guidance computer did not wait for failures to announce themselves. It classified errors by severity and responded proportionally. pg_orbit follows the same pattern across three mechanisms.

Every propagation function that can fail has a _safe() variant that returns NULL instead of raising a PostgreSQL ERROR. This lets callers handle failure in SQL without BEGIN/EXCEPTION blocks:

-- Raises ERROR if TLE has decayed past validity
SELECT sgp4_propagate(tle, now())
FROM catalog;

sat_code returns six distinct error codes. pg_orbit classifies them into two categories based on physical meaning:

CodeMeaningSeverityResponse
-1Nearly parabolic orbit (e1e \geq 1)Fatalereport(ERROR)
-2Negative semi-major axis (decayed)Fatalereport(ERROR)
-3Orbit within EarthWarningereport(NOTICE), return result
-4Perigee within EarthWarningereport(NOTICE), return result
-5Negative mean motionFatalereport(ERROR)
-6Kepler equation divergedFatalereport(ERROR)

The distinction between warnings and errors is physical, not numerical. A satellite with perigee below Earth’s surface is plausible during reentry --- the state vector is still mathematically valid. A negative semi-major axis means the model has broken down entirely.

TLE parsing errors are caught in tle_in(), not during propagation. Invalid TLEs never enter the database. A marginal TLE might parse correctly but fail during propagator initialization --- that failure surfaces at query time with a clear error message.

The Apollo computer had a priority scheduler that shed low-priority tasks under overload rather than crashing. pg_orbit applies a similar principle in pass prediction: failures degrade gracefully instead of aborting the scan.

When elevation_at_jd() encounters a propagation error during the coarse scan, it returns π-\pi radians --- well below any physical horizon elevation. The scan treats this as “satellite below horizon” and continues searching.

static double
elevation_at_jd(/* ... */)
{
int err = propagate_tle(&sat, tsince, pos, vel);
if (err < -2) /* hard errors: treat as below horizon */
return -M_PI;
/* ... compute actual elevation ... */
}

This matters because a TLE might be valid for the first three days of a seven-day search window and then decay past model validity. The pass finder should return the three days of valid passes, not abort the entire query.

Hamilton defined ultra-reliable software as software that behaves correctly under all possible input combinations, including combinations the designer did not anticipate. pg_orbit achieves this through four structural guarantees.

There are no file-scope variables, no static locals, no caches. Every function computes from its arguments alone. This is not a style preference --- it is required for PostgreSQL’s PARALLEL SAFE declaration. All 57 pg_orbit functions carry this declaration, meaning the query planner can distribute work across multiple CPU cores without coordination.

All seven pg_orbit types use STORAGE = plain and fixed INTERNALLENGTH. No TOAST, no detoasting, no variable-length headers. The tle type is exactly 112 bytes. Direct pointer access via PG_GETARG_POINTER(n) --- no copies, no allocations on read.

All heap allocation goes through palloc()/pfree(). No malloc(), no new, no static buffers. PostgreSQL’s memory context system owns every byte, and frees it automatically when the query completes.

Given the same TLE and timestamp, pg_orbit produces the same result on every platform, every time. No floating-point non-determinism from threading, no stale caches, no accumulated state from previous calls.

Hamilton insisted that software engineering was a real engineering discipline, not an ad hoc craft. For pg_orbit, this means every equation in the codebase traces to a published, peer-reviewed source.

The Theory-to-Code Mapping page provides the complete table. A sample:

EquationSourceCode
SGP4/SDP4 propagationHoots & Roehrich, STR#3 (1980)sat_code/sgp4.cpp, sdp4.cpp
VSOP87 planetary positionsBretagnon & Francou (1988)src/vsop87.c
GMST computationVallado (2013) Eq. 3-47src/coord_funcs.c:gmst_from_jd()
Lambert solverIzzo (2015)src/lambert.c
Precession J2000 to dateLieske et al. (1977)src/precession.c

Every constant has a provenance. Every algorithm has a citation. If a future maintainer needs to understand why 0.40909280422232897 appears in types.h, the comment says “23.4392911 degrees in radians” and the design document traces it to the IAU value for the obliquity of the ecliptic at J2000.

Hamilton’s approach to the Apollo software was holistic --- she understood that modifying one subsystem could cascade through the entire stack. pg_orbit embodies this through the observation pipeline, a seven-stage flow from heliocentric coordinates to topocentric azimuth and elevation.

  1. VSOP87 heliocentric ecliptic J2000 position for the target body (AU)
  2. VSOP87 heliocentric ecliptic J2000 position for Earth
  3. Geocentric ecliptic = target minus Earth
  4. Ecliptic-to-equatorial rotation by J2000 obliquity (23.4392911°23.4392911\degree)
  5. IAU 1976 precession from J2000 to the date of observation
  6. GMST for sidereal time (Vallado Eq. 3-47, IAU 1982)
  7. Equatorial-to-horizontal transform for the observer’s latitude and longitude

You cannot modify stage 4 without understanding what stage 3 produces and what stage 5 expects. You cannot swap the GMST model without understanding that the SGP4 output is only accurate to the precision of its own internal GMST --- applying a higher-precision rotation would not improve accuracy and could introduce systematic offsets.

See Observation Pipeline for the full flow with equations.

pg_orbit defends against three categories of unexpected input that would silently produce wrong results in a naive implementation.

What happens when someone computes a transfer from Earth to Earth?

SELECT * FROM lambert_transfer(3, 3, '2028-01-01', '2028-06-01');

The departure and arrival positions are the same body at different times. The Lambert solver would converge on a trivial solution that does not represent a physical transfer orbit. pg_orbit validates dep_body_id != arr_body_id and returns an error before invoking the solver.

SELECT * FROM lambert_transfer(3, 4, '2029-06-15', '2028-10-01');

A negative time of flight. The Lambert solver might converge on a mathematically valid but physically meaningless retrograde solution. pg_orbit checks arr_time > dep_time and returns an error.

When computing the topocentric observation of Earth (body ID 3), the geocentric vector is zero --- the observer is on the body being observed. Division by zero in the range computation. pg_orbit catches this case and returns a clear error rather than NaN or infinity propagating through the rest of the pipeline.

These are not edge cases in the traditional sense. They are the inputs that a SQL user will inevitably produce when exploring the system with ad hoc queries, and they must produce clear errors rather than silently wrong results.