Main Content

Simulate, Detect, and Track Anomalies in a Landing Approach

The example shows how to automatically detect deviations and anomalies in aircraft making final approaches to an airport runway. In this example, you will model an ideal landing approach trajectory and generate variants from it, simulate radar tracks, and issue warnings as soon as the tracks deviate from safe landing rules.

Introduction

Landing is a safety critical stage of flight. An aircraft in final approach to landing must align itself with the runway, gradually descend to the ground, and reduce its ground speed while keeping it safely above stall speed. All these steps are done to ensure that the aircraft touches the ground softly to reduce the risk to passengers and to avoid physical damage to the aircraft or the runway. These rules can be easily defined by an aviation professional or they can be inferred from tracking data using machine learning [1]. In this example, you assume that the rules are already defined.

Major airports usually have multiple runways oriented in different directions. Approaching aircraft are guided by the air traffic controllers in the airport tower to land on one of the runways that is best aligned against the direction of wind at that time. During the approach, controllers monitor the aircraft based on tracking systems. Airport traffic has increased over the last decades and, with it, the workload on air traffic controllers has increased as well. As a result, there is a need to automatically and reliably alert air traffic controllers to aircraft that approach the landing point in an unsafe manner: not aligned well with the runway, descending too quickly or too slowly, or approaching too quickly or too slowly.

Generate and Label Truths

You define a landing approach trajectory into Logan International Airport in Boston, MA using a geoTrajectory object. The trajectory waypoints are aligned with runway 22L, which runs from north-east to south-west, and a glide slope of 3 degrees. The time of arrival and climb rate are defined to slow the approaching aircraft to a safe speed and a smooth touchdown. Note that the positive value of climb rate is used for a descending trajectory. You use helperPertScenarioGlobeViewer (see the supporting file) to visualize the trajectory on the map.

baselineApproachTrajectory = geoTrajectory([42.7069 -70.8395 1500; 42.5403 -70.9203 950; 42.3736 -71.001 0],[0;180;400],...
    'ClimbRate', [6; 3.75; 3.75]);

viewer = helperPertScenarioGlobeViewer;
viewer.TargetHistoryLength = 0;
viewer.TrackHistoryLength = 0;
viewer.TrackLabelScale = 0.75;
positionCamera(viewer, [42.3072 -70.8463 12455], [0 -34 335]);
plotTrajectory(viewer, baselineApproachTrajectory, 'Color', [15 255 255]/255, "Width", 1);

For a trajectory coming to land at Logan airport on runway 22L to be safe, the trajectory must satisfy the following rules:

  • The trajectory must be closely aligned with the runway direction.

  • The glide slope must be between 2.5 and 4 degrees in the last 20963 meters. At distances above 20963 meters, the altitude must be at least 3000 ft.

  • The speed must be between 120 knots and 180 knots at the landing point. The upper speed bound can increase linearly with distance from the landing point.

You define these rules using the helperTrajectoryValidationRule (see the supporting file) and visualize the rules on the map.

% Define and show trajectory rules.
trajRules = defineTrajectoryRules();
showRules(viewer, trajRules)
snap(viewer)

You use the perturbations object function to define a normal distribution around the baseline trajectory, approachTrajectory. Each waypoint in the trajectory is perturbed with a zero-mean normal distribution and a standard deviation that gets smaller from the first waypoint to the last (the landing point). At the first waypoint, the standard deviation is 5e-3 degrees in longitude and 300 meters in altitude. The standard deviation decreases to 1e-3 degrees in longitude and 150 meters in altitude in the mid-point and then to 1e-4 degrees in longitude and 0 in altitude at the end-point on the ground.

% Define perturbation to the approach trajectory
perturbations(baselineApproachTrajectory, 'Waypoints', 'Normal', zeros(3,3), [0 5e-3 300; 0 1e-3 150; 0 1e-4 0]);

To create 20 trajectories that are perturbed from the baseline trajectory, first clone the trajectory and then perturb it.

% Generate perturbed trajectories
s = rng(2021, 'twister'); % Set random noise generator for repeatable results
numTrajectories = 20;
trajectories = cell(1,numTrajectories);
for i = 1:numTrajectories
    trajectories{i} = clone(baselineApproachTrajectory);
    perturb(trajectories{i});
end

To see which perturbed trajectories satisfy the rules of a safe approach to landing, you use the helper function validateTrajectory, provided at the bottom of this page. The function declares a trajectory to be anomalous if at least 1% of the points sampled from it violate any trajectory rule.

[truthAnomalyFlags, truthPercentAnomalousSteps] = validateTrajectory(trajectories, trajRules);

