Skip to content

Conjunction Assessment

This example runs the ConjunctionAssessment workflow between two space objects: it finds the time of closest approach (TCA) inside a search window, reports the miss distance in RIC components, and computes probability of collision from either TLE-derived or user-supplied covariance. Use it for close-approach screening and risk evaluation.

ts
import {
  ConjunctionAssessment,
  CovarianceFrame,
  EpochUTC,
  ForceModel,
  Kilometers,
  RungeKutta89Propagator,
  StateCovariance,
  Tle,
} from 'ootk';

Run it

bash
npm run build
npx tsx ./examples/conjunction-assessment-example.ts

Scenario setup

Two fabricated TLEs place the objects in nearly identical LEO orbits so a close approach exists inside the 6-hour search window. The hard body radii are required for any probability-of-collision calculation; without radii on both objects the assessment only reports geometry.

ts
// Sample TLE data for two satellites in close proximity
const primaryTle = new Tle(
  '1 25544U 98067A   25019.50000000  .00016717  00000-0  10270-3 0  9005',
  '2 25544  51.6400 339.8000 0002571  90.5000 269.6000 15.50000000000000',
);

const secondaryTle = new Tle(
  '1 44691U 19074A   25019.50000000  .00016500  00000-0  10200-3 0  9006',
  '2 44691  51.6450 339.8050 0002600  90.5050 269.6050 15.50005000000000',
);

// Search window (6 hours, starting half a day after the TLE epoch)
const startTime = EpochUTC.fromDateTimeString('2025-01-19T12:00:00.000Z');
const endTime = EpochUTC.fromDateTimeString('2025-01-19T18:00:00.000Z');

// Hard body radii used for probability of collision
const primaryRadius = 0.05 as Kilometers; // 50 meters
const secondaryRadius = 0.01 as Kilometers; // 10 meters

Basic SGP4 assessment

With plain TLE inputs, assess() propagates both objects with SGP4, does a coarse 60-second sweep of the window, then refines the TCA with a golden-section search. The returned ConjunctionEvent carries the TCA, total and per-axis (radial, intrack, crosstrack) miss distances, and relative velocity; isHighRisk() applies miss-distance and Pc thresholds.

ts
console.log('=== Example 1: Basic Conjunction Assessment ===\n');

// With TLE inputs and no extra options, both objects are propagated with SGP4
const basicAssessment = new ConjunctionAssessment(
  { name: 'ISS (Zarya)', tle: primaryTle, radius: primaryRadius },
  { name: 'Secondary Object', tle: secondaryTle, radius: secondaryRadius },
);

const basicEvent = basicAssessment.assess({ startTime, endTime });

console.log(basicEvent.toString());
console.log(`\nHigh Risk: ${basicEvent.isHighRisk(1.0 as Kilometers)}`);

High fidelity with covariance

For higher fidelity, seed a RungeKutta89Propagator from the SGP4 state at the window start and pass it via the propagator override on each object input. The ForceModel here uses 8x8 spherical harmonic gravity plus atmospheric drag and solar radiation pressure (both take spacecraft mass in kg and area in m^2; the drag model is still marked work-in-progress in the library). propagateCovariance: true builds sigma-point covariance samples from each TLE, scales them by TLE quality and age, and propagates them to TCA to produce a combined covariance and Pc.

ts
console.log('\n=== Example 2: High-Fidelity Assessment with Covariance ===\n');

// Build a force model with 8x8 spherical harmonic gravity plus drag and
// solar radiation pressure (both need spacecraft mass in kg and area in m^2)
const forceModel = new ForceModel();

forceModel.setEarthGravity(8, 8);
forceModel.setAtmosphericDrag(460000, 1500);
forceModel.setSolarRadiationPressure(460000, 2500);

// Seed each numerical propagator with the SGP4 state at the window start,
// then hand the propagators to the assessment via the propagator override.
const primaryJ2000 = primaryTle.propagate(startTime).toJ2000();
const secondaryJ2000 = secondaryTle.propagate(startTime).toJ2000();

const hifiAssessment = new ConjunctionAssessment(
  {
    name: 'ISS (Zarya)',
    tle: primaryTle,
    radius: primaryRadius,
    propagator: new RungeKutta89Propagator(primaryJ2000, forceModel),
  },
  {
    name: 'Secondary Object',
    tle: secondaryTle,
    radius: secondaryRadius,
    propagator: new RungeKutta89Propagator(secondaryJ2000, forceModel),
  },
);

const hifiEvent = hifiAssessment.assess({
  startTime,
  endTime,
  forceModel,
  propagateCovariance: true, // Propagate TLE-based covariances via sigma points
});

console.log(hifiEvent.toString());

if (hifiEvent.probabilityOfCollision !== undefined) {
  console.log(`\nProbability of Collision: ${hifiEvent.probabilityOfCollision.toExponential(6)}`);
}

const mahalanobis = hifiEvent.getMahalanobisDistance();

if (mahalanobis !== undefined) {
  console.log(`Mahalanobis Distance: ${mahalanobis.toFixed(3)} sigma`);
}

Custom covariance

If you already have covariance data (for example from an OD process or a CDM), supply StateCovariance.fromSigmas() matrices on the object inputs instead. RIC-frame covariances are used as-is at TCA; ECI-frame covariances are rotated into RIC automatically.

ts
console.log('\n=== Example 3: Custom Covariance Matrices ===\n');

