Skip to content

Conjunction Screening

Conjunction screening identifies pairs of satellites that might approach each other closely enough to pose a collision risk. The brute-force approach — computing pairwise distances for all objects in the catalog at every time step — scales as O(n^2) and is impractical for large catalogs. pg_orbit solves this with a GiST index on the tle type that enables spatial filtering by altitude band and orbital inclination, reducing the candidate set before running full propagation.

Operational conjunction screening uses several established tools and data sources:

  • STK/SOCRATES (AGI): Commercial tool that monitors the catalog and generates close-approach reports. Industry standard for satellite operators. Expensive.
  • Space-Track CDMs: The 18th Space Defense Squadron publishes Conjunction Data Messages (CDMs) for predicted close approaches. Free but requires registration and covers only US-tracked objects.
  • CelesTrak SOCRATES: Dr. Kelso’s web-based close-approach listing. Updated regularly, covers the full public catalog. Not queryable; you read reports.
  • Python scripts: Propagate the catalog in a loop, compute pairwise distances, filter by threshold. Works for small catalogs. Does not scale.

The fundamental challenge: a catalog of 25,000+ tracked objects produces over 300 million unique pairs. Even checking each pair at a single epoch takes significant time. Checking over a 7-day window at 1-minute resolution is computationally prohibitive without pre-filtering.

pg_orbit attacks the problem in two stages:

Stage 1: GiST index reduces candidates. The GiST index on the tle column stores a 2-D key for each TLE: altitude band (perigee to apogee) and inclination range. The && operator tests whether two TLEs occupy overlapping regions in this 2-D space. Only TLEs that share an altitude shell AND a similar inclination can possibly conjunct. This typically reduces 300 million pairs to a few thousand candidates.

Stage 2: Full propagation verifies candidates. For the remaining candidates, tle_distance() computes the actual Euclidean distance between two TLEs at a given time using full SGP4/SDP4 propagation. Step through time at the required resolution and filter to close approaches.

The two operators:

OperatorTypeWhat it checks
tle && tlebooleanAltitude band AND inclination range overlap
tle <-> tlefloat8Minimum altitude-band separation in km

The && operator is used for overlap queries (find all objects in the same shell). The <-> operator is used for nearest-neighbor queries (find the N closest objects by altitude separation).

  • Not a probability of collision. pg_orbit does not compute Pc (probability of collision). It identifies objects in overlapping orbital shells and computes distances at discrete time steps. For Pc calculation, use CARA (Conjunction Assessment Risk Analysis) methods.
  • No covariance propagation. SGP4 does not produce covariance matrices. The distance values have no uncertainty bounds. For operational conjunction assessment, use SP ephemerides with covariance (from CDMs or owner/operator data).
  • Altitude-band approximation. The GiST key uses perigee-to-apogee altitude as a 1-D range and inclination as a second dimension. Two TLEs can share an altitude shell and never approach because their RAANs or phases are far apart. Always follow GiST filtering with full propagation.
  • No maneuver planning. pg_orbit identifies close approaches. It does not compute avoidance maneuvers (delta-v, timing, constraints).

The workflow is: GiST narrows → tle_distance() verifies → operator/analyst decides.

Create a small catalog with satellites at different orbital regimes:

