Skip to content

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.

Three major sources provide TLE data, each with trade-offs:

SourceAuthCoverageFreshness
Space-TrackLogin requiredFull catalog (~30k+ on-orbit)Hours to days
CelesTrakNoneActive sats + operator supplemental GPMinutes to hours
SatNOGSNoneCommunity-tracked objectsVaries

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.

Terminal window
# Run directly (no install needed)
uvx pg-orrery-catalog --help
# Or install permanently
uv pip install pg-orrery-catalog
# For direct database loading (adds psycopg)
uv pip install "pg-orrery-catalog[pg]"

The typical workflow is three steps. Each can run independently.

  1. Download TLE data from remote sources into the local cache:

    Terminal window
    pg-orrery-catalog download

    This 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 --force is passed.

    To download from a specific source:

    Terminal window
    pg-orrery-catalog download --source celestrak
    pg-orrery-catalog download --source spacetrack --force
  2. Build a merged catalog and output it:

    Terminal window
    pg-orrery-catalog build | psql -d mydb

    With no arguments, build merges all cached files. You can also pass specific TLE files:

    Terminal window
    pg-orrery-catalog build /path/to/spacetrack.tle /path/to/celestrak.tle

    The 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 objects
    Regimes: LEO: 31542, GEO: 1203, MEO: 385, HEO: 123

    Notice how SupGP updated 7,398 Starlink entries --- those are fresher epochs from SpaceX overriding stale Space-Track data.

  3. Load directly into PostgreSQL (requires [pg] extra):

    Terminal window
    pg-orrery-catalog load \
    --database-url postgresql:///mydb \
    --table satellites \
    --create-index

    The --create-index flag creates both GiST and SP-GiST indexes on the tle column, ready for spatial queries and KNN ordering.

Three layers, highest precedence first:

  1. CLI flags --- --table, --source, --database-url
  2. Environment variables --- SPACETRACK_USER, SOCKS_PROXY, DATABASE_URL
  3. Config file --- ~/.config/pg-orrery-catalog/config.toml

Space-Track requires a free account. Set credentials via environment variables:

Terminal window
export SPACETRACK_USER="you@example.com"
export SPACETRACK_PASSWORD="secret"
pg-orrery-catalog download --source spacetrack

Or in the config file:

[spacetrack]
user = "you@example.com"
password = "secret"

CelesTrak is sometimes unreachable from certain networks. Route through a SOCKS5 proxy:

Terminal window
export SOCKS_PROXY="localhost:1080"
pg-orrery-catalog download
[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"

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 topo
FROM satellites
WHERE 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 el
FROM satellites
WHERE topo_elevation(
observe_safe(tle, '40.0N 105.3W 1655m'::observer, now())
) > 10
ORDER BY el DESC;
-- Predict ISS passes for the next 24 hours
SELECT 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 set
FROM satellites,
predict_passes(tle, '40.0N 105.3W 1655m'::observer,
now(), now() + interval '24 hours', 10.0) p
WHERE tle_norad_id(tle) = 25544;

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:

CaseFormatRangeExample
Traditionalddddd0 — 99,99925544 (ISS)
Alpha-5Ldddd100,000 — 339,999T0002 = 270,002
Super-5 case 3xxxxX340,000 — 906,309,6630000A = 340,000
Super-5 case 4xxxXd906,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.

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.tle

Check what’s cached:

Terminal window
pg-orrery-catalog info --cache

Files older than 24 hours are considered stale and re-downloaded automatically. Use --force to override fresh cache entries.

For a regularly-updated catalog, a cron job or systemd timer works well:

Terminal window
# Update catalog daily at 03:00
0 3 * * * pg-orrery-catalog download && pg-orrery-catalog build --table satellites | psql -d mydb

Or with the direct load command:

Terminal window
0 3 * * * pg-orrery-catalog download && pg-orrery-catalog load --database-url postgresql:///mydb --table satellites --create-index

pg-orrery-catalog can also be imported as a Python library:

from pg_orrery_catalog.tle import decode_norad, parse_3le_file
from pg_orrery_catalog.catalog import merge_sources
from pg_orrery_catalog.regime import regime_summary
from pg_orrery_catalog.output.sql import generate_sql
# Parse and merge
merged, stats = merge_sources(["spacetrack.tle", "celestrak.tle"])
print(f"{stats.total_unique} unique objects")
# Classify
regimes = regime_summary(merged)
print(regimes) # {'LEO': 31542, 'MEO': 385, 'GEO': 1203, 'HEO': 123}
# Generate SQL
sql = generate_sql(merged, table="my_catalog")

With a catalog loaded, see: