pg_orrery/CLAUDE.md
Ryan Malloy 15a830dc40 Initial implementation of pg_orbit PostgreSQL extension
6 custom types (tle, eci_position, geodetic, topocentric, observer,
pass_event), 67 SQL functions, 2 operators (&&, <->), and a GiST
operator class for altitude-band indexing. Wraps Bill Gray's sat_code
for SGP4/SDP4 propagation with WGS-72 constants for propagation and
WGS-84 for coordinate output. All 5 regression tests pass on PG 18.
2026-02-15 17:07:07 -07:00

148 lines
5.7 KiB
Markdown

# pg_orbit — PostgreSQL Extension for Orbital Mechanics
## What This Is
A PostgreSQL extension that makes TLE/orbital data first-class types — the way PostGIS does for geographic data. Native C extension using PGXS, wrapping Bill Gray's `sat_code` SGP4/SDP4 implementation.
## Build System
```bash
make # Compile with PGXS
make install # Install to PostgreSQL extensions dir
make installcheck # Run regression tests
```
Requires: PostgreSQL 14+ development headers (`pg_config` in PATH), GCC, Make.
## Project Layout
```
pg_orbit.control # Extension metadata
Makefile # PGXS build
sql/
pg_orbit--0.1.0.sql # All type/function/operator definitions
src/
pg_orbit.c # PG_MODULE_MAGIC entry point
tle_type.c # TLE custom type (input/output/binary/accessors)
eci_type.c # ECI position type + geodetic/topocentric types
observer_type.c # Observer location type with flexible parsing
sgp4_funcs.c # sgp4_propagate(), sgp4_propagate_series()
coord_funcs.c # eci_to_geodetic(), eci_to_topocentric(), subsatellite_point()
pass_funcs.c # next_pass(), predict_passes(), pass_visible()
gist_tle.c # GiST operator class for altitude-band indexing
types.h # Shared struct definitions
lib/
sat_code/ # Bill Gray's SGP4 (MIT license, git submodule)
test/
sql/ # Regression test SQL
expected/ # Expected output
data/
vallado_518.csv # 518 verification test vectors
docs/
DESIGN.md # Architecture decisions, theory-to-code mappings
```
## Type System
### Core Types (all varlena or fixed-size, stored in tuples)
| Type | Storage | Description |
|------|---------|-------------|
| `tle` | ~160 bytes fixed | Parsed mean elements (not raw text) |
| `eci_position` | 48 bytes | x,y,z + vx,vy,vz (km, km/s) in TEME |
| `geodetic` | 24 bytes | lat, lon (radians), alt (km) above WGS-84 |
| `topocentric` | 32 bytes | azimuth, elevation, range, range_rate |
| `observer` | 24 bytes | lat, lon (radians), alt_m (meters) |
| `pass_event` | 56 bytes | AOS/MAX/LOS times + max_el + AOS/LOS az |
### TLE Internal Struct
Stores all parsed mean elements from the two-line format:
- epoch (Julian date, float64)
- inclination, eccentricity, RAAN, arg_perigee, mean_anomaly (radians, float64)
- mean_motion (rev/day, float64), mean_motion_dot, mean_motion_ddot
- bstar (drag coefficient, float64)
- norad_id (int32), elset_num (int32), rev_num (int32)
- classification (char), intl_designator (8 chars)
- ephemeris_type (int8)
## Constant Chain of Custody
**This is the most critical design constraint.**
TLEs are mean elements fitted using WGS-72 constants. The elements absorb geodetic model biases — using WGS-84 constants for propagation silently corrupts position accuracy by kilometers.
### Rules
1. **SGP4 propagation**: WGS-72 constants ONLY (mu, ae, J2, J3, J4, ke)
2. **Coordinate output** (geodetic, topocentric): Convert to WGS-84 (a=6378.137km, f=1/298.257223563)
3. **TEME frame**: Use only 4 of 106 IAU-80 nutation terms (matching SGP4's internal model)
4. **Never mix**: WGS-72 propagation + WGS-84 output. No other combination.
### WGS-72 Constants (from Hoots & Roehrich STR#3)
```c
#define WGS72_MU 398600.8 /* km^3/s^2 */
#define WGS72_AE 6378.135 /* km */
#define WGS72_J2 0.001082616
#define WGS72_J3 -0.00000253881
#define WGS72_J4 -0.00000165597
#define WGS72_KE 0.0743669161 /* (min)^(-1), = sqrt(mu) * 60 / ae^(3/2) */
#define WGS72_XPDOTP 1440.0 / (2.0 * M_PI) /* min/rev */
```
### WGS-84 Constants (for output only)
```c
#define WGS84_A 6378.137 /* km */
#define WGS84_F (1.0 / 298.257223563)
#define WGS84_E2 (WGS84_F * (2.0 - WGS84_F))
```
## sat_code Submodule
Bill Gray's SGP4 implementation: https://github.com/Bill-Gray/sat_code
Key files we use:
- `sgp4.c` / `sgp4.h` — SGP4/SDP4 propagator
- `norad.h` — TLE struct definitions and constants
The submodule lives at `lib/sat_code/`. To initialize:
```bash
git submodule update --init
```
### Integration Pattern
```c
#include "lib/sat_code/norad.h"
// Parse TLE lines into sat_code's tle_t struct
// Call SGP4_init() once per TLE
// Call SGP4() with minutes-since-epoch for each propagation
```
## Testing
### Vallado 518 Test Vectors
The definitive SGP4 verification dataset. Each row: NORAD ID, minutes since epoch, expected x,y,z,vx,vy,vz. All 518 must pass to machine epsilon before any other work proceeds.
### Regression Tests
Standard PostgreSQL `make installcheck` framework:
- `test/sql/*.sql` — test queries
- `test/expected/*.out` — expected output
- Tests run against a temporary database
### Test Categories
1. **tle_parse** — TLE input/output round-trip, malformed input rejection
2. **sgp4_propagate** — Vallado vectors, edge cases (deep space, high eccentricity)
3. **coord_transforms** — TEME->geodetic, TEME->topocentric accuracy
4. **pass_prediction** — Known ISS passes, edge cases (polar, retrograde)
5. **gist_index** — Index scan vs sequential scan equivalence
## Coding Style
- Standard PostgreSQL extension C style
- `ereport(ERROR, ...)` for user-facing errors, never `elog(ERROR, ...)`
- All memory allocation through `palloc`/`pfree` (PostgreSQL memory contexts)
- Comments explain "why", not "what"
- No global mutable state — all computation from function arguments
- Functions that call `SGP4()` must handle the error return code
## Git Conventions
- One commit per logical change
- Branch per phase: `phase/1-tle-sgp4`, `phase/2-coordinates`, etc.
- Tag releases: `v0.1.0`, `v0.2.0`, etc.
- Commit messages: imperative mood, no AI attribution