New guide: guides/catalog-management.mdx covering the full
download/merge/load pipeline with pg-orrery-catalog CLI.
Updated tracking-satellites.mdx to reference the companion tool
instead of "No TLE fetching". Added cross-reference in benchmarks
setup instructions.
Bug: inner_consistent used sma_low for footprint calculation, but
ground footprint grows with altitude. High-SMA bins (GTO, HEO)
need sma_high to compute the maximum footprint — using sma_low
caused 453 false negatives at high-latitude observers (Tromsoe).
Fix: use sma_high (not sma_low) in L1 inclination pruning.
Added regression test: GTO-debris (inc 5 deg, e=0.73) at Tromsoe
must return identical results from seqscan and index scan.
Benchmark on 65,886-object catalog (full Space-Track including
decayed): 80-92% pruning, zero false negatives across 7 query
patterns. SP-GiST beats seqscan for high-latitude observers.
Space-Track USSPACECOM catalog: 29,784 objects from full GP query.
Benchmark shows SP-GiST index reaches parity with seqscan at 30k:
- Delta: +1.6ms (14k) -> +0.9ms (20k) -> +0.0ms (30k)
- Planner voluntarily chooses Index Only Scan at this scale
- Zero heap fetches (all data served from index pages)
- 75.9% candidate pruning on 2h/10deg query
Archive includes TLEs from Space-Track, TLE API, and SatNOGS.
- types.mdx: "seven" → "seven base types + one SQL composite",
add observer_window section with field table and usage example
- operators-gist.mdx: "three operators" → "four operators", reframe
SP-GiST performance as scalability feature (honest about seqscan
being faster at 14k catalog size, index helps at 100k+)
- installation.mdx: "14 test suites" → "15 test suites", list all
suites including od_fit, spgist_tle, vallado_518
- design-principles.mdx: clarify observer_window is SQL composite
(variable-length, query-time only), base types still STORAGE=plain
- pass-prediction.mdx: lead with operator value (80-90% elimination),
SP-GiST index framed as optional for large catalogs
New docs:
- guides/pass-prediction.mdx: two-stage workflow (SP-GiST filter
then SGP4 propagation), query window comparison tabs, GiST/SP-GiST
coexistence example
- reference/operators-gist.mdx: &? operator signature and description,
observer_window type reference, SP-GiST operator class docs with
eccentricity/HEO limitation aside
Benchmarks on 14,376 CelesTrak active satellites:
- SP-GiST index: 2,344 kB, builds in 19 ms
- GiST index: 2,904 kB, builds in 45 ms
- Consistency: 0 false negatives, 0 false positives
- At 14k catalog size, seqscan (~6 ms) still beats index scan (~8 ms)
due to low page count; cross-over expected at ~100k objects
GiST: entryvec->vector[] uses 1-based indexing (FirstOffsetNumber),
not 0-based. Reading vector[0] hit uninitialized memory, causing
SIGSEGV on large catalogs (14k+ satellites). Fixed in gist_tle_union
and gist_tle_picksplit.
SP-GiST: PostgreSQL requires the indexed column as the LEFT argument
of the operator to form a ScanKey (skey.h:23-26). Flipped &? from
(observer_window, tle) to (tle, observer_window) so inner_consistent
receives scankeys for tree-level pruning.
Removed L0 altitude pruning from inner_consistent — SMA bins don't
carry eccentricity, so HEO satellites (e.g. CLUSTER II, e=0.88,
SMA ~70000 km, perigee ~2000 km) were falsely pruned. L0 now only
narrows SMA range for L1 footprint computation.
All 15 regression tests pass. Consistency check on 14,376 satellites
confirms 0 false negatives, 0 false positives.
Fix M2: clamp picksplit nBins to nTuples to prevent out-of-bounds
read on the entries array when called with a single tuple.
Fix H2: use WGS72_AE as effective bin_low for the first bin in L0
inner_consistent, preventing false negatives when objects with lower
SMA than the first label are inserted after index creation.
Fix H3: reject degenerate TLEs (mean_motion <= 0) early in the
visibility filter rather than propagating nonsensical values.
Fix L1: extract shared tle_passes_visibility_filter() to eliminate
duplicated 3-stage filter logic between leaf_consistent and the
standalone tle_visibility_possible operator.
Add boundary tests: degenerate TLE, polar observer, zero-duration
window, and post-insert index-vs-seqscan consistency check.
2-level SP-GiST index on TLE data: SMA at L0, inclination at L1, with
query-time RAAN filter via J2 secular precession. New &? operator
(observer_window &? tle) returns true when a satellite might be visible
from a ground observer during a time window.
Index prunes by altitude band, inclination+footprint vs observer
latitude, and RAAN alignment against local sidereal time. Operator
class tle_spgist_ops is opt-in (not default), coexists with existing
GiST tle_ops. Equal-population picksplit with sqrt(n) bins.
Evolved from the original KTrie custom AM proposal (preserved as
KTRIE-SPEC-ORIGINAL.md). Key design decisions: 2-level trie (SMA +
inclination) instead of 5, SP-GiST framework instead of custom AM,
query-time RAAN filter instead of trie level, propagation-aware cost
estimation via traversalValue.
New pages:
- OD function reference (tle_from_eci, tle_from_topocentric,
tle_from_angles, tle_fit_residuals)
- OD guide (ECI, topocentric, angles-only, range rate, weights,
multi-observer, covariance interpretation)
- From find_orb to SQL (OD workflow comparison)
- From Poliastro to SQL (Lambert/Kepler comparison)
Updated pages:
- Corrected stale "No orbit determination" claim
- Updated function counts and test suite counts
- Added v0.4.0-v0.6.0 upgrade paths
- Added OD to capabilities table, theory-to-code mapping,
constants/accuracy reference
- Added OD examples to Skyfield comparison and SQL Advantage
- Fixed stale version references across workflow pages
Range rate: topocentric residuals now include an optional 4th component
(dot(Δr, v_ecef) / |Δr|) with OD_RR_SCALE=10.0 for unit balancing.
Controlled via fit_range_rate parameter on tle_from_topocentric().
Weighted observations: per-observation weights applied as √w scaling
to both residuals and Jacobian rows, producing the weighted normal
equations H'WH without explicit W construction. Weights parameter
added to tle_from_eci, tle_from_topocentric, and tle_from_angles.
Gauss angles-only IOD: Vallado Algorithm 52 implementation for
seed-free orbit recovery from 3+ RA/Dec observations. New RA/Dec
residual function with cos(dec) scaling and wrap-around handling.
New tle_from_angles() and tle_from_angles_multi() SQL functions
accepting RA in hours [0,24), Dec in degrees [-90,90].
New standalone test suite: test_od_gauss (17 assertions).
New regression tests: Tests 18-25 covering range rate, weights,
angles-only with/without seed, and error cases.
Computes formal covariance (H^T·H)^{-1} via LAPACK dpotrf_/dpotri_
after DC convergence. Returns upper-triangle array (21 elements for
6-state, 28 for 7-state with B*), condition number from SVD, and
nstate count. Covariance is computed even for perfect-seed fits.
Bumps extension to v0.5.0 with full install SQL and migration path.
Eliminates the seed TLE requirement for topocentric fitting by
computing an initial orbit estimate from 3 well-spaced observations
using the Gibbs method. ECI fitting retains the single-observation
r,v approach (exact for two-body) with Gibbs as fallback.
Extend od_observation_t with observer_idx so each observation can
reference a different ground station. Config now holds an array of
observers instead of a single pointer. The existing single-observer
tle_from_topocentric() is unchanged (sets observer_idx=0 for all obs).
New overload: tle_from_topocentric(topo[], ts[], observer[], int4[], ...)
accepts parallel observer_ids array indexing into the observers array.
PG function overloading resolves by argument types.
Tests 9-11: two-station fit converges, single-station via multi-observer
API matches, out-of-range observer_id raises error.
Scale step limits by a trust-region factor that halves on divergence
(RMS increases > 1%) and relaxes toward full step on good convergence
(RMS decreases > 10%). Prevents oscillation with poor initial guesses
without affecting well-seeded fits. Also stores SVD condition number
for diagnostic use in upcoming covariance output.
Existing 8 OD regression tests + 67 standalone math tests unaffected
(adaptive_factor starts at 1.0, round-trip tests never trigger
divergence).
Batch weighted least-squares differential correction using equinoctial
elements, LAPACK dgelss_() for SVD solve, vendored SGP4/SDP4 as the
propagation engine. Per Vallado & Crawford (2008) AIAA 2008-6770.
New SQL functions:
- tle_from_eci(): fit TLE from ECI position/velocity ephemeris
- tle_from_topocentric(): fit TLE from az/el/range observations
- tle_fit_residuals(): per-observation position residuals diagnostic
Solver features: 6-state (orbital) or 7-state (+ B*) fitting,
equinoctial elements for singularity-free optimization, tiered step
limiting, Brouwer/Kozai Newton-Raphson conversion, auto initial guess
from first ECI observation when no seed TLE provided.
Tested: 8 regression tests (LEO/MEO/near-circular round-trips,
B* recovery, topocentric, seedless, error handling, diagnostics),
67 standalone math unit tests, all 14 suites pass.
Shell script drives the Dockerfile builder stage across PG versions,
capturing pass/fail + timing per version. Makefile targets: test-matrix,
test-pg%, test-matrix-clean. Also runs standalone DE reader test in the
builder stage to catch compiler-version regressions.
Fix pork chop grid test: add ORDER BY to CROSS JOIN (optimizer chooses
different join nesting across PG versions, reordering rows).
An existing product called PG Orbit (a mobile PostgreSQL client)
creates a naming conflict. pg_orrery — a database orrery built from
Keplerian parameters and SQL instead of brass gears.
Build system: control file, Makefile, Dockerfile, docker init script.
C source: GUC prefix, PG_FUNCTION_INFO_V1 symbol, header guards,
ereport prefixes, comments across ~30 files including vendored SGP4.
SQL: all 5 install/migration scripts, function name pg_orrery_ephemeris_info.
Tests: 9 SQL suites, 8 expected outputs, standalone DE reader test.
Documentation: CLAUDE.md, README.md, DESIGN.md, Starlight site infra,
36 MDX pages, OG renderer, logo SVG, docker-compose, agent threads.
All 13 regression suites pass. Docs site builds (37 pages).
Replace the sat_code git submodule (lib/sat_code/) with vendored
sources in src/sgp4/. The upstream .cpp files are renamed to .c —
the code is valid C99 with zero C++ features. This eliminates the
g++ and -lstdc++ build dependencies.
Adds 518 Vallado test vectors (AIAA 2006-6753-Rev1) as a 13th
regression suite to verify byte-identical numerical output.
Updates all documentation (CLAUDE.md, DESIGN.md, 11 MDX pages,
Dockerfile) to reflect the new layout and pure-C compilation.
Replace generic blackAndWhite preset with custom renderer using
site colors (#0a0e17 dark background, #f59e0b amber accents),
decorative orbital rings watermark, branded footer with pg_orbit
name and site URL. Inter font in 400/700 weights.
Hero tagline: "It's not rocket science. (It's celestial mechanics.
But now it's just SQL.)" across README, index.mdx, and meta description.
Add astro-icon with Lucide icon set, astro-seo-meta, and
astro-opengraph-images (blackAndWhite preset, Inter font). Override
Starlight Head component to inject og:image and twitter card tags
with auto-generated 1200x630 PNG images for all 37 pages.
Standalone test (test/test_de_reader.c): generates a synthetic 12KB
DE binary with known Chebyshev coefficients, exercises header parsing,
polynomial evaluation at 5 domain points, Earth derivation via
center=99, Moon geocentric, layout validation, range/body error paths,
NAN sentinel, and garbage file rejection. 55 tests, no PG dependency.
Makefile: add `make test-de-reader` target; also includes the earlier
sat_code C++ -> pure C sgp4 migration that was missed from prior commits.
astro_math.h: document that ecliptic_to_equatorial/equatorial_to_ecliptic
are NOT safe for aliased (in-place) calls — equ[1] is written before
equ[2] reads ecl[1]. The vendored sgp4/sdp4.c has separate in-place
versions using a temp variable.
Three independent reviews (manual, failure-mode analysis, JPL spec
cross-reference) confirmed the mathematical core is correct. This
commit addresses defensive coding and operational behavior:
- Fix header byte-offset comments (12 groups = 144 bytes, not 156)
- Add layout validation before Chebyshev interpolation (prevent
buffer underread for bodies absent from a DE edition)
- Clamp Chebyshev argument to [-1,+1] with debug assertion for
values beyond 1e-10 tolerance (catches structural normalization
errors vs normal FP boundary rounding)
- Add O_CLOEXEC to prevent FD leaks to child processes
- Change GUC from PGC_SIGHUP to PGC_BACKEND to match actual
one-shot initialization behavior
- Fix provider consistency: planet_velocity_de() now accepts a
use_de flag to match the provider used for positions (rule 7)
- Optimize eph_de_moon() to use raw geocentric Moon (center=-1)
instead of computing Earth just to subtract it back out
- Pre-compute obliquity trig constants (verified to full precision)
- Tighten canary check from 0.9-1.1 AU to 0.97-1.04 AU
- Return NAN for missing constants (0.0 was ambiguous)
- Add _Static_assert for sizeof(double) == 8
- Remove unused HDR_* macros
- Zero Datum values before setting null flags in ephemeris_info
- Replace magic numbers with DE_MOON/DE_SUN constants
All 13 regression tests pass. Zero compiler warnings.
Clean-room DE binary reader (~400 lines C) with Chebyshev/Clenshaw
evaluation — no GPL dependency on jpl_eph. Per-backend lazy
initialization preserves PARALLEL SAFE. Existing VSOP87/ELP82B
functions stay IMMUTABLE; new _de() variants are STABLE with
automatic fallback to compiled-in ephemerides on any DE failure.
Implementation:
- de_reader.c: header parse, record seek, Clenshaw recurrence
- eph_provider.c: GUC (pg_orbit.ephemeris_path), lazy init,
ICRS-to-ecliptic frame rotation, on_proc_exit cleanup
- de_funcs.c: 11 new SQL functions (_de variants + diagnostics)
- Constant chain of custody rules 6-8 (frame rotation,
same-provider, AU consistency)
Extract observe_from_geocentric() to astro_math.h for shared use
by planet_funcs.c, moon_funcs.c, and de_funcs.c.
57 → 68 functions, 11 → 12 regression test suites, all passing.
README covers all 57 functions across 9 domains with quick-start
examples, capability matrix, body ID tables, and performance benchmarks.
Links to Starlight docs site for detailed reference.
Adds Docker Compose deployment for docs site behind caddy-docker-proxy
with dev/prod modes and Vite HMR support through reverse proxy.
Reflects the full solar system expansion: 57 functions, 7 types,
11 test suites, 36 source files, body ID tables, error handling
patterns, astronomical constants, and the Starlight docs site.
Add Universal Variable Lambert solver for computing transfer orbits
between any two planets. Enables pork chop plot generation as SQL:
SELECT dep_date, arr_date, lambert_c3(3, 4, dep_date, arr_date)
FROM generate_series(...) dep CROSS JOIN generate_series(...) arr;
New functions:
- lambert_transfer(dep_body, arr_body, dep_time, arr_time) → RECORD
Returns C3 departure/arrival (km^2/s^2), v_infinity (km/s),
time of flight (days), and transfer orbit SMA (AU).
- lambert_c3(dep_body, arr_body, dep_time, arr_time) → float8
Convenience: departure C3 only, NULL on solver failure.
The solver uses Stumpff functions for unified elliptic/parabolic/hyperbolic
handling, with Newton-Raphson iteration and bisection fallback.
Each solve is sub-millisecond; PARALLEL SAFE for batch computation.
All 11 regression tests pass.
Add observation functions for 19 planetary moons across four systems:
- Galilean moons (Io, Europa, Ganymede, Callisto) via clean-room L1.2 theory
- Saturn moons (Mimas through Hyperion) via TASS 1.7
- Uranus moons (Miranda through Oberon) via GUST86
- Mars moons (Phobos, Deimos) via MarsSat
Add Jupiter decametric radio burst prediction for Radio JOVE operators:
- io_phase_angle() — Io orbital phase from superior conjunction
- jupiter_cml() — System III Central Meridian Longitude with light-time correction
- jupiter_burst_probability() — Carr et al. (1983) source regions A, B, C, D
L1.2 Galilean theory is a clean-room MIT implementation from the published
IMCCE FORTRAN coefficients. All other ephemeris libraries are MIT-licensed
extractions from Stellarium with static caching removed for PARALLEL SAFE.
All 10 regression tests pass. Extension .so grows from 2.4MB to 2.5MB.
Phase 1 — Stars, comets, Keplerian propagation:
- star_observe() / star_observe_safe(): fixed star alt/az via IAU 1976
precession, equatorial-to-horizontal transform
- kepler_propagate(): two-body Keplerian orbit propagation for
elliptic, parabolic, and hyperbolic orbits
- comet_observe(): observe comets/asteroids from orbital elements
- heliocentric type: ecliptic J2000 position (x, y, z in AU)
Phase 2 — VSOP87 planets, ELP82B Moon, Sun:
- planet_heliocentric(): VSOP87 heliocentric ecliptic J2000 positions
for Mercury through Neptune (Bretagnon & Francou, MIT)
- planet_observe(): full observation pipeline for any planet
- sun_observe(): Sun position from negated Earth VSOP87
- moon_observe(): ELP2000-82B lunar position (Chapront-Touzé, MIT)
- Clean-room precession (IAU 2006) and sidereal time (IERS 2010)
- elliptic_to_rectangular utility (Stellarium, MIT)
All Stellarium extractions are MIT-licensed, thread-safe (static
caching removed for PARALLEL SAFE), zero external data files.
All 9 regression tests pass (90ms total).
Three-stage Dockerfile: Ubuntu 22.04 builder (glibc-matched to
TimescaleDB-HA), scratch artifact image (~748KB), and standalone
postgres:17 image. All 6 regression suites run during build.
Makefile gains docker-build, docker-push, and docker-test targets.
The 1-D altitude-band index only pruned ~25% of the 22k satellite
catalog (eliminates MEO/GEO/HEO but 75% is LEO). Adding inclination
as a second indexed dimension prunes an additional ~40% of remaining
candidates — objects in equatorial or low-inclination orbits that
geometrically cannot pass over the observer's latitude.
Key changes:
- tle_alt_range (16 bytes) → tle_orbital_key (32 bytes) with
inc_low/inc_high fields
- All 8 GiST support functions updated for 2-D bounding boxes
- Penalty uses margin (half-perimeter) not area to avoid degeneracy
when leaf entries have zero-width inclination ranges
- Picksplit selects split dimension by normalized spread
- && operator now checks altitude AND inclination overlap
- <-> operator remains altitude-only (conjunction screening is
altitude-dominant)
- SQL operator comments updated for 2-D semantics
- Test adds Equatorial-LEO satellite at ISS altitude but 5° inclination
to validate inclination-based pruning
Implements 5 new C functions requested by the Craft (Astrolock) API team:
- tle_from_lines(text, text): two-argument TLE constructor
- observer_from_geodetic(float8, float8, float8): numeric observer constructor
- observe(tle, observer, timestamptz): single-call propagate + topocentric
- sgp4_propagate_safe(tle, timestamptz): returns NULL on propagation error
- observe_safe(tle, observer, timestamptz): returns NULL on propagation error
Refactors do_propagate() into safe/unsafe variants to support NULL returns.
Adds regression test (convenience.sql) covering all new functions including
an equivalence test verifying observe() matches the manual two-step pipeline.
All 6 regression tests pass.
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.