Skip to content

Design Principles

pg_orrery 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_orrery, 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_orrery 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;

The vendored SGP4/SDP4 library returns six distinct error codes. pg_orrery 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_orrery 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_orrery achieves this through four structural guarantees.

For v0.1.0/v0.2.0 functions, there are no file-scope variables, no static locals, no caches. Every function computes from its arguments alone. The v0.3.0 DE ephemeris layer introduces per-backend static state (a file descriptor and coefficient cache in eph_provider.c), but each backend gets its own copy after fork() --- no shared state between processes. All 68 pg_orrery functions carry the PARALLEL SAFE declaration, meaning the query planner can distribute work across multiple CPU cores without coordination.

All seven pg_orrery 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_orrery 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_orrery, 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)src/sgp4/sgp4.c, sdp4.c
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_orrery 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_orrery 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_orrery 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_orrery 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_orrery 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.

The v0.3.0 DE integration introduces five new failure domains --- external file dependency, per-backend mutable state, OS-level file descriptor management, frame boundary crossings, and volatility contract management --- into a system that previously had none.

Each is handled by the principle of graceful degradation: DE functions fall back to VSOP87 on any DE-specific failure. The existing VSOP87 pipeline is the bulkhead --- always compiled in, always works, always IMMUTABLE. See the DE Ephemeris guide for details on the fallback strategy.