Main Content

Detect and Track LEO Satellite Constellation with Ground Radars

This example shows how to import a Two-Line Element (TLE) file of a satellite constellation, simulate radar detections of the constellation, and track the constellation.

The task of populating and maintaining a catalog of space objects orbiting Earth is crucial in space surveillance. This task consists of several processes: detecting and identifying new objects and adding them to the catalog, updating known objects orbits in the catalog, tracking orbit changes throughout their lifetime, and predicting reentries in the atmosphere. In this example, we are study how to detect and track new satellites and add them to a catalog.

To guarantee safe operations in space and prevent collisions with other satellites or known debris, it important to correctly detect and track newly launched satellites. Space agencies typically share prelaunch information, which can be used to select a search strategy. A Low Earth Orbit (LEO) satellite search strategy consisting of fence-type radar systems is commonly used. A fence-type radar system searches a finite volume in space and detects satellites as they pass through its field of view. This strategy can detect and track newly launched constellation quickly. [1]

Importing a satellite constellation from a TLE file

Two-Line Element sets are a common data format to save orbital information of satellites. You can use the satelliteScenario object to import satellite orbits defined in a TLE file. By default, the imported satellite orbits are propagated using the SGP4 orbit propagation algorithm which provides good accuracy for LEO objects. In this example, these orbits provide with the ground truth to test the radar tracking system capability to detect newly launched satellites.

% Create a satellite scenario
satscene = satelliteScenario;
% Add satellites from TLE file.
tleFile = "leoSatelliteConstellation.tle";
constellation = satellite(satscene, tleFile);

Use the satellite scenario viewer to visualize the constellation.


Simulating synthetic detections and track constellation

Modeling space surveillance radars

Define two stations with fan-shaped radar beams looking into space. The fans cut through the satellite orbits to maximize the number of detections. The radar stations located on North America form an East-West fence.

% First station coordinates in LLA
station1 = [48 -80 0];

% Second station coordinates in LLA
station2 = [50 -117 0];

Each station is equipped with a radar, which is modeled by using a fusionRadarSensor object. In order to detect satellites in the LEO range, the radar has the following requirements:

  • Detecting a 10 dBsm object up to 2000 km away

  • Resolving objects horizontally and vertically with a precision of 100 m at 2000 km range

  • Having a field of view of 120 degrees in azimuth and 30 degrees in elevation

  • Looking up into space