CREATE TABLE catalog (
norad_id integer PRIMARY KEY,
name text NOT NULL,
tle tle NOT NULL
);
-- ISS (LEO, ~400km, inc 51.64 deg)
INSERT INTO catalog VALUES (25544, 'ISS',
'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');
-- Hubble (LEO, ~540km, inc 28.47 deg)
INSERT INTO catalog VALUES (20580, 'Hubble',
'1 20580U 90037B 24001.50000000 .00000790 00000+0 39573-4 0 9992
2 20580 28.4705 61.4398 0002797 317.3115 42.7577 15.09395228 00008');
-- GPS IIR-M (MEO, ~20200km, inc 55.44 deg)
INSERT INTO catalog VALUES (28874, 'GPS-IIR',
'1 28874U 05038A 24001.50000000 .00000012 00000+0 00000+0 0 9993
2 28874 55.4408 300.3467 0117034 51.6543 309.5420 2.00557079 00006');
-- Equatorial LEO: same altitude as ISS but inc ~5 deg
INSERT INTO catalog VALUES (99901, 'Equatorial-LEO',
'1 99901U 24999A 24001.50000000 .00016717 00000-0 10270-3 0 9990
2 99901 5.0000 208.9163 0006703 30.1694 61.7520 15.50100486 00001');
CREATE INDEX catalog_orbit_gist ON catalog USING gist (tle);

The index builds in milliseconds for a small table. For a full 25,000-object catalog, expect about 200ms.

Before screening, inspect the orbital characteristics of the catalog:

SELECT name,
round(tle_perigee(tle)::numeric, 0) AS perigee_km,
round(tle_apogee(tle)::numeric, 0) AS apogee_km,
round(tle_inclination(tle)::numeric, 1) AS inc_deg,
round(tle_period(tle)::numeric, 1) AS period_min
FROM catalog
ORDER BY tle_perigee(tle);

Find all pairs of satellites in overlapping orbital shells:

SELECT a.name AS sat_a,
b.name AS sat_b,
a.tle && b.tle AS overlaps
FROM catalog a, catalog b
WHERE a.norad_id < b.norad_id
ORDER BY a.name, b.name;

Key insight: ISS and Equatorial-LEO are at the same altitude but different inclinations. The && operator returns false for this pair because the 2-D key requires overlap in BOTH altitude AND inclination. Two objects at the same altitude but in very different orbital planes are unlikely to conjunct.

The <-> operator returns the minimum separation between altitude bands, in km:

SELECT a.name AS sat_a,
b.name AS sat_b,
round((a.tle <-> b.tle)::numeric, 0) AS alt_separation_km
FROM catalog a, catalog b
WHERE a.norad_id < b.norad_id
ORDER BY a.tle <-> b.tle;

ISS and Equatorial-LEO should show ~0 km separation (same altitude shell). ISS and GPS should show ~19,800 km (vastly different orbits).

Force the query planner to use the index and find all objects in the same shell as the ISS:

SET enable_seqscan = off;
SELECT name
FROM catalog
WHERE tle && (SELECT tle FROM catalog WHERE norad_id = 25544)
ORDER BY name;
RESET enable_seqscan;

This should return only ISS itself (and not Equatorial-LEO, which has a different inclination). The GiST index scan avoids checking every object in the catalog.

Find the 3 closest objects to the ISS by altitude band separation, ordered by distance:

SET enable_seqscan = off;
SELECT name,
round((tle <-> (SELECT tle FROM catalog WHERE norad_id = 25544))::numeric, 0) AS alt_dist_km
FROM catalog
WHERE norad_id != 25544
ORDER BY tle <-> (SELECT tle FROM catalog WHERE norad_id = 25544)
LIMIT 3;
RESET enable_seqscan;

This uses the GiST distance operator for efficient ordering. PostgreSQL’s KNN-GiST infrastructure handles this without computing all distances upfront.

Every TLE overlaps with itself:

SELECT name,
tle && tle AS self_overlap
FROM catalog
ORDER BY name;

All rows should return true.

The complete two-stage workflow for a larger catalog:

  1. Build the catalog and index:

    -- Assuming your catalog table is already populated from CelesTrak or Space-Track
    CREATE INDEX IF NOT EXISTS catalog_orbit_gist ON catalog USING gist (tle);
  2. Stage 1: GiST filter to find candidates for a target satellite:

    CREATE TEMPORARY TABLE candidates AS
    SELECT c.norad_id, c.name, c.tle
    FROM catalog c
    WHERE c.tle && (SELECT tle FROM catalog WHERE norad_id = 25544)
    AND c.norad_id != 25544;

    For the ISS in a 25,000-object catalog, this typically returns a few hundred candidates.

  3. Stage 2: Time-resolved distance computation:

    WITH iss AS (
    SELECT tle FROM catalog WHERE norad_id = 25544
    )
    SELECT c.name,
    t AS check_time,
    round(tle_distance(iss.tle, c.tle, t)::numeric, 1) AS dist_km
    FROM candidates c, iss,
    generate_series(
    '2024-01-01 00:00:00+00'::timestamptz,
    '2024-01-02 00:00:00+00'::timestamptz,
    interval '1 minute'
    ) AS t
    WHERE tle_distance(iss.tle, c.tle, t) < 25.0
    ORDER BY dist_km;

    This propagates each candidate pair at 1-minute resolution over 24 hours and filters to approaches within 25 km. Only the GiST candidates are checked, not the full catalog.

  4. Review results and take action.

    The output lists object name, time of closest approach, and distance. An analyst or automated system decides whether to issue a CDM, plan a maneuver, or accept the risk.

Extend the workflow to screen for conjunctions between any pair of objects in a subset:

-- All pairs in the LEO catalog (tle_perigee < 2000 km) that share an orbital shell
SELECT a.name AS sat_a,
b.name AS sat_b,
round((a.tle <-> b.tle)::numeric, 0) AS alt_sep_km,
round(tle_distance(a.tle, b.tle, '2024-01-01 12:00:00+00')::numeric, 0) AS actual_dist_km
FROM catalog a, catalog b
WHERE a.norad_id < b.norad_id
AND a.tle && b.tle
AND tle_perigee(a.tle) < 2000
AND tle_perigee(b.tle) < 2000
ORDER BY actual_dist_km;

Run a conjunction check at regular intervals and store results for trend analysis:

CREATE TABLE conjunction_events (
id serial PRIMARY KEY,
sat_a integer NOT NULL,
sat_b integer NOT NULL,
event_time timestamptz NOT NULL,
dist_km float8 NOT NULL,
checked_at timestamptz DEFAULT now()
);
-- Periodic screening job (run daily or as needed)
INSERT INTO conjunction_events (sat_a, sat_b, event_time, dist_km)
WITH iss AS (
SELECT norad_id, tle FROM catalog WHERE norad_id = 25544
)
SELECT iss.norad_id, c.norad_id, t, tle_distance(iss.tle, c.tle, t)
FROM catalog c, iss,
generate_series(
now(),
now() + interval '7 days',
interval '5 minutes'
) AS t
WHERE c.tle && iss.tle
AND c.norad_id != iss.norad_id
AND tle_distance(iss.tle, c.tle, t) < 50.0;

This builds a history of close approaches that you can query, trend, and alert on. The GiST filter ensures it runs efficiently even against a full catalog.