Plot the trajectories in yellow for anomalous trajectories and cyan for safe approaches. Overall, there are 7 anomalous trajectories out of the 20 generated trajectories.

plotTrajectory(viewer, trajectories(truthAnomalyFlags), 'Color', [255 255 17]/255, "Width", 1);
plotTrajectory(viewer, trajectories(~truthAnomalyFlags), 'Color', [15 255 255]/255, "Width", 1);
positionCamera(viewer, [42.4808 -70.916 1136], [0 0 340]);
snap(viewer)

Define a Scenario

Detecting anomalies in real time based on tracking data is a challenge for several reasons. First, since the tracking data is imperfect with noise, the tracking results are uncertain. As a result, some tolerances must be provided to avoid issuing false warnings. Second, the sensors report false detections, and the tracking system must be careful not to confirm tracks based on these false detections. The careful confirmation requires that the tracking system takes more time to confirm the tracks. To avoid excessive warnings on false tracks, you issue warnings only after a track is confirmed.

You define an Earth-centered tracking scenario.

% Create an Earth-centered tracking scenario
scenario = trackingScenario('UpdateRate', 1, 'IsEarthCentered', true);

Aircraft approaching landing in an airport are scheduled to avoid aerodynamic impact from one aircraft on the one following in its wake. The minimum safe time difference between two aircraft is one minute.

You use the perturbations and perturb object functions again to perturb the TimeOfArrival of each trajectory and to make sure no additional perturbations are applied to the Waypoints. Then you attach each trajectory to a new platform. To perturb the entire scenario, you use the perturb object function.

% Schedule the trajectories and attach each to a platform.
for i = 1:numTrajectories
    perturbations(trajectories{i}, 'TimeOfArrival', 'Uniform',(i-1)*60, (i-1)*60+10);
    perturbations(trajectories{i}, 'Waypoints', 'None');
    platform(scenario, 'Trajectory', trajectories{i});
end
perturb(scenario);

Like other major airports in the USA, Logan uses an Airport Surface Detection Equipment - Model X (ASDE-X) to track aircraft during final approach and on the ground [2]. ASDE-X relies on an airport surveillance radar, automatic dependent surveillance–broadcast (ADS–B) reports from the approaching aircraft, and other methods to provide accurate tracking which is updated every second (for more details, see [1]).

To simplify the model of this tracking system, you use a statistical radar model, fusionRadarSensor, attached to the airport tower, and connect the sensor to a trackerGNN object. You configure the tracker to be conservative about confirming tracks by setting the ConfirmationThreshold to confirm if a track receives 4-out-of-5 updates.

asdex = fusionRadarSensor(1, ...
    'ScanMode', 'No Scanning', ...
    'MountingAngles', [0 0 0], ...
    'FieldOfView', [360;20], ...
    'UpdateRate', 1, ...
    'ReferenceRange', 40000,...
    'RangeLimits', [0 50000], ...
    'RangeResolution', 100, ...
    'HasElevation', true, ...
    'HasINS', true, ...
    'DetectionCoordinates', 'Scenario', ...
    'FalseAlarmRate', 1e-7, ...
    'ElevationResolution', 0.4, ...
    'AzimuthResolution', 0.4);
p = platform(scenario, 'Position', [42.3606 -71.011 0], 'Sensors', asdex);
tracker = trackerGNN("AssignmentThreshold", [100 2000], "ConfirmationThreshold", [4 5]);
tam = trackAssignmentMetrics('AssignmentThreshold', 100, 'DivergenceThreshold', 200);

Run the Scenario and Detect Anomalous Tracks

In the following lines, you simulate the scenario and track the approaching aircraft. You use the validateTracks helper function to generate anomaly warnings for the tracks. You can see the code for the function at the bottom of this page.

Tracks that violate the safe approach rules are shown in yellow while tracks that follow these rules are shown in cyan. Note that the warning is issued immediately when the track violates any rule and is removed when it satisfies all the rules.

% Clean the display and prepare it for the simulation.
clear(viewer)

positionCamera(viewer, [42.3072 -70.8463 12455], [0 -34 335]);
showRules(viewer, trajRules)
clear validateTracks

% Main loop
while advance(scenario)
    % Collect detections
    dets = detect(scenario);
    
    % Update the tracker and output tracks.
    if ~isempty(dets) || isLocked(tracker)
        tracks = tracker(dets, scenario.SimulationTime);
    else
        tracks = objectTrack.empty;
    end
    
    % Get platform poses and assignment between tracks and truths.
    poses = platformPoses(scenario,"Quaternion","CoordinateSystem","Cartesian");
    tam(tracks,poses);
    [assignedTrackIDs, assignedTruthIDs] = currentAssignment(tam);
    
    % Validate the tracks with rules to find anomalous tracks.
    [tracks, trackAnomalyHistory] = validateTracks(tracks, trajRules, assignedTrackIDs, assignedTruthIDs);
    
    % Visualize
    updateDisplay(viewer,scenario.SimulationTime,[scenario.Platforms{:}],dets,[],tracks);
