Skip to content

Operators & GiST Index

pg_orbit defines two operators on the tle type and a GiST operator class that enables indexed conjunction screening over large satellite catalogs. The operators work on the orbital altitude band and inclination range extracted from TLE elements, providing a fast necessary-condition filter for proximity analysis.


Tests whether two TLEs have overlapping orbital envelopes. The envelopes are defined by the altitude band (perigee to apogee) and inclination range. Overlap is a necessary but not sufficient condition for a conjunction: two satellites whose altitude bands and inclination ranges do not overlap can never come close to each other.

tle && tle → boolean

Returns true if both of the following conditions hold:

  1. The altitude bands overlap (one satellite’s perigee is below the other’s apogee, and vice versa)
  2. The inclination ranges are compatible (the orbits could geometrically intersect)

Returns false if the orbits are guaranteed to never intersect based on these geometric bounds.

-- Find all satellites whose orbits could potentially intersect with the ISS
WITH iss AS (
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
)
SELECT norad_id, name
FROM satellite_catalog, iss
WHERE satellite_catalog.tle && iss.tle;

Computes the minimum separation between the altitude bands of two TLEs, in kilometers. If the altitude bands overlap, returns 0.

tle <-> tle → float8

This is an altitude-only metric. It computes:

  • max(0, perigee_a - apogee_b) and max(0, perigee_b - apogee_a)
  • Returns the minimum of these two values

The result is the minimum possible radial separation. A result of 0 means the altitude bands overlap (but the satellites may still be far apart in along-track or cross-track distance).

-- Altitude band separation between ISS and a GEO satellite
WITH iss AS (
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
),
geo AS (
SELECT '1 28884U 05041A 24001.50000000 -.00000089 00000-0 00000-0 0 9997
2 28884 0.0153 93.0424 0001699 138.1498 336.5718 1.00271128 67481'::tle AS tle
)
SELECT round((iss.tle <-> geo.tle)::numeric, 1) AS separation_km
FROM iss, geo;
-- Order catalog by altitude proximity to a target satellite
WITH target AS (
SELECT tle FROM satellite_catalog WHERE norad_id = 25544
)
SELECT norad_id, name,
round((satellite_catalog.tle <-> target.tle)::numeric, 1) AS alt_sep_km
FROM satellite_catalog, target
ORDER BY satellite_catalog.tle <-> target.tle
LIMIT 20;

The tle_ops operator class enables GiST indexing on tle columns. With this index in place, the && (overlap) and <-> (distance) operators use index scans instead of sequential scans, making conjunction screening over large catalogs practical.

CREATE INDEX idx_tle_gist ON satellite_catalog USING gist (tle tle_ops);

The GiST index stores a bounding representation of each TLE’s orbital envelope:

  • Altitude band: perigee altitude to apogee altitude (km, above WGS-72)
  • Inclination range: orbital inclination (degrees)

These are extracted from the TLE’s mean motion and eccentricity at index creation time. The index does not store time-varying quantities; it represents the geometric envelope of the orbit as defined by the current osculating elements.

-- Find all catalog objects that could intersect with the ISS orbit
-- Uses the GiST index to avoid a full catalog scan
WITH iss AS (
SELECT tle FROM satellite_catalog WHERE norad_id = 25544
)
SELECT c.norad_id, c.name
FROM satellite_catalog c, iss
WHERE c.tle && iss.tle
AND c.norad_id != 25544;

Without the GiST index, the && operator requires a sequential scan of the entire catalog (O(n) per query). With the index, overlap queries run in O(log n) time. For a catalog of 12,000 active TLEs, this reduces conjunction screening from seconds to milliseconds.

The GiST index is maintained automatically by PostgreSQL on INSERT, UPDATE, and DELETE. When TLEs are refreshed (e.g., daily catalog updates), the index is updated in place. No manual REINDEX is needed under normal operation.

If you perform a bulk catalog replacement (truncate + reload), run REINDEX after loading:

TRUNCATE satellite_catalog;
COPY satellite_catalog FROM '/path/to/catalog.csv' WITH (FORMAT csv);
REINDEX INDEX idx_tle_gist;