Building TLE Catalogs
Every pg_orrery workflow starts with TLEs in a table. The Tracking Satellites guide shows how to insert a few satellites by hand --- but a real catalog has tens of thousands of objects from multiple sources, each with different freshness and coverage. pg-orrery-catalog handles the download, merge, and load pipeline.
The problem with multiple TLE sources
Section titled “The problem with multiple TLE sources”Three major sources provide TLE data, each with trade-offs:
| Source | Auth | Coverage | Freshness |
|---|---|---|---|
| Space-Track | Login required | Full catalog (~30k+ on-orbit) | Hours to days |
| CelesTrak | None | Active sats + operator supplemental GP | Minutes to hours |
| SatNOGS | None | Community-tracked objects | Varies |
The same satellite often appears in all three. CelesTrak’s supplemental GP (SupGP) data is particularly valuable --- operators like SpaceX submit Starlink ephemerides that are often hours fresher than Space-Track’s own catalog.
The question is which entry to keep. pg-orrery-catalog answers with epoch-based deduplication: when the same NORAD ID appears in multiple sources, the entry with the newest epoch wins. This means SupGP data automatically overrides stale Space-Track entries where available.
Install
Section titled “Install”# Run directly (no install needed)uvx pg-orrery-catalog --help
# Or install permanentlyuv pip install pg-orrery-catalog
# For direct database loading (adds psycopg)uv pip install "pg-orrery-catalog[pg]"Download, build, load
Section titled “Download, build, load”The typical workflow is three steps. Each can run independently.
-
Download TLE data from remote sources into the local cache:
Terminal window pg-orrery-catalog downloadThis fetches from all configured sources (CelesTrak by default, Space-Track if credentials are set). Files are cached in
~/.cache/pg-orrery-catalog/and reused unless stale (>24h) or--forceis passed.To download from a specific source:
Terminal window pg-orrery-catalog download --source celestrakpg-orrery-catalog download --source spacetrack --force -
Build a merged catalog and output it:
Terminal window pg-orrery-catalog build | psql -d mydbTerminal window pg-orrery-catalog build --table satellites -o catalog.sqlTerminal window pg-orrery-catalog build --format 3le -o merged.tleTerminal window pg-orrery-catalog build --format json -o catalog.jsonWith no arguments,
buildmerges all cached files. You can also pass specific TLE files:Terminal window pg-orrery-catalog build /path/to/spacetrack.tle /path/to/celestrak.tleThe merge reports what happened:
spacetrack_everything: 33053 objects (33053 new, 0 updated)celestrak_active: 14376 objects (2 new, 0 updated)satnogs_full: 1488 objects (121 new, 5 updated)supgp_starlink: 9703 objects (77 new, 7398 updated)Total: 33253 unique objectsRegimes: LEO: 31542, GEO: 1203, MEO: 385, HEO: 123Notice how SupGP updated 7,398 Starlink entries --- those are fresher epochs from SpaceX overriding stale Space-Track data.
-
Load directly into PostgreSQL (requires
[pg]extra):Terminal window pg-orrery-catalog load \--database-url postgresql:///mydb \--table satellites \--create-indexThe
--create-indexflag creates both GiST and SP-GiST indexes on thetlecolumn, ready for spatial queries and KNN ordering.
Configuration
Section titled “Configuration”Three layers, highest precedence first:
- CLI flags ---
--table,--source,--database-url - Environment variables ---
SPACETRACK_USER,SOCKS_PROXY,DATABASE_URL - Config file ---
~/.config/pg-orrery-catalog/config.toml
Space-Track credentials
Section titled “Space-Track credentials”Space-Track requires a free account. Set credentials via environment variables:
export SPACETRACK_USER="you@example.com"export SPACETRACK_PASSWORD="secret"pg-orrery-catalog download --source spacetrackOr in the config file:
[spacetrack]user = "you@example.com"password = "secret"SOCKS proxy
Section titled “SOCKS proxy”CelesTrak is sometimes unreachable from certain networks. Route through a SOCKS5 proxy:
export SOCKS_PROXY="localhost:1080"pg-orrery-catalog downloadFull config reference
Section titled “Full config reference”[spacetrack]user = "you@example.com"password = "secret"
[celestrak]proxy = "localhost:1080"supgp_groups = ["starlink", "oneweb", "planet", "orbcomm"]
[output]table = "satellites"
[database]url = "postgresql://localhost/mydb"Working with the generated SQL
Section titled “Working with the generated SQL”The SQL output creates a table with three columns:
CREATE TABLE satellites ( id serial, name text, tle tle);Once loaded, the full pg_orrery function set is available:
-- Where is every LEO satellite right now?SELECT name, observe(tle, '40.0N 105.3W 1655m'::observer, now()) AS topoFROM satellitesWHERE tle_mean_motion(tle) > 11.25;
-- Which satellites are overhead right now?SELECT name, round(topo_elevation( observe_safe(tle, '40.0N 105.3W 1655m'::observer, now()) )::numeric, 1) AS elFROM satellitesWHERE topo_elevation( observe_safe(tle, '40.0N 105.3W 1655m'::observer, now()) ) > 10ORDER BY el DESC;
-- Predict ISS passes for the next 24 hoursSELECT pass_aos_time(p)::timestamp(0) AS rise, round(pass_max_elevation(p)::numeric, 1) AS max_el, pass_los_time(p)::timestamp(0) AS setFROM satellites, predict_passes(tle, '40.0N 105.3W 1655m'::observer, now(), now() + interval '24 hours', 10.0) pWHERE tle_norad_id(tle) = 25544;NORAD ID encoding
Section titled “NORAD ID encoding”TLE files use a 5-character field for the NORAD catalog number. With more than 100,000 tracked objects, the original 5-digit numeric format ran out of space. The encoding has evolved through four cases:
| Case | Format | Range | Example |
|---|---|---|---|
| Traditional | ddddd | 0 — 99,999 | 25544 (ISS) |
| Alpha-5 | Ldddd | 100,000 — 339,999 | T0002 = 270,002 |
| Super-5 case 3 | xxxxX | 340,000 — 906,309,663 | 0000A = 340,000 |
| Super-5 case 4 | xxxXd | 906,309,664+ | 000A0 = 906,309,664 |
Alpha-5 skips the letters I and O (they look like 1 and 0). Super-5 uses a base-64 alphabet: digits 0—9, uppercase A—Z, lowercase a—z, plus + and -.
pg-orrery-catalog decodes all four cases, matching the get_norad_number() implementation in pg_orrery’s vendored SGP4 library. This means Alpha-5 objects like Starlink satellites (NORAD IDs above 100,000) load correctly.
Cache management
Section titled “Cache management”Downloaded TLE files are stored under ~/.cache/pg-orrery-catalog/, organized by source:
~/.cache/pg-orrery-catalog/ celestrak/ celestrak_active.tle supgp_starlink.tle supgp_oneweb.tle ... satnogs/ satnogs_full.tle spacetrack/ spacetrack_everything.tleCheck what’s cached:
pg-orrery-catalog info --cacheFiles older than 24 hours are considered stale and re-downloaded automatically. Use --force to override fresh cache entries.
Automating catalog updates
Section titled “Automating catalog updates”For a regularly-updated catalog, a cron job or systemd timer works well:
# Update catalog daily at 03:000 3 * * * pg-orrery-catalog download && pg-orrery-catalog build --table satellites | psql -d mydbOr with the direct load command:
0 3 * * * pg-orrery-catalog download && pg-orrery-catalog load --database-url postgresql:///mydb --table satellites --create-indexUsing as a library
Section titled “Using as a library”pg-orrery-catalog can also be imported as a Python library:
from pg_orrery_catalog.tle import decode_norad, parse_3le_filefrom pg_orrery_catalog.catalog import merge_sourcesfrom pg_orrery_catalog.regime import regime_summaryfrom pg_orrery_catalog.output.sql import generate_sql
# Parse and mergemerged, stats = merge_sources(["spacetrack.tle", "celestrak.tle"])print(f"{stats.total_unique} unique objects")
# Classifyregimes = regime_summary(merged)print(regimes) # {'LEO': 31542, 'MEO': 385, 'GEO': 1203, 'HEO': 123}
# Generate SQLsql = generate_sql(merged, table="my_catalog")What’s next
Section titled “What’s next”With a catalog loaded, see:
- Tracking Satellites --- observe, predict passes, screen conjunctions
- Satellite Pass Prediction --- detailed pass prediction workflows
- Conjunction Screening --- find close approaches using GiST indexes
- Benchmarks --- performance data with catalogs of 33k—66k objects