end

The following gif was taken when for a minute of simulation from the 900 seconds to the 960 seconds. It shows tracks identified as safe in cyan and tracks identified as anomalous in yellow. This identification is done at every simulation step as can be seen for track 1893.

myGif.gif

Compare Track Anomaly Reports to Truth

To verify that the anomaly warnings were issued for the right tracks, you use the analyze helper function, shown at the bottom of this page.

The function uses the trackAnomalyHistory collected during the simulation and compares it to the truthPercentAnomalousSteps calculated for each trajectory. Similar to truth, tracks are assigned anomaly flags if they were declared anomalous at least 1% of the time steps. You can see that the anomalies are issued correctly for the seven trajectories that were found to be anomalous.

comparisonTable = analyze(trackAnomalyHistory ,truthPercentAnomalousSteps);
disp(comparisonTable)
    TruthID    Truth Anomaly Flag    Track Anomaly Flag
    _______    __________________    __________________

       1             false                 false       
       2             false                 false       
       3             false                 false       
       4             false                 false       
       5             false                 false       
       6             false                 false       
       7             true                  true        
       8             true                  true        
       9             false                 false       
      10             true                  true        
      11             false                 false       
      12             false                 false       
      13             true                  true        
      14             true                  true        
      15             false                 false       
      16             false                 false       
      17             false                 false       
      18             false                 false       
      19             true                  true        
      20             true                  true        

Summary

In this example, you learned how to use tracking data to generate real-time warnings for anomalies like an unsafe landing approach.

You used geoTrajectory to define an ideal landing approach trajectory in geographic coordinates. You then used perturbations and perturb to create 20 trajectories that deviate from the ideal landing approach trajectory and to schedule the trajectories one after the other in the trackingScenario. To model an airport tracking system, you simplified the system model using a statistic radar model, by the fusionRadarSensor System object, and a tracker, by the trackerGNN System object.

References

  1. Raj Deshmukh and Inseok Hwang, "Anomaly Detection Using Temporal Logic Based Learning for Terminal Airspace Operations", AIAA SciTech Forum, 2019.

  2. Federal Aviation Administration, "Fact Sheet - Airport Surface Detection Equipment, Model X (ASDE-X)". Retrieved May 2020.

Supporting Functions

defineTrajectoryRules Define trajectory rules

function trajRules = defineTrajectoryRules
% This function defines rules for safe approach to landing on runway 22L at
% Logan International Airport in Boston, MA.

% The function uses the helperTrajectoryValidationRule attached as a
% supporting file to this example

% The trajectory must be closely aligned with the runway direction.
longitudeRule = helperTrajectoryValidationRule([42.37 42.71], [0.4587, -90.4379], [0.5128, -92.730]);

% The glide slope must be between 2.5 and 4 degrees in the last 20963
% meters. At distances above 20963 meters, the altitude must be at least
% 3000 ft. The rules are relative to range from the runway landing point.
altitudeRule1 = helperTrajectoryValidationRule([100 20963], [sind(2.5) 0], [sind(4) 0]); 
altitudeRule2 = helperTrajectoryValidationRule([20963 40000], 3000 * 0.3048, [sind(4) 0]); 

% The speed must be between 120 knots and 180 knots at the landing point.
% The upper speed bound can increase linearly with distance from the
% landing point.
speedRule = helperTrajectoryValidationRule([0 40000], 61.733, [1e-3 100]);

% Collect all the rules.
trajRules = [longitudeRule;altitudeRule1;altitudeRule2;speedRule];
end

validateTrajectory Validate each trajectory

function [truthAnomalyFlags, percentAnomalousSteps] = validateTrajectory(trajectories, rules)
numTrajectories = numel(trajectories);
numAnomalousSteps = zeros(1, numTrajectories);
for tr = 1:numTrajectories
    if iscell(trajectories)
        traj = trajectories{tr};
    elseif numTrajectories == 1
        traj = trajectories;
    else
        traj = trajectories(tr);
    end
    timesamples = (traj.TimeOfArrival(1):traj.TimeOfArrival(end));
    [pos,~,vel] = lookupPose(traj, timesamples);
    posECEF = lookupPose(traj, timesamples, 'ECEF');
    landingPoint = [1.536321  -4.462053   4.276352]*1e6;
    
    for i = 1:numel(timesamples)
        distance = norm(posECEF(i,:) - landingPoint);
        isLongitudeValid = validate(rules(1),pos(i,1),pos(i,2));
        isAltitudeValid = (validate(rules(2),distance,pos(i,3)) && validate(rules(3),distance,pos(i,3)));
        isSpeedValid = validate(rules(4),distance,norm(vel(i,:)));
        isValid = isLongitudeValid && isAltitudeValid && isSpeedValid;
        numAnomalousSteps(tr) = numAnomalousSteps(tr) + ~isValid;
    end