% Create fan-shaped monostatic radars
fov = [120;40];
radar1 = fusionRadarSensor(1,...
    'UpdateRate',0.1,... 10 sec
    'ScanMode','No scanning',...
    'MountingAngles',[0 90 0],... look up
    'FieldOfView',fov,... degrees
    'ReferenceRange',2000e3,... m
    'RangeLimits',  [0 2000e3], ... m
    'ReferenceRCS', 10,... dBsm
    'HasNoise', true,...
    'AzimuthResolution',0.03,... degrees
    'ElevationResolution',0.03,... degrees
    'RangeResolution',2000, ... m % accuracy ~= 2000 * 0.05 (m)
    'DetectionCoordinates','Sensor Spherical',...

radar2 = clone(radar1);
radar2.SensorIndex = 2;

Radar Processing Chain

In this example, several coordinate conversions and axes transformation are performed to properly run the radar tracking chain. The diagram below illustrates how the inputs defined above are transformed and passed to the radar.

In the first step, you calculate each satellite pose in the local radar station NED axes. You achieve this by first obtaining the ground station ECEF pose and converting the satellite position and velocity to the ECEF coordinates. The radar input is obtained by taking the differences between the satellite pose and the ground station pose and rotating the differences into ground station local NED axes. See the assembleRadarInputs supporting function for the implementation details.

In the second step, you add the required information to the detection object so that the tracker can operate with an ECEF state. You use the MeasurementParameters property in each object detection to achieve that purpose, as shown in the addMeasurementParams supporting function.

Defining a tracker

The radar models you defined above output detections. To estimate the satellite orbits, you use a tracker. The Sensor Fusion and Tracking Toolbox™ provides a variety of multi-object trackers. In this example, you choose a Joint Probabilistic Data Association (JPDA) tracker because it offers a good balance of tracking performance and computational cost.

You need to define a tracking filter for the tracker. You can use a lower fidelity model than SGP4, such as a Keplerian integration of the equation of motion, to track the satellite. Often, the lack of fidelity in the motion model of targets is compensated by measurement updates and incorporating process noise in the filter. The supporting function initKeplerUKF defines the tracking filter.

% Define the tracker
tracker = trackerJPDA('FilterInitializationFcn',@initKeplerUKF,...
    'DeletionThreshold',[5 8],...
    'ConfirmationThreshold',[5 8]);

Running the simulation

In the remainder of this example, you step through the scenario to simulate radar detections and track satellites. This section uses a separate helper class helperScenarioGlobeViewer for visualization. You can use this helper class to display sensor and tracking data with uncertainty ellipses and show the true position of each satellite.

globeDisplay = helperScenarioGlobeViewer;

% Define coverage configuration of each radar and visualize it on the globe
ned1 = dcmecef2ned(station1(1), station1(2));
ned2 = dcmecef2ned(station2(1), station2(2));
covcon(1) = coverageConfig(radar1,lla2ecef(station1),quaternion(ned1,'rotmat','frame'));
covcon(2) = coverageConfig(radar2,lla2ecef(station2),quaternion(ned2, 'rotmat','frame'));
plotCoverage(globeDisplay, covcon)

You first generate the entire history of the states of the constellation over 5 hours. Then, you simulate radar detections and generate tracks in a loop.

satscene.StopTime = satscene.StartTime + hours(5);
satscene.SampleTime = 10;
numSteps = ceil(seconds(satscene.StopTime - satscene.StartTime)/satscene.SampleTime);

% Get constellation positions and velocity over the course of the simulation
plats = repmat(...
    struct('PlatformID',0,'Position',[0 0 0], 'Velocity', [0 0 0]),...
    numSteps, 40);
for i=1:numel(constellation)
    [pos, vel] = states(constellation(i),'CoordinateFrame',"Geographic");
    for j=1:numSteps
        plats(j,i).Position = pos(:,j)';
        plats(j,i).Velocity = vel(:,j)';
        plats(j,i).PlatformID = i;

% Initialize tracks and tracks log
confTracks = objectTrack.empty(0,1);
trackLog = cell(1,numSteps);

% Initialize radar plots
radarplt = helperRadarPlot(fov);

% Set random seed for reproducible results
s = rng;
step = 0;
while step < numSteps
    time = step*satscene.SampleTime;
    step = step + 1;
    % Generate detections of the constellation following the radar
    % processing chain
    targets1 = assembleRadarInputs(station1, plats(step,:));
    [dets1,numdets1] = radar1(targets1, time);
    dets1 = addMeasurementParams(dets1,numdets1,station1);
    targets2 = assembleRadarInputs(station2, plats(step,:));
    [dets2, numdets2] = radar2(targets2, time);
    dets2 = addMeasurementParams(dets2, numdets2, station2);
    detections = [dets1; dets2];
    updateRadarPlots(radarplt,targets1, targets2 ,dets1, dets2)
    % Generate and update tracks
    detectableInput = isDetectable(tracker,time, covcon);
    if ~isempty(detections) || isLocked(tracker)
        [confTracks,~,~,info] = tracker(detections,time,detectableInput);
%         confTracks = deleteBadTracks(tracker,confTracks);
    trackLog{step} = confTracks;

    % Update globe display

The plot above shows the orbits (blue dots) and the detections (red circles) from the point of view of each radar.

% Restore previous random seed state

After 5 hours of tracking, about half the constellation is tracked successfully. Maintaining tracks with a partial orbit coverage is challenging since satellites can often stay undetected for long periods of time in this configuration. In this example, there are only two radar stations. Additional tracking stations are expected to generate better tracking performance. The assignment metrics, which evaluate the tracking performance by comparing between the true objects and the tracks, are shown below.

% Show Assignment metrics
truthIdFcn = @(x)[x.PlatformID];

tam = trackAssignmentMetrics('DistanceFunctionFormat','custom',...

for i=1:numSteps
    % Extract the tracker and ground truth at the i-th tracker update
    tracks = trackLog{i};
    truths = plats(i,:);
    % For correct truth-track assignment evaluation, the truth is converted
    % to ECEF
    for j=1:numel(truths)
        R = dcmecef2ned(truths(j).Position(1), truths(j).Position(2));
        truths(j).Velocity = truths(j).Velocity * R;
        truths(j).Position = lla2ecef(truths(j).Position);
    % Extract summary of assignment metrics against tracks and truths
    [trackAM,truthAM] = tam(tracks, truths);

% Show cumulative metrics for each individual recorded truth object
results = truthMetricsTable(tam);
ans=40×4 table
    TruthID    AssociatedTrackID    BreakLength    EstablishmentLength
    _______    _________________    ___________    ___________________

       1               57                5                 232        
       2               46                0                1283        
       3               32                0                 668        
       4               62               12                 492        
       5               22                0                 436        
       6               54                5                 807        
       7               26                0                 513        
       8               31                0                 665        
       9               43                0                1221        
      10               56                0                1500        
      11               49                0                1339        
      12               41                0                1056        
      13              NaN                0                1800        
      14              NaN                0                1800        
      15              NaN                0                1800        
      16              NaN                0                1800        

The table above lists 40 satellites in the launched constellation and shows the tracked satellites with associated track IDs. A track ID of value NaN indicates that the satellite is not tracked by the end of the simulation. This either means that the orbit of the satellite did not pass through the field of view of one of the two radars or the track of the satellite has been dropped. The tracker can drop a track due to an insufficient number of initial detections, which leads to a large uncertainty on the estimate. Alternately, the tracker can drop the track if the satellite is not re-detected soon enough, such that the lack of updates leads to divergence and eventually deletion.


In this example, you have learned how to use the satelliteScenario object from the Aerospace Toolbox™ to import orbit information from TLE files. You propagated the satellite trajectories using SGP4 and visualized the scenario using the Satellite Scenario Viewer. You learned how to use the radar and tracker models from the Sensor Fusion and Tracking Toolbox™ to model a space surveillance radar tracking system. The constructed tracking system can predict the estimated orbit of each satellite using a low fidelity model.

Supporting functions

initKeplerUKF initializes an Unscented Kalman filter using the Keplerian motion model.

function filter = initKeplerUKF(detection)

radarsphmeas = detection.Measurement;
[x, y, z] = sph2cart(deg2rad(radarsphmeas(1)),deg2rad(radarsphmeas(2)),radarsphmeas(3));
radarcartmeas = [x y z];
Recef2radar = detection.MeasurementParameters.Orientation;
ecefmeas = detection.MeasurementParameters.OriginPosition + radarcartmeas*Recef2radar;
% This is equivalent to:
% Ry90 = [0 0 -1 ; 0 1 0; 1 0 0]; % frame rotation of 90 deg around y axis
% nedmeas(:) = Ry90' * radarcartmeas(:);
% ecefmeas = lla2ecef(lla) +  nedmeas * dcmecef2ned(lla(1),lla(2));
initState = [ecefmeas(1); 0; ecefmeas(2); 0; ecefmeas(3); 0];

sigpos = 2;% m
sigvel = 0.5;% m/s^2

filter = trackingUKF(@keplerorbit,@cvmeas,initState,...
    'StateCovariance', diag(repmat([1000, 10000].^2,1,3)),...
    'ProcessNoise',diag(repmat([sigpos, sigvel].^2,1,3)));


function state = keplerorbit(state,dt)
% keplerorbit performs numerical integration to predict the state of
% Keplerian bodies. The state is [x;vx;y;vy;z;vz]

% Runge-Kutta 4th order integration method:
k1 = kepler(state);
k2 = kepler(state + dt*k1/2);
k3 = kepler(state + dt*k2/2);
k4 = kepler(state + dt*k3);

state = state + dt*(k1+2*k2+2*k3+k4)/6;

    function dstate=kepler(state)
        x =state(1,:);
        vx = state(2,:);
        vy = state(4,:);
        vz = state(6,:);

        mu = 398600.4405*1e9; % m^3 s^-2
        omega = 7.292115e-5; % rad/s
        r = norm([x y z]);
        g = mu/r^2;
        % Coordinates are in a non-intertial frame, account for Coriolis
        % and centripetal acceleration
        ax = -g*x/r + 2*omega*vy + omega^2*x;
        ay = -g*y/r - 2*omega*vx + omega^2*y;
        az = -g*z/r;
        dstate = [vx;ax;vy;ay;vz;az];

isDetectable is used in the example to determine which tracks are detectable at a given time.

function detectInput = isDetectable(tracker,time,covcon)

if ~isLocked(tracker)
    detectInput = zeros(0,1,'uint32');
tracks = tracker.predictTracksToTime('all',time);
if isempty(tracks)
    detectInput = zeros(0,1,'uint32');
    alltrackid = [tracks.TrackID];
    isDetectable = zeros(numel(tracks),numel(covcon),'logical');
    for i = 1:numel(tracks)
        track = tracks(i);
        pos_scene = track.State([1 3 5]);
        for j=1:numel(covcon)
            config = covcon(j);
            % rotate position to sensor frame:
            d_scene = pos_scene(:) - config.Position(:);
            scene2sens = rotmat(config.Orientation,'frame');
            d_sens = scene2sens*d_scene(:);
            [az,el] = cart2sph(d_sens(1),d_sens(2),d_sens(3));
            if abs(rad2deg(az)) <= config.FieldOfView(1)/2 && abs(rad2deg(el)) < config.FieldOfView(2)/2
                isDetectable(i,j) = true;
                isDetectable(i,j) = false;
    detectInput = alltrackid(any(isDetectable,2))';

deleteBadTracks is used to remove tracks that obviously diverged. Specifically, diverged tracks in this example are tracks whose current position has fallen on the surface of the earth and tracks whose covariance has become too large.

function tracks = deleteBadTracks(tracker,tracks)
% remove divergent tracks:
%   - tracks with covariance > 1e10 (100 km standard deviation)
%   - tracks with estimated position outside of LEO bounds
n = numel(tracks);
toDelete = zeros(1,n,'logical');
for i=1:numel(tracks)
    [pos, cov] = getTrackPositions(tracks(i),[ 1 0 0 0 0 0 ; 0 0 1 0 0 0; 0 0 0 0 1 0]);
    if norm(pos) < 6500*1e3 || norm(pos) > 8500*1e3 || max(cov,[],'all') > 1e10
        toDelete(i) =true;
tracks(toDelete) = [];

assembleRadarInput is used to construct the constellation poses in each radar body frame. This is the first step described in the diagram.

function targets = assembleRadarInputs(station, constellation)
% For each satellite in the constellation, derive its pose with respect to
% the radar frame.
% inputs:
%          - station        :  LLA vector of the radar ground station
%          - constellation  :  Array of structs containing the LLA position
%                              and NED velocity of each satellite
% outputs:
%          - targets        :  Array of structs containing the pose of each
%                              satellite with respect to the radar, expressed in the local
%                              ground radar frame (NED)

% Template structure for the outputs which contains all the field required
% by the radar step method
targetTemplate =  struct( ...
                'PlatformID', 0, ...
                'ClassID', 0, ...
                'Position', zeros(1,3), ...
                'Velocity', zeros(1,3), ...
                'Acceleration', zeros(1,3), ...
                'Orientation', quaternion(1,0,0,0), ...
                'AngularVelocity', zeros(1,3),...
                'Dimensions', struct( ...
                             'Length', 0, ...
                             'Width', 0, ...
                             'Height', 0, ...
                             'OriginOffset', [0 0 0]), ...
                'Signatures', {{rcsSignature}});

% First convert the current satellite pose to ECEF
targetPoses = repmat(targetTemplate, 1, numel(constellation));
for i=1:numel(constellation)
    lla = constellation(i).Position;
    targetPoses(i).Position = lla2ecef(lla);
    vned = constellation(i).Velocity;
    Rned2ecef = dcmecef2ned(lla(1),lla(2))';
    targetPoses(i).Velocity(:) = Rned2ecef*vned(:);
    targetPoses(i).PlatformID = constellation(i).PlatformID;
    % Orientation and angular velocity are left null, assuming satellite to
    % be point targets with a uniform rcs

% Then derive the radar pose in ECEF based on the ground station location
Recef2station = dcmecef2ned(station(1), station(2));
radarPose.Orientation = quaternion(Recef2station,'rotmat','frame');
radarPose.Position = lla2ecef(station);
radarPose.Velocity = zeros(1,3);
radarPose.AngularVelocity = zeros(1,3);

% Finally, take the difference and rotate each vector to the ground station
% NED axes
targets = targetPoses;
for i=1: numel(targetPoses)
    thisTgt = targetPoses(i);
    pos = Recef2station*(thisTgt.Position(:) - radarPose.Position(:));
    vel = Recef2station*(thisTgt.Velocity(:) - radarPose.Velocity(:)) - cross(radarPose.AngularVelocity(:),pos(:));
    angVel = thisTgt.AngularVelocity(:) - radarPose.AngularVelocity(:);
    orient = radarPose.Orientation' * thisTgt.Orientation;

    % Store into target structure array
    targets(i).Position(:) = pos;
    targets(i).Velocity(:) = vel;
    targets(i).AngularVelocity(:) = angVel;
    targets(i).Orientation = orient;

addMeasurementParam implements step 2 in the radar chain process as described in the diagram.

function dets = addMeasurementParams(dets, numdets, station)
% Add radar station information to the measurement parameters
Recef2station = dcmecef2ned(station(1), station(2));
for i=1:numdets
    dets{i}.MeasurementParameters.OriginPosition = lla2ecef(station);
    dets{i}.MeasurementParameters.IsParentToChild = true; % parent = ecef, child = radar
    dets{i}.MeasurementParameters.Orientation = dets{i}.MeasurementParameters.Orientation' * Recef2station;

distanceFcn is used with the assignment metrics to evaluate the tracking assignment.

function d = distanceFcn(track, truth)

estimate = track.State([1 3 5 2 4 6]);
true = [truth.Position(:) ; truth.Velocity(:)];
cov = track.StateCovariance([1 3 5 2 4 6], [1 3 5 2 4 6]);
d = (estimate - true)' / cov * (estimate - true);


[1] Sridharan, Ramaswamy, and Antonio F. Pensa, eds. Perspectives in Space Surveillance. MIT Press, 2017.