/*
 * Define custom covariances (1-sigma values in RIC frame)
 * [radial, intrack, crosstrack, radial_vel, intrack_vel, crosstrack_vel]
 */
const primaryCovariance = StateCovariance.fromSigmas(
  [
    0.5, // 500 m radial uncertainty
    1.5, // 1.5 km intrack uncertainty
    0.5, // 500 m crosstrack uncertainty
    0.001, // 1 m/s radial velocity uncertainty
    0.003, // 3 m/s intrack velocity uncertainty
    0.001, // 1 m/s crosstrack velocity uncertainty
  ],
  CovarianceFrame.RIC,
);

const secondaryCovariance = StateCovariance.fromSigmas(
  [0.3, 1.0, 0.3, 0.0005, 0.002, 0.0005],
  CovarianceFrame.RIC,
);

const customAssessment = new ConjunctionAssessment(
  {
    name: 'ISS (Zarya)',
    tle: primaryTle,
    covariance: primaryCovariance,
    radius: primaryRadius,
  },
  {
    name: 'Secondary Object',
    tle: secondaryTle,
    covariance: secondaryCovariance,
    radius: secondaryRadius,
  },
);

const customEvent = customAssessment.assess({ startTime, endTime });

console.log(customEvent.toString());

Multi object screening

Screening loops one primary against a list of candidate TLEs, then filters events by miss distance and flags any with Pc above a reporting threshold. For large candidate sets the library also provides CatalogScreener with orbital-shell prefilters.

ts
console.log('\n=== Example 4: Multi-Object Screening ===\n');

// List of potential conjunction objects
const secondaryTles = [
  secondaryTle,
  new Tle(
    '1 12345U 81001A   25019.50000000  .00016400  00000-0  10100-3 0  9007',
    '2 12345  51.6500 339.8100 0002650  90.5100 269.6100 15.50010000000000',
  ),
];

const screeningThreshold = 5.0 as Kilometers; // 5 km screening threshold
const pcThreshold = 1e-6; // Pc > 1e-6 is concerning

console.log(`Screening ${secondaryTles.length} objects for conjunctions...\n`);

secondaryTles.forEach((tle, index) => {
  const screeningAssessment = new ConjunctionAssessment(
    { tle: primaryTle, radius: primaryRadius },
    { tle, radius: secondaryRadius },
  );

  const event = screeningAssessment.assess({
    startTime,
    endTime,
    propagateCovariance: true,
  });

  if (event.missDistance < screeningThreshold) {
    console.log(`Object ${index + 1}: CLOSE APPROACH DETECTED`);
    console.log(`  TCA: ${event.tca.toString()}`);
    console.log(`  Miss Distance: ${event.missDistance.toFixed(3)} km`);

    if (event.probabilityOfCollision !== undefined && event.probabilityOfCollision > pcThreshold) {
      console.log(`  Pc: ${event.probabilityOfCollision.toExponential(3)} [HIGH RISK]`);
    } else if (event.probabilityOfCollision !== undefined) {
      console.log(`  Pc: ${event.probabilityOfCollision.toExponential(3)}`);
    }
    console.log();
  } else {
    console.log(`Object ${index + 1}: no close approach inside ${screeningThreshold} km`);
    console.log(`  Miss Distance: ${event.missDistance.toFixed(3)} km\n`);
  }
});

console.log('=== All Examples Complete ===');

Output

The covariance sampling scales with TLE age relative to the current system date, so probability-of-collision values will differ per run.

txt
=== Example 1: Basic Conjunction Assessment ===

[Conjunction Event]
  TCA: 2025-01-19T12:08:10.081Z
  Miss Distance: 1.533883 km
    Radial:     0.026031 km
    Intrack:    1.531291 km
    Crosstrack: 0.085245 km
  Relative Velocity: 0.001941 km/s
  Combined Hard Body Radius: 0.060 km

High Risk: false

=== Example 2: High-Fidelity Assessment with Covariance ===

[Conjunction Event]
  TCA: 2025-01-19T12:08:06.773Z
  Miss Distance: 1.533903 km
    Radial:     0.025963 km
    Intrack:    1.531154 km
    Crosstrack: 0.088044 km
  Relative Velocity: 0.001941 km/s
  Probability of Collision: 1.319935e-3
  Combined Hard Body Radius: 0.060 km

Probability of Collision: 1.319935e-3
Mahalanobis Distance: 0.000 sigma

=== Example 3: Custom Covariance Matrices ===

[Conjunction Event]
  TCA: 2025-01-19T12:08:10.081Z
  Miss Distance: 1.533883 km
    Radial:     0.026031 km
    Intrack:    1.531291 km
    Crosstrack: 0.085245 km
  Relative Velocity: 0.001941 km/s
  Probability of Collision: 1.578144e-3
  Combined Hard Body Radius: 0.060 km

=== Example 4: Multi-Object Screening ===

Screening 2 objects for conjunctions...

Object 1: CLOSE APPROACH DETECTED
  TCA: 2025-01-19T12:08:10.081Z
  Miss Distance: 1.534 km
  Pc: 1.324e-3 [HIGH RISK]

Object 2: CLOSE APPROACH DETECTED
  TCA: 2025-01-19T12:07:54.425Z
  Miss Distance: 3.043 km
  Pc: 3.370e-4 [HIGH RISK]

=== All Examples Complete ===

Released under the AGPL-3.0 License.