end
percentAnomalousSteps = numAnomalousSteps ./ numel(timesamples) * 100;
truthAnomalyFlags = (percentAnomalousSteps > 1);
end

validateTrack Validate tracks vs anomaly rules

function [tracks, history] = validateTracks(tracks, rules, assignedTrackIDs, assignedTruthIDs)
persistent trackAnomalyHistory
if isempty(trackAnomalyHistory)
    trackAnomalyHistory = repmat(struct('TrackID', 0, 'AssignedTruthID', 0, 'NumSteps', 0, 'NumAnomalousSteps', 0),30,1);
end

posECEF = getTrackPositions(tracks, [1 0 0 0 0 0; 0 0 1 0 0 0; 0 0 0 0  1 0]);
pos = fusion.internal.frames.ecef2lla(posECEF);
vel = getTrackVelocities(tracks, [0 1 0 0 0 0; 0 0 0 1 0 0; 0 0 0 0 0 1]);
numTracks = numel(tracks);
landingPoint = [1.536321  -4.462053   4.276352]*1e6;

trackIDs = [trackAnomalyHistory.TrackID];

for tr = 1:numTracks
    % Only validate tracks if altitude is greater than 0
    if pos(tr,3) > 15
        distance = norm(posECEF(tr,:) - landingPoint);
        isLongitudeValid = validate(rules(1),pos(tr,1),pos(tr,2));
        isAltitudeValid = (validate(rules(2),distance,pos(tr,3)) && validate(rules(3),distance,pos(tr,3)));
        isSpeedValid = validate(rules(4),distance,norm(vel(tr,:)));
        isValid = isLongitudeValid && isAltitudeValid && isSpeedValid;
    else
        isValid = true;
    end
    
    tracks(tr).ObjectClassID = uint8(~isValid) + uint8(isValid)*6; % To get the right color for tracks
    
    % Update anomlay history
    inHistory = (tracks(tr).TrackID == trackIDs);
    if any(inHistory)
        trackAnomalyHistory(inHistory).NumSteps = trackAnomalyHistory(inHistory).NumSteps + 1;
        trackAnomalyHistory(inHistory).NumAnomalousSteps = trackAnomalyHistory(inHistory).NumAnomalousSteps + ~isValid;
    else
        ind = find(trackIDs == 0, 1, 'first');
        trackAnomalyHistory(ind).AssignedTruthID = assignedTruthIDs(tracks(tr).TrackID == assignedTrackIDs);
        trackAnomalyHistory(ind).TrackID = tracks(tr).TrackID;
        trackAnomalyHistory(ind).NumSteps = 1;
        trackAnomalyHistory(ind).NumAnomalousSteps = ~isValid;
    end
end
history = trackAnomalyHistory;
end

analyze - Analyze the track anomaly history and compare it to the truth anomaly percentage

function comparisonTable = analyze(trackAnomalyHistory, percentTruthAnomalous)
trackAnomalyHistory = trackAnomalyHistory([trackAnomalyHistory.TrackID] > 0);
anomalousSteps = [trackAnomalyHistory.NumAnomalousSteps];
numSteps = [trackAnomalyHistory.NumSteps];
trackAssignedTruths = [trackAnomalyHistory.AssignedTruthID];
assignedTruths = unique(trackAssignedTruths);
numTrackAnomalousSteps = zeros(numel(assignedTruths),1);
numTrackSteps = zeros(numel(assignedTruths),1);

for i = 1:numel(assignedTruths)
    inds = (assignedTruths(i) == trackAssignedTruths);
    numTrackAnomalousSteps(i) = sum(anomalousSteps(inds));
    numTrackSteps(i) = sum(numSteps(inds));
end
percentTrackAnomalous = numTrackAnomalousSteps ./ numTrackSteps * 100;
trueAnomaly = (percentTruthAnomalous > 1)';
anomalyFlags = (percentTrackAnomalous > 1);
comparisonTable = table((1:20)',trueAnomaly, anomalyFlags,...
    'VariableNames', {'TruthID', 'Truth Anomaly Flag', 'Track Anomaly Flag